首页 /  技术专区  /  C 屏幕太窄?试试伸展一下吧 >

c程序的内存结构

1. 再说说程序的内存结构

程序的内存结构,也叫内存布局,也叫存储映像。

1.1 什么是内存结构?

前面已经介绍过,其实就是程序运行时在内存中存储结构。

不管是裸机还是基于OS虚拟内存运行的情况,内存布局基本都差不多,所以我们这里只介绍程序基于OS虚拟内存运行时的内存布局,这个内存布局很重要,希望大家理解并记住它,至于说为什么重要,后面的课程会体现出来。

image.png

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这两个函数的内部算法,我们作为应用程序开发者,其实无需关心,既然讲到这里了,我们就多说两句,作为了解即可。




0/200
图片验证码