1. 再说说程序的内存结构
程序的内存结构,也叫内存布局,也叫存储映像。
1.1 什么是内存结构?
前面已经介绍过,其实就是程序运行时在内存中存储结构。
不管是裸机还是基于OS虚拟内存运行的情况,内存布局基本都差不多,所以我们这里只介绍程序基于OS虚拟内存运行时的内存布局,这个内存布局很重要,希望大家理解并记住它,至于说为什么重要,后面的课程会体现出来。
1.2 指令 与 数据的存放
程序在内存中存储时,就存储两个东西,一是指令,二是数据。
1.2.1 指令
指令存储在代码段中的.init和.text节中。
.init节:放启动代码相关的指令
.text节:主要放我们自己所写程序的指令
1.2.2 数据
(1)数据的存储形态
有两种存储形态,
1)第一种:数据存储在常量空间中
数据存储在常量空间中时,由于常量空间只能读不能写,因此常量保存的数据将一直不变。
2)第二种:数据存储在变量空间中
由于变量空间的读写权限为可读/可写,所以数据存在变量中时,是可以被新的数据改写的。
(2)常量
常量空间要求是只读的,在程序的内存布局中,只有代码段是只读的,因此常量空间肯定只能在代码段中。
在代码段中有两个地方可以用于开辟常量空间,一个是在.text中,另一个是在.rodata中。
1)在.text中的情况
此时,直接作为指令的一部分,放在了.text中。
比如:a = a + 100;
编译后100直接作为指令的一部分存储在了.text中,由于.text只能读不能写,因此数据100就保存在了常量中。
2)在.rodata中的情况
比如:
int a = 100; printf("a = %d", a); char *p = "hello world";
字符串"a = %d"/"hello world"被保存在了.rodata中,由于.rodata是只读的,因此"a = %d"/"hello world的存储空间是一个常量,在一般情况下为了好称呼,我们直接将"a = %d"/"hello world”称为常量。
有关char *p = "hello world"这种情况,我们后面讲字符串时,还会详细的介绍到。
疑问:为什么不将这些字符串和指令一起直接保存在.text中
太长了,无法作为指令的一部分存储在.text中。
(2)变量
变量空间只能开辟于在数据段,因为只有数据段是可读可写的。
变量分为两种,一种是静态变量,另一种是动态变量。
· 静态变量:空间开辟于静态数据段(.data/.bss)
· 动态变量:空间开辟于动态数据段(堆/栈)
1) 静态变量
分两种情况,一个是在.data中,另一个是在.bss
· .data:初始化了的静态变量
· .bss:未初始化的静态变量
(a).data
其实.data中的内容在编译时就决定好了,加载程序时,只需要将“可执行目标文件”的.data的内容,拷贝到内存即可。
初始化了的静态变量分两种:初始化了的全局变量 和 初始化了的静态局部变量,它们都在.data中。
+ 初始化了的全局变量
int a = 100; int main(void) { ... }
+ 初始化了的静态局部变量
int main(void) { static int a = 100;//main函数的初始化了的静态局部变量 ... }
(b) .bss
同样两种情况:
+ 未初始化的全局变量
int a; int main(void) { ... }
+ 未初始化的静态局部变量
int main(void) { static int a; ... }
在“可执行目标文件”中,.bss并不存在,程序加载后才会开辟.bss的空间,然后再在.bss里面开辟未初始化静态变量的空间,并自动初始化为0。
这就是以前常说的,未初始化的全局变量和静态局部变量,会被自动初始化为0。
2) 动态变量
之所称为动态的,是因为变量空间并不是在编译时决定的,而是在程序运行时才有的。
动态变量分两种,一种是自动局部变量,另一种是手动开辟的变量。
· 自动局部变量:空间开辟在栈中
· 手动开辟的变量:空间开辟在堆中
(a)自动局部变量(开辟于栈中)
没有加static修饰的函数局部变量都是自动局部变量,我们这里将形参也归到自动局部变量里面。
为什么称为自动局部变量?
变量空间是函数运行时自动开辟,运行结束时自动释放的,所以把它称为“自动”的。
正是由于是自动开辟和释放的,因此栈也被称为自动存储区。
int fun(int a)//a形参 { int b = 100;//没有加static修饰的局部变量 int c; return a+b+c; }
a、b、c都是开辟于栈中。
疑问:编译得到可执行目标文件后,函数中的自动局部变量是一个什么样存在?
fun函数被编译后,自动局部变量的定义会变为操作栈的指令(压栈指令push、弹栈指令pop)
· 压栈
函数运行时会调用压栈指令,会将“栈顶指针”向低地址方向移动需要的字节数,挪出来的这个空间就是自动局部变量的空间。
- 如果有初始化的话,就将初始化值写到空间中。
- 如果没有初始化的话,空间中的内容就是别人之前使用后遗留的内容。
这就是我们以前常说的,如果函数的自动局部变量不初始化的话,就是一个随机值。
旁注:有关局部变量随机值问题。
随机值其实是一个隐患,因为如果程序在运算时使用了不确定的随机值的话,会给程序的功能带来不确定的影响。
比如程序控制的是机械设备的话,这种随机值是非常要不得的,如果程序进行控制运算时使用了随机值,设备的机械运动可能会出现无法预测的结果,这是非常危险的。
因此我们要求必须给自动局部变量赋一个明确的值,特别是指针变量更是如此,如果你不知道赋什么值,那最起码要赋一个NULL空指针,以防止出现因随机值所导致的野指针。
· 弹栈
调用函数中的弹栈指令时,栈顶指针会向高地址方向回退相应的字节数,这样就把变量的空间给释放出来了,释放时并不会对空间清0,因此这一次使用的值,就变成了下一次别人使用这个空间时的随机值。
(b)手动开辟的变量(开辟于堆中)
栈中空间是自动开辟和释放的,但是堆空间不是的,对于堆来说,需要程序员自己在程序手动的调malloc函数,按需从堆中分配空间,当不再需要该空间时,就调用free函数来释放。
因为堆空间是手动开辟和释放的,因此在堆也被称为手动存储区。
· malloc、free的使用举例
int main(void) { int *p = NULL; p = malloc(4); //在堆中开辟4字节的空间,然后将首字节地址给p,以便访问空间 if(p == NULL) { printf("malloc fail\n"); exit(-1);//开辟空间失败就结束进程 } bzero(p, 4); //将开辟的空间清零 *p = 100; //使用开辟的空间 free(p);//删除p所指向的4字节空间 }
堆空间也存在和栈一样的随机值问题,所以我们从堆中开辟出空间后,必须主动清零,否者会影响写入的数据。
比如程序中调用bzero(p, 4)的目的,就是将p所指向的4个字节空间清0。
· 疑问:如果没有调用free来释放开辟的堆空间的话,程序运行结束后,堆空间会释放吗?
当然,程序结束后,不仅堆空间,所占用所有的空间都会释放。
· 疑问:既然程序终止时会释放所有堆空间,那为什么还需要调用free函数来释放呢?
因为真实的程序,很多一旦运行起来后会长时间运行,甚至有些可能会永久运行,如果程序在运行的过程中每次malloc后都不free的话,程序的堆空间会越用越少,严重的话堆和栈的空间会顶到一起,相互篡改数据,最后导致程序死机,对于很多重要程序来说是绝不能死机的,因为死机所带来的经济损失可能是非常巨大的。
其实正常情况下,栈和堆之间的空间距离非常大,如果正常操作(开辟后及时释放)的话,它们之间是不可能会碰到一起的,但是如果堆空降只开辟不释放的话,这种情况就有可能发生。
为了保证堆空间不会越用越少,及时调用free是必不可少的。
· 如何理解空间被占用
空间被占用从而导致可用空间不足的情况,与现实生活中仓库很像。
就好比你们家仓库是挺大的,但是就是每次放完东西后从来不清理,垃圾越来越多,最后导致仓库的空间全被垃圾占用,此时虽然仓库还是那个仓库,但是可用空间确是越来越少,严重时根本没有空间可用。
1.2.2 有关堆变量和栈变量,再提两句
(1)栈变量
从栈中开辟变量时,为了避免随机值带来的问题,一定要初始化
1)基于OS虚拟内存运行时,随机值来源有两个
(a)最开始时来自于子进程以前栈的遗留
(b)新程序运行一段段时间后,随机值来自于自己使用后的的遗留
2)裸机运行时
没有OS虚拟内存,不存在父进程一说,上电运行时内存默认就是清零的,所以随机值来自于自己使用后的遗留。
(2)堆变量
1)基于OS虚拟内存运行时,随机值来源有两个
(a)最开始时来自于子进程以前堆的遗留
(b)来自于自己使用后的遗留
2)裸机运行时
没有OS虚拟内存时,不存在父进程一说,所以随机值也是来自于自己使用后的遗留
3)malloc和free函数对堆内存的管理
(a)malloc从堆中开辟的变量空间往往大于实际想要的空间
malloc为了能够更好的管理堆内存,开辟空间时,并不是你想开辟多少,就刚好开辟多少,而是会做圆整处理,比如我想申请212字节,最终malloc实际开辟时会“圆整”为256。
这就好比你去开房,为了好管理房间,人家肯定是一间一间来管理的,你去跟前台说我开半间房,前台肯定会给“圆整”为整间房,凡是大块的内存管理都是这样的,都是按“块”来管理的,目的是为了提高管理效率。
疑问:圆整规则是怎样的?
这是malloc内部的算法机理,这个我们无需关心。
(b)如果忘了free,实际上导致的堆内存泄漏,要大于明面上所开辟的空间
这个道理很简单,我想开辟212字节,但是malloc实际上开辟了256字节,所以如果忘了free,实际泄露的堆内存空间为256,大于明面上的212字节。
有关malloc、free是如何管理堆内存的,这个属于malloc和free这两个函数的内部算法,我们作为应用程序开发者,其实无需关心,既然讲到这里了,我们就多说两句,作为了解即可。