C 程序存储和运行时的几个区域

C 语言中有四个存储区

  • 栈区
  • 堆区
  • 数据区
    • 全局区(静态区)
    • 常量区(字面量区)
  • 代码区

我在网上找到了很多不同的版本,各有各的说法。最后,我觉得研究这个意义不大,因为不同的编译器有着不同的行为。但是,我们可以看到一些共通的地方。

  1. 栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。const 修饰的局部变量也是放在栈里的,而不是放在常量区!

  2. 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由系统回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

  3. 数据区:包括 静态全局区 常量区因此其实可以分为五区,如果要站在汇编角度细分的话还可以分为很多小的区。

    • 全局区(静态区,static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(bss 段,Block Started by Symbol)。程序结束后由系统释放。

    • 常量区(注意是 字面量 literal 的意思,而不是 const 的意思):常量字符串就是放在这里的,只有可读属性,比如 char *s = "hello world",这时候 "hello world" 就在常量区。由于没有可写属性,所以修改内容会出错。另外,全局的 const 变量也放在常量区里,这和 C++ 程序设计语言里对 const 变量存放位置是不符合的,因为存储器各有各的差异。程序结束后由系统释放。【最后有代码示例】

    其实可以把 堆栈区 统称为 动态区全局区 也可以称为 静态区,或者统称为 全局静态区

  4. 代码区:存放函数体的二进制代码。

存储时&运行时的几个区域

栈和堆的「生长」方式

堆和栈

再谈 bss 段

bss 是(Block Started by Symbol)的缩写,是专门用来存储未初始化的全局变量和未初始化的静态变量的一块内存区域。有人便会有疑问,既然有专门存放数据的数据段(data),那么 bss 段的存在意义是什么?

这是因为未初始化的全局或静态变量因加载程序时未知其实际的值,程序其实不必为其分配内存空间。而且,bss 段可被读写,所以其实并不需要像 data 段一样,编译成目标文件后马上为其分配空间。利用 bss 段这种做法可以优化文件大小,无需分配过多的空间。

bss 段在目标文件中是不占空间的,只有大小信息,在加载程序时,为 bss 段分配空间。如果是有初始值的全局变量,那么一般是在 data 段中,该段的内容在目标文件中存在且有值的信息,加载程序的时候,加载器分配 data 段的空间,并把目标文件中的 data 段内容复制到内存中。

但还是建议手动初始化,原因请参考编译流程中的符号表一节。

bss 段示意图

局部变量在内存栈中的存储方式

C 语言局部变量在内存栈中的顺序
局部变量内存分配详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int main()
{
#if 0 // 栈是从高地址到低地址发展
int a; // 0x7ffeec1c75c8 4 bytes(ca ~ c8)
char b; // 0x7ffeec1c75c7 1 byte
int c; // 0x7ffeec1c75c0 7 bytes (这里补了3个字节)
char d; // 0x7ffeec1c75bf 1 byte(低地址)

a = 1;
b = '2';
c = 3;
d = 4;

printf("a = %p \nb = %p \nc = %p \nd = %p\n ", &a, &b, &c, &d);
#else

char b; // 0x7ffeed77e5cb 1 byte
int a; // 0x7ffeed77e5c4 7 bytes(偏移1,不是4的倍数,补了3个字节,3+4 = 7,符合内存对齐原则)
int c; // 0x7ffeed77e5c0 4 bytes
int d; // 0x7ffeed77e5bc 4 bytes

b = 2;
a = 1;
c = 3;
d = 4;
printf("a = %p \nb = %p \nc = %p \nd = %p\n ", &a, &b, &c, &d);

#endif

return 0;
}

我在网上还找到其它分配规则,但至少我的电脑不是这样的。

  • 规则 1:内存由低到高优先分配给占位 8 字节、4 字节、2 字节、1 字节的数据类型
    数据类型占位说明:

    • 8 字节:double、long long int
    • 4 字节:int、float、long int、unsigned int
    • 2 字节:short 、unsigned short
    • 1 字节:char 、unsigned char

    例如,分别定义下列变量,内存地址中由低到高分别为:
    double < int < short < char

  • 规则 2:同种占位的类型按定义变量的先后顺序内存地址会增加

  • 规则 3:在规则 2 前提下,定义数组不会和同种数据类型混占内存

这样解释了为什么有些编码规范中建议的相同结构的定义在一起,不仅是美观,而且节省内存。

局部变量在汇编代码的形式

参考:深入了解 C 语言(局部变量的定义)从汇编来看c语言之变量

汇编里面没有变量名的说法,是直接按顺序定位的。C 语言函数中的局部变量的空间一般都是放在堆栈里面。在进入函数前,通过 SUB SP, +XX 来为这些局部变量分配堆栈空间。然后,通过 BP 寄存器来对这些局部变量进行访问。函数结束时,MOV SP, BP 还原堆栈指针,局部变量随之而消失。最后以 POP BP 还原 BP,结束该函数。

值得注意的是,C 语言会自动为 C 函数中经常使用 int 类型变量设置成 resigter int。这样的局部变量就不是使用堆栈空间的了,而就是直接存放在 SI 寄存器。

关于 const 小插曲

全局和局部的 const 的存储位置不同,说明 const 其实只是一个标识,检查发生在编译的时候。

详细请看:const 的实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int c = 10;
const int d = 100;

int main()
{
const int a = 5;
int b = 31231;
static const int e = 50;

// 局部
printf("%x\n", &a); // e57e05e8
printf("%x\n", &b); // e57e05e4
// 静态 / 全局
printf("%x\n", &c); // a420018 静态区
printf("%x\n", &d); // a41ffac 常量区
printf("%x\n", &e); // a41ffb0 常量区

return 0;
}

参考

已在对应的标题下。