目录
[(一)malloc 函数](#(一)malloc 函数)
[(二)free 函数](#(二)free 函数)
[(三)calloc 函数](#(三)calloc 函数)
[3、与 malloc 的详细对比](#3、与 malloc 的详细对比)
[(四)realloc 函数](#(四)realloc 函数)
[(一)对 NULL 指针的解引用操作](#(一)对 NULL 指针的解引用操作)
[(三)对非动态开辟内存使用 free 释放](#(三)对非动态开辟内存使用 free 释放)
[(四)使用 free 释放一块动态开辟内存的一部分](#(四)使用 free 释放一块动态开辟内存的一部分)
[(一)例题 1:传值调用导致空指针解引用](#(一)例题 1:传值调用导致空指针解引用)
[(二)例题 2:返回栈区地址导致野指针](#(二)例题 2:返回栈区地址导致野指针)
[(三)例题 3:释放后未置空导致非法访问](#(三)例题 3:释放后未置空导致非法访问)
[1、步骤 1:申请包含柔性数组的结构体内存](#1、步骤 1:申请包含柔性数组的结构体内存)
[2、步骤 2:访问柔性数组](#2、步骤 2:访问柔性数组)
[3、步骤 3:调整柔性数组大小](#3、步骤 3:调整柔性数组大小)
[4、步骤 4:释放内存](#4、步骤 4:释放内存)
[六、C/C++ 程序内存区域划分](#六、C/C++ 程序内存区域划分)
[3、数据段(静态区,data segment)](#3、数据段(静态区,data segment))
[4、代码段(text segment)](#4、代码段(text segment))
[1、栈区 vs 堆区核心区别](#1、栈区 vs 堆区核心区别)
[2、数据段 vs 代码段核心区别](#2、数据段 vs 代码段核心区别)
一、动态内存分配的必要性
(一)传统开辟内存的方式
在 C/C++ 编程中,传统内存开辟方式存在显著局限性,仅支持两种固定模式,无法满足灵活的内存需求:
1、单个固定大小变量
如int a,在 32 位系统中固定占用 4 字节,空间大小从定义时就确定,运行过程中无法改变。
2、固定长度数组
如 int arr[10],编译时就确定占用 40 字节(10 个int类型)。
若实际存储的数据量超出数组长度,会导致数据溢出;若数据量远小于数组长度,则造成内存空间浪费。
(二)传统方式的核心缺陷
1、空间大小不可变
数组一旦创建,其长度无法在运行时扩容或缩容。
例如,定义 int arr[10] 用于存储班级学生成绩,若班级实际有 20 名学生,数组空间不足;若仅 5 名学生,数组中 5 个整型空间(20 字节)就会被闲置浪费。
2、适配性差
无法根据程序运行时的实际数据量动态调整内存 ,在处理不确定数据量的场景(如用户输入数据、文件读取数据)时,灵活性严重不足。
所以C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。
(三)动态内存的核心优势
1、灵活性高
支持在程序运行过程中,根据实际需求按需申请、扩容或缩容内存空间,精准匹配数据存储需求,避免空间不足或浪费。
2、自主管理
程序员可通过专用函数主动控制内存的申请与释放,能够在不需要内存时及时回收,提高内存利用率,避免资源长期闲置。
二、动态内存管理相关函数
动态内存管理依赖 4 个核心函数,均声明在 <stdlib.h> 头文件中,需熟练掌握其参数 、返回值 、功能逻辑 及使用规范。
(一)malloc 函数
1、函数原型
void* malloc(size_t size);// memory allocation 内存分配
2、功能解析
(1)参数含义
size_t size 表示申请内存的总字节数。
例如,需申请 10 个int类型的连续空间,需计算总字节数为10 * sizeof(int)(32 位系统中为 40 字节),并将该值传入size参数。
(2)返回值逻辑
① 申请成功
返回指向所开辟内存空间起始地址的 void* 指针。由于 void* 是通用指针类型,无法直接用于访问特定类型数据,需强制转换为目标数据类型指针(如int*、char*)。
② 申请失败
返回 NULL。失败原因通常是系统内存不足,如申请的空间过大(如malloc(INT_MAX)),超出系统剩余可用内存。
3、详细使用步骤
(1)内存申请与类型转换 (★)
根据存储数据类型,将 malloc 返回的 void* 指针强制转换为对应类型指针,示例如下:
cpp
// 申请10个int类型的连续空间,总字节数为10 * sizeof(int)
int* p = (int*)malloc(10 * sizeof(int));
(2)申请结果校验
必须对返回值进行非空校验,避免后续对NULL指针操作导致程序崩溃,示例如下:
cpp
if (p == NULL) {
// 使用perror打印错误信息,便于定位问题
perror("malloc failed: ");
// 终止程序或进行其他错误处理
return 1;
}
(3)内存空间使用
通过指针算术运算或数组下标方式访问连续内存空间,两种方式等价,示例如下:
cpp
// 方式1:数组下标访问(更直观)
for (int i = 0; i < 10; i++) {
p[i] = i + 1; // 给第i个元素赋值为i+1
}
// 方式2:指针算术运算访问(本质逻辑)
for (int i = 0; i < 10; i++) {
*(p + i) = i + 1; // p+i指向第i个元素的地址,解引用后赋值
}
4、关键注意事项
(1)零字节申请
若size参数为 0,C 标准未定义具体行为,不同编译器处理方式不同(部分返回NULL,部分返回一个无效地址),无实际使用意义,不建议这样操作。
(2)内存区域
malloc申请的内存位于堆区,与**"栈区的局部变量"、"静态区的全局变量 / 静态变量"**存储位置完全不同,具体对比如下:
|--------------|--------------------|----------------------------------|---------------------------------|
| 内存区域 | 存储内容 | 示例 | 生命周期 |
| 栈区 | 局部变量、函数参数、局部数组 | ① int arr[10]; ② int x; | 函数执行结束后自动释放 |
| 堆区 | 动态内存 | ① malloc(40); | 手动 free 释放或程序结束后由操作系统回收 |
| 静态区 | 全局变量、static修饰的静态变量 | ① int g_val; ② static int s_val; | 程序运行期间始终存在,程序结束后释放 |
(3)大空间申请风险
当申请的空间过大(如malloc(INT_MAX),INT_MAX为 21 亿多),系统通常无法满足,malloc 会返回 NULL,必须通过非空校验规避风险。
(二)free 函数
1、函数原型
void free(void* ptr);
2、功能解析
(1)参数含义
void* ptr 必须是 malloc、calloc 或 realloc 函数 返回的动态内存起始地址,用于指定需要释放的内存空间。
(2)核心作用
将指定的动态内存空间归还给操作系统 ,释放后的内存可被系统重新分配给其他程序使用,避免内存泄漏。
3、详细使用规范
(1)成对使用原则
malloc 与 free必须成对使用,每一次 malloc 申请的内存,都必须通过一次 free 释放,从而避免内存的浪费与泄露,示例如下:
cpp
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
perror("malloc failed: ");
return 1;
}
// 内存使用逻辑...
// 释放内存,与malloc成对使用
free(p);
// 释放后将指针置空,避免野指针
p = NULL;
(2)禁止非法参数
若 ptr 指向非动态内存(如栈区的局部数组、局部变量,静态区的全局变量),调用 free 会触发未定义行为,导致程序崩溃,示例如下:
cpp
int arr[4]; // 栈区数组,非动态内存
int* p = arr;
free(p); // 错误:非法释放非动态内存,程序崩溃
若 ptr 为 NULL,free 函数无任何操作,不会报错,因此释放内存后将指针置空是安全的操作,同时可以避免野指针的产生。
4、野指针问题深度解析
(1)野指针定义
指针指向**已释放的内存空间或非法地址,此时指针仍保留原地址值,**但该地址对应的内存已不属于当前程序,继续使用会导致数据损坏或程序崩溃。
(2)野指针产生场景
free释放内存后,未将指针置空,后续误操作该指针(如解引用、赋值)。
(3)规避方案
free释放内存后,立即将指针赋值为NULL,示例如下:
cpp
free(p);
p = NULL; // 置空指针,避免后续误操作
(三)calloc 函数
C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。
1、函数原型
void* calloc(size_t num, size_t size);// contiguous allocation 连续分配 (分配 + 初始化)
2、功能解析
(1)参数含义
**① num:**表示申请内存的元素个数,即需要存储的数据个数。
**② size:**表示单个元素的字节数,如sizeof(int)(4 字节)、sizeof(char)(1 字节)。
(2)返回值逻辑
① 申请成功
返回指向连续内存空间起始地址的 void* 指针,且该内存空间的所有字节会被初始化为 0。
② 申请失败
返回 NULL,失败原因与 malloc 一致(如内存不足)。
3、与 malloc 的详细对比
|--------------|-----------------------------------------|--------------------------------------------|
| 对比维度 | malloc | calloc |
| 参数形式 | 需手动计算总字节数(num * size),参数为总字节数 | 分别传入元素个数(num)和单个元素字节数(size),无需手动计算总字节数 |
| 内存初始化 | 不进行初始化,内存空间内容为随机值 (如内存中的残留数据0xCDCDCDCD) | 自动将内存空间的所有字节初始化为 0,无需额外初始化操作 |
| 使用场景 | 无需初始化的场景 (如临时存储计算结果、中间变量) | 需要初始化为 0 的场景 (如统计数组、计数器、初始化链表节点) |
| 效率 | 无初始化操作,效率略高于calloc | 需执行初始化操作,效率略低于malloc |
4、详细使用示例
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main() {
// 申请10个int类型的连续空间,单个元素4字节,总字节数10*4=40字节,且初始化为0
int* p = (int*)calloc(10, sizeof(int));
// 申请结果校验
if (p == NULL) {
perror("calloc failed: ");
return 1;
}
// 直接使用初始化后的内存(所有元素初始为0)
for (int i = 0; i < 10; i++) {
printf("%d ", p[i]); // 输出:0 0 0 0 0 0 0 0 0 0
}
// 释放内存,与malloc申请的内存释放方式一致
free(p);
p = NULL;
return 0;
}

(四)realloc 函数
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
1、函数原型
void* realloc(void* ptr, size_t size); // reallocate memory 调整内存
2、功能解析
(1)参数含义
**① ptr:**指向需要调整大小的动态内存起始地址,必须是malloc、calloc或realloc返回的地址,不可传入任意地址(如指针移动后的地址)。
**② size:**调整后的内存总字节数,如将 10 个 int 空间扩容为 20 个,需传入 20 * sizeof(int) 。
(2)返回值逻辑
① 调整成功: 返回调整后内存空间的起始地址**(可能是原地址,也可能是新地址,取决于内存布局)**。
**② 调整失败:**返回NULL,此时原内存空间保持不变,仍可正常使用,需避免原地址丢失。
3、内存调整机制深度解析
realloc 根据原内存空间后方的可用内存情况,采用两种不同的调整策略,具体如下:
(1)原空间后方有足够连续内存
① 处理机制: 直接在原内存空间的末尾追加所需内存,无需移动原有数据
② 返回值: 原内存空间的起始地址
③ 数据安全性: 原有数据完全保留,无数据丢失风险
④ 效率: 高,仅需扩展空间,无需数据拷贝
(2)原空间后方无足够连续内存
**① 处理机制:**先在堆区寻找一块大小满足需求的新连续内存;再将原内存空间中的所有数据拷贝到新空间;再自动释放原内存空间,避免内存泄漏;最后返回新的地址。
② 返回值: 新内存空间的起始地址
③ 数据安全性: 原有数据通过拷贝保留,无数据丢失风险
④ 效率: 低,需拷贝数据并释放原空间
(3)调整失败(如内存不足)
① 处理机制: 不修改原内存空间,直接返回NULL
② 返回值: NULL
③ 数据安全性: 原数据完全保留,原内存空间仍有效

4、详细使用步骤与规范
(1)使用临时指针接收返回值
由于 realloc 可能返回 NULL,若直接用原指针接收,会导致原内存地址丢失(内存泄漏),因此必须用临时指针接收。
在使用 realloc 时,无论它实际采用哪种扩容机制(原地扩容还是迁移数据),我们都应该默认按 "原空间后方无足够连续内存" 来编写代码,这是最安全的实践原则。示例如下:
cpp
// 原内存:申请10个int空间(40字节)
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
perror("malloc failed: ");
return 1;
}
// 需求:将内存扩容为20个int空间(80字节)
// 步骤1:用临时指针接收realloc返回值
int* temp = (int*)realloc(p, 20 * sizeof(int));
// 步骤2:判断调整是否成功
if (temp != NULL) {
// 调整成功,更新原指针为新地址
// 此时两者指向同一块地址,所以不需要也不能释放temp
// 后续操作结束后释放 p 即可
p = temp;
} else {
// 调整失败,处理错误(如打印信息、释放原内存)
perror("realloc failed: ");
free(p); // 释放原内存,避免泄漏
p = NULL;
return 1;
}
(2)仅传入动态内存起始地址
ptr 参数必须是动态内存的起始地址,不可传入指针移动后的地址(如p++后的地址),否则realloc 无法识别需调整的内存范围,导致内存泄漏或程序崩溃,示例如下:
cpp
int* p = (int*)malloc(10 * sizeof(int));
p++; // 指针移动到第二个元素地址,不再是起始地址
// 错误:传入非起始地址,realloc无法正常工作
realloc(p, 20 * sizeof(int));
(3)缩小内存空间
realloc 也支持缩小内存,只需传入更小的size参数即可,此时 realloc 会直接截断原内存空间,多余部分被释放,示例如下:
cpp
// 原内存:20个int空间(80字节)
int* p = (int*)malloc(20 * sizeof(int));
// 缩小为5个int空间(20字节)
int* temp = (int*)realloc(p, 5 * sizeof(int));
if (temp != NULL) {
p = temp;
}
(五)四个函数使用的步骤总结
"申请 - 检查 - 使用 - 调整 - 释放"
**第一步:**开辟内存,并由具体指针接收(使用 malloc 或者 calloc)
**第二步:**判断是否为空指针(即是否开辟成功),同时保存该指针用于释放。
**第三步:**使用内存的操作
**第四步:**如果要修改内存,先创建临时指针,再判断是否成功,成功则 p = temp
**第五步:**使用结束后,则释放内存,指针再置为空。
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main() {
// 第一步:开辟内存(10个int)
int* p = (int*)malloc(10 * sizeof(int));
// 第二步:判断是否开辟成功
if (p == NULL) {
perror("malloc failed");
return 1;
}
// 第三步:使用内存(初始化前10个元素)
for (int i = 0; i < 10; i++) {
p[i] = i;
}
// 第四步:修改内存(尝试扩容到20个int)
int* temp = (int*)realloc(p, 20 * sizeof(int));
if (temp != NULL) {
p = temp; // 扩容成功,更新指针
// 扩容后可使用新空间(第11-20个元素)
for (int i = 10; i < 20; i++) {
p[i] = i;
}
}
else {
perror("realloc failed, continue with original memory");
// 扩容失败,仍可使用原10个元素
}
// 再次使用内存(打印所有可用元素)
//确定元素个数
int count;
if (temp != NULL)
count = 20; // 如果扩容成功,有效元素个数是20
else
count = 10; // 如果扩容失败,仍使用原来的10个元素
for (int i = 0; i < count; i++) {
printf("%d ", p[i]);
}
printf("\n");
// 第五步:使用结束,释放内存并置空
free(p);
p = NULL; // 避免野指针
return 0;
}
三、常见的动态内存错误
动态内存操作过程中,因逻辑疏忽或对函数特性不熟悉,易引发多种错误,需深入理解错误本质并掌握规避方法。
(一)对 NULL 指针的解引用操作
1、错误本质
未校验 malloc、calloc 或 realloc 的返回值,直接对可能为 NULL 的指针进行解引用(如赋值、访问元素),触发未定义行为。
2、典型错误场景
cpp
// 申请过大空间(如INT_MAX字节),系统内存不足,malloc返回NULL
int* p = (int*)malloc(INT_MAX);
// 错误:未校验p是否为NULL,直接解引用赋值
*p = 20;
free(p);
3、错误后果
程序可能直接崩溃(如 Windows 系统下触发 "访问违规" 错误),或出现随机的内存损坏,难以定位问题。
4、解决方案
必须在使用动态内存前,对函数返回值进行非空校验,示例如下:
cpp
int* p = (int*)malloc(10 * sizeof(int));
// 非空校验,确保指针有效
if (p == NULL) {
perror("malloc failed: ");
return 1;
}
// 校验通过后,安全使用内存
*p = 20;
free(p);
p = NULL;
(二)对动态开辟空间的越界访问
1、错误本质
访问超出 malloc / calloc / realloc 申请的内存空间范围的地址,破坏其他内存区域的数据(如相邻的堆内存、栈内存)。
2、典型错误场景
cpp
// 申请10个int空间(40字节),索引范围为0~9
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
perror("malloc failed: ");
return 1;
}
// 错误:循环条件为i <= 10,访问了索引10(第11个元素),超出空间范围
for (int i = 0; i <= 10; i++) {
p[i] = i;
}
free(p);
p = NULL;
3、错误后果
(1)若越界地址为其他动态内存,会篡改该内存中的数据,导致逻辑错误(如变量值异常、链表节点损坏)。
(2)若越界地址为栈内存或系统关键内存,会触发程序崩溃(如 "栈溢出""访问违规")。
4、解决方案
**(1)明确动态内存的边界:**根据申请时的元素个数或总字节数,确定合法的访问范围(如申请n个int,索引范围为0~n-1)。
(2)严格控制循环条件或指针偏移: 避免超出合法范围,可通过定义常量存储元素个数,减少硬编码错误,示例如下:
cpp
#define NUM 10 // 定义常量,明确元素个数
int* p = (int*)malloc(NUM * sizeof(int));
if (p == NULL) {
perror("malloc failed: ");
return 1;
}
// 循环条件为i < NUM,确保不越界
for (int i = 0; i < NUM; i++) {
p[i] = i;
}
(三)对非动态开辟内存使用 free 释放
1、错误本质
free 函数仅用于释放堆区的动态内存 (malloc / calloc / realloc申请),若对栈区(局部变量、数组)或静态区(全局变量、static变量)的内存调用 free,会触发未定义行为。
2、典型错误场景
cpp
// 场景1:释放栈区局部数组
int arr[10]; // 栈区数组,非动态内存
int* p = arr;
free(p); // 错误:释放非动态内存
// 场景2:释放静态区变量
static int s_val = 5; // 静态区变量
int* p = &s_val;
free(p); // 错误:释放非动态内存
3、错误后果
程序立即崩溃,不同系统报错信息不同(如 Windows 下 "堆损坏",Linux 下 "双重释放或损坏(fasttop)")。
4、解决方案
(1)明确内存类型
在调用 free 前,确认指针指向的内存是否为动态内存(是否由 malloc/calloc/realloc申请)。
(2)避免混淆指针来源
不将非动态内存的地址赋值给动态内存指针,或在释放前通过注释标注指针来源。
(四)使用 free 释放一块动态开辟内存的一部分
1、错误本质
指针通过算术运算(如p++、p += 2)移动后,指向动态内存的中间位置,此时调用 free 释放该指针,free 无法识别需释放的完整内存范围,导致内存泄漏或堆结构损坏。
2、典型错误场景
cpp
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
perror("malloc failed: ");
return 1;
}
// 指针移动:p指向第2个元素地址(非起始地址)
p++;
// 错误:释放的是内存的一部分,非完整动态内存
free(p);
p = NULL;
3、错误后果
(1)内存泄漏
free 仅释放指针指向位置后的内存(具体行为未定义),起始地址到指针指向位置之间的内存无法释放,导致泄漏。
(2)堆结构损坏
破坏堆区的内存管理链表,后续动态内存申请 / 释放可能出现异常(如malloc返回NULL、程序崩溃)。
4、解决方案
保存动态内存起始地址:若需移动指针访问内存,需先保存起始地址(如用另一个指针存储malloc返回的地址),释放时使用起始地址,示例如下:
cpp
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
perror("malloc failed: ");
return 1;
}
// 保存起始地址,用于后续释放
int* p_start = p;
// 指针移动访问内存
for (int i = 0; i < 10; i++) {
*p = i + 1;
p++;
}
// 使用起始地址释放完整内存
free(p_start);
p_start = NULL;
p = NULL; // 避免野指针
(五)对同一块动态内存多次释放
1、错误本质
对已释放的动态内存再次调用 free,破坏堆区的内存管理结构,导致未定义行为。
2、典型错误场景
cpp
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
perror("malloc failed: ");
return 1;
}
// 第一次释放:内存归还给系统
free(p);
// 错误:第二次释放已释放的内存
free(p);
3、错误后果
程序崩溃,或导致后续动态内存操作异常(如 malloc 返回无效地址),不同系统报错不同(如 Linux 下 "双重释放" 错误)。
4、解决方案
(1)释放后立即置空指针
free 释放内存后,将指针赋值为 NULL,由于 free(NULL) 无任何操作,可避免二次释放,示例如下:
cpp
free(p);
p = NULL; // 置空指针
free(p); // 无害,free(NULL)无操作
(2)规范内存管理逻辑
在大型程序中,可通过状态标记(如int is_freed = 0)记录内存是否已释放,释放前检查状态,避免重复操作。
(六)动态开辟内存忘记释放(内存泄漏)
1、错误本质
申请动态内存后,未通过 free 释放,且丢失了指向该内存的指针(如指针被覆盖、函数提前返回未释放),导致内存无法回收,长期运行会耗尽系统内存。
2、典型错误场景
cpp
void test() {
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
perror("malloc failed: ");
return;
}
int flag = 1;
// 场景1:提前返回,跳过free,导致内存泄漏
if (flag) {
return;
}
// 场景2:指针被覆盖,原地址丢失
p = (int*)malloc(20 * sizeof(int));
free(p);
p = NULL;
}
3、错误后果
**(1)短期运行程序:**程序结束后,操作系统会自动回收所有内存,影响较小。
**(2)长期运行程序(如服务器、嵌入式设备):**内存逐渐被耗尽,程序性能下降,最终崩溃(如 "内存不足" 错误)。
4、解决方案
(1)遵循 "申请即释放" 原则
在申请动态内存时,明确释放时机,确保每个 malloc / calloc / realloc 都有对应的 free。
(2)函数中处理所有返回路径
若函数有多个返回点,需在每个返回前释放内存,或使用 goto 语句统一释放(仅在内存释放场景中推荐),示例如下:
cpp
void test() {
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
perror("malloc failed: ");
return;
}
int flag = 1;
// 统一释放标签
if (flag) {
goto FREE;
}
FREE:
free(p);
p = NULL;
}
(3)使用内存泄漏检测工具
如 Valgrind(Linux)、Visual Leak Detector(Windows),辅助定位未释放的内存。
四、动态内存经典笔试题分析
(一)例题 1:传值调用导致空指针解引用
1、题目代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void get_memory(char* p) {
// 申请100字节内存,地址存入形参p(局部变量)
p = (char*)malloc(100);
}
int main() {
char* str = NULL;
// 传值调用:形参p是str的副本,修改p不影响str
get_memory(str);
// 错误:str仍为NULL,解引用崩溃
strcpy(str, "hello");
printf(str);
free(str);
return 0;
}
2、问题深度分析
(1)传值调用缺陷
get_memory 函数的参数 p 是 main 函数中 str 的副本,malloc 返回的内存地址仅存入 p,而str 的值仍为 NULL(未被修改)。函数调用结束后,形参 p 被销毁,malloc申请的 100 字节内存地址丢失,导致内存泄漏。
(2)空指针解引用
strcpy(str, "hello") 中,str 为 NULL,strcpy 尝试向 NULL 指向的内存写入数据,触发未定义行为,程序崩溃。
(3)内存泄漏
malloc 申请的 100 字节内存地址仅存于形参p,函数结束后 p 销毁,内存无法释放,造成泄漏。
3、正确修改方案
(1)方案 1:使用二级指针传递地址(推荐)
通过二级指针修改一级指针的值,将malloc返回的地址传递回main函数的str,示例如下:
cpp
void get_memory(char** p) {
// *p表示修改一级指针str的值
*p = (char*)malloc(100);
}
int main() {
char* str = NULL;
// 传入str的地址,通过二级指针修改str
get_memory(&str);
if (str != NULL) {
strcpy(str, "hello");
// 此时 str 是一个指向字符串 "hello" 的指针,而字符串 "hello" 中不包含任何格式占位符。
// 因此 printf(str) 等价于 printf("hello"),printf 会直接输出字符串 "hello" 的内容。
printf(str); // 正确输出:hello
free(str);
str = NULL;
}
return 0;
}
(2)方案 2:通过函数返回值传递地址
将 malloc 返回的地址作为函数返回值,由main函数的str接收,示例如下:
cpp
char* get_memory() {
char* p = (char*)malloc(100);
return p;
}
int main() {
char* str = get_memory();
if (str != NULL) {
strcpy(str, "hello");
printf("%s\n", str); // 正确输出:hello
free(str);
str = NULL;
}
return 0;
}
(二)例题 2:返回栈区地址导致野指针
1、题目代码
cpp
#include <stdio.h>
char* get_str() {
// 栈区局部数组,函数结束后空间释放
char arr[] = "hello";
// 错误:返回栈区地址
return arr;
}
int main() {
// str变为野指针,指向已释放的栈区地址
char* str = get_str();
// 错误:访问野指针,打印随机值(如"烫烫烫")
printf("%s\n", str);
return 0;
}

2、问题深度分析
(1)栈区空间生命周期
arr 是 get_str 函数的局部数组,存储于栈区。函数执行时,栈为 arr 分配空间;函数执行结束后,栈帧销毁,arr 的空间被回收,归还给系统,该地址不再属于当前程序。
(2)野指针产生
get_str 返回 arr 的地址,但该地址对应的内存已被释放,main 函数中的 str 成为野指针。访问野指针时,内存中的数据可能是其他程序的残留数据(如0xCDCDCDCD,对应中文 "烫"),导致打印随机值。
3、正确修改方案
(1)方案 1:使用动态内存(堆区)
malloc 申请的堆区内存,函数结束后不会自动释放,需手动free,示例如下:
cpp
char* get_str() {
// 申请6字节(存储"hello\0",包含字符串结束符)
char* p = (char*)malloc(6);
if (p == NULL) {
perror("malloc failed: ");
return NULL;
}
strcpy(p, "hello");
return p;
}
int main() {
char* str = get_str();
if (str != NULL) {
printf("%s\n", str); // 正确输出:hello
free(str);
str = NULL;
}
return 0;
}
(2)方案 2:使用静态数组(静态区)
static 修饰的数组存储于静态区,生命周期与程序一致,函数结束后空间不释放,示例如下:
cpp
char* get_str() {
// 静态区数组,生命周期与程序一致
static char arr[] = "hello";
return arr;
}
int main() {
char* str = get_str();
printf("%s\n", str); // 正确输出:hello
return 0;
}
(三)例题 3:释放后未置空导致非法访问
1、题目代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char* str = (char*)malloc(100);
if (str == NULL) {
perror("malloc failed: ");
return 1;
}
strcpy(str, "hello");
// 释放内存,但未置空str,str仍保留原地址
free(str);
// 错误:str非NULL,条件为真,执行非法操作
if (str != NULL) {
// 错误:向已释放的内存写入数据,触发未定义行为
strcpy(str, "world");
printf("%s\n", str);
}
return 0;
}
2、问题深度分析
(1)野指针残留
free(str) 释放内存后,str 仍保留原内存地址值,未被置为NULL。此时str指向的内存已归还给系统,不再属于当前程序,但str的值非NULL,导致if (str != NULL)条件误判为真。
(2)非法内存访问
strcpy(str, "world") 尝试向已释放的内存写入数据,可能篡改其他程序的内存(如系统内存、其他动态内存),导致程序崩溃或数据损坏。
3、正确修改方案
free 释放内存后,立即将指针置为NULL,避免后续误操作,示例如下:
cpp
free(str);
str = NULL; // 置空指针,使str != NULL条件为假
if (str != NULL) {
// 条件为假,跳过非法操作
strcpy(str, "world");
printf("%s\n", str);
}
五、柔性数组
柔性数组是 C99 标准引入的特性,指结构体中最后一个成员允许为未知大小的数组,支持动态调整数组大小,适用于需要灵活扩展数组且希望内存连续的场景。
(一)柔性数组的定义与核心特点
1、语法规则
必须定义在结构体内部,且是结构体的最后一个成员,称为柔性数组成员,前面必须包含至少一个其他成员(否则结构体大小无法计算)。
数组大小未知,语法为 "数组名[ ]" 或 "数组名[0]",两种写法等价,部分编译器仅支持其中一种。
2、正确与错误定义示例
cpp
// 正确定义:柔性数组arr是最后一个成员,前有其他成员n
struct S {
int n; // 用于存储柔性数组的元素个数,便于管理
int arr[]; // 柔性数组成员(未知大小)
};
// 错误定义1:柔性数组不是最后一个成员
struct S1 {
int arr[];
int n; // 错误:柔性数组后有其他成员
};
// 错误定义2:无其他成员,结构体大小无法计算
struct S2 {
int arr[]; // 错误:前无其他成员,sizeof(struct S2)为0
};
3、核心特点
(1)sizeof 计算规则
sizeof (struct 结构体名) 仅计算结构体中除柔性数组外的成员大小,不包含柔性数组的内存。例如:
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct S {
int a;
int arr[ ];
};
int main()
{
// struct S中仅int n占4字节,柔性数组arr不被计算
printf("%zd\n", sizeof(struct S)); // 输出:4(32位系统)
return 0;
}
(2)动态内存依赖
柔性数组本身不占用内存,其内存需通过 malloc 动态申请 ,且申请的总内存大小需包含结构体基础大小(除柔性数组外的成员大小)和柔性数组所需空间。
(3)内存连续性
结构体成员与柔性数组的内存连续,减少内存碎片,提高访问效率(缓存命中率更高)。
(二)柔性数组的详细使用步骤
1、步骤 1:申请包含柔性数组的结构体内存
需计算结构体基础大小与柔性数组所需空间的总和,示例如下(申请struct S结构体,柔性数组arr存储 10 个int):
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
// 定义包含柔性数组的结构体
struct S {
int len;
int arr[]; // 柔性数组成员,没有指定大小
};
int main() {
// 1、确定柔性数组需要存储的元素个数,这里为5个int类型元素
int n = 5;
// 2、计算需要申请的内存大小,包括结构体本身大小和柔性数组所需空间
size_t totalSize = sizeof(struct S) + n * sizeof(int);
// 3、申请内存
struct S *flex = (struct S *)malloc(totalSize);
if (flex == NULL) {
perror("malloc failed");
return 1;
}
// 4、初始化结构体中的len成员,记录柔性数组元素个数
flex->len = n;
// 此处用于证明内存申请成功
// 在 C 语言中,printf 函数使用 %p 格式占位符输出指针地址时,
// 要求对应的参数必须是 void* 类型。这是 C 语言标准对 %p 占位符的明确规定。
// 虽然大多数编译器会自动进行隐式转换,但这不符合 C 语言标准规范,因此这样更安全
printf("内存申请成功,结构体地址: %p\n", (void*)flex);
printf("柔性数组起始地址: %p\n", (void*)flex->arr);
// 后续步骤可在此基础上继续添加代码,此处先释放内存,仅作内存申请演示
free(flex);
flex = NULL;
return 0;
}
2、步骤 2:访问柔性数组
通过结构体指针,以数组下标或指针算术方式访问柔性数组,语法与普通数组一致,示例如下:
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
// 定义包含柔性数组的结构体
struct S {
int len; // 用于记录柔性数组的元素个数
int arr[]; // 柔性数组成员,没有指定大小
};
int main()
{
int n = 5;
size_t totalSize = sizeof(struct S) + n * sizeof(int);
struct S *flex = (struct S *)malloc(totalSize);
if (flex == NULL) {
perror("malloc failed");
return 1;
}
flex->len = n;
// 1、使用柔性数组,给数组元素赋值
for (int i = 0; i < flex->len; i++) {
flex->arr[i] = i + 1;
}
// 2、打印柔性数组的元素
printf("柔性数组元素: \n");
for (int i = 0; i < flex->len; i++)
{
printf("flex->arr[%d] = %d\n", i, flex->arr[i]);
}
free(flex);
flex = NULL;
return 0;
}
3、步骤 3:调整柔性数组大小
通过 realloc 调整柔性数组的空间大小,需重新计算总内存大小(结构体基础大小 + 新的柔性数组空间),示例如下(扩容为 20 个int):
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
// 定义包含柔性数组的结构体
struct S {
int len; // 用于记录柔性数组的元素个数
int arr[]; // 柔性数组成员,没有指定大小
};
int main()
{
int n = 5;
size_t totalSize = sizeof(struct S) + n * sizeof(int);
struct S *flex = (struct S *)malloc(totalSize);
if (flex == NULL) {
perror("malloc failed");
return 1;
}
flex->len = n;
for (int i = 0; i < flex->len; i++) {
flex->arr[i] = i + 1;
}
printf("柔性数组元素: \n");
for (int i = 0; i < flex->len; i++)
{
printf("flex->arr[%d] = %d\n", i, flex->arr[i]);
}
// 1、要扩容到的元素个数
int new = 10;
size_t newTotalSize = sizeof(struct S) + new * sizeof(int);
// 2、扩容柔性数组
struct S *temp = (struct S *)realloc(flex, newTotalSize);
if (temp == NULL)
{
perror("realloc failed");
free(flex);
return 1;
}
flex = temp;
flex->len = new;
// 3、给扩容后的柔性数组新增元素赋值
for (int i = n; i < flex->len; i++)
{
flex->arr[i] = i + 1;
}
// 4、打印扩容后柔性数组的元素
printf("\n扩容后柔性数组元素: \n");
for (int i = 0; i < flex->len; i++)
{
printf("flex->arr[%d] = %d\n", i, flex->arr[i]);
}
free(flex);
flex = NULL;
return 0;
}

4、步骤 4:释放内存
仅需一次 free 操作,即可释放结构体及柔性数组的全部内存(因内存连续)。
(三)柔性数组的优势
传统方式中,可通过 "结构体 + 指针" 模拟动态数组,但柔性数组在内存管理、效率和安全性上更具优势,具体对比如下:
|--------------|-----------------------------|---------------------------------------|
| 对比维度 | 柔性数组方案 | 指针模拟方案 |
| 内存申请次数 | 1 次(结构体基础内存 + 柔性数组内存,连续分配) | 2 次(先申请结构体内存,再申请指针指向的数组内存,两次分配独立) |
| 内存连续性 | 完全连续(结构体成员与柔性数组在同一块内存中) | 不连续(结构体与数组内存是两块独立的堆内存,可能存在碎片) |
| 释放复杂度 | 1 次free(释放结构体指针即可,无需单独释放数组) | 2 次free(需先释放数组内存,再释放结构体内存,顺序不可颠倒,易遗漏) |
| 访问效率 | 高(连续内存,缓存命中率高,无指针跳转) | 低(需通过结构体指针跳转至数组指针,再访问数组,存在额外开销) |
| 野指针风险 | 低(仅需维护一个结构体指针,无额外指针管理) | 高(需维护结构体指针和数组指针,若数组指针丢失,会导致内存泄漏) |
| 代码简洁性 | 简洁(无需单独管理数组指针,语法与普通数组一致) | 繁琐(需单独申请 / 释放数组内存,代码逻辑更复杂) |
指针模拟方案示例(对比参考):
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
// 定义结构体,包含一个指向 int 数组的指针
struct PointerArray {
int len; // 记录数组元素个数
int* arr; // 指向动态分配的 int 数组
};
int main() {
int initial_count = 5;
// 步骤1:为结构体和数组分别分配内存
// 为结构体分配内存
struct PointerArray* pa = (struct PointerArray*)malloc(sizeof(struct PointerArray));
if (pa == NULL) {
perror("malloc for struct failed");
return 1;
}
// 为数组分配内存
pa->arr = (int*)malloc(initial_count * sizeof(int));
if (pa->arr == NULL) {
perror("malloc for array failed");
free(pa); // 释放结构体内存,避免泄漏
return 1;
}
pa->len = initial_count;
// 步骤2:使用数组
for (int i = 0; i < pa->len; i++) {
pa->arr[i] = i * 10;
}
printf("初始数组元素:\n");
for (int i = 0; i < pa->len; i++) {
printf("pa->arr[%d] = %d\n", i, pa->arr[i]);
}
// 步骤3:扩容数组
int new_count = 10;
int* new_arr = (int*)realloc(pa->arr, new_count * sizeof(int));
if (new_arr == NULL) {
perror("realloc for array failed");
// 原数组仍有效,需释放结构体和原数组内存
free(pa->arr);
free(pa);
return 1;
}
pa->arr = new_arr;
pa->len = new_count;
// 为新增元素赋值
for (int i = initial_count; i < pa->len; i++) {
pa->arr[i] = i * 10;
}
printf("\n扩容后数组元素:\n");
for (int i = 0; i < pa->len; i++) {
printf("pa->arr[%d] = %d\n", i, pa->arr[i]);
}
// 步骤4:释放内存
free(pa->arr);
free(pa);
pa = NULL;
return 0;
}
六、C/C++ 程序内存区域划分
C/C++ 程序运行时,内存被划分为多个功能区域,不同区域存储不同类型的数据,遵循不同的生命周期和管理规则,具体划分如下:
(一)各内存区域的详细解析
1、栈区 (stack)
(1)存储内容:局部变量、函数参数、函数返回数据、返回地址、局部数组。
(2)生命周期:函数执行期间存在,执行结束后自动释放(栈帧销毁)。
(3)管理方式:编译器自动管理(基于处理器栈指令),无需手动操作。
(4)空间大小:较小(通常几 MB,如 Windows 默认 1MB)。
(5)关键特点:效率极高(指令级操作),空间连续,遵循 "先进后出" 原则。
2、堆区 (heap)
(1)存储内容:动态内存(malloc/calloc/realloc申请的空间)
(2)生命周期:手动申请后存在,free释放或程序结束后由操作系统回收。
(3)管理方式:程序员手动管理(申请 / 释放),需避免泄漏。
(4)空间大小:较大(通常几 GB,取决于系统内存)
(5)关键特点:空间不连续(分配方式类似链表),灵活性高,效率低于栈区。
3、数据段(静态区,data segment)
(1)存储内容:全局变量、static修饰的静态变量(局部静态、全局静态)、初始化的常量数据
(2)生命周期:程序运行期间始终存在,程序结束后由操作系统释放。
(3)管理方式:编译器自动管理,无需手动操作。
(4)空间大小:中等(取决于全局 / 静态变量的数量和大小)
(5)关键特点:数据在编译期初始化,分为 "已初始化数据段" 和 "未初始化数据段(BSS 段)"。
4、代码段(text segment)
(1)存储内容:函数体(全局函数、类成员函数)的二进制指令、只读常量(如"hello"字符串)
(2)生命周期:程序运行期间只读,程序结束后释放。
(3)管理方式:操作系统管理,内存属性为只读。
(4)空间大小:较小(取决于代码量)
(5)关键特点:防止指令被篡改,确保程序运行安全,可被多个进程共享(如共享库代码)。
5、内核空间
(1)存储内容:操作系统内核代码、内核数据结构(如进程控制块、内存管理表)
(2)生命周期:操作系统运行期间始终存在
(3)管理方式:操作系统内核管理,用户程序不可访问
(4)空间大小:固定(如 32 位系统通常占 1GB)
(5)关键特点:用户程序无法直接读写,需通过系统调用间接访问

(二)关键区域对比与内存分布示例
1、栈区 vs 堆区核心区别
**(1)管理方式:**栈区自动管理,堆区手动管理。
**(2)空间大小:**栈区小(MB 级),堆区大(GB 级)。
**(3)效率:**栈区高(指令级),堆区低(需链表查找、数据拷贝)。
**(4)连续性:**栈区连续(栈帧依次分配),堆区不连续(按需分配)。
2、数据段 vs 代码段核心区别
**(1)数据可改性:**数据段存储可修改数据(如全局变量、静态变量),代码段存储只读数据(指令、只读常量)。
**(2)存储内容:**数据段存储数据,代码段存储指令。
3、示例代码内存分布
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
// 全局变量:存储于数据段(已初始化)
int g_val = 10;
// 静态全局变量:存储于数据段(已初始化)
static int g_static_val = 20;
void test() {
// 局部变量:存储于栈区
int l_val = 30;
// 静态局部变量:存储于数据段(未初始化时默认0)
static int l_static_val = 40;
// 动态内存:存储于堆区
int* p = (int*)malloc(4);
*p = 50; // 动态内存中的数据:存储于堆区
free(p); // 释放堆区内存
p = NULL;
}
int main() {
// 函数调用:test函数栈帧创建于栈区
test();
// 只读常量:存储于代码段
printf("hello\n");
return 0;
}
(三)内存区域划分的实际意义
1、提高内存利用率
不同区域按需分配空间,避免资源浪费(如栈区小而快,用于短期数据;堆区大而灵活,用于长期数据)。
2、确保程序安全
代码段只读,防止指令被篡改;内核空间隔离,避免用户程序破坏系统。
3、简化内存管理
自动管理区域(栈区、数据段、代码段)减少程序员负担,手动管理区域(堆区)提供灵活性,平衡便捷性与可控性。
以上即为 动态内存管理 的全部内容,麻烦三连支持一下呗~


