概述
前阵子看《C++应用程序性能优化》,其中提到进程中的内存布局(以前只是知道内存中有这几个不同的区域,但并没有深入了解它们的分布位置)。为了加深理解,决定还是写个代码验证一下。
《C++应用程序性能优化》中谈到的内存布局大致如下(以Linux系统为例)
图片很清晰明了,不过有个问题,Linux下分布是这样的,那Windows下面有没有什么差异呢?上网查了下,看到别人贴的Windows进程中的内存布局,大致和Linux下也没特别大的不同,内核区、代码区、全局区、堆、栈至少在位置分布上是一致的:
好了,以上是理论基础,接下来写个程序验证一下吧!
硬件:Intel64位CPU
系统:WIN7 64位系统
软件平台:WIN32控制台程序(32位编译器)
1. 首先验证下全局变量和静态变量
// 全局变量
int nGlobalValue = 1;
double dGlobalValue = 2;
int _tmain(int argc, _TCHAR* argv[])
{
// 静态变量
static char cStatisValue = 2;
printf("&nGlobalValue: 0x%x\n", &nGlobalValue);
printf("&dGlobalValue: 0x%x\n", &dGlobalValue);
printf("&cStatisValue: 0x%x\n", &cStatisValue);
printf("\n");
getchar();
return 0;
}
执行结果:
从以上代码可以发现两个现象
(1) 前两个全局变量地址之间间隔了8个字节:
可是nGlobalValue明明是个int型啊,应该只占用了4个字节,为什么不是在int变量的地址之后4个字节,开始存放double变量呢?原因就是内存对齐。
为什么要做内存对齐呢?
因为CPU从内存中取数据是以固定长度来取的,譬如以上代码示例是在64位CPU上运行的,CPU取数据会按8字节(64位)长度来取。代码中第二个全局变量是double型,假如它的地址是在0x16004开始的,那么CPU获取该double变量需要经过两次取数据操作:首先从0x16000地址取8个字节,获取它的高32位放入dGlobalValue变量的低32位,然后从0x16008地址取8个字节,获取它的低32位放入dGlobalValue变量的高32位。如果加了内存对齐,让double型变量都从8倍数的地址开始分配,CPU就只需要执行一次取数据的操作就可以了。因此基于效率考虑,操作系统会做内存对齐,dGlobalValue从0x16008地址开始分配。
(2)静态变量的地址夹在两个全局变量中间
这正好说明全局变量和静态变量都是放在全局数据区,它们之间没有分界;还有就是编译器会做内存优化,把后分配的char型变量放到了0x16004,充分利用了空闲的内存空间,防止内存浪费。
2. 接下来增加字符串常量
// 全局变量
int nGlobalValue = 1;
double dGlobalValue = 2;
int _tmain(int argc, _TCHAR* argv[])
{
// 静态变量
static char cStatisValue = 2;
printf("&nGlobalValue: 0x%x\n", &nGlobalValue);
printf("&dGlobalValue: 0x%x\n", &dGlobalValue);
printf("&cStatisValue: 0x%x\n", &cStatisValue);
printf("\n");
// 常量字符串
const char *pLocalString1 = "hello";
const char *pLocalString2 = "world";
printf("pLocalString1: 0x%x\n", pLocalString1);
printf("pLocalString2: 0x%x\n", pLocalString2);
printf("&pLocalString1: 0x%x\n", &pLocalString1);
printf("&pLocalString2: 0x%x\n", &pLocalString2);
printf("\n");
getchar();
return 0;
}
执行结果
pLocalString1和pLocalString2是常量字符串的地址,从内存地址看,和全局区的内存地址相隔比较近,但又不是连续的。说明常量数据区和全局数据区很可能是挨着的。&pLocalString1和&pLocalString2是存放字符串地址的指针,从代码来看很显然是存放在栈上。由此可见,栈内存与全局、常量数据区内存之间相隔比较远。
接下来稍微修改一下:(把pLocalString2字符串改成和pLocalString1一样的,同时创建另外一个字符串数组,其存储的字符串也设置成"hello")
// 全局变量
int nGlobalValue = 1;
double dGlobalValue = 2;
int _tmain(int argc, _TCHAR* argv[])
{
// 静态变量
static char cStatisValue = 2;
printf("&nGlobalValue: 0x%x\n", &nGlobalValue);
printf("&dGlobalValue: 0x%x\n", &dGlobalValue);
printf("&cStatisValue: 0x%x\n", &cStatisValue);
printf("\n");
// 常量字符串
const char *pLocalString1 = "hello";
const char *pLocalString2 = "hello";
char cLocalString[] = "hello";
printf("pLocalString1: 0x%x\n", pLocalString1);
printf("pLocalString2: 0x%x\n", pLocalString2);
printf("&pLocalString1: 0x%x\n", &pLocalString1);
printf("&pLocalString2: 0x%x\n", &pLocalString2);
printf("cLocalString: 0x%x\n", cLocalString);
printf("\n");
getchar();
return 0;
}
执行结果:
发现没,pLocalString2和pLocalString1地址是一样的,也就是说同一常量字符串在内存中只会有一份数据,而不是每个变量都有各自的副本。查资料可知,字符串常量存放的地方叫字符串字面量池,目的就是保证多次用到同一字面量时,在内存中只有一个副本。
新增的cLocalString变量,从内存地址看,它是在栈上分配的,并不是直接指向常量数据区。
从上面代码看,栈内存也是连续分配的,而且地址从高往低分配。
3. 接下来从堆上申请内存
// 全局变量
int nGlobalValue = 1;
double dGlobalValue = 2;
int _tmain(int argc, _TCHAR* argv[])
{
// 静态变量
static char cStatisValue = 2;
printf("&nGlobalValue: 0x%x\n", &nGlobalValue);
printf("&dGlobalValue: 0x%x\n", &dGlobalValue);
printf("&cStatisValue: 0x%x\n", &cStatisValue);
printf("\n");
// 常量字符串, 栈内存
const char *pLocalString1 = "hello";
const char *pLocalString2 = "hello";
char cLocalString[] = "hello";
printf("pLocalString1: 0x%x\n", pLocalString1);
printf("pLocalString2: 0x%x\n", pLocalString2);
printf("&pLocalString1: 0x%x\n", &pLocalString1);
printf("&pLocalString2: 0x%x\n", &pLocalString2);
printf("cLocalString: 0x%x\n", cLocalString);
printf("\n");
// 堆内存
int *pNewInts = new int[3];
short *pNewShort = new short;
printf("pNewInts: 0x%x\n", pNewInts);
printf("pNewShort: 0x%x\n", pNewShort);
printf("&pNewInts: 0x%x\n", &pNewInts);
printf("&pNewShort: 0x%x\n", &pNewShort);
printf("\n");
getchar();
return 0;
}
执行结果
&pNewInts和&pNewShort是指针的地址,同样也是栈上分配的,和前面几块栈内存是连续的。pNewInts和pNewShort就是从堆上申请的内存,从地址来看,与全局区、常量区、栈内存都不接近。而且两块堆内存的地址也并不连续(因为堆内存是通过链表管理的,从空闲堆链表中找到满足条件的内存块来分配,并不保证先后分配的两块内存一定会是连续的;而且堆内存包含记录内存大小以及链表节点的头,所以实际上占用的内存大小比实际申请的大小要大)。
根据以上分析可以判断,内存区中:全局/静态区、常量区、栈、堆这几个内存分块的结论是成立的。但是同时也发现一个问题,这几个分块通过代码执行的结果来看,和上面图片展示的分布位置并不一致,这是为什么呢?后续再探讨下,有知道的朋友也可以留言指教。。