众所周知,C++内存区域被分为5大类:栈、堆、自由存储区、全局/静态存储区、常量存储区。
栈由编译器控制,栈空间的申请、使用和释放全权由编译器处理。这里的“全权处理”意思是责任归属,并不是说编译器在程序运行时介入管理。实际上,编译器的工作在编译期就完成了,它对栈的管理体现在编译时对寄存器esp的维护上。
堆即程序员使用new和delete操作符进行管理的内存区域。对堆的访问往往要比栈要慢,原因在于栈由系统实现,有硬部件上的优势;而堆只是C++语言中的数据结构,是纯粹软件的,访问堆上的变量一般都需要栈的参与才能完成。
自由存储区这个概念存在争议,争议的焦点在于它与堆是不是同一个东西。自由存储区也由程序员管理,但用到的是malloc和free操作。其实,自由存储区和堆是有区别的:一方面,管理方法不同,malloc和free是函数,new和delete是C++操作符;另一方面,malloc只负责开辟空间,返回指针是空类型指针void*,new不仅开辟空间而且还将调用对象构造函数,类似的,free只管释放空间,delete还将调用对象析构函数。
全局/静态存储区正如其名,是全局变量和静态变量的集中地。“全局”着眼于全局变量的全局作用域;“静态”着眼于static变量的生命周期。前者容易理解,后者稍微有些难懂。其实,C++中的动和静,往往都是从对象的生命周期这个角度去界定的。对于变量来说,“动态”的变量在内存区域里进进出出的变量,它们的生命仅限于自己的作用域,一旦离开自己的作用域就将从内存中被擦除,动态变量占用的那片内存区域在程序执行过程中有可能被释放掉;“静态”的变量则不同,它最大的特点就是随程序生,随程序死,步调很一致,只要程序启动了,静态变量就在静态存储区拥有了自己的空间,程序执行过程中不会被释放,只有程序结束运行,它们才被释放。可见,静态存储区强调的是变量的生命长度与程序一致。
常量存储区也很容易理解,即存储常量的地方。需要注意的是字面数字并不存放在这个区域,常量存储区里存储的一般是常量字符串。相同的常量字符串在常量存储区只保留一份拷贝,因此下面这个例子结果为T就容易理解了。
const char* st1="Astring";
const char* st2="Astring";
cout<<(st1==st2?‘T’:‘F’);
全局/静态存储区默认情况下是一片初始为0x00的区域,因此全局/静态变量如果没被初始化,它将默认被置为0;常量存储区是一片只读区域,不允许程序对里面任何一个bit做删改。
接下来,用一个简单的例子进一步说明。例子代码如mregions.cpp所示。
/*++ mregions.cpp
* Purpose:区分各种类型数据的内存区域
* Created:2011-08-19
* Author:btwsmile
--*/
int a=30;
void main()
{
//a,b,c在全局区
static int b=10;
static int c=20;
int d=40;
//常量区
const char* str1="Astring";
const char* str2="Newstring";
//堆
int* p=new int[3];
p[0]=a;
p[1]=b;
p[2]=c;
delete []p;
}
变量a是全局变量,b和c是静态变量,它们被存储在全局/静态存储区;"Astring"和"Newstring"是常量字符串,它们被存储在常量存储区;new int[3]在堆上开辟了可容纳3个整型数据的空间,delete []p释放了堆上的空间。栈并没有特别的举例,因为程序处处都用到栈。
接下来查看mregion.cpp对应的Disassembly,截取重要片段如下所示。
9: //a,b,c在全局区
10: static int b=10;
11: static int c=20;
12: int d=40;
001913AE mov dword ptr [d],28h
13: //常量区
14: const char* str1="Astring";
001913B5 mov dword ptr [str1],offset string "Astring" (195748h)
15: const char* str2="Newstring";
001913BC mov dword ptr [str2],offset string "Newstring" (19573Ch)
16: //堆
17: int* p=new int[3];
001913C3 push 0Ch
001913C5 call operator new (191177h)
001913CA add esp,4
001913CD mov dword ptr [ebp-104h],eax
001913D3 mov eax,dword ptr [ebp-104h]
001913D9 mov dword ptr [p],eax
18: p[0]=a;
001913DC mov eax,dword ptr [p]
001913DF mov ecx,dword ptr [a (197000h)]
001913E5 mov dword ptr [eax],ecx
19: p[1]=b;
001913E7 mov eax,dword ptr [p]
001913EA mov ecx,dword ptr [b (197004h)]
001913F0 mov dword ptr [eax+4],ecx
20: p[2]=c;
001913F3 mov eax,dword ptr [p]
001913F6 mov ecx,dword ptr [c (197008h)]
001913FC mov dword ptr [eax+8],ecx
21: delete []p;
001913FF mov eax,dword ptr [p]
00191402 mov dword ptr [ebp-0F8h],eax
00191408 mov ecx,dword ptr [ebp-0F8h]
0019140E push ecx
0019140F call operator delete (191082h)
00191414 add esp,4
注意行号18-20这三句对应的反汇编码,不难发现变量a/b/c各自对应的内存地址为ds:197000h/197004h/197008h,这是它们各自在全局/静态数据区占用的内存空间地址。观察14、15行代码对应的反汇编码,可以发现"Astring"和"Newstring"各自在常量存储区中的地址为195748h/19573Ch。从17行代码对应的反汇编码中看不出new得到的堆首地址,但在vs2010中让鼠标悬停在eax上,可查看eax的值为00101ae0h。而栈的地址,都以寄存器esp为基准浮动。在最终生成的可执行文件中,看不到变量名a,b,c,str,只有它们在编译时就被转换成的地址,用esp表示。
注意各类数据内存地址所在的区段:不同类型的变量处在不同区段,相同类型的变量处在相同区段并且地址连续。它们的地址被归纳在表1中。
表1 各种类型变量在内存中的地址