C语言内存管理:栈与堆的解析
2026/8/9 11:22:31 网站建设 项目流程

任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究内存布局,逐个了解不同内存区域的特性。每个 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 &sum; //不能返回函数的局部变量地址 } 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)检测内存泄漏。
  • 遵循"谁申请,谁释放"原则:明确内存的所有权和释放责任,避免遗漏。
  • 在函数退出前统一释放:用gotoif分支确保所有路径都能释放已申请的内存。

1.3 堆与栈的区别

1、 管理方式不同:

  • 栈:由系统自动分配空间,同时系统自动释放空间。
  • 堆:需要程序员手动申请并且手动释放,并指明大小。在 C 语言中 malloc 函数申请,释放 free 函数,在 C++ 中 new 和 delete 实现。

2、 存储内容不同:

  • 栈:在函数调用时,栈中存放的是函数中各个参数(局部变量)
  • 堆:一般是在堆的头部用一个字节存放堆的大小(不是必须的)。堆中的具体内容由程序员安排。

3、 空间大小不同:

  • 栈:获取空间较小。
  • 堆:比较灵活,比较大。

4、 能否产生碎片不同:

  • 栈:不会产生碎片,空间连续。
  • 堆:采用的是链表的存储方式,会产生碎片。

5、 分配方式不同:

  • 栈:静态分配,由编译器完成
  • 堆:动态分配的,使用malloc函数

6、 分配效率不同

  • 栈:由系统自动分配,速度较快。但程序员是无法控制的。
  • 堆:由malloc分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来方便。

7、 生长方向不同

  • 栈:向低地址扩展的数据结构,是一块连续的内存的区域。
  • 堆:向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询