C程序内存构成
C程序运行之前,操作系统会为其分配内存,并将C程序的二进制代码、程序数据装载到内存,C程序在内存的存储结构如下图所示:

从图中可以看出,C程序内存结构由程序代码区、常量区、全局数据区、堆区和栈区构成,内存地址由低到高。
程序代码区存储C程序的二进制代码,程序从代码区的第1条指令开始执行。常量区存储程序中定义的常量数据。全局数据区存储程序中定义的全局变量和静态变量。堆区为程序在运行过程中动态分配的内存,申请的内存由程序使用和释放。栈区内存由编译器负责申请和释放,用于调用函数时存储函数内部的局部变量、传递的参数、返回值等数据。
案例解读C程序的内存结构
下面是一段C程序代码:
#include <stdio.h>
// 全局变量,存储在数据区
int global_var = 10;
// 函数定义,函数的机器指令存储在代码区
void modify_global() {
global_var = 20; // 修改数据区的内容
}
int main() {
// 局部变量,存储在栈区
int local_var = 5;
printf("Before modification: global_var = %d\n", global_var);
modify_global(); // 调用函数,执行代码区的指令
printf("After modification: global_var = %d\n", global_var);
return 0;
}
以上面的代码段为例,modify_global函数和main函数的机器指令(编译后二进制代码)存储到程序代码区,全局变量global_var存储在全局数据区,局部变量local_var存储在栈区。
当程序执行时,CPU会从代码区读取并执行存储在代码区的机器指令,根据机器指令从栈区读取局部变量local_var的值,当机器指令执行到调用函数时,会在栈区申请内存,将函数的参数、局部变量、返回值等数据压入到栈中,CPU开始执行函数内部的机器指令,当执行到modify_global函数指令时,会从全局数据区读取全局变量global_var,并将该变量的值修改为20。
程序代码区
程序代码区用于存储C程序的二进制代码(C源代码编译后的程序指令),该内存区域只能读取不能写入。程序在运行期间不能改写程序指令,因为修改程序指令可能会导致程序发生不可预测的行为或崩溃。
程序代码区的大小是固定的,其占用的内存区域基本没有变化,因为程序编译后其大小不会发生变化。
程序代码区存储的程序指令可以被其它程序调用。例如当多个进程运行相同的程序时,它们可能都使用相同的代码区,而不是每个进程都有自己的副本,这有助于节省内存。
下面是一段C程序代码:
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
当上述代码段被编译和链接成一个可执行文件时,main 函数和其他必要的函数(如 printf)的程序指令将被放置在代码区中。当程序运行时,这些指令会被CPU执行。
常量区
常量区用于存储程序中定义的常量,定义的常量可以是整型、浮点型、字符型或字符串等数据类型。存储在常量区的数据大小固定且不允许修改。例如,程序内定义的全局常量、使用const关键字修饰的变量、字符串常量等都存储在常量区。
在常量区存储的数据初始化后,在程序运行期间不能修改,其生命周期与程序的生命周期相同。
下面是一段C程序代码:
#include <stdio.h>
int main() {
const int a = 10; // a 是一个常量,存储在常量区
int *p = &a; // 定义一个指针p指向a
*p = 20; // 试图修改a的值,但这是不允许的
printf("a = %d\n", a); // 输出a的值,应该是10而不是20
return 0;
}
当上述代码段被编译和链接成一个可执行文件时,整型常量a被存储到常量区。程序运行后,程序试图通过指针p修改常量a的值,但C语言编译器会阻止这种操作,因为常量区的数据是不允许修改的。
注意:不同的C编译器可能会对C程序内存结构和常量区的实现有不同的细节。例如,有些编译器可能会将字符串常量(如"Hello, World!")直接嵌入到代码段中,而不是放在常量区。
全局数据区
全局数据区通常用于存储需要在整个程序生命周期中存在的数据,或者需要在多个函数之间共享的数据。
全局数据区用于存储程序中定义的全局变量和静态变量。全局数据区的数据在程序开始执行时就被初始化,并且在整个程序的执行过程中一直存在,直到程序结束;在整个程序运行过程中,任何程序指令都可以访问全局区的数据;若程序没有显示地初始化全局区的数据,数据会被自动初始化为零或空指针。
下面是一段C程序代码:
#include <stdio.h>
// 全局变量
int global_counter = 0;
void increment_counter() {
// 在函数内部访问全局变量
global_counter++;
}
int main() {
// 输出初始的全局变量值
printf("Initial global counter: %d\n", global_counter);
// 调用函数修改全局变量
increment_counter();
increment_counter();
// 输出修改后的全局变量值
printf("Updated global counter: %d\n", global_counter);
return 0;
}
当上述代码段被编译和链接成一个可执行文件时,全局变量global_counter被存储到全局数据区。global_counter在代码内都可以被访问或修改。
全局变量比较适用于数据在不同函数之间的共享,但不必要的过度使用,会导致代码难以理解和维护,而且全局变量可以在任何位置被修改,这可能导致程序出现不可预料的错误。
栈区
栈区主要用于存储函数内定义的局部变量和函数调用时的上下文信息。当函数被调用时,系统会为其在栈上分配一块内存,用于存储该函数的局部变量。同时,函数的参数也会通过栈传递。当函数执行完毕返回时,这些局部变量和参数所占用的栈内存会被自动释放。
栈区的特点:
自动分配和释放:栈区的内存分配和释放是自动进行的。当函数被调用时,其所需的局部变量空间会在栈上自动分配;当函数返回时,这些空间又会自动被释放。
后进先出(LIFO)结构:栈区的内存是按照后进先出(Last In, First Out)的原则进行管理的。这意味着最后一个进入栈的数据项将是第一个被取出的。
大小有限:栈区的大小通常是有限的,这意味着如果程序尝试使用超过栈容量的内存,可能会导致栈溢出(Stack Overflow)错误。
下面是一段C程序代码:
#include <stdio.h>
void foo(int a, int b) {
int c = a + b; // 局部变量c存储在栈区
printf("The sum is: %d\n", c);
}
int main() {
int x = 5; // 局部变量x存储在栈区
int y = 10; // 局部变量y存储在栈区
foo(x, y); // 调用函数foo,函数参数和局部变量通过栈传递和存储
return 0;
}
当上述代码段被编译和链接成一个可执行文件并执行后,main 函数中的局部变量 x 和 y,以及 foo 函数中的局部变量 c,都存储在栈区。当 foo 函数被调用时,它的参数 a 和 b 以及局部变量 c 都会在栈上分配内存,当 foo 函数执行完毕返回时,这些栈内存会被自动释放。
堆区
堆区是由程序在运行期间进行管理的内存区域,堆区内存空间的大小取决于操作系统和可用的物理内存。在32位系统上,堆区最大约为2GB的空间,这是由于32位系统使用32位指针,指针的最大值限制了其能够寻址的内存范围。在64位系统上,堆区的大小可以非常大,通常受系统内存总量的限制。
C语言提供了malloc()或calloc()函数从堆区中分配内存。malloc()函数接受一个表示字节数的参数,并返回一个指向分配的内存块的指针。如果内存分配成功,该指针将指向分配的内存块的首地址;如果分配失败,则返回NULL。calloc()函数与malloc()类似,但它还接受一个表示元素数量的参数,并将分配的内存初始化为零。
申请的堆内存使用完成后,程序需要及时释放这些内存,不然会耗尽堆内存。使用C语言提供的free()函数可以释放之前分配的内存块。在释放内存后,指针本身并不会自动置为NULL,需要程序员需要手动将其置为NULL,以防止悬挂指针(无法正常使用的指针)的出现。
堆区的特点:
动态分配:与栈区不同,堆区的内存分配是在程序运行时动态进行的。程序员可以在运行时根据需要申请任意大小的内存块,并在不再需要时释放这些内存。
非连续性:堆区中的内存块是分散的,不是连续的。每个内存块的大小和位置可能在程序运行期间发生变化。
手动管理:在C语言中,程序员需要显式地申请和释放堆区中的内存,主要通过malloc()、calloc()和free()等函数实现。
下面是一段申请堆内存的C程序代码:
int *ptr = (int *)malloc(sizeof(int) * 10);
if (ptr == NULL) {
// 内存分配失败
}
上述代码使用malloc函数在堆内存申请了sizeof(int) * 10个字节的内存空间,内存申请完成后,需要判断接收申请内存地址的指针是否为NULL。