参考网址:https://blog.youkuaiyun.com/dongtuoc/article/details/79132137
内存分配
c++运行时,内存主要包括几个部分:
- 堆
- 动态链接库(内存映射区)
- 栈
- 全局区
- 代码区
- (文字常量区)
如下图所示:
注:对于这个图,我在网上搜到的图,虚拟地址全都是这样的,然而在我实际使用C++程序测试时,返回的地址指针却并不符合,这里我不太懂是为什么,但是他们的上下关系是对的,比如常量区确实在代码区的高地址方向。如果有大佬知道,还望不吝赐教。
堆
堆中存储由程序员用new和malloc等函数分配的变量。空间最大,以链表形式管理,物理空间不连续,由低地址向上扩展。需要手动释放,不然容易内存泄漏。每个进程有一个堆。每次申请堆内存时,从小到大遍历链表,直到找到大于等于申请空间的内存,然后取出其中等于申请空间的内存,将剩余内存返回堆中。
栈
栈存储非静态局部变量、函数参数、以及一些程序调用时的寄存器值。空间小,容易溢出,物理空间连续,由高地址向下扩展。每个线程都有自己的栈。
比如对于如下程序:
void func(int x, int y)
{
x = 5;
int z = 12;
int w = 345;
std::cout << "In func(). x = " << x
<< ", variable in stack: " << z << ", " << w << std::endl;
}
int main(void)
{
int x = 90, y = 26;
func(x, y);
return 0;
}
其中,x、y、z、w这些变量就是存储在栈上的。
另外在进行函数调用的时候,还会对栈进行一系列操作,过程如下:
- 首先将函数调用的参数从左向右依次拷贝,并将拷贝入栈;
- 然后记录函数返回时跳转的地址(即main函数中调用func的位置,对应寄存器eip的值);
- 栈底指针ebp入栈,然后将ebp指向esp,转到新函数的栈;
- 然后为新函数的栈变量预留一部分空间;
- 然后将ebx(基地址寄存器),esi(源索引寄存器),edi(目标索引寄存器)入栈。(这里ebx,esi,edi为何需要入栈,我现在还不太懂,似乎和回调函数有关?如有大神知道,还望指教)
函数调用结束后同样会做一些处理,过程如下:
- 首先将ebx、esi、edi出栈;
- 然后释放栈变量的空间;
- 然后将ebp出栈,赋值给ebp寄存器,从而回到main函数的调用栈;
- 将eip值出栈,返回main函数;
- 最后将参数值出栈
栈的变化总体如下图所示:
这样的过程也解释了,为什么函数内部的对参数的改变,当函数调用结束后,外部并不能看到,因为改变的是栈中参数的拷贝,而不是本来的值。比如上图中,在func函数中改变了参数x的值,只是改变了栈中的拷贝,当返回main函数的调用栈时,main函数看到的仍然是90,而不是5。
另外,所谓的出栈也只是改变了esp寄存器,栈中实际内存的数据并没有进行任何修改,因此在合适的时机(调用新函数前),其实仍然是能看到这些数据的,比如在func函数中将w的地址作为返回值返回,然后在main函数中马上查看对应地址的值,仍然是能看到345的。但是一旦调用了新函数,这个值就可能被覆盖掉。因此C++并不推荐返回栈变量的地址。
最后关于函数的返回值,如果小于8字节(32位系统则是4字节,主要取决于寄存器大小),会被存放在EAX寄存器中,然而如果是更大的数据,那么具体会被存放在哪里我不太了解,但是会将其地址存放在EAX寄存器中。
全局区
全局区存储全局变量和静态变量。
代码区
代码区存放运行代码。
常量区
我现在只了解,文字常量存储在该区域。比如:
const char *p = "hello world!";
其中,p指向的地址就在常量区,即“hello world!”这段文字常量即存储在文字常量区。
对于常量:
最后,对于常量,经验证,内置类型的常量,会被vs编译器直接转化为立即数进行使用。因此可以理解为实际存在了代码区。
但是一旦对其进行取地址,那么编译器会为其在栈中分配地址。然而在实际调用常量时,并不会访问该地址,仍然使用立即数。但通过指针访问的话,会访问该地址。因此会出现,通过指针强行修改常量的值后,指针明明指向常量的地址,但指针读到的数值与常量不一样的情况。
动态链接库
其实应该叫内存映射区,因为这里不仅有动态链接库,共享内存也会映射到这个区域。