> 「C语言进程虚拟内存」:栈、堆、数据段、代码段各自职责、生命周期、典型API 。
一、虚拟内存总览:四个大区
任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究财经处内存布局,逐个了解不同内存区域的特性。
每个C语言进程都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是**所有的虚拟内存布局都是相同的,极大地方便内核管理不同的进程。例如三个完全不相干的进程p1、p2、p3,它们很显然会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。
- PM:Physical Memory,物理内存。
- VM:Virtual Memory,虚拟内存。

区段 | 生长方向 | 谁管理 | 存放内容 | 生命周期 |
---|---|---|---|---|
栈 | 高→低 | 系统自动 | 局部变量、形参、返回地址 | 函数调用~返回 |
堆 | 低→高 | 开发者 | 动态申请内存 | malloc~free |
数据段 | 固定 | 系统 | 全局变量、static 局部 | 程序开始~结束 |
代码段 | 只读 | 系统 | 机器指令、常量字符串 | 程序开始~结束 |
> 内核区与 0x0~0x08048000 为禁闭区,应用程序无法访问。
虚拟内存中各个区段的详细内容:

二、栈内存:自动分配与释放
① 存放内容
- 命令行参数
argc/argv
(在命令行中运行程序时,携带的参数) - 环境变量
envp
(用于指明一些默认的数据,比如用户名,工作路径,可执行文件的路径,库的路径..) - 局部变量(含数组、结构体)( 在函数体内部 { } 定义的所有变量都属于局部变量 )(包括形参)
- 函数返回地址、寄存器现场
② 特点
- 空间有限,默认 (kbytes, -s) 8192 = 8 MB(可
ulimit -s
查看) - 从高地址向低地址增长
- 每当一个函数被调用,栈就会向下增长一段(该区域内存从高到低分配),用以存储该函数的局部变量。
- 每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。
- 注意: 栈内存的分配和释放,都是由系统规定(自动化完成)的,我们无法干预。
③ 内存分配规则
栈内存是从高地址往低地址分配的
void func4(void) { int a4; printf("&a4:%p\n", &a4); }
void func3(void) { int a3; printf("&a3:%p\n", &a3); func4(); }
void func2(void) { int a2; printf("&a2:%p\n", &a2); func3(); }
void func1(void) { int a1; printf("&a1:%p\n", &a1); func2(); }
int main(void){int a; printf("&a:%p\n", &a); func1(); return 0;
}
运行结果
&a :0x7ffd'efbf'f96c
&a1:0x7ffd'efbf'f94c ↓ 地址递减 → **向下生长**
&a2:0x7ffd'efbf'f92c
&a3:0x7ffd'efbf'f90c
&a4:0x7ffd'efbf'f8ec

三、获取环境变量 & 命令行参数
① 环境变量
#include <stdio.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{char * name = getenv("LOGNAME");printf("当前用户名是:%s\n" , name );char * pwd = getenv("PWD");printf("当前工作路径是:%s\n" , pwd );char * homePath = getenv("HOME");printf("当前用户家目录是:%s\n" , homePath );return 0;
}
② 命令行参数
#include <stdio.h>// int argc 命令函中传递的参数数量
// char const *argv[] 数组用于存储命令行中的每一个参数的值(入口地址)
int main(int argc, char const *argv[])
{printf("命令行的参数数量argc:%d\n" , argc );for (int i = 0; i < argc ; i++){printf("argv[%d]:%s\n" , i , argv[i]);}return 0;
}运行时,通过命令进行传递并使用空白符进行分割:
$ ./a.out Hello 123 Even GZ2536 你好运行结果:
命令行的参数数量argc:6
argv[0]:./a.out
argv[1]:Hello
argv[2]:123
argv[3]:Even
argv[4]:GZ2536
argv[5]:你好
四、堆内存
堆内存(heap)又被称为动态内存、自由内存,简称堆。堆是唯一可被开发者自定义的区段,开发者可以根据需要申请内存的大小、决定使用的时间长短等。但又由于这是一块系统“飞地”,所有的细节均由开发者自己把握,系统不对此做任何干预,给予开发者绝对的“自由”,但也正因如此,对开发者的内存管理提出了很高的要求。对堆内存的合理使用,几乎是软件开发中的一个永恒的话题。
堆内存基本特征:
-
相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。
-
相比栈内存,堆内存从下往上增长。
-
堆内存是匿名的,只能由指针来访问(申请堆内存时系统会返回该内存的入口地址)。
-
自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出(系统会在程序运行结束时把整个虚拟内存进行销毁)。
-
因此堆内存也是造成内存泄露的一大灾区

① 基本 API 速查
函数 | 功能 | 是否清零 | 返回 |
---|---|---|---|
malloc(size) |
申请一块字节 | ❌ | void * |
calloc(n, size) |
申请 n 块 | ✅ | void * |
realloc(ptr, new_size) |
扩张/收缩 | ❌ | void * |
free(ptr) |
释放内存 | - | void |
规则:malloc/calloc/realloc 必须与 free 成对出现!
malloc 申请堆内存
void *malloc(size_t size);size --> 需要申请的内存区尺寸,以字节为单位
返回值:成功 返回申请到的入口地址失败 返回NULL
注意:该函数不会对申请到的内存进行初始化
calloc 申请堆内存(堆数组)
void *calloc(size_t nmemb, size_t size);nmemb --> N块内存size --> 每一块多大
返回值:成功 返回申请到的入口地址失败 返回NULL
注意:该函数会把申请到的内存进行清空
realloc 重新申请堆内存
- 注意:
- 该函数如果发生了内存扩张,那么就有可能需要把我们原本的数据进行搬运(拷贝)到新的内存空间中,原来的指针所执行的内存会被该函数释放掉,不能再访问。
- 该函数如果发生了内存缩小,那么新内存不包含的区域会被释放掉一部分,如果再次访问则造成非法访问。
- 因此该函数的返回值,建议都使用原来的指针进行接收。
void *realloc(void *_Nullable ptr, size_t size);ptr --> 目前已有的内存入口地址size --> 新的目标内存尺寸
返回值:成功 返回申请到的入口地址失败 返回NULL
reallocarray 重新申请堆内存(堆数组)
void *reallocarray(void *_Nullable ptr, size_t nmemb, size_t size);ptr --> 目前已有的内存入口地址nmemb --> 新内存需要N块内存size --> 新内存每一块多大
返回值:成功 返回申请到的入口地址失败 返回NULL
清零内存
#include <strings.h>void bzero(void s[.n], size_t n);
参数分析:s --> 需要清空的目标内存空间(内存必须连续)n --> 希望清空的字节数
返回值:无#include <string.h>void *memset(void s[.n], int c, size_t n);
参数分析:s --> 需要设置的目标内存空间(内存必须连续)c --> 需要设置的目标内存中的数据(实际上他是字符的ASCII默认是1字节)n --> 希望设置的字节数
返回值:返回指针指向第一个参数 s
释放堆内存
void free(void *_Nullable ptr);ptr --> 需要释放的堆内存的入口地址
- 注意:
malloc()
申请的堆内存,默认情况下是随机值,一般需要用bzero()
或memset()
来清零。calloc()
申请的堆内存,默认情况下是已经清零了的,不需要再清零。free()
只能释放堆内存,并且只能释放整块堆内存,不能释放别的区段(栈、数据段、代码段) 的内存或者释放一部分堆内存。free
函数只能对堆内存的入口地址进行操作
- 释放内存的含义:
- 释放内存意味着将内存的使用权归还给系统。
- 释放内存并不会改变指针的指向(因此指针直接变成了野指针,需要手动设置为
NULL
)。 - 释放内存并不会对内存做任何修改,更不会将内存清零。
②如何检测是否有内存泄露:Valgrind 一键扫描
1.安装一个内存泄露的检测工具
sudo apt install valgrind
2.使用该工具来运行程序代码
$ valgrind ./a.out
==5822== Memcheck, a memory error detector
==5822== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==5822== Using Valgrind-3.22.0 and LibVEX; rerun with -h for copyright info
==5822== Command: ./a.out
==5822==
==5822== error calling PR_SET_PTRACER, vgdb might block
==5822==
==5822== HEAP SUMMARY: // 着重关注此处!
==5822== in use at exit: 0 bytes in 0 blocks
==5822== total heap usage: 1 allocs, 1 frees, 100 bytes allocated
==5822==
==5822== All heap blocks were freed -- no leaks are possible
==5822==
==5822== For lists of detected and suppressed errors, rerun with: -s
==5822== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
五、静态数据
概念
静态指的是数据的生命周期是静态的,也就是不会随着程序于运行的过程产生或释放,他的生命周期于整个进程保持一致,也就是只要程序运行静态区的内存就会被分配,直到程序退出才能(由系统自动)释放。
C语言中,静态数据有两种:
- 全局变量:定义在函数体
{ }
外部的变量。 - 静态局部变量:定义在函数内部,且被
static
修饰的变量。
示例:
int a; // 全局变量,退出整个程序之前不会释放
void f(void)
{// 所有的静态数据的初始化语句只会被执行一次static int b = 1 ; // 静态局部变量,退出整个程序之前不会释放printf("%d\n", b);b++;
}int main(void)
{f();f(); // 重复调用函数 f(),会使静态局部变量 b 的值不断增大
}
为什么需要静态数据?
- 1.全局变量在默认的情况下,对所有文件可见,为某些需要在各个不同文件和函数间访问的数据提供操作上的方便。
- 2.当我们希望一个函数退出后依然能保留局部变量的值,以便于下次调用时还能用时,静态局部变量可帮助实现这样的功能。
注意1:
- 若定义时未初始化,则系统会将所有的静态数据自动初始化为0
- 静态数据初始化语句,只会执行一遍。
- 静态数据从程序开始运行时便已存在,直到程序退出时才释放。
注意2static
的作用:
static
修饰局部变量:使之由栈内存临时数据,变成了静态数据(存储区从栈转移到静态区【数段】)。static
修饰全局变量:使之由各文件可见的静态数据,变成了本文件可见的静态数据【拓展多文件编译】。static
修饰函数:使之由各文件可见的函数,变成了本文件可见的静态函数【拓展多文件编译】。
六、数据段与代码段
分段 | 段名 | 存放内容 | 读写属性 | 生命周期 |
---|---|---|---|---|
数据段 | .bss |
未初始化全局/静态,它们将被系统自动初始化为0(该区域的所有数据初始化时都是0) | RW | 程序开始~结束 |
数据段 | .data |
已初始化全局/静态 | RW | 程序开始~结束 |
数据段 | .rodata |
常量字符串、const 全局 | RO | 程序开始~结束 |
代码段 | .text |
机器指令(函数体)用户代码 | RO | 程序开始~结束 |
代码段 | .init |
系统初始化代码(由编译器自动生成并添加,用于初始化本程序所需的内存空间) | RO | 程序开始~结束 |

七、作用域
基本概念
C语言中,标识符(变量名、函数名等....)都有一定的可见范围(可访问范围),这些可见范围保证了标识符只能在一个有限的区域内使用,这个可见范围,被称为作用域(scope)。
软件开发中,尽量缩小标识符的作用域是一项基本原则,一个标识符的作用域超过它实际所需要的范围时,就会对整个软件的命名空间造成污染,导致一些不必要的名字冲突和误解。
函数声明作用域
-
概念: 在函数的声明式中定义的变量,其可见范围仅限于该声明式。
-
示例:
void func(int fileSize, char *fileName);
要点:
- 变量
fileSize
和fileName
只在函数声明式中可见。 - 变量
fileSize
和fileName
可以省略,但一般不这么做,它们的作用是对参数的注解,方便函数的使用者更直观地使用函数,没必要一定去查询手册。
局部作用域
- 概念: 在代码块内部定义的变量,其可见范围从其定义处开始,到代码块结束为止
}
。 - 示例:
int main()
{int a=1;int b=2; // 变量 c 的作用域是第4行到第9行{int c=4;int d=5; // 变量 d 的作用域是第7行到第8行int a = 100;}
}
要点:
- 代码块指的是一对花括号
{ }
括起来的区域。 - 代码块可以嵌套包含,外层的标识符会被内嵌的同名标识符临时掩盖变得暂时不可见。
- 代码块作用域的变量,由于其可见范围是局部的,因此被称为局部变量。
全局作用域
- 概念: 在代码块外定义的变量,其可见范围可以跨越多个文件。
- 示例:
// 文件:a.c
int global = 888; // 变量 global 的作用域是第2行到本文件结束
int main()
{
}
void f()
{
}
// 文件:b.c
extern int global; // 声明在 a.c 中定义的全局变量,使其在 b.c 中也可见
void f1()
{
}
void f2()
{
}
要点:
- 代码块之外定义的标识符,即处于任何
{}
之外。 - 整个翻译单元可见;可被
extern
扩展到其它源文件。 - 生命周期 = 进程开始 ~ 进程结束。
- 未显式初始化时系统自动清零(
.bss
),已初始化进.data
。 - 默认外部链接(全局可共享);加
static
后变内部链接,仅当前文件可见。
作用域的临时掩盖
如果有多个不同的作用域相互嵌套,那么小范围的作用域会临时 “遮蔽” 大范围的作用域中的同名标识符,被 “遮蔽” 的标识符不会消失,只是临时失去可见性。
- 示例代码:
int a = 100;// 函数代码块1
int main(void)
{printf("%d\n", a); // 输出100int a = 200;printf("%d\n", a); // 输出200// 代码块2 {printf("%d\n", a); // 输出200int a = 300;printf("%d\n", a); // 输出300}printf("%d\n", a); // 输出200
}void f()
{printf("%d\n", a); // 输出100
}
static
关键字
C语言的一大特色,是相同的关键字,在不同的场合下,具有不同的含义。
static
关键字在C语言中有两个不同的作用:
1.将可见范围设定为标识符所在的文件(缩小标识符的可见范围):
- 修饰全局变量:使得全局变量由原来的跨文件可见,变成仅限于本文件可见。
- 修饰普通函数:使得函数由原来的跨文件可见,变成仅限于本文件可见。
2.将存储区域设定为数据段:
- 修饰局部变量:使得局部变量由原来存储在栈内存,变成存储在数据段。
- 示例:
int a; // 普通全局变量,跨文件可见
static int b; // 静态全局变量,仅限本文件可见void f1() // 普通函数,跨文件可见
{}static void __f2() // 静态函数,仅限本文件可见
{}int main()
{int c; // 普通局部变量,存储于栈内存static int d; // 静态局部变量,存储于数据段
}
八、存储期
基本概念
C语言中,变量都是有一定的生存周期的,所谓生存周期指的是从分配到释放的时间间隔。为变量分配内存相当于变量的诞生,释放其内存相当于变量的死亡。从诞生到死亡就是一个变量的生命周期。
根据定义方式的不同,变量的生命周期有三种形式:
- 自动存储期
- 静态存储期
- 自定义存储期

自动存储期
在栈内存中分配的变量,统统拥有自动存储期,因此也都被称为自动变量。这里自动的含义,指的是这些变量的内存管理不需要开发者操心,都是全自动的:在变量定义处自动分配,出了变量的作用域后自动释放。
- 以下三个概念是等价的:
- 自动变量:从存储期的角度,描述变量的时间特性。
- 临时变量:同上。
- 局部变量:从作用域的角度,描述变量的空间特性。
可以统一把它们称为栈变量,下面是示例代码:
int main()
{int a, b; // 自动存储期static int c;f(a, b);
}void f(int x, int y) // 自动存储期
{
}
静态存储期
在数据段中分配的变量,统统拥有静态存储期,因此也都被称为静态变量。这里静态的含义,指的是这些变量的不会因为程序的运行而发生临时性的分配和释放,它们的生命周期是恒定的,跟整个程序一致。
静态变量包含:
- 全局变量:不管加不加
static
,任何全局变量都是静态变量。 static
型局部变量。
示例代码:
int g1; // 静态存储期
static int g2; // 静态存储期int main()
{int a, b;static int c; // 静态存储期
}
注意1:
- 若定义时未初始化,则系统会将所有的静态数据自动初始化为0
- 静态数据初始化语句,只会执行一遍。
- 静态数据从程序开始运行时便已存在,直到程序退出时才释放。
注意2:
static
修饰局部变量:使之由栈内存临时数据,变成了静态数据。static
修饰全局变量:使之由各文件可见的静态数据,变成了本文件可见的静态数据。
自定义存储期
在堆中分配的变量,统统拥有自定义存储期,也就是说这些变量的分配和释放,都是由开发者自己决定的。由于堆内存拥有高度自治权,因此堆是程序开发中用得最多的一片区域。

相关API:
- 申请堆内存:malloc() / calloc()
- 清零堆内存:bzero()
- 释放堆内存:free()