# 1.内存各分区的顺序问题
上周我简单介绍了内存分区:[内存分区](http://192.168.6.130/wiki/#/view/20/1482)
代码区
- **程序启动时**:当程序开始执行时,首先会加载代码区。操作系统将可执行文件中的代码段加载到内存的代码区,这其中包含了程序的所有指令,这些指令是 CPU 执行的依据,程序的执行流从代码区的起始位置开始,按照指令的顺序依次执行。
- **整个程序运行期间**:代码区的内容在程序运行过程中一般是只读的,不允许被修改。这是为了保证程序的指令的稳定性和安全性 ,防止程序在运行过程中由于误操作或恶意修改而导致程序崩溃或出现不可预测的行为。
常量区
- **程序启动时或首次访问常量时**:常量区在程序启动时通常也会随着程序的加载而被初始化,它用于存放程序中的常量数据,如字符串常量、const 修饰的全局常量和字面量等。这些常量在程序运行期间是不可修改的。当程序中首次访问到某个常量时,相关的常量数据就会被加载到常量区。
- **整个程序运行期间**:在程序运行的整个生命周期内,常量区的数据都保持不变,可供程序的各个部分共享和访问。多个地方使用相同的常量时,实际上都是指向常量区中的同一个存储位置,这样可以节省内存空间并提高程序的运行效率。
栈区
- **函数调用时**:当程序调用一个函数时,会在栈区为该函数分配一块栈帧,用于存储函数的局部变量、函数参数、返回地址等信息。栈帧的大小在编译时就大致确定,函数执行完毕后,栈帧会被自动释放,其所占用的栈空间也会被回收。这种分配和释放的方式遵循 “后进先出” 的原则,即最后进入栈的函数最先退出并释放栈帧。
- **嵌套函数调用时**:如果存在嵌套的函数调用,每进入一层新的函数调用,就会在栈顶为新的函数分配一个新的栈帧,随着函数调用的层层深入,栈区的空间会不断增长;而当函数依次返回时,栈帧会依次被弹出,栈区的空间逐渐缩小。
堆区
- **动态内存分配时**:堆区的内存分配和释放是由程序员手动控制的,通过`new`、`malloc`等操作符在程序运行期间动态地申请内存空间。当程序需要使用一块在编译时无法确定大小的内存时,就会在堆区进行动态内存分配。
- **动态内存释放时**:在使用完动态分配的内存后,必须通过`delete`、`free`等操作符显式地释放内存,否则会导致内存泄漏,即这块内存一直被占用,直到程序结束才会被操作系统回收。堆区的内存分配和释放时间完全由程序员根据程序的逻辑来决定,因此其使用顺序相对比较灵活,但也需要程序员更加谨慎地管理内存,以避免出现内存错误。
## 1.1 代码示例
```
#include <iostream>
using namespace std;
// 用于记录内存分配相关顺序的全局变量
int globalOrder = 0;
// 自定义类,用于观察构造和析构情况,辅助展示内存区域特点
class MyClass {
public:
int num;
MyClass(int n) : num(n) {
cout << "构造对象,值为" << num << " ,分配顺序: " << ++globalOrder << endl;
}
~MyClass() {
cout << "析构对象,值为" << num << endl;
}
};
// 全局对象,存放在全局区
MyClass globalObj(1);
// 函数声明,函数代码存放在代码区
void func();
// 特殊函数,用来模拟在代码区靠前部分执行的操作
void initFunction() {
cout << "执行initFunction,这部分代码在代码区靠前位置执行" << endl;
}
// 函数定义,同样代码存放在代码区,这里体现函数本身的代码所在区域
void func() {
MyClass stackObj(2); // 局部对象,存放在栈区
MyClass* heapObj = new MyClass(3); // 在堆区动态分配对象
cout << "堆区对象地址: " << heapObj << endl;
delete heapObj;
}
int main() {
// 代码区的代码从main函数开始按顺序执行
initFunction();
cout << "程序开始,先观察全局区对象情况" << endl;
func();
cout << "函数调用结束,继续执行主函数后续部分" << endl;
return 0;
}
```
运行结果为:
![image.png](doc-wiki/common/file?uuid=uIkbCkPtopSsGmtFGluu5t)
如果我将`调用构造函数创建对象`和`new一个对象`调换一下顺序。
```
// 函数定义,同样代码存放在代码区,这里体现函数本身的代码所在区域
void func() {
MyClass* heapObj = new MyClass(3); // 在堆区动态分配对象
MyClass stackObj(2); // 局部对象,存放在栈区
cout << "堆区对象地址: " << heapObj << endl;
delete heapObj;
}
```
会发现在堆的执行顺序就比栈的高了,说明二者在执行时并没有明确的先后顺序之分。
![image.png](doc-wiki/common/file?uuid=oCDeXDr0Qr3gDAK3n7RVlf)
从上面的运行结果貌似可以得出以下结论:全局区的代码会首先执行,其加载时间点是在main函数之前的;然后程序开始执行(main函数开始执行);在程序中根据代码逻辑的不同,栈区和堆区才会先后进行加载。
在更复杂的程序中,栈区和堆区的使用先后顺序完全取决于代码的具体逻辑和功能需求。
但是这种结论存在一些误区。
## 1.2 常量区和代码区的加载顺序
**加载**:一般情况下,在程序启动时,操作系统会先将可执行文件中的代码段加载到内存的代码区,之后才会在代码执行过程中,当需要访问常量时,再将常量加载到常量区(常量区是按需加载的)。因为代码区存放着程序的指令,是程序执行的基础,CPU 需要先获取这些指令才能开始执行程序的逻辑,如函数调用、循环控制等操作。从这个角度说,代码区先于常量区被加载。
在进入 `main` 函数之前,C++ 运行时环境会负责执行一系列的初始化操作,这些操作包括对全局对象和静态对象的初始化,这也是为什么上面的示例中全局区的代码会优先于main函数的执行,全局区的对象首先进行了初始化。
## 1.3 常量区和代码区的执行顺序
**执行**:
- **代码区主导运行流程**
- 程序的运行是以代码区的指令顺序为基础展开的。CPU 从代码区的起始地址开始,按照指令的先后顺序依次执行,每一条指令决定了程序下一步的操作,如进行数据运算、调用其他函数、进行内存访问等。整个程序的运行流程完全由代码区的指令所控制和引导。
- 以一个包含多个函数的程序为例,`main`函数中的指令首先被执行,当遇到调用其他函数的指令时,CPU 会暂停`main`函数的执行,转而跳转到被调用函数在代码区的指令位置开始执行,被调用函数执行完毕后,再返回`main`函数继续执行后续的指令。
- **常量区配合代码区运行**
- 常量区在运行时主要是为代码区的指令提供所需的常量数据支持。当代码区的指令需要使用某个常量时,例如进行数据比较、赋值操作或作为函数的参数等,就会从常量区读取相应的常量值。常量区的常量在程序运行过程中一般是只读的,不会被程序随意修改,以确保程序的稳定性和数据的一致性。
- 例如,在一个计算圆面积的程序中,代码区的指令在计算过程中需要使用圆周率`3.14`这个常量,当执行到相关计算指令时,就会从内存的常量区获取`3.14`这个值,然后与半径值进行运算,从而得到圆的面积。
# 2. nullptr_t
上周对NULL和nullptr进行了一些区别的总结:[NULL和nullptr](http://192.168.6.130/wiki/#/view/20/1619)
这里对 `nullptr_t` 进行一些分享:
## 2.1 nullptr_t 介绍
首先再介绍一下`nullptr`,`nullptr` 是一种特殊的常量,表示空指针,其类型是 `std::nullptr_t`。使用 `nullptr` 可以避免上述类型歧义的问题。
`nullptr_t` 是表示 `nullptr` 的类型的类型名。它是一个特殊的类型,可以用来声明和定义接受或返回 `nullptr` 的变量或函数。
## 2.2 特性
1.所有定义为 `nullptr_t` 类型的数据都是等价的,行为也是完全一致的。
2. `nullptr_t` 类型数据可以隐式转换成任意一个指针类型,但是不能转化为非指针类型,即使使用reinterpret_cast<nullptr_t>()的方式也不可以。
3. `nullptr_t` 类型数据不用于算术运算表达式,可以用于关系运算表达式(仅限于`nullptr_t`类型数据或者指针类型数据进行比较)
## 2.3 特殊情况
虽然nullptr_t看起来像是个指针类型,但是**在把nullptr_t应用于模板中时,模板会把它作为一个普通的类型来进行推导**,并不会将其视为T*指针。以下是一个示例:
```
template<typename T>
void g(T* t){}
template<typename T>
void h(T t){}
int main(int argc, char *argv[])
{
g(nullptr); //编译失败,因为nullptr的类型是nullptr_t,而不是指针
g((float*) nullptr);//推导出T=float
h(0); //推导出T=int
h(nullptr); //推导出T=nullptr_t
h((float*)nullptr); //推导出T=float*
}
```