一、程序内存五大分区(Linux 64 位系统)

程序运行时操作系统会划分 5 块独立内存区域,分区特性自动 / 手动管理区分明显:

  1. 内核区 操作系统专属空间,用户代码无权限读写。

  2. 栈区(stack)

    1. 存储内容:局部变量、函数形参、函数返回地址、函数调用现场保护数据

    2. 管理方式:操作系统自动分配释放,函数执行完毕自动回收

    3. 限制:默认大小 8M,空间很小,不适合存储大容量数据

    4. 特性:读写速度快,内存连续向下分配

  3. 堆区(heap)

    1. 存储内容:手动申请的动态内存(malloc/calloc/realloc开辟)

    2. 管理方式:程序员手动申请、手动 free 释放

    3. 特点:空间巨大,生命周期不受函数限制;忘记释放会造成内存泄漏

    4. 分配规则:内存向上分配,碎片化问题

  4. 数据区(全局静态区) 分为 3 个子段:

    1. .data段:已初始化的全局变量、static 静态变量

    2. .bss段:未初始化全局 /static 静态变量,程序加载时系统自动清 0

    3. 字符串常量区:存放"abc"这类双引号字符串,只读不可修改

  5. 文本区(代码段 .text) 存放编译后的二进制指令、常量代码,只读,防止程序被篡改。

栈区 vs 堆区 对比表

对比项

栈 stack

堆 heap

管理者

操作系统自动管理

程序员手动 malloc/free

生命周期

函数结束立即回收

不 free 则持续占用直到程序退出

空间大小

默认 8M,容量小

操作系统剩余内存,容量极大

读写速度

速度更快

速度慢于栈

内存碎片

几乎无碎片

频繁开辟释放容易产生碎片

适用场景

临时小变量、函数参数

大容量数组、跨函数长期使用数据

二、动态内存分配(堆区核心 API)

头文件必须引入:#include <stdlib.h>

1. malloc 开辟内存

void *malloc(size_t size);

  • 参数size:需要申请的字节总数

  • 返回值:成功返回堆空间首地址;失败返回NULL

  • 特点:申请的内存不会自动清零,内部是随机脏数据 示例:

// 申请4字节int空间 int *p = malloc(sizeof(int)); if(p == NULL) // 必须判空,防止开辟失败 { perror("malloc fail"); return -1; } *p = 100; free(p); // 使用完释放

2. calloc 批量开辟并清零

void *calloc(size_t nmemb, size_t size);

  • 参数 1nmemb:元素个数;参数 2size:单个元素字节大小

  • 功能:开辟nmemb * size字节堆内存,自动全部初始化为 0 示例:

// 开辟10个int数组空间,全部置0 int *arr = calloc(10, sizeof(int)); free(arr);

3. realloc 扩容 / 缩容已有堆内存

void *realloc(void *ptr, size_t size);

  • 参数ptr:已经通过 malloc/calloc 开辟的堆指针;size:新总字节数

  • 逻辑:

    • 原空间后方有足够空闲内存:直接向后扩容,返回原指针

    • 后方空间不足:开辟一块全新 size 空间,拷贝旧数据,释放旧空间,返回新地址

    • size 缩小:截断尾部多余空间,保留前面数据

  • 注意:不能直接ptr = realloc(ptr, new_size),开辟失败会覆盖原有指针导致丢失 标准写法:

int *tmp = realloc(arr, 20 * sizeof(int)); if(tmp != NULL) { arr = tmp; }

4. free 释放堆内存

void free(void *ptr);

  • 参数:malloc/calloc/realloc 返回的堆首地址

  • 规则:

    • 同一块堆内存只能 free 一次,重复 free 程序崩溃

    • free 后指针不会自动置空,变成野指针,建议手动ptr = NULL

    • 不能 free 栈、全局区、常量区地址,只能释放堆指针

三、动态内存常见问题

1. 内存泄漏

成因:堆空间使用完毕,没有调用 free 释放,程序运行期间持续占用内存。 触发场景:函数内 malloc,函数结束前没有 free;指针覆盖丢失堆地址。 规避:每一次 malloc/calloc/realloc,配套对应 free。

2. 野指针

两种情况:

  1. 指针未初始化,随机指向未知内存

  2. 堆内存 free 之后,没有将指针置空,继续解引用*p操作 解决方案:指针定义初始化为NULL;free 后立刻p = NULL

3. 越界访问

访问范围超过 malloc 申请的字节长度,修改堆相邻内存,造成程序崩溃、逻辑错乱。

4. 重复释放

对同一个堆地址多次调用 free,操作系统报双重释放错误,程序直接终止。

四、堆区使用典型案例

案例 1:动态 int 数组

#include <stdio.h>

#include <stdlib.h> int main(void)

{ int len = 5; int *p = malloc(len * sizeof(int));

if(p == NULL) { perror("malloc"); return -1; } // 赋值

for(int i = 0; i < len; i++) p[i] = i; // 扩容到10个元素

int *tmp = realloc(p, 10 * sizeof(int)); if(tmp != NULL) p = tmp; // 打印

for(int i = 0; i < 10; i++)

printf("%d ", p[i]); free(p);

p = NULL; // 置空防野指针

return 0; }

案例 2:动态结构体


#include <stdio.h>

#include <stdlib.h> #include <string.h>

struct Student { char name[32]; int age; int score; };

int main(void) { struct Student *stu = malloc(sizeof(struct Student));

if(stu == NULL) return -1; strcpy(stu->name, "ZhangSan");

stu->age = 18; stu->score = 95;

printf("%s %d %d\n", stu->name, stu->age, stu->score);

free(stu);

stu = NULL; return 0; }

五、补充:变量生命周期与存储类型关联

  1. 局部 auto 变量:存栈区,函数结束销毁,生命周期仅限当前代码块

  2. static 静态变量:存.data/.bss 数据区,程序运行全程存在,仅初始化一次

  3. 全局变量:存数据区,程序启动开辟、程序结束释放,全文件可访问

  4. 堆动态内存:手动 malloc 创建、free 销毁,生命周期完全由程序员控制

  5. register 寄存器变量:优先存 CPU 寄存器,无内存地址,无法取&;仅编译器建议,不强制

六、配套编译四步骤(内存分区前置知识点)

gcc 编译 C 程序分为 4 阶段,内存分区在链接阶段完成布局:

  1. 预处理 gcc -E main.c -o main.i:展开宏、头文件、条件编译

  2. 编译 gcc -S main.i -o main.s:C 代码转汇编指令

  3. 汇编 gcc -c main.s -o main.o:汇编转二进制目标文件

  4. 链接 gcc main.o -o a.out:整合库函数,划分内存五大分区,生成可执行文件 a.out 可执行文件存硬盘,运行时操作系统加载到内存各分区执行。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐