C/C++语言在内存中一共分为如下几个区域,分别是:
内存栈区: 编译期间就能确定存储大小,运行时自动分配释放。存放函数的参数值、返回地址、局部变量的值等。在函数作用域内创建,在离开作用域后自动销毁。.esp 始终指向栈顶, 栈中的数据越多, esp的值越小。其操作方式类似于数据结构中的栈。
存储空间是连续的,两个紧密挨着定义的局部变量,他们的存储空间也是紧挨着的。栈的大小是有限的,通常Visual C++编译器的默认栈的大小为1MB,所以不要定义int a[1000000]这样的超大数组。
内存堆区: 存放new或者malloc出来的对象,一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。动态分配得到的内存区域附带有分配信息, 所以你能够 free和delete它们。
存储空间是不连续的,一般由malloc(或new)函数来分配内存块,并且需要用free(delete)函数释放内存。C/C++不提供垃圾回收机制,因此需要对堆中的数据进行及时销毁,如果程序员没有释放掉,那么就会出现常说的内存泄漏问题。需要注意的是,两个紧挨着定义的指针变量,所指向的malloc出来的两块内存并不一定的是紧挨着的,所以会产生内存碎片。另外需要注意的一点是,堆的大小几乎不受限制,理论上每个程序最大可达4GB。
静态区:全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域(RW), 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(ZI)。程序结束后由系统释放
和“栈”一样,通常是用于那些在编译期间就能确定存储大小的变量的存储区,但它用于的是在整个程序运行期间都可见的全局变量和静态变量。
常量区: 存放局部变量或者全局变量的值,常量字符串就是放在这里的。 程序结束后由系统释放 (RO)
和“全局/静态存储区”一样,通常是用于那些在编译期间就能确定存储大小的常量的存储区,并且在程序运行期间,存储区内的常量是全局可见的。这是一块比较特殊的存储去,他们里面存放的是常量,不允许被修改。
代码区:存放函数体的二进制代码。 (RO)
栈 | 堆 | |
---|---|---|
存储内容 | 局部变量 | 变量 |
作用域 | 函数作用域、语句块作用域 | 函数作用域、语句块作用域 |
编译期间大小是否确定 | 是 | 否 |
大小 | 1MB | 4GB |
内存分配方式 | 地址由高向低减少 | 地址由低向高增加 |
内容是否可以修改 | 是 | 是 |
全局/静态存储区 | 常量存储区 | |
---|---|---|
存储内容 | 全局变量、静态变量 | 常量 |
编译期间大小是否确定 | 是 | 是 |
内容是否可以修改 | 是 | 否 |
扒了一张图,这张图中所示内存空间,地址由下往上增长,分别标示了 .text、.data、.bss、stack和heap的内存分部情况。 我们可以看到:
text、data(gvar)、bss 在内存中地址较低低的位置(low level address),而堆栈则在相对较高的位置。
堆(Heap)往高地址方向生长,栈(Stack)往低地址方向生长。
知道如上一些内存分配机制,有助于我们理解指针的概念。
一个程序被加载到内存中,这块内存首先就存在两种属性:静态分配内存和动态分配内存。
静态分配内存:是在程序编译和链接时就确定好的内存。
动态分配内存:是在程序加载、调入、执行的时候分配/回收的内存。
Text & Data & Bss
.text: 也称为代码段(Code),用来存放程序执行代码,同时也可能会包含一些常量(如一些字符串常量等)。该段内存为静态分配,只读(某些架构可能允许修改)。
这块内存是共享的,当有多个相同进程(Process)存在时,共用同一个text段。
.data: 也有的地方叫GVAR(global value),用来存放程序中已经初始化的非零全局变量。静态分配。
data又可分为读写(RW)区域和只读(RO)区域。
-> RO段保存常量所以也被称为.constdata
-> RW段则是普通非常全局变量,静态变量就在其中
.bss: 存放程序中为初始化的和零值全局变量。静态分配,在程序开始时通常会被清零。
text和data段都在可执行文件中,由系统从可执行文件中加载;而bss段不在可执行文件中,由系统初始化。
这三段内存就组成了我们编写的程序的本体,但是一个程序运行起来,还需要更多的数据和数据间的交互,否则这个程序就是死的,无用的。所以我们还需要为更多的数据和数据交互提供一块内存——堆栈。
重点说下 堆(Heap)、栈(Stack):
1) 堆(Stack)向高内存地址生长,其大小由系统内存/虚拟内存上限决定,速度较慢,但自由性大,可用空间大。
2) 栈(Stack)向低内存地址生长,其最大大小由编译时确定,速度快,但自由性差,最大空间不大。
3) 堆和栈相向而生,堆和栈之间有个临界点,称为stkbrk。
4) 堆和栈都是动态分配内存,两者空间大小都是可变的。每个线程都会有自己的栈,但是堆空间是共用的。
Tips:
char* p = new char[20];
// 这行代码在Heap中开辟了20个char长度的空间,同时在Stack上压入了p,
// 指针变量p存在于栈上,其值为刚刚在堆上开辟的空间的首地址。
变量地址验证代码:
#include
#include
#include
int a = 0; //全局初始化区,保存在数据段
char *p1; //全局未初始化区,保存在BSS段
void main()
{
int b; // 栈
char s[] = "abc"; //"abc"在常量区,s在栈上。并将常量区的"abc\0"复制到该数组中。这个数组可以随意修改而不会有任何隐患,
//而"abc"这个字符串依然会保留在静态区中。
char *p2; //栈
char *p3 = "123456"; /* p3保存在栈上,"123456\0"保存在data区的read-only部分
注意:如果令p3[1] = '9'; 则程序崩溃,指针可以访问但不允许改变常量区的内容
声明了一个指针p3并指向"123456\0"在静态区中的地址,事实上,p3应该声明为
char const *,以免可以通过p3[i]='n'这一类的语法去修改这个字符串的内容。如果这样
做了,在支持“常量区”的系统中可能会导致异常,在“合并相同字符串”的编译方法下会导致其它
地方的字符串常量古怪地发生变化。*/
static int c =0; //全局(静态)初始化区,保存在数据段
p1 = (char *)malloc(10); //分配得来得10和20字节的区域就在堆区。
p2 = (char *)malloc(20); //p2的地址在栈上,p2的值(也是一个地址)是堆上一块空间的地址
strcpy(p1, "123456"); //"123456"放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
printf("&a \t &p1 \t &b \t &s \t&p2 \t &p3 \t &c \t &(\"abc\") \t&(\"123456\")\r\n");
printf("%p, %p, %p, %p, %p, %p, %p, %p, %p \r\n\r\n", &a, &p1, &b, &s, &p2, &p3, &c, &("abc"), &("123456"));
printf("p1 \t s \t p2 \t p3\r\n");
printf("%p, %p, %p, %p \r\n\r\n", p1, s, p2, p3);
}
#include
void Func(int a,int b)
{
int c=111;
int d=222;
printf("Func(): %p , &a : %p , &b : %p , &c : %p , &d : %p\n",Func,&a,&b,&c,&d);
}
void main()
{
int a = 123;
int b = 456;
printf("main(): %p , &a : %p , &b : %p\n",main,&a,&b);
Func(a,b);
}
运行结果:
函数参数,从右至左压入栈;
局部变量,从上至下压入栈。
当然由于调用了printf函数,所以会有printf函数的栈,栈继续向上增长,即往低地址方向增长。进入栈的依次有函数参数,c中约定是从右至左的方式进栈,然后是返回信息,返回地址等,最后是函数内定义的局部变量。
在本文结束前,我也还是来介绍下自己吧。我从03年初学编程,一直到现在都在做软件开发方面的事,做过VC++、VB/Delphi/C++Builder、C#/Asp.Net、Java、Web前后端、数据库、服务器端、客户端、js、lua、Flash AS、cocos2dx/Unity3d……都有接触过,甚至做过很长一段时间的运营、产品策划、美工、客服、线下推广员、公司经理 多职务……技术只是一种工具,用来实现业务需求,所以最大的特长是逻辑思维、挖掘业务需求、强化用户体验、快速的产品实现,最自以为豪的,我做过的很多产品,很稳定,bug极少(当然也出现过没考虑周全的漏洞在所难免但总体上也还算少),有很多独立开发的游戏运营到现在还在稳定的运行。不是技术极客,而是业务需求带动技术的学习研究,所以也不怎么挑语言,编程语言大体上也都是相通的,底层原理也有很多类同的地方。所以缺点从不敢说精通哪一门语言。遇到小工具、小需求的订制实现,我个人最喜欢的还是用VC6,简洁干净,编译出来的东西 体积小,速度快,占CPU内存小。当然实际工作中,还是随项目。欢迎大家和我交流,我的QQ是:七6.肆-陆_柒-4`7_四.
生活中的我,热爱 街舞、飙车、花式游泳、游戏一条命通关……极限运动,我抖音号(一不小心玩出花样)是:1917940952
如果函数参数有引用传递(地址传递):
#include
void Func(int &a,int b)
{
int c=111;
int d=222;
printf("Func(): %p , &a : %p , &b : %p , &c : %p , &d : %p\n",Func,&a,&b,&c,&d);
}
void main()
{
int a = 123;
int b = 456;
printf("main(): %p , &a : %p , &b : %p\n",main,&a,&b);
Func(a,b);
}
====================================================
假设现在有一个程序,它的函数调用顺序如下:
main(…) ->; func_1(…) ->; func_2(…) ->; func_3(…),即:主函数main调用函数func_1; 函数func_1调用函数func_2; 函数func_2调用函数func_3。
当一个程序被操作系统调入内存运行, 其对应的进程在内存中的映射如下图所示:
函数调用时所建立的栈帧包含下面的信息:
1) 函数的返回地址。返回地址是存放在主调函数的栈帧还是被调用函数的栈帧里,取决于不同系统的实现;
2) 主调函数的栈帧信息, 即栈顶和栈底;
3) 为函数的局部变量分配的栈空间;
4) 为被调用函数的参数分配的空间取决于不同系统的实现。
注意:
代码段(.txt)
.txt段存放代码(如函数)与部分整数常量,.txt段的数据可以被执行
数据段(.data)
.data用于存放初始化过的全局变量。若全局变量值为0,为了优化编译器会将它放在.bss段中
bss段(.bss)
.bss段被用来存放那些没有初始化或者初始化为0的全局变量。bss段只占运行时的内存空间而不占文件空间。在程序运行的整个周期内,.bss段的数据一直存在
.data和.bss段的区别可以通过下面程序验证:
char global_arr[1024 * 1024]; //存放在.bss段
void main()
{
}
char global_arr[1024 * 1024] = {4}; //存放在.data段,占文件空间
void main()
{
}
这两种代码,release编译后,文件大小分别是:36KB、1.03MB
常量数据段(.rodata)
ro表read only,用于存放不可变修改的常量数据,一旦程序中对其修改将会出现段错误:
(1) 程序中的常量不一定就放在rodata中,有的立即数和指令编码放在.text中
(2) 对于字符串常量,若程序中存在重复的字符串,编译器会保证只存在一个
(3) rodata是在多个进程间共享的
(4) 有的嵌入式系统,rodata放在ROM(或者NOR FLASH)中,运行时直接读取无需加载至RAM( 哈佛和冯诺依曼,从STM32的const全局变量说起有所记录)
想要将数据放在.rodata只需要加上const属性修饰即可。
栈
栈是用于存放临时变量和函数调用的。栈也是一种先进后出的数据结构,函数的递归调用正得益于栈的存在。需注意存在栈的数据只在当前函数和子函数中有效,一旦函数返回数据将会被自动释放。
堆
堆的使用周期有使用者控制,程序中的内存泄漏多因程序员对堆的管理不当引起,需谨慎。
.comment段
它存放的是编译器版本等信息。除了.comment,还有.note、.hash等其他段,了解即可。