这篇文章介绍下 C++ 程序的四个区,以及一个智能指针的简单实现。
起因
最近在公司审查代码的时候,coverity 对以下代码:
cpp
T fun()
{
Obj obj;
//代码逻辑
}
报出了 obj 占用空间过大,有可能栈溢出的问题。
以前从来没有考虑过C++的代码存放位置的问题,这次顺便就学习了下。
C++四区
在执行一个C/C++语言程序时,此程序将拥有唯一的"内存四区"------栈区、堆区、全局区、代码区。不过这里的栈和堆并不是数据结构里的栈和堆。
栈区(stack)
存放各种临时变量,包括函数内部声明的临时变量,函数调用的参数以及函数本身调用产生的入栈出栈操作等。
堆区
存放程序员自己分配的内存,如果程序员自己分配了,没有释放,那程序结束以后可能会被操作系统释放?
静态区
存放静态变量,包括全局变量,static 修饰的变量。C语言时期,静态区的变量又分为初始化的和未初始化的,两者分别在不同的位置。C++取消了这个设计。
文本区
存放常量,以及代码本身。
我们用一个例子来展示这几个区:
cpp
int global_a = 0;
char global_c;
int global_b;
我们声明三个全局变量,分别打印出他们的地址(防止入栈出栈操作干扰,这里用宏定义):
cpp
#define PRINT_ADDR(val) cout << (int)&(val) << endl
打印出来的结果如下:
bash
3264544
3264548
3264552
int 类型占用 4 个字节,char 类型占用 1 个字节,这里占用 4 个可能是由于内存对齐的问题。
接下来我们在 main 函数内部声明三个局部变量:
cpp
int temp_c;
double *temp_d = new double(1.0);
int temp_e = 3;
打印的结果为:
bash
10942212
10942208
10942204
与上面变量的内存地址明显有很大差别,另外注意这里的 double 类型虽然占用 8 字节,但是我们声明的是一个指针,无关类型,是固定的 4 字节。同样,new 出来的内存在堆上,将堆上的地址返回存储在 temp_d中。
最后是两个静态变量和一个常量:
cpp
static int static_e = 2;
static int static_f = 4;
string temp_f = "f";
地址为:
cpp
3207168
3207172
3215360
本次出问题的地方就在栈区。因此将临时变量改为从堆上 new/delete 内存即可。但是需要注意的是,new/delete 的操作会在运行期间动态分配内存,而入栈与出栈操作仅在编译期间就可以完成,这相当于把编译的内存压力转给了运行期,并不是一个合理的做法。
智能指针的简易实现
使用 new/delete 的组合容易出现忘记 delete 的问题,事实上,我们可以使用一个工具类"包裹"原始的类,在构造时调用 new,在析构时调用 delete,我给出一个简单的实现如下:
cpp
template <typename Obj>
struct AutoObj
{
AutoObj(Obj* obj)
{
actualObj = obj;
}
~AutoObj()
{
if (actualObj != nullptr)
{
delete actualObj;
}
}
private:
Obj *actualObj = nullptr;
};
可以编写一个类,测试一下效果:
cpp
struct Student
{
Student(int age):age(age)
{
cout << "Student construct" << endl;
}
~Student()
{
cout << "Student delete" << endl;
}
private:
int age;
};
cpp
AutoObj<Student> stu{new Student(10)};
看打印发现析构函数已经被正确地调用了:
bash
Student construct
Student delete
这个简易的智能指针的实现实在过于简易了,无法处理一些异常情况,需要根据具体情况去实现其他比如复制构造函数之类的接口。
当然,你也可以直接用智能指针,见【C++】智能指针(一)