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_element
和max_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. 浅拷贝构造函数
浅拷贝构造函数仅复制对象的成员变量值,对于指针类型成员变量,只复制指针的地址,并不会为指针所指向的资源分配新的内存。因此,多个对象将共享同一个内存区域。浅拷贝是编译器默认生成的行为。
浅拷贝的特点:
- 对于普通数据类型(如
int
、char
等),拷贝值。 - 对于指针类型,只复制指针的地址,多个对象共享同一个指针所指向的内存。
- 浅拷贝不会涉及到动态内存的管理。
潜在问题:
- 当一个对象被析构时,它会释放指针所指向的内存。如果是浅拷贝,另一个对象的指针指向同一片内存区域,当它也试图释放内存时,将导致重复释放内存的错误(如双重释放、程序崩溃等)。
2. 深拷贝构造函数
深拷贝构造函数不仅仅复制成员变量的值,还为动态分配的内存分配新的空间,并复制原来对象的数据。因此,两个对象各自拥有独立的内存,不会互相影响。
深拷贝的特点:
- 不仅拷贝指针地址,还会分配新的内存并复制指针所指向的内容。
- 每个对象都有自己的动态内存,避免了浅拷贝中的内存共享问题。
- 深拷贝构造函数需要显式定义,编译器不会自动生成。
3、浅拷贝与深拷贝的比较
特性 | 浅拷贝 | 深拷贝 |
---|---|---|
内存分配 | 复制指针地址,多个对象共享同一内存 | 分配新的内存,两个对象各自独立 |
内存释放 | 会出现重复释放或内存泄漏的问题 | 安全,不会出现重复释放内存的问题 |
性能 | 较快(仅复制指针) | 较慢(需要分配和复制数据) |
适用场景 | 当对象不包含动态分配的资源时适用 | 当对象包含动态分配的资源时适用 |
总结来说,浅拷贝和深拷贝主要的区别在于指针类型的成员变量的处理上。对于指针类型的成员变量,浅拷贝仅复制指针的地址,而深拷贝会分配新的内存并复制内容。对于含有动态内存分配的类,深拷贝能避免内存管理的问题,通常是更安全的选择。
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
赋值操作背后的内存处理
在C++中,简单的赋值操作背后涉及到一系列的内存操作,包括在栈中重新开辟内存和值复制。以下是一个简单的赋值操作的步骤:
-
确定变量的类型和大小:
- 编译器首先需要知道赋值操作涉及的两个变量的类型和大小。例如,
int a = 5;
中,a
是一个int
类型的变量,通常占用4个字节(具体取决于编译器和平台)。
- 编译器首先需要知道赋值操作涉及的两个变量的类型和大小。例如,
-
分配内存:
- 对于左边的变量(即接收赋值的变量),编译器会在栈上为其分配足够的内存空间。例如,
int a;
会在栈上分配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)
堆区是用于动态分配内存的区域。程序可以在运行时通过 new
或 malloc
等函数手动申请内存,堆上的内存可以跨函数调用持久存在,直到程序员显式释放它(通过 delete
或 free
)。
- 存储内容:动态分配的对象和数组。
- 生命周期:程序员手动管理,申请的内存可以在多个函数间传递,直到显式释放为止,否则可能导致内存泄漏。
- 内存管理:程序员需要手动释放堆上的内存,C++ 中使用
delete
或free
。 - 特点:堆空间较大,适合存储大对象或需要跨函数调用持久存在的数据,但由于手动管理内存,容易导致内存泄漏和碎片化。
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
关键字声明,它们的生命周期延长至整个程序的执行期间,而作用域依然是局部的。
静态对象主要有两类:
- 局部静态对象:在函数或代码块内部定义,但生命周期跨越整个程序的执行。
- 类静态成员对象:在类中通过
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
动态创建宿主对象时,所有成员对象的内存也会动态分配,并在宿主对象销毁时自动调用析构函数。
全局对象在程序启动时创建,程序结束时销毁。
内存分配:
- 全局对象的内存在静态存储区中分配,类似于静态对象。
生命周期:
- 生命周期贯穿整个程序的运行周期,从程序开始到结束。