本文探讨了C++程序内存布局的基础知识,对堆、栈、全局数据区和代码区的概念进行了简单介绍,并介绍了内存对齐和进程地址空间(虚拟内存)方面的知识。
今天一大早起来,收到外校的同学传给我的一道C++面试题,该公司做Windows平台下的C++开发。面试题有一道考C++程序内存布局,很具有代表性。
已知有这样一段代码:
#include <iostream>
#include <string>
using std::string;
using std::cout;
using std::endl;
int global_a = 5; //全局对象
static global_b = 6; //全局静态对象
int main()
{ int a = 5; //声明一个变量5
char b = 'a';
int c = 8;
static int d = 7;
cout<<&a<<endl;
cout<<&c<<endl; cout<<&d<<endl;
cout<<&global_a<<endl;
cout<<&global_b<<endl;
return 0; }
问题如下:这段代码运行后会打印出变量的地址(这个很明显),以变 量a为例,编译后第一次执行和第二次执行打印出来的变量地址是否一样?如果重新编译再次运行,打印出来的变量地址会不会变化?请解释原因。
我拿这道题问了几位同学,得出的答案主要有以下两种,第一种认为三次打印出来的变量a的地址都不一样,第二种认为前两次打印出来的变量a的地址一样,第三次打印出来的变量a的地址与前两次不同。
那么实际的结果是如何呢?在VC++6.0和VS2008下分别进行实验,请看三次程序运行的截图:
![图一](http://p.ananas.chaoxing.com/star/1024_0/1393379963058wtlun.png)
![图二](http://p.ananas.chaoxing.com/star/1024_0/1393379962718exngp.png)
说明了什么?首先说明了我问的几位同学都提供的是错误的答案,再就是为什么会一样呢?一样会不会导致内存冲突(貌似而且实际上也是内存地址是一样的)。要解释这两个问题,必须了解C++程序的内存模型和Windows平台下的内存管理机制。
首先看C++程序的内存模型(本文不讨论C++对象内存模型),下面是一张经典的C++内存模型图:
![图三](http://p.ananas.chaoxing.com/star/1024_0/1393380030904pqgvm.png)
从上面的图示中,可以建立起对C++程序运行的一个整体认识,共分为栈、堆、全局数据区和程序代码区。文首程序中的变量a便是局部变量,所以被分配在栈区。大家回忆一下栈的知识,一个栈顶指针,栈从高地址向低地址生长,栈有大小限制。好了,问题来了,为什么三次打印出来的对象a的地址一样。
首先,在VC++6.0下,在“a=5”这行语句上加一个断点,按F5j进入调试模式,然后按ALT+8查看编译器生成的汇编代码:
![图四](http://p.ananas.chaoxing.com/star/1024_0/1393380080538hropj.png)
这里面ebp就是当前的栈顶地址,注意到a占用三个字节,而且答应出来的a 的地址实际上是a低字节的地址(回忆下计算机组成结构的知识)。那好,在第一个数据a入栈前的栈顶指针是多少呢?计算一下0012FF7C+4=0012FF80,这个值是由编译器决定的,从上面可以看出,VC++6.0下和VC++2008下这个值是不一样的。同理,也可以计算出全局数据区的起始地址。
好的,我们在深入一下,讨论另外一个问题,内存对齐。在声明变量a之后,又接着声明了字符b(占一个字节),最后又声明了整形变量c,那么c的地址应该是多少呢?计算一下:0012FF7C-1-4=0012FF77。但是实际上打印出来的是0012FF74。为什么呢?答案就是内存对齐。 在现代计算机体系结构中,为了使CPU对变量进行高效、快速地访问,变量的地址应该具有某种特性,那就是“对齐”,对齐行为是有编译器来来实施的。如本例中对于4个字节大小的整形变量,其起始地址应该位于4个字节边界上,即能够被4整除,汇编上的黑话叫做“模四地址”。 现在我们修改一下文首的代码,将动态对象加入,注意到动态对象是在堆中分配的,增加下面几行代码:
int * pinteger = new int(5); // 在堆上分配内存
cout<<pinteger<<endl;
int * pinteger2 = new int(5);
cout<<pinteger2<<endl; delete pinteger;
delete pinteger2;
![图五](http://p.ananas.chaoxing.com/star/1024_0/1393380115865rhseb.png)
注意到最后打印出的两个变量地址就是堆上的地址,注意到两个地址不是连续增长的。而在栈上和全局数据区分配的内存地址则是连续的。再就是注意到堆的地址在栈和全局变量区之间,这和上面所示的C++内存布局图示是一致的。那好,问第二个问题,三个程序打印出来的变量地址一模一样,会不会发生地址冲突?答案是不会发生地址冲突,因为打印出来的地址不是变量的实际物理地址,而是虚拟地址,也叫逻辑地址。三个程序(实际上是同一个程序的三个运行实例)
在操作系统中被当成三个独立的进程,彼此拥有独立的地址空间。它们之间互不影响,比如同样地址为0012FF7C的内存,在不同的进程中,它们的数据可能是完全不同的(在我们这个例子中是相同的)。在Windows操作系统中,通过虚拟内存机制,为每个进程提供了一个一致的内存视图,同时使得逻辑内存与物理内存分隔开来。虚拟内存的识这里不再赘述,推荐《操作系统概念》一书,对虚拟内存有完整的介绍。