目录
一、动态内存分配的必要性
(一)传统开辟内存的方式
1、单个固定大小变量
2、固定长度数组
(二)传统方式的核心缺陷
1、空间大小不可变
2、适配性差
(三)动态内存的核心优势
1、灵活性高
2、自主管理
二、动态内存管理相关函数
(一)malloc 函数
1、函数原型
2、功能解析
3、详细使用步骤
4、关键注意事项
(二)free 函数
1、函数原型
2、功能解析
3、详细使用规范
4、野指针问题深度解析
(三)calloc 函数
1、函数原型
2、功能解析
3、与 malloc 的详细对比
4、详细使用示例
(四)realloc 函数
1、函数原型
2、功能解析
3、内存调整机制深度解析
4、详细使用步骤与规范
(五)四个函数使用的步骤总结
三、常见的动态内存错误
(一)对 NULL 指针的解引用操作
1、错误本质
2、典型错误场景
3、错误后果
4、解决方案
(二)对动态开辟空间的越界访问
1、错误本质
2、典型错误场景
3、错误后果
4、解决方案
(三)对非动态开辟内存使用 free 释放
1、错误本质
2、典型错误场景
3、错误后果
4、解决方案
(四)使用 free 释放一块动态开辟内存的一部分
1、错误本质
2、典型错误场景
3、错误后果
4、解决方案
(五)对同一块动态内存多次释放
1、错误本质
2、典型错误场景
3、错误后果
4、解决方案
(六)动态开辟内存忘记释放(内存泄漏)
1、错误本质
2、典型错误场景
3、错误后果
4、解决方案
四、动态内存经典笔试题分析
(一)例题 1:传值调用导致空指针解引用
1、题目代码
2、问题深度分析
3、正确修改方案
(二)例题 2:返回栈区地址导致野指针
1、题目代码
2、问题深度分析
3、正确修改方案
(三)例题 3:释放后未置空导致非法访问
1、题目代码
2、问题深度分析
3、正确修改方案
五、柔性数组
(一)柔性数组的定义与核心特点
1、语法规则
2、正确与错误定义示例
3、核心特点
(二)柔性数组的详细使用步骤
1、步骤 1:申请包含柔性数组的结构体内存
编辑
2、步骤 2:访问柔性数组
编辑
3、步骤 3:调整柔性数组大小
4、步骤 4:释放内存
(三)柔性数组的优势
六、C/C++ 程序内存区域划分
(一)各内存区域的详细解析
1、栈区 (stack)
2、堆区 (heap)
3、数据段(静态区,data segment)
4、代码段(text segment)
5、内核空间
(二)关键区域对比与内存分布示例
1、栈区 vs 堆区核心区别
2、数据段 vs 代码段核心区别
3、示例代码内存分布
(三)内存区域划分的实际意义
一、动态内存分配的必要性
(一)传统开辟内存的方式
在 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* 指针强制转换为对应类型指针,示例如下:
// 申请10个int类型的连续空间,总字节数为10 * sizeof(int)
int* p = (int*)malloc(10 * sizeof(int));
(2)申请结果校验
必须对返回值进行非空校验,避免后续对NULL指针操作导致程序崩溃,示例如下:
if (p == NULL) {
// 使用perror打印错误信息,便于定位问题
perror("malloc failed: ");
// 终止程序或进行其他错误处理
return 1;
}
(3)内存空间使用
通过指针算术运算或数组下标方式访问连续内存空间,两种方式等价,示例如下:
// 方式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 释放,从而避免内存的浪费与泄露,示例如下:
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
perror("malloc failed: ");
return 1;
}
// 内存使用逻辑…
// 释放内存,与malloc成对使用
free(p);
// 释放后将指针置空,避免野指针
p = NULL;
(2)禁止非法参数
若 ptr 指向非动态内存(如栈区的局部数组、局部变量,静态区的全局变量),调用 free 会触发未定义行为,导致程序崩溃,示例如下:
int arr[4]; // 栈区数组,非动态内存
int* p = arr;
free(p); // 错误:非法释放非动态内存,程序崩溃
若 ptr 为 NULL,free 函数无任何操作,不会报错,因此释放内存后将指针置空是安全的操作,同时可以避免野指针的产生。
4、野指针问题深度解析
(1)野指针定义
指针指向已释放的内存空间或非法地址,此时指针仍保留原地址值,但该地址对应的内存已不属于当前程序,继续使用会导致数据损坏或程序崩溃。
(2)野指针产生场景
free释放内存后,未将指针置空,后续误操作该指针(如解引用、赋值)。
(3)规避方案
free释放内存后,立即将指针赋值为NULL,示例如下:
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、详细使用示例
#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 时,无论它实际采用哪种扩容机制(原地扩容还是迁移数据),我们都应该默认按 “原空间后方无足够连续内存” 来编写代码,这是最安全的实践原则。示例如下:
// 原内存:申请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 无法识别需调整的内存范围,导致内存泄漏或程序崩溃,示例如下:
int* p = (int*)malloc(10 * sizeof(int));
p++; // 指针移动到第二个元素地址,不再是起始地址
// 错误:传入非起始地址,realloc无法正常工作
realloc(p, 20 * sizeof(int));
(3)缩小内存空间
realloc 也支持缩小内存,只需传入更小的size参数即可,此时 realloc 会直接截断原内存空间,多余部分被释放,示例如下:
// 原内存: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
第五步:使用结束后,则释放内存,指针再置为空。
#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、典型错误场景
// 申请过大空间(如INT_MAX字节),系统内存不足,malloc返回NULL
int* p = (int*)malloc(INT_MAX);
// 错误:未校验p是否为NULL,直接解引用赋值
*p = 20;
free(p);
3、错误后果
程序可能直接崩溃(如 Windows 系统下触发 “访问违规” 错误),或出现随机的内存损坏,难以定位问题。
4、解决方案
必须在使用动态内存前,对函数返回值进行非空校验,示例如下:
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、典型错误场景
// 申请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)严格控制循环条件或指针偏移:避免超出合法范围,可通过定义常量存储元素个数,减少硬编码错误,示例如下:
#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、典型错误场景
// 场景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、典型错误场景
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返回的地址),释放时使用起始地址,示例如下:
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、典型错误场景
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) 无任何操作,可避免二次释放,示例如下:
free(p);
p = NULL; // 置空指针
free(p); // 无害,free(NULL)无操作
(2)规范内存管理逻辑
在大型程序中,可通过状态标记(如int is_freed = 0)记录内存是否已释放,释放前检查状态,避免重复操作。
(六)动态开辟内存忘记释放(内存泄漏)
1、错误本质
申请动态内存后,未通过 free 释放,且丢失了指向该内存的指针(如指针被覆盖、函数提前返回未释放),导致内存无法回收,长期运行会耗尽系统内存。
2、典型错误场景
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 语句统一释放(仅在内存释放场景中推荐),示例如下:
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、题目代码
#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,示例如下:
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接收,示例如下:
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、题目代码
#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,示例如下:
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 修饰的数组存储于静态区,生命周期与程序一致,函数结束后空间不释放,示例如下:
char* get_str() {
// 静态区数组,生命周期与程序一致
static char arr[] = "hello";
return arr;
}
int main() {
char* str = get_str();
printf("%s\\n", str); // 正确输出:hello
return 0;
}
(三)例题 3:释放后未置空导致非法访问
1、题目代码
#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,避免后续误操作,示例如下:
free(str);
str = NULL; // 置空指针,使str != NULL条件为假
if (str != NULL) {
// 条件为假,跳过非法操作
strcpy(str, "world");
printf("%s\\n", str);
}
五、柔性数组
柔性数组是 C99 标准引入的特性,指结构体中最后一个成员允许为未知大小的数组,支持动态调整数组大小,适用于需要灵活扩展数组且希望内存连续的场景。
(一)柔性数组的定义与核心特点
1、语法规则
必须定义在结构体内部,且是结构体的最后一个成员,称为柔性数组成员,前面必须包含至少一个其他成员(否则结构体大小无法计算)。
数组大小未知,语法为 “数组名[ ]” 或 “数组名[0]”,两种写法等价,部分编译器仅支持其中一种。
2、正确与错误定义示例
// 正确定义:柔性数组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 结构体名) 仅计算结构体中除柔性数组外的成员大小,不包含柔性数组的内存。例如:
#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):
#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:访问柔性数组
通过结构体指针,以数组下标或指针算术方式访问柔性数组,语法与普通数组一致,示例如下:
#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):
#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(需先释放数组内存,再释放结构体内存,顺序不可颠倒,易遗漏) |
|
访问效率 |
高(连续内存,缓存命中率高,无指针跳转) |
低(需通过结构体指针跳转至数组指针,再访问数组,存在额外开销) |
|
野指针风险 |
低(仅需维护一个结构体指针,无额外指针管理) |
高(需维护结构体指针和数组指针,若数组指针丢失,会导致内存泄漏) |
|
代码简洁性 |
简洁(无需单独管理数组指针,语法与普通数组一致) |
繁琐(需单独申请 / 释放数组内存,代码逻辑更复杂) |
指针模拟方案示例(对比参考):
#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、示例代码内存分布
#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、简化内存管理
自动管理区域(栈区、数据段、代码段)减少程序员负担,手动管理区域(堆区)提供灵活性,平衡便捷性与可控性。
以上即为 动态内存管理 的全部内容,麻烦三连支持一下呗~






