动态内存管理

目录

一、动态内存分配的必要性

(一)传统开辟内存的方式

1、单个固定大小变量

2、固定长度数组

(二)传统方式的核心缺陷

1、空间大小不可变

2、适配性差

(三)动态内存的核心优势

1、灵活性高

2、自主管理

二、动态内存管理相关函数

[(一)malloc 函数](#(一)malloc 函数)

1、函数原型

2、功能解析

3、详细使用步骤

4、关键注意事项

[(二)free 函数](#(二)free 函数)

1、函数原型

2、功能解析

3、详细使用规范

4、野指针问题深度解析

[(三)calloc 函数](#(三)calloc 函数)

1、函数原型

2、功能解析

[3、与 malloc 的详细对比](#3、与 malloc 的详细对比)

4、详细使用示例

[(四)realloc 函数](#(四)realloc 函数)

1、函数原型

2、功能解析

3、内存调整机制深度解析

4、详细使用步骤与规范

(五)四个函数使用的步骤总结

三、常见的动态内存错误

[(一)对 NULL 指针的解引用操作](#(一)对 NULL 指针的解引用操作)

1、错误本质

2、典型错误场景

3、错误后果

4、解决方案

(二)对动态开辟空间的越界访问

1、错误本质

2、典型错误场景

3、错误后果

4、解决方案

[(三)对非动态开辟内存使用 free 释放](#(三)对非动态开辟内存使用 free 释放)

1、错误本质

2、典型错误场景

3、错误后果

4、解决方案

[(四)使用 free 释放一块动态开辟内存的一部分](#(四)使用 free 释放一块动态开辟内存的一部分)

1、错误本质

2、典型错误场景

3、错误后果

4、解决方案

(五)对同一块动态内存多次释放

1、错误本质

2、典型错误场景

3、错误后果

4、解决方案

(六)动态开辟内存忘记释放(内存泄漏)

1、错误本质

2、典型错误场景

3、错误后果

4、解决方案

四、动态内存经典笔试题分析

[(一)例题 1:传值调用导致空指针解引用](#(一)例题 1:传值调用导致空指针解引用)

1、题目代码

2、问题深度分析

3、正确修改方案

[(二)例题 2:返回栈区地址导致野指针](#(二)例题 2:返回栈区地址导致野指针)

1、题目代码

2、问题深度分析

3、正确修改方案

[(三)例题 3:释放后未置空导致非法访问](#(三)例题 3:释放后未置空导致非法访问)

1、题目代码

2、问题深度分析

3、正确修改方案

五、柔性数组

(一)柔性数组的定义与核心特点

1、语法规则

2、正确与错误定义示例

3、核心特点

(二)柔性数组的详细使用步骤

[1、步骤 1:申请包含柔性数组的结构体内存](#1、步骤 1:申请包含柔性数组的结构体内存)

​编辑

[2、步骤 2:访问柔性数组](#2、步骤 2:访问柔性数组)

​编辑

[3、步骤 3:调整柔性数组大小](#3、步骤 3:调整柔性数组大小)

[4、步骤 4:释放内存](#4、步骤 4:释放内存)

(三)柔性数组的优势

[六、C/C++ 程序内存区域划分](#六、C/C++ 程序内存区域划分)

(一)各内存区域的详细解析

1、栈区 (stack)

2、堆区 (heap)

[3、数据段(静态区,data segment)](#3、数据段(静态区,data segment))

[4、代码段(text segment)](#4、代码段(text segment))

5、内核空间

(二)关键区域对比与内存分布示例

[1、栈区 vs 堆区核心区别](#1、栈区 vs 堆区核心区别)

[2、数据段 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* 指针强制转换为对应类型指针,示例如下:

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、简化内存管理

自动管理区域(栈区、数据段、代码段)减少程序员负担,手动管理区域(堆区)提供灵活性,平衡便捷性与可控性。

以上即为 动态内存管理 的全部内容,麻烦三连支持一下呗~

相关推荐
我能坚持多久4 小时前
D20—C语言文件操作详解:从基础到高级应用
c语言·开发语言
(❁´◡`❁)Jimmy(❁´◡`❁)5 小时前
CF2188 C. Restricted Sorting
c语言·开发语言·算法
想放学的刺客5 小时前
单片机嵌入式试题(第27期)设计可移植、可配置的外设驱动框架的关键要点
c语言·stm32·单片机·嵌入式硬件·物联网
BackCatK Chen6 小时前
第 1 篇:软件视角扫盲|TMC2240 软件核心特性 + 学习路径(附工具清单)
c语言·stm32·单片机·学习·电机驱动·保姆级教程·tmc2240
梵刹古音7 小时前
【C语言】 格式控制符与输入输出函数
c语言·开发语言·嵌入式
VekiSon7 小时前
Linux内核驱动——基础概念与开发环境搭建
linux·运维·服务器·c语言·arm开发
无限进步_7 小时前
面试题 02.02. 返回倒数第 k 个节点 - 题解与详细分析
c语言·开发语言·数据结构·git·链表·github·visual studio
Hello World . .7 小时前
数据结构:栈和队列
c语言·开发语言·数据结构·vim
zhangx1234_9 小时前
C语言 数据在内存中的存储
c语言·开发语言