目录
内存分区
运行之前
代码区
全局初始化数据区 、静态数据区 (data)
未初始化数据区(bss(Block Started by Symbol)区)
总结
运行之后
代码区 (text segment)
未初始化数据区(bss)
全局初始化数据区,静态数据区(data segment)
栈区(stack)
堆区(heap)
内存分区
运行之前
如果要执行一个C程序,那么第一步需要对这个程序进行编译。
1 | 预处理 | 宏定义展开,头文件展开,条件编译,这里不会检查语法 |
2 | 编译 | 检查语法,将预处理后的文件编译成汇编文件 |
3 | 汇编 | 将汇编文件生成目标文件(二进制) .o文件已生成 |
4 | 链接 | 将目标文件链接为可执行程序 二进制文件转换可执行文件 类似.ext |
当编译完成生成可执行文件之后,我们可以通过linux下的size买了查看一个可执行二进制文件基本情况:
通过上图可以得知,在没有运行程序前,也就是说,程序没有加载到内存前,可执行程序内部已分好3段信息,分别是 代码区(text) , 数据区(data) 和未初始化数据区(bss) 3个部分(可以把data和bss合起来叫做静态区,或者全局区)
以下是细分:
bss区域放未初始化的数据如: static int a; //未初始化数据。
static int a = 10 ;//这个时候数据放在数据区 data区。
代码区
存放CPU执行的机器质量。通常代码是可以共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码通常是只读的,使其只读的原因是防止程序意外的修改他的指令。另外,代码区还规划了局部变量的相关信息。
说白了,代码区,就是放代码的
以上重点
共享:比如我们创建了一个a.exe和a1.exe两个代码是一样然后,第一次点击a.exe ,第二次点击a1.ext其实运行的还是a.exe原因是代码一样,共享
只读:比如我们在开发一个游戏币,创建了游戏币和人名币两个变量,如果是可写的,那么吧游戏币写到人名币里面那这样就是大事故,所以设置成只读。
全局初始化数据区 、静态数据区 (data)
该区包含了在程序中明确被初始化的全局变量,已经初始化的静态变量(包括全局静态变量)和常量数据(字符串常量)
未初始化数据区(bss(Block Started by Symbol)区)
存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化未0后者空(NULL)
总结
程序源代码被编译之后主要分成两段: 程序指令(代码区) 和 程序数据(数据区) 。代指码段属于程序指令,而数据域段和bss段属于程序数据
为什么要分开?
程序被加载到内存中之后,可以将数据和代码分别映射到两个内存区域。由于数据区域对进程来说是可读可写的,而指令域对程序来说是只读的,所以区分之后,可以将程序指令区域和数据区域分别设置成可读可写或者只读。这样可以防止程序的有意或者无意被修改
当程序中运行着多个同样的程序的时候,这些程序执行的指令都是一样的,所以只需要内中保存一份程序的指令就可以了,只是每一个程序运行中数据不一样而已,这样可以节省大量的内存。
运行之后
程序在加载到内存前,代码区和全局区(data 和 bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,操作系统吧物理硬盘程序,加载到内存,除了根据可执行程序的信息分出代码区(text) , 数据区(data) 和未初始化数据区(bss)之外,还额外增加了栈区,堆区
代码区 (text segment)
加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的
案例:
int main() {
int a = 1; // 这一行对应的机器指令就存储在代码区
return 0;
}
未初始化数据区(bss)
加载的是可执行文件bss段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生命周期未整个程序运行过程。
案例
int a; // 存储在BSS段,默认值为0
static int i; // 局部静态变量,默认值也为0,存储在BSS段
全局初始化数据区,静态数据区(data segment)
加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,字符常量(只读))的数据的生存周期为整个程序运行过程。
案例
int a= 10; // 存储在数据段
static int i= 20; // 局部静态变量,同样存储在数据段
栈区(stack)
栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值,返回值,局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
栈的空间是有限的,尽量用完就释放掉
1是第一个进入。
如果1想出来,那要吧4先扔掉,3在扔掉,2在扔掉,才是1
可以认为吃米饭一样,先吃上面的,才能见碗底。
堆区(heap)
堆是一个大容器,它的容量要远远大于栈,但没有栈那样的先进后出的顺序。用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序不释放,程序结束时由操作系统回收
大容量:大容量,到底有多大,要看机器有多好,看机器
分配:使用malloc函数分配
释放:使用free函数释放 ,如果不释放程序会在系统结束后回收 注意,一定要手动释放
生命周期
类型 | 作用域 | 生命周期 | 存储位置 |
auto变量 | 一对{}内 | 当前函数 | 栈区 |
static局部变量 | 一对{}内 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern变量 | 整个程序 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
static全局变量 | 当前文件 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
extern函数 | 整个程序 | 整个程序运行期 | 代码区 |
static函数 | 当前文件 | 整个程序运行期 | 代码区 |
register变量 | 一对{}内 | 当前函数 | 运行时存储在CPU寄存器 |
字符串常量 | 当前文件 | 整个程序运行期 | data段 |
栈 注意事项
案例1
int* func() {
int a = 10;
return &a;
}
void test01()
{
int* p = func();
printf("p = %d\n",p);
}
运行结果:
从上面结果来看,不是我们预期的结果,我们预期结果是 p = 10
为什么是这样?
首先我们来看func函数,函数定义的是int a = 10, 函数最终返回了a的地址,所以a在栈区的值已经释放了,我们没有去操作这一块内存。
案例2
char * getMyName()
{
char myName[] = "达帮主";
return &myName;
}
void test02()
{
char* p = getMyName();
printf("my name p = %s\n",p);
}
运行结果:
问题与案例1一样,也是释放了,不要在意结果。
栈的释放过程
从上面图中可以看出,当getMyName方法运行完成之后,常量区的内容是会被释放的,放回p收到的只是地址。所以上面案例2是乱码,内容被释放,我们根本不知道是上面东西。
总结
不要返回局部变量地址,局部变量在函数执行之后就释放了,我们没有权限去操作释放后的内存。
堆 注意事项
案例1
int* getSpace() {
//手动分配堆空间
int *p = malloc(sizeof(int)*5);
if (p == NULL) {
return 0;
}
for (int i = 0; i < 5; i++) {
p[i] = 1000 + i;
}
return p;
}
void test01() {
int* p = getSpace();
for (int i = 0; i < 5; i++)
{
printf("p:%d \n",p[i]);
}
//手动释放堆空间
free(p);
p = NULL; //防止野指针
}
int main()
{
test01();
printf("\n\n");
system("pause");
return EXIT_SUCCESS;
}
运行结果:
从上面代码来看我们使用了malloc来分配空间,分配的内存是存在堆中,所以数据没释放是一直存在的。
案例2
void getMyName(char *pp)
{
//分配内存
char * temp = malloc(sizeof(char)*50);
if (temp == NULL) {
return;
}
memset(temp,0,50);
//赋值
strcpy_s(temp,50,"达帮主");
pp = temp;
}
void test02()
{
char* p = NULL;
getMyName(p);
printf("%s\n",p);
}
运行结果
上面的原因是因为同级指针通过函数参数是无法修饰到p的,所以我们要在函数参数写二级指针。
如果主调函数中没有给指针分配内存,被调函数用同级指针是修饰不到主调函数中的指针的。
看下面案例
void getMyName(char **pp)
{
//分配内存
char * temp = malloc(sizeof(char)*50);
if (temp == NULL) {
return;
}
memset(temp,0,50);
//赋值
strcpy_s(temp,50,"达帮主");
*pp = temp;
}
void test02()
{
char* p = NULL;
getMyName(&p);
printf("%s\n",p);
}
运行结果:
上下的区别是加入二级指针,以及传的是地址,最后吧分配的内存修饰给二级指针
流程图
总结
在理解C内存分区时,常会碰到术语:数据区,堆,栈,静态区,常量区,全局区,字符串常量区,文字常量区,代码区等等。在这里,尝试捋清楚以上分区的关系。
- 数据区包括:堆,栈,全局/静态存储区。
- 全局/静态存储区包括:常量区,全局区、静态区。
- 常量区包括:字符串常量区、常变量区。
- 代码区:存放程序编译后的二进制代码,不可寻址区。
可以说,C/C++内存分区其实只有两个,即代码区和数据区。