c++ 知识点总结

C-Resource/第1阶段C++ 匠心之作 从0到1入门/C++基础入门讲义/C++基础入门.md at master · Blitzer207/C-Resource (github.com)

c++  lambda表达式加标准库的算法

在 C++ 中,lambda 表达式的捕获变量指的是,lambda 表达式中使用到的外部作用域的变量。这些外部变量在 lambda 表达式中被称为“捕获变量”。Lambda 通过捕获机制可以访问这些变量,而不用显式地将它们作为参数传递给 lambda。

捕获变量的方式有两种主要形式:按值捕获按引用捕获,这决定了 lambda 表达式内部如何使用和修改这些变量。

捕获列表语法:捕获列表位于 [] 内部,用来指定 lambda 如何捕获外部变量:

  • [x]:按值捕获变量 x
  • [&x]:按引用捕获变量 x
  • [=]:按值捕获所有外部变量。
  • [&]:按引用捕获所有外部变量。
  • [=, &y]:按值捕获除 y 外的所有外部变量,y 按引用捕获。
  • [&, x]:按引用捕获除 x 外的所有外部变量,x 按值捕获。
  • Lambda 表达式的基本语法

    [capture](parameters) -> return_type { // 函数体 }

        [ ](int x, int y) -> int { int z = x + y; return z + x; }

  • capture:捕获列表,指定了如何捕获外部作用域中的变量。捕获列表可以为空,也可以通过引用或值捕获外部变量。
  • parameters:参数列表,与普通函数的参数列表类似。如果没有参数,可以省略。
  • return_type:返回类型,可以省略,编译器会自动推导。如果函数体包含 return 语句,通常不需要显式指定。
  • 函数体:要执行的代码块。
  • 例如:auto add = [](int a, int b) -> int { return a + b; }; int result = add(3, 4); // result = 7
  • []:捕获列表为空,表示不捕获任何外部变量。
  • (int a, int b):参数列表,接收两个整数参数。
  • -> int:返回类型为 int,但在这个例子中可以省略返回类型,因为编译器可以自动推导。

    auto shotIter = std::find_if(shotsLayout.begin(), shotsLayout.end(),
                        [&](const auto &pair) { return sizeof(pair.second) > 0 && pair.first == shotIndex; });

c++ 数据类型

1、基本的数据类型:布尔型、字符型、整形、浮点型、双浮点型、无类型(void)、宽字符型(wchar_t))
2、typedef  为已有的类型取一个新的名字  eg: typedef type newname
3、枚举类型(enum)
4、数据类型转换:静态转换、动态转换、常量转换、重新解释转换

 4.1静态转换(static_cast)(数据类型的强制转换:newType newVar static_cast<newType>(var);静态转换不进行类型检查)
int i =10;
float f = static_cast<float>(i);
静态转换是编译时,在兼容的类型之间进行转换,基本类型转换,int,float,double等
4.2动态转换(dynamic_cast),动态转换通常用于将一个基类指针或者引用,转换为派生类的指针或引用。动态转换在运行时进行类型检查,如果不能转换则返回空指针或引发异常
class Base{};
class Derived:public Base{};
Base* ptr_base = new Derived;
Derived* ptr_derived = dynamic_cast<Derived*>(ptr_base);//将基类指针转换为派生类指针
4.3常量转换(const cast)将const类型的对象转换为非const类型的对象,只能用于转换掉const属性,不能改变对象的类型
const int i =10;
int & r = const_cast<int&>(i); //常量转换,将const int 转换为int
4.4重新解释转换(reinterpret_cast)
将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同数据类型之间进行转换,不进行任何类型检查,可能会导致未定义的问题
int i=10;
float f = reinterpret_cast<float&>(i);//重新解释将int类型转换为float类型
注:编译时的转换,在不相关的类型之间进行强制转换,把一个类型的位模式直接解释为另一个不想管类型的位模式,几乎适用于所有类型之间的转换,
包括完全不相关的类型,可以进行指针类型的转换,指针和基本数据类型的转换;适用于底层变成,对位模式直接操作。没有类型安全检查,错误的使用可能导致未定义错误

对比static_cast 与 reinterpret_cast
static_cast:用于兼容类型之间的转换,提供一定的类型安全性,适合常规的类型转换。
reinterpret_cast:用于低级别的位模式转换,没有类型安全保障,适合不相关类型之间的转换,需要谨慎使用。

c++引用和指针

 1、&
 声明变量前使用&代表引用
使用变量前加入& 代表取地址; 地址可以赋值给指针变量
2、*
声明变量前加*,代表声明了一个指针变量
使用变量前加*。代表获取指针变量指向的变量值
 int& ref2 = *ptr;  指针变量的引用,*取值,使用&  表示使用ref2引用ptr指的变量

c++数据结构

数组 array 、链表 linked list、栈stack、队列queue、优先队列priority queue、图graph、树tree、二叉树binary tree、二叉搜索树binary search tree BST:该二叉树要求左子树的值小于根节点,右子树的值大于根节点。平衡二叉树:平衡二叉树在插入和删除操作后可以自动调整,保持书的高度平衡,例如AVL树和红黑树

c++ stl  标准库

c++标准库包含一组头文件,涵盖功能包括:输入输出,容器,算法,多线程,正则表达式。
1、输入输出:<iostream>标准输入输出流,<fstream>:文件输入输出流,<sstream>:字符串流,<iomanip>输入输出流格式化。
2、容器:<array>定长数组,<vector>动态数组,<deque>双端队列,<list>双向链表,<forward_list>单向链表,<stack>栈适配器。
<deque>队列适配器,<priority_queue>优先队列适配器,<set>集合(基于平衡二叉树,<unordered_set>无序集合(基于哈希表),<map>映射(键值对,基于平衡二叉树),<unordered_map>无序映射(基于哈希表),<bitset>二进制位容器。
3、算法和迭代器:<algorithm>算法,<iterator>迭代器。
4、函数对象和绑定:<functional>定义函数对象及相关工具。
5、数学和数值运算:<numeric>数值操作累计乘积等,<complex>复数运算,<valarray>数组类及相关操作,<cmath>数学函数。
6、字符串和正则表达式:<string>标准字符串类,<regex>正则表达式。
7、时间和日期:<ctime>时间处理,<chrono>时间库。
8、多线程和并发:<thread>多线程支持,<mutex>互斥量,<condition_variable>条件变量,<future>异步编程支持,<atomic>原子操作。
9、内存管理:<memory>智能指针和动态内存管理,<new>动态内存分配。
10、类型特性和运行时类型识别:<type_traits>类型特性,<typeinfo>运行时类型识别。
11、异常处理:<exception>异常处理基类及相关工具,<stdexcept>常用异常类。
12、输入输出操作:<cstdio>:c风格输入输出,<cstdint>定长整数类型。
13、其它工具:<random>随机数生成,<utility>通用工具。等其他库。

stl容器

C++ 标准库提供了一系列通用的容器类模板,用于存储和管理数据集合。

1、顺序容器 (Sequence Containers)

  • std::vector:动态数组,支持快速随机访问。
  • std::deque:双端队列,支持快速随机访问以及头尾两端的插入和删除操作。
  • std::list:双向链表,支持快速的插入和删除操作。
  • std::forward_list:单向链表,支持快速的插入和删除操作。
  • std::array:固定大小的数组,支持快速随机访问。
  • std::string:字符串类,实际上是一个特殊的 std::vector<char>

2、关联容器 (Associative Containers)

  • std::set:有序集合,元素唯一。
  • std::multiset:有序集合,元素可以重复。
  • std::map:有序映射,键值对唯一。
  • std::multimap:有序映射,键值对可以重复。

3、容器适配器 (Container Adapters)

  • std::stack:栈,后进先出(LIFO)的数据结构。
  • std::queue:队列,先进先出(FIFO)的数据结构。
  • std::priority_queue:优先队列,元素根据优先级排列。

迭代器 (Iterators)

迭代器是一种通用指针,用于遍历容器中的元素。

#include <iterator>

// 使用迭代器遍历容器
for (ContainerType::iterator it = container.begin(); it != container.end(); ++it) {
    // 访问元素 *it
}

不同迭代器的划分主要是看容器支持的操作有那些 containerType::iterator it

输入迭代器,输出迭代器,前向迭代器,双向迭代器,随机访问迭代器

stl算法

C++ 标准库提供了大量的算法,用于处理容器中的数据。

1. 非修改序列操作算法

这些算法不会修改容器中的数据,只是进行查找、统计、比较等操作。

  • 查找算法:find  find_if   find_if_not   adjacent_find

    • find:查找指定值。
    • find_if:查找满足某一条件的元素。
    • find_if_not:查找不满足某一条件的元素。
    • adjacent_find:查找相邻的相同元素。
    • std::vector<int> v = {1, 2, 3, 4, 5}; auto it = std::find(v.begin(), v.end(), 3); // 找到值为 3 的元素
  • 统计算法:count   count_if  

    • count:统计某个值在容器中出现的次数。
    • count_if:统计满足某一条件的元素个数。
    • int cnt = std::count(v.begin(), v.end(), 3); // 统计值为 3 的元素个数
  • 比较算法:equal   mismatch

    • equal:判断两个范围内的元素是否相等。
    • mismatch:寻找两个范围内第一个不相等的元素。
    • std::vector<int> v2 = {1, 2, 3, 4, 5}; bool result = std::equal(v.begin(), v.end(), v2.begin()); // 判断是否相等
  • 搜索算法:search  search_n

    • search:在范围内查找子序列。
    • search_n:查找连续 n 个相同元素。

2. 修改序列操作算法

这些算法会修改容器中的元素,或生成新的数据。

  • 复制和替换:copy  copy_if  replace  replace_if

    • copy:将一段数据复制到另一段范围中。
    • copy_if:将满足某一条件的元素复制到目标范围。
    • replace:将所有等于某个值的元素替换为另一个值。
    • replace_if:将满足某一条件的元素替换为另一个值。
    • std::vector<int> v = {1, 2, 3, 2, 4}; std::replace(v.begin(), v.end(), 2, 5); // 将所有的 2 替换为 5
  • 删除操作:remove   remove_if

    • remove:删除所有等于指定值的元素,但并不会改变容器的大小。
    • remove_if:删除满足某一条件的元素。
    • auto it = std::remove(v.begin(), v.end(), 5); // 移除值为 5 的元素 v.erase(it, v.end()); // 必须手动调用 erase 以调整容器大小
  • 交换和填充:swap_ranges    fill   generate

    • swap_ranges:交换两个范围内的元素。
    • fill:用指定的值填充范围内的元素。
    • generate:用函数生成数据并填充范围。
    • std::fill(v.begin(), v.end(), 0); // 用 0 填充整个容器
  • 变换算法:transform

    • transform:将某个函数应用于一段范围的元素,并将结果存储到另一个范围中。

    std::vector<int> v = {1, 2, 3}; std::transform(v.begin(), v.end(), v.begin(), [](int x) { return x * 2; }); // 所有元素乘以 2

3. 排序算法

这些算法用于对容器中的元素进行排序和重新排列。

  • 排序相关:sort     partial_sort    nth_element     is_sorted

    • sort:对范围内的元素进行排序,默认升序。
    • partial_sort:对部分元素进行排序(例如前 N 个)。
    • nth_element:对范围内的第 N 个元素排序,保证第 N 个位置上的元素是有序的。
    • is_sorted:判断范围是否已经排序。
    • std::sort(v.begin(), v.end()); // 对 v 进行升序排序
  • 重新排列:shuffle   reverse   rotate  

    • shuffle:随机打乱一段范围内的元素(C++11 起引入,需要随机数生成器)。
    • reverse:将一段范围内的元素反转。
    • rotate:循环移动一段范围内的元素。
    • std::reverse(v.begin(), v.end()); // 反转整个容器

4. 集合算法

这些算法主要用于处理有序范围的集合操作。

  • 集合运算:merge  set_union   set_intersection   set_difference   set_symmetric_difference

    • merge:将两个有序范围合并为一个有序范围。
    • set_union:计算两个有序集合的并集。
    • set_intersection:计算两个有序集合的交集。
    • set_difference:计算两个有序集合的差集。
    • set_symmetric_difference:计算两个有序集合的对称差集。
    • std::set<int> s1 = {1, 2, 3}; std::set<int> s2 = {2, 3, 4}; std::vector<int> result; std::set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(), std::back_inserter(result));

5. 二分查找算法

这些算法要求容器中的元素已经排序,用于高效查找和判断元素。

二分查找   binary_search   lower_bound    upper_bound   equal_rang

  • binary_search:二分查找,判断元素是否存在于有序范围内。

  • lower_bound:查找不小于给定值的第一个元素的位置。

  • upper_bound:查找大于给定值的第一个元素的位置。

  • equal_range:查找等于给定值的范围(返回一对迭代器)。

  • std::sort(v.begin(), v.end()); // 先排序 bool found = std::binary_search(v.begin(), v.end(), 3); // 判断 3 是否存在

6. 堆操作算法

这些算法用于处理以数组形式表示的堆(通常用于优先队列)。

堆操作     push_heap   pop_heap  make_heap   sort_heap

  • push_heap:将元素插入堆。

  • pop_heap:移除堆顶元素。

  • make_heap:将范围内的元素转化为堆。

  • sort_heap:对堆进行排序。

  • std::vector<int> v = {3, 1, 4, 1, 5}; std::make_heap(v.begin(), v.end()); // 将 v 转换为堆

7. 数值算法

这些算法用于进行数值计算,通常适用于数字容器。

数值算法    accumulate    inner_product   adjacent_difference   partial_sum   

  • accumulate:计算范围内所有元素的和。

  • inner_product:计算两个范围内元素的内积。

  • adjacent_difference:计算相邻元素的差。

  • partial_sum:计算部分和。

  • int sum = std::accumulate(v.begin(), v.end(), 0); // 计算元素的总和

8. 其他算法

其它    min_element    max_element   clamp  for_each   

  • min_elementmax_element:返回范围内的最小或最大元素。

  • clamp(C++17):限制一个值在指定的范围内。

  • for_each:对范围内的每个元素应用一个函数。

  • auto minIt = std::min_element(v.begin(), v.end()); // 找到最小值

this指针

C++中成员变量和成员函数是分开存储的

每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码

那么问题是:这一块代码是如何区分那个对象调用自己的呢?

c++通过提供特殊的对象指针,this指针解决上述问题。this指针指向被调用的成员函数所属的对象

this指针是隐含每一个非静态成员函数内的一种指针

this指针不需要定义,直接使用即可

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this

构造函数

构造函数,拷贝构造函数,拷贝构造函数的调用时机,构造函数的调用规则,构造函数中的深拷贝与浅拷贝。

两种分类方式:按参数分为: 有参构造和无参构造。按类型分为: 普通构造和拷贝构造。

三种调用方式:括号法、显示法、隐式转换法

	//2.1  括号法,常用
	Person p1(10);
	//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
	//Person p2();
	//2.2 显式法
	Person p2 = Person(10); 
	Person p3 = Person(p2);
	//Person(10)单独写就是匿名对象  当前行结束之后,马上析构
	//2.3 隐式转换法
	Person p4 = 10; // Person p4 = Person(10); 
	Person p5 = p4; // Person p5 = Person(p4); 
	//注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
	//Person p5(p4);

深拷贝与浅拷贝

1. 浅拷贝构造函数

浅拷贝构造函数仅复制对象的成员变量值,对于指针类型成员变量,只复制指针的地址,并不会为指针所指向的资源分配新的内存。因此,多个对象将共享同一个内存区域。浅拷贝是编译器默认生成的行为。

浅拷贝的特点

  • 对于普通数据类型(如 intchar 等),拷贝值。
  • 对于指针类型,只复制指针的地址,多个对象共享同一个指针所指向的内存。
  • 浅拷贝不会涉及到动态内存的管理。

潜在问题

  • 当一个对象被析构时,它会释放指针所指向的内存。如果是浅拷贝,另一个对象的指针指向同一片内存区域,当它也试图释放内存时,将导致重复释放内存的错误(如双重释放、程序崩溃等)。
2. 深拷贝构造函数

深拷贝构造函数不仅仅复制成员变量的值,还为动态分配的内存分配新的空间,并复制原来对象的数据。因此,两个对象各自拥有独立的内存,不会互相影响。

深拷贝的特点

  • 不仅拷贝指针地址,还会分配新的内存并复制指针所指向的内容。
  • 每个对象都有自己的动态内存,避免了浅拷贝中的内存共享问题。
  • 深拷贝构造函数需要显式定义,编译器不会自动生成。
  3、浅拷贝与深拷贝的比较
特性浅拷贝深拷贝
内存分配复制指针地址,多个对象共享同一内存分配新的内存,两个对象各自独立
内存释放会出现重复释放或内存泄漏的问题安全,不会出现重复释放内存的问题
性能较快(仅复制指针)较慢(需要分配和复制数据)
适用场景当对象不包含动态分配的资源时适用当对象包含动态分配的资源时适用

总结来说,浅拷贝和深拷贝主要的区别在于指针类型的成员变量的处理上。对于指针类型的成员变量,浅拷贝仅复制指针的地址,而深拷贝会分配新的内存并复制内容。对于含有动态内存分配的类,深拷贝能避免内存管理的问题,通常是更安全的选择。

浅拷贝:简单的赋值拷贝操作

深拷贝:在堆区重新申请空间,进行拷贝操作

赋值操作背后的内存处理

在C++中,简单的赋值操作背后涉及到一系列的内存操作,包括在栈中重新开辟内存和值复制。以下是一个简单的赋值操作的步骤:

  1. 确定变量的类型和大小

    • 编译器首先需要知道赋值操作涉及的两个变量的类型和大小。例如,int a = 5; 中,a 是一个 int 类型的变量,通常占用4个字节(具体取决于编译器和平台)。
  2. 分配内存

    • 对于左边的变量(即接收赋值的变量),编译器会在栈上为其分配足够的内存空间。例如,int a; 会在栈上分配4个字节的内存。
  3. 读取右边的值

    • 编译器会读取右边变量的值。如果右边是一个常量(如 5),编译器会直接使用这个值。如果右边是一个变量,编译器会从该变量的内存地址读取其值。
  4. 写入左边的内存

    • 编译器将右边的值写入左边变量的内存地址。对于简单的赋值操作,这通常是一个直接的内存写入操作。
  5. 可能的类型转换

    • 如果左右两边的变量类型不同,编译器会进行必要的类型转换。例如,double d = 5; 中,整数 5 会被转换为浮点数 5.0,然后写入 d 的内存地址。

函数参数传递的三种方式:

1.值传递(在栈区开辟一块新的空间,不会改变实参的值)

2.地址传递(函数的参数是指向原来变量的指针,也就是存储的是原变量的地址,两者存储同一块空间)

3.引用传递(通过给变量起别名可以简化指针修改形参),引用的本质是一个指针常量。

void func(int& ref)
{
    ref = 100;//自动转换为*ref=100
}
int main()
{
    int a = 20;
    int& ref = a;
    //自动转换为int* const ref=&a const修饰的是ref,所以指针指向不可改变,所以引用不可改变
    ref = 20;//(内部发现ref是引用,自动转换为*ref=20)
    cout << "a=" << a << endl;
    cout << "ref=" << ref << endl;
    func(a);
    cout << "ref=" << ref << endl;
}
 

4、引用做函数返回值

函数调用可以作为左值

        因为test()函数实际上返回的是a,而result就是a的别名,将test()=1000,a的值就为1000,自然result的结果也为1000.

int& test()
{
    static int a = 20;
    return a;
}
int main()
{
    int& result = test();
    cout << "result=" << result << endl;//20
    test() = 1000;
    cout << "result=" << result << endl;//1000
}

不要返回局部变量的引用

#include<iostream>
using namespace std;
int& test()
{
    int a = 20;
    return a;
}
int main()
{
    int& result = test();
    cout << "result=" << result << endl;//返回一个随机值
}

结果不是20

这是因为a在函数test内一个局部变量,因为函数是以int&为返回值的,本质上还是一个指针,当test函数运行完之后a的内存就被回收了,你之后还想访问这片空间就是非法访问。
这里只要在变量a上加上static,变量将存入全局区。

内存管理

一、存储区域划分

1. 代码段 (Text Segment)

代码段存储的是程序的机器指令(编译后的代码)。这是一个只读的区域,防止代码被意外修改。这部分内存只会在程序加载时分配,并且在整个程序运行期间一直存在。

  • 存储内容:程序的可执行指令(函数体的代码)。
  • 生命周期:程序执行期间始终存在。
  • 访问方式:只读,不能被修改。

2. 数据段 (Data Segment)

数据段分为两部分:

  • 初始化数据段 (.data):用于存储程序中已初始化的全局变量、静态变量、和常量数据。它们的值在编译时已知,并且在程序启动时就被加载到内存中。

  • 未初始化数据段 (.bss):用于存储未初始化的全局变量和静态变量(它们默认会被初始化为 0)。这个段的大小在编译时确定,数据在程序启动时被初始化为 0。

  • 存储内容

    • 初始化的全局变量、静态变量和常量位于 .data 段。
    • 未初始化的全局和静态变量位于 .bss 段。
  • 生命周期:从程序开始执行到程序结束,数据段中的变量一直存在。

3. 栈区 (Stack Segment)

栈区用于存储局部变量、函数调用的参数和返回地址等。栈是一种后进先出(LIFO)的数据结构,因此每当函数调用时,局部变量、参数和返回地址会被压入栈,函数返回时这些数据会被弹出。

  • 存储内容:局部变量、函数参数、返回地址。
  • 生命周期:栈上的变量在函数调用时创建,函数结束时自动销毁。局部变量只在它们所在的函数或代码块中有效。
  • 内存管理:由系统自动管理,程序员不需要手动释放栈上的内存。
  • 特点:栈的空间有限,通常适合存储较小的对象。大对象或大量递归调用可能会导致栈溢出。

4. 堆区 (Heap Segment)

堆区是用于动态分配内存的区域。程序可以在运行时通过 newmalloc 等函数手动申请内存,堆上的内存可以跨函数调用持久存在,直到程序员显式释放它(通过 deletefree)。

  • 存储内容:动态分配的对象和数组。
  • 生命周期:程序员手动管理,申请的内存可以在多个函数间传递,直到显式释放为止,否则可能导致内存泄漏。
  • 内存管理:程序员需要手动释放堆上的内存,C++ 中使用 deletefree
  • 特点:堆空间较大,适合存储大对象或需要跨函数调用持久存在的数据,但由于手动管理内存,容易导致内存泄漏和碎片化。

5. 常量区

常量区专门用于存储程序中不可修改的常量(如字符串字面量)。虽然常量通常存储在数据段中,但由于它们的特殊属性,很多编译器会将它们放入独立的常量区。这个区域是只读的,防止修改。

  • 存储内容:字符串常量、const 修饰的全局变量等。
  • 生命周期:程序运行期间一直存在。
  • 访问方式:只读,不能修改常量区的内容,任何修改操作都会导致运行时错误。

内存区域的总体划分图:

 

6. 寄存器区

寄存器区存储临时的局部变量。这些变量是通过编译器优化,被放置在 CPU 寄存器中的,以加快访问速度。寄存器的数量非常有限,因此这种优化只适用于少量的局部变量。

  • 存储内容:部分局部变量(通常是经常访问的变量)。
  • 生命周期:与栈上的局部变量类似,只在函数执行期间有效。
  • 内存管理:由编译器自动管理。

各存储区域的对比总结

存储区域存储内容生命周期作用域管理方式
代码段程序的机器指令程序启动到程序结束全局只读,不可修改
数据段 (.data)初始化的全局变量、静态变量、常量程序启动到程序结束全局自动管理
数据段 (.bss)未初始化的全局变量、静态变量程序启动到程序结束全局自动管理
栈区局部变量、函数参数、返回地址函数调用时分配,函数结束时释放局部(函数或代码块)自动管理
堆区动态分配的内存(对象、数组)申请到释放可跨函数或全局程序员手动管理
常量区字符串字面量、const 常量程序启动到程序结束全局只读,不可修改
寄存器区编译器优化的局部变量函数调用时分配,函数结束时释放局部自动管理

总结:

  • 代码段存储程序指令,数据段存储已初始化和未初始化的全局/静态变量,栈区用于局部变量,堆区用于动态分配的内存。
  • 常量区存储不可修改的常量,寄存器区用于编译器优化的局部变量。

二、全局对象与静态对象

1. 全局对象

定义:

全局对象是在所有函数外部定义的对象,它可以在整个程序的任何地方访问,并且在程序的整个生命周期内都有效。

局对象通常用于需要跨多个函数或文件共享的状态或数据。比如日志系统、配置管理器等,但需要谨慎使用,以免引入不可控的依赖和难以调试的问题。

特点:
  • 存储位置:全局对象存储在静态存储区。
  • 生命周期:全局对象从程序启动时创建,一直到程序结束时销毁。即,它们的生命周期贯穿整个程序的执行过程。
  • 作用域:全局对象的作用域为整个程序(文件级作用域),可以在定义的文件内任何地方直接访问,甚至可以通过 extern 关键字在其他文件中引用。
  • 初始化顺序:全局对象的初始化顺序与它们在翻译单元中的定义顺序相关。跨翻译单元的全局对象的初始化顺序在编译期间是不确定的。
  • 析构顺序:全局对象在程序结束时会自动调用析构函数,析构顺序与初始化顺序相反。
  • #include <iostream>

    class GlobalClass {
    public:
        GlobalClass() { std::cout << "Global Constructor\n"; }
        ~GlobalClass() { std::cout << "Global Destructor\n"; }
    };

    // 全局对象
    GlobalClass globalObj;

    int main() {
        std::cout << "In main function\n";
        return 0;
    }

2. 静态对象

定义:

静态对象可以是局部对象或类的成员对象,但通过 static 关键字声明,它们的生命周期延长至整个程序的执行期间,而作用域依然是局部的。

静态对象主要有两类:

  1. 局部静态对象:在函数或代码块内部定义,但生命周期跨越整个程序的执行。
  2. 类静态成员对象:在类中通过 static 关键字声明的静态成员,属于类本身,而不是类的某个实例。
适用场景:
  • 局部静态对象适用于需要在函数中保留状态的场景,比如计数器、缓存等。
  • 类静态成员对象适用于需要在类的所有对象之间共享数据的场景,比如单例模式中的唯一实例。
特点:
  • 存储位置:静态对象也存储在静态存储区。
  • 生命周期:静态对象在它们第一次被访问时初始化,并且直到程序结束时才销毁。即,它们具有程序级的生命周期。
  • 作用域
    • 局部静态对象的作用域是定义它的函数或代码块,外部无法直接访问。
    • 类的静态成员的作用域是整个类,并且可以通过类名访问,而不是通过对象实例访问。
  • 初始化顺序:局部静态对象的初始化是按需(Lazy Initialization),即它们在第一次使用时初始化,确保线程安全。

局部静态对象:

#include <iostream>

void func() {
    static int counter = 0; // 局部静态对象
    counter++;
    std::cout << "Counter: " << counter << std::endl;
}

int main() {
    func(); // 第一次调用,静态对象初始化为0
    func(); // 静态对象值被保存,输出2
    return 0;
}
类静态成员

#include <iostream>

class MyClass {
public:
    static int staticValue; // 静态成员声明

    static void printStaticValue() {
        std::cout << "Static Value: " << staticValue << std::endl;
    }
};

// 静态成员定义并初始化
int MyClass::staticValue = 5;

int main() {
    MyClass::printStaticValue(); // 通过类名访问静态成员
    return 0;
}

3. 全局对象与静态对象的对比

特性全局对象静态对象
存储位置静态存储区静态存储区
生命周期从程序启动到程序结束局部静态对象从第一次调用到程序结束;类静态成员从程序启动到结束
作用域全局作用域,整个文件局部静态对象的作用域是定义它的函数或代码块,类静态成员的作用域是类
初始化时机程序启动时按顺序初始化局部静态对象在第一次使用时初始化(懒加载),类静态成员程序启动时初始化
析构时机程序结束时析构程序结束时析构
使用场景需要跨多个函数或文件共享的数据局部静态对象用于函数内保留状态,类静态成员用于类的所有实例共享数据

4. 注意事项

  • 全局对象的初始化顺序问题:跨翻译单元的全局对象初始化顺序是不确定的,这可能会导致一些未定义行为。因此全局对象的使用需要格外小心,特别是在有多个文件时。
  • 静态对象的多线程问题:C++11 之后,局部静态对象的初始化是线程安全的,编译器会保证只有一个线程能够初始化静态对象。

总结来说,全局对象和静态对象虽然都具有静态存储周期,但它们的作用域和初始化时机有所不同。全局对象通常用于需要跨函数共享状态的情况,而静态对象用于局部状态持久化或类成员的共享数据。

三、析构函数的调用时间

总结:

  • 栈上对象:作用域结束时自动调用析构函数。
  • 堆上对象:显式调用 delete 时析构函数被调用。
  • 静态对象:程序结束时调用析构函数。
  • 全局对象:程序结束时调用析构函数。
  • 成员对象:宿主对象析构时,成员对象依次析构。
  • 智能指针管理的对象:引用计数归零或 unique_ptr 超出作用域时自动析构。
  • 临时对象:表达式结束时自动析构。

类的成员对象的内存和生命周期取决于宿主对象的内存分配方式。

栈上的类包含的成员对象:
  • 宿主对象在栈上创建时,所有成员对象会随之创建,析构时成员对象会先被析构。
堆上的类包含的成员对象:
  • 当使用 new 动态创建宿主对象时,所有成员对象的内存也会动态分配,并在宿主对象销毁时自动调用析构函数。

全局对象在程序启动时创建,程序结束时销毁。

内存分配:
  • 全局对象的内存在静态存储区中分配,类似于静态对象。
生命周期:
  • 生命周期贯穿整个程序的运行周期,从程序开始到结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值