任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究内存布局,逐个了解不同内存区域的特性。每个 C 语言进程都拥有一片结构相同的虚拟内存,不同的程序会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。将其中一个 C 语言进程的虚拟内存放大来看,会发现其内部包含区域:
- 栈(stack)
- 堆(heap)
- 数据段
- 代码段
1.1 栈内存
栈,是现代计算机程序极为重要的组成部分,几乎所有的函数调用都依赖于栈进行实现。
栈空间中包括:
- 函数返回地址:记录了函数执行完成后,退出函数调用时需要执行的下一条指令
- 命令行参数 ./a.exe abc 123
- 局部变量:(包括形参)函数调用过程中使用的非静态局部变量均默认保存在栈上,函数退出后,这些变量也会被释放而不可使用
- 保存的寄存器(环境变量等):保存了函数调用前后需要保持不变的寄存器
在程序运行过程中,每次执行函数调用,都会建立一个栈帧,连续的过程调用会让栈不断朝着“栈顶”的方向进行扩展;在函数调用完成后,栈帧会被自动释放,栈也会随之缩减。在大多数体系结构上,栈的扩展方向都是由高地址向低地址进行扩展。
栈内存有什么特点?
- 空间有限,尤其在嵌入式环境下。因此不可以用来存储尺寸太大的变量。Linux 栈内存大小为 8M
- 系统为函数分配栈内存时,遵循从上(高地址)往下(低地址)分配原则,由系统规定的,我们无法干预
- 每当一个函数被调用时,系统将自动分配一段栈内存给该函数,用于存放其局部变量,每当一个函数有退出时,系统将自动回收其栈内存
- 栈空间申请的变量随着函数结束,空间自动释放
在函数的作用域中定义局部变量是保存在栈中,在函数结束时系统会自动回收此函数中创建的栈空间,即 return 之后不可再访问函数的局部变量,因此返回一个局部变量的地址通常是错误的。
# include <stdio.h> int *add(int a, int b) { int sum; sum = a + b; return ∑ //不能返回函数的局部变量地址 } int main(void) { int *p = add(2,5); }1.2 堆内存
堆内存(heap)又被称为动态内存、自由内存,简称堆。堆是唯一可被开发者自定义的区段,开发者可以根据需要申请内存的大小、决定使用的时间长短等。但又由于这是一块系统“飞地”,所有的细节均由开发者自己把握,系统不对此做任何干预,给予开发者绝对的“自由”,但也正因如此,对开发者的内存管理提出了很高的要求。对堆内存的合理使用,几乎是软件开发中的一个永恒的话题。
例如 malloc 它分配的内存来自进程的虚拟内存空间。虚拟内存是操作系统提供的一种内存管理机制,它允许程序使用比物理内存更多的内存
堆内存基本特征:
- 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。
- 相比栈内存,堆内存从下往上增长。
- 堆内存是匿名的,只能由指针来访问。
- 自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。
堆内存使用完成后,一定要进行手动释放内存,否则可能会使内存泄漏导致程序崩溃
相关API:
- 申请堆内存:malloc() / calloc()
功能 | 申请堆内存 | |
所需头文件 | #include <stdlib.h> | |
函数原型 | void *malloc(size_t size); void *calloc(size_t count, size_tsize); | |
参数 | size | 所申请的一块堆内存的大小,单位是字节 |
count | 所申请的堆内存的块数,所有的内存块是连续分布的、无间隔的 | |
返回值 | 成功 | 返回所申请的内存空间的指针 |
失败 | NULL | |
#include <stdio.h> #include <stdlib.h> int main() { // malloc: 申请一块内存,内容是随机的 int *p1 = (int *)malloc(5 * sizeof(int)); // calloc: 申请 5 块内存,申请的内存会自动清零 int *p2 = (int *)calloc(5, sizeof(int)); free(p1); free(p2); return 0; }- 清零堆内存:bzero()
功能 | 清零指定内存 | |
所需头文件 | #include <strings.h> | |
函数原型 | void bzero(void *s, size_t n); | |
参数 | s | 指向要清零的内存的指针 |
n | 要清零的内存大小,单位是字节 | |
返回值 | 无 | |
#include <stdio.h> #include <stdlib.h> #include <strings.h> int main() { int arr[5] = {1, 2, 3, 4, 5}; bzero(arr, sizeof(arr)); // 整块清零 printf("bzero后 arr[0] = %d, arr[4] = %d\n", arr[0], arr[4]); return 0; }- 释放堆内存:free()
功能 | 释放堆内存 | |
所需头文件 | #include <stdlib.h> | |
函数原型 | voidfree(void *ptr); | |
参数 | ptr | 堆内存指针 |
返回值 | 无 | |
#include <stdio.h> #include <stdlib.h> int main() { int *p = (int *)malloc(3 * sizeof(int)); p[0] = 10; p[1] = 20; p[2] = 30; printf("使用前: p[1] = %d\n", p[1]); free(p); // 释放堆内存 p = NULL; // 置空防止悬空指针 printf("已释放,p = NULL\n"); return 0; }- realloc()申请的堆内存,在原来内存基础上可进行扩容。
// 申请堆空间 char *p = calloc(1, sizeof(char)); if (p == NULL) { printf("calloc failed:"); return -1; } char buf[] = "afjoiajfajflajlfjalflafjslfjsljfls"; // 扩容,如果不扩容会溢出 char *ptr = realloc(p, 100); // 是将空间大小从 1 扩容到 100(不是扩容了 100)注意:
- malloc()申请的堆内存,默认情况下是随机值,一般需要用 bzero() 来清零。
- calloc()申请的堆内存,默认情况下是已经清零了的,不需要再清零。
- free()只能释放堆内存,不能释放别的区段的内存。
释放内存的含义:
- 释放内存意味着将内存的使用权归还给系统。
- 释放内存并不会改变指针的指向。
- 释放内存并不会对内存做任何修改,更不会将内存清零。
内存泄漏示例
以下是一个典型的内存泄漏代码:在函数中申请堆内存后忘记释放,导致每次调用都泄漏内存。
#include <stdio.h> #include <stdlib.h> void leak_memory() { int *p = (int *)malloc(100 * sizeof(int)); // 忘记调用 free(p),函数返回后指针 p 丢失,内存却未归还系统 } int main() { for (int i = 0; i < 100000; i++) { leak_memory(); // 每次调用泄漏 400 字节,累计约 40MB } printf("程序结束\n"); return 0; }后果:随着循环不断调用leak_memory(),堆内存持续增长而永不释放。在长时间运行的程序(如服务器进程)中,内存泄漏会逐渐耗尽可用内存,最终导致系统 OOM(Out of Memory)或程序崩溃。即使程序结束,泄漏的内存在程序运行期间也无法被其他进程使用。
如何避免:
- 配对使用 malloc 和 free:每次调用
malloc后,确保在合适的时机调用free。 - 使用内存检测工具:如 Valgrind(Linux)或 Dr. Memory(Windows)检测内存泄漏。
- 遵循"谁申请,谁释放"原则:明确内存的所有权和释放责任,避免遗漏。
- 在函数退出前统一释放:用
goto或if分支确保所有路径都能释放已申请的内存。
1.3 堆与栈的区别
1、 管理方式不同:
- 栈:由系统自动分配空间,同时系统自动释放空间。
- 堆:需要程序员手动申请并且手动释放,并指明大小。在 C 语言中 malloc 函数申请,释放 free 函数,在 C++ 中 new 和 delete 实现。
2、 存储内容不同:
- 栈:在函数调用时,栈中存放的是函数中各个参数(局部变量)
- 堆:一般是在堆的头部用一个字节存放堆的大小(不是必须的)。堆中的具体内容由程序员安排。
3、 空间大小不同:
- 栈:获取空间较小。
- 堆:比较灵活,比较大。
4、 能否产生碎片不同:
- 栈:不会产生碎片,空间连续。
- 堆:采用的是链表的存储方式,会产生碎片。
5、 分配方式不同:
- 栈:静态分配,由编译器完成
- 堆:动态分配的,使用malloc函数
6、 分配效率不同
- 栈:由系统自动分配,速度较快。但程序员是无法控制的。
- 堆:由malloc分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来方便。
7、 生长方向不同
- 栈:向低地址扩展的数据结构,是一块连续的内存的区域。
- 堆:向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。