第10章 内存管理

内存的基本概念

内存区域

C 程序在运行时使用的内存通常分为以下几个区域,每个区域都有其特定的用途和特点:

  1. 代码区(Text Segment)

    • 存储程序的可执行指令
    • 通常是只读的,防止程序意外修改指令
    • 大小在编译时确定
    • 包含函数体的二进制机器码
  2. 全局/静态区(Data Segment)

    • 存储全局变量和静态变量
    • 分为初始化部分(.data)和未初始化部分(.bss)
    • .data 段:存储已初始化的全局变量和静态变量
    • .bss 段:存储未初始化的全局变量和静态变量,会被自动初始化为 0
    • 大小在编译时确定
  3. 常量区(Constant Segment)

    • 存储字符串字面量和其他常量
    • 通常是只读的
    • 例如:"Hello, World!" 存储在常量区
  4. 栈区(Stack)

    • 存储局部变量、函数参数和返回地址
    • 自动分配和释放,由编译器管理
    • 后进先出(LIFO)结构
    • 大小通常较小(几兆字节)
    • 栈溢出会导致程序崩溃
  5. 堆区(Heap)

    • 存储动态分配的内存
    • 由程序员手动分配和释放
    • 大小通常较大(几百兆字节到几吉字节)
    • 内存分配和释放的顺序可以任意

内存布局示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
+------------------------+
| 栈区(Stack) | 高地址
| | |
| V |
| |
| ^ |
| | |
+------------------------+
| 堆区(Heap) |
| | |
| V |
| |
| ^ |
| | |
+------------------------+
| 全局/静态区(Data) |
| - 初始化的全局变量 |
| - 初始化的静态变量 |
+------------------------+
| 全局/静态区(BSS) |
| - 未初始化的全局变量 |
| - 未初始化的静态变量 |
+------------------------+
| 常量区 |
| - 字符串字面量 |
| - 其他常量 |
+------------------------+
| 代码区 | 低地址
| - 可执行指令 |
+------------------------+

内存管理的重要性

  • 正确性 - 确保程序正常运行,避免内存泄漏、悬空指针和崩溃
  • 效率 - 优化程序性能,减少内存消耗和内存访问时间
  • 安全性 - 防止缓冲区溢出、内存破坏等安全漏洞
  • 可维护性 - 良好的内存管理使代码更易于理解和维护
  • 可扩展性 - 有效的内存管理支持程序规模的增长

内存管理的挑战

  • 内存泄漏 - 分配的内存未释放,导致内存使用量不断增加
  • 悬空指针 - 指针指向已释放的内存
  • 重复释放 - 对同一块内存多次调用 free
  • 缓冲区溢出 - 写入超出分配内存范围的数据
  • 内存碎片 - 频繁分配和释放小块内存导致的内存浪费
  • 内存不足 - 无法分配足够的内存空间

静态内存和自动内存

静态内存

静态内存是在编译时分配的内存,程序运行期间一直存在,直到程序结束才会释放。静态内存包括:

  • 全局变量 - 在函数外部声明的变量
  • 静态局部变量 - 使用 static 关键字声明的局部变量
  • 静态全局变量 - 使用 static 关键字声明的全局变量(作用域限制在当前文件)

静态内存的特点

  • 生命周期长 - 从程序启动到程序结束
  • 空间固定 - 编译时确定大小,运行时不变
  • 初始化时机 - 全局变量和静态变量在程序启动时初始化
  • 默认初始化 - 未初始化的静态内存会被自动初始化为 0
  • 作用域 - 全局变量作用域为整个程序,静态局部变量作用域为声明它的函数,静态全局变量作用域为声明它的文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 全局变量 - 存储在全局/静态区的 .data 段
int global_var = 10;

// 未初始化的全局变量 - 存储在全局/静态区的 .bss 段
int uninitialized_global;

void function(void)
{
// 静态局部变量 - 存储在全局/静态区
static int static_var = 20;
static_var++;
printf("static_var: %d\n", static_var);
}

int main(void)
{
function(); // 输出 21
function(); // 输出 22
function(); // 输出 23
printf("uninitialized_global: %d\n", uninitialized_global); // 输出 0
return 0;
}

自动内存

自动内存是在函数执行时在栈上分配的内存,函数返回时自动释放。自动内存包括:

  • 局部变量 - 在函数内部声明的变量(默认存储类别为 auto
  • 函数参数 - 传递给函数的参数
  • 临时变量 - 表达式计算过程中创建的临时变量

自动内存的特点

  • 自动管理 - 函数执行时分配,函数返回时自动释放
  • 生命周期短 - 仅在函数执行期间存在
  • 空间有限 - 栈空间通常较小(几兆字节)
  • 高效 - 栈操作(压栈/弹栈)非常快
  • 后进先出(LIFO) - 最后分配的内存最先释放
  • 无需手动管理 - 由编译器自动处理内存分配和释放
1
2
3
4
5
6
7
8
9
10
11
12
void function(int param)  // param 存储在栈区
{
int local_var = 10; // local_var 存储在栈区
auto int auto_var = 20; // auto 关键字显式声明自动变量
printf("param: %d, local_var: %d, auto_var: %d\n", param, local_var, auto_var);
}

int main(void)
{
function(20);
return 0;
}

栈内存的深入理解

栈帧

当函数被调用时,会在栈上创建一个栈帧(Stack Frame),用于存储:

  • 返回地址 - 函数执行完毕后要返回的地址
  • 函数参数 - 传递给函数的参数
  • 局部变量 - 函数内部声明的变量
  • 保存的寄存器 - 被函数修改但需要在函数返回后恢复的寄存器

栈帧的大小在编译时确定,函数调用时栈指针(Stack Pointer)向下移动(在大多数系统中)为栈帧分配空间,函数返回时栈指针向上移动释放栈帧。

栈溢出

栈溢出(Stack Overflow)是指栈空间被耗尽的情况,通常由以下原因导致:

  • 递归过深 - 递归函数没有正确的终止条件,导致无限递归
  • 局部变量过大 - 声明了占用大量栈空间的局部变量(如大型数组)
  • 函数调用链过长 - 函数调用层次过深

栈溢出会导致程序崩溃,通常表现为 “Segmentation fault” 或 “Stack overflow” 错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 栈溢出示例 - 递归过深
void deep_recursion(int n)
{
printf("n: %d\n", n);
deep_recursion(n + 1); // 无限递归,最终导致栈溢出
}

int main(void)
{
deep_recursion(1);
return 0;
}

// 栈溢出示例 - 局部变量过大
void large_local_variable(void)
{
char big_array[1024 * 1024 * 10]; // 10MB 局部数组,可能导致栈溢出
printf("Array address: %p\n", big_array);
}

int main(void)
{
large_local_variable();
return 0;
}

静态内存 vs 自动内存

特性静态内存自动内存
分配时机编译时函数执行时
释放时机程序结束时函数返回时
存储位置全局/静态区栈区
空间大小编译时确定,通常较大运行时确定,通常较小
初始化自动初始化为 0未初始化(值不确定)
访问速度较快非常快
作用域全局或局部局部
管理方式自动管理自动管理
适用场景全局配置、共享状态临时变量、函数参数

动态内存分配

动态内存是在程序运行时在堆上分配的内存,由程序员手动管理。动态内存分配允许程序根据运行时的需要灵活地分配和释放内存,是 C 语言中实现动态数据结构(如链表、树、图等)的基础。

内存分配函数

C 语言提供了以下动态内存分配函数,它们都在 <stdlib.h> 头文件中声明:

  • malloc - 分配指定大小的未初始化内存
  • calloc - 分配指定数量和大小的内存,并初始化为 0
  • realloc - 调整已分配内存的大小
  • free - 释放已分配的内存
  • aligned_alloc - 分配对齐的内存(C11 标准)

malloc 函数

malloc 函数用于分配指定大小的内存块,返回指向该内存块的指针。

函数原型

1
void *malloc(size_t size);

参数和返回值

  • 参数 - size:要分配的内存大小(以字节为单位)
  • 返回值 - 成功时返回指向分配内存的指针,失败时返回 NULL

内存分配机制

  • malloc 从堆中分配连续的内存块
  • 分配的内存未初始化,内容是不确定的
  • 实际分配的内存大小可能大于请求的大小(用于内存对齐和管理)
  • 分配失败通常是因为内存不足

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
// 分配内存
int *ptr = (int *)malloc(5 * sizeof(int));
if (ptr == NULL)
{
perror("内存分配失败");
return EXIT_FAILURE;
}

// 使用内存
for (int i = 0; i < 5; i++)
{
ptr[i] = i + 1;
printf("%d ", ptr[i]);
}
printf("\n");

// 释放内存
free(ptr);
ptr = NULL; // 避免悬空指针

return EXIT_SUCCESS;
}

calloc 函数

calloc 函数用于分配指定数量和大小的内存块,并将所有字节初始化为 0。

函数原型

1
void *calloc(size_t count, size_t size);

参数和返回值

  • 参数 - count:元素数量,size:每个元素的大小(以字节为单位)
  • 返回值 - 成功时返回指向分配内存的指针,失败时返回 NULL

与 malloc 的区别

  • calloc 会将分配的内存初始化为 0,而 malloc 不会
  • calloc 接受两个参数(元素数量和每个元素的大小),而 malloc 接受一个参数(总大小)
  • calloc 通常用于分配数组,因为它会自动计算总大小并初始化内存

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
// 分配内存并初始化为 0
int *ptr = (int *)calloc(5, sizeof(int));
if (ptr == NULL)
{
perror("内存分配失败");
return EXIT_FAILURE;
}

// 打印初始值(都是 0)
printf("初始值:");
for (int i = 0; i < 5; i++)
{
printf("%d ", ptr[i]);
}
printf("\n");

// 使用内存
for (int i = 0; i < 5; i++)
{
ptr[i] = i + 1;
}

// 打印值
printf("赋值后:");
for (int i = 0; i < 5; i++)
{
printf("%d ", ptr[i]);
}
printf("\n");

// 释放内存
free(ptr);
ptr = NULL;

return EXIT_SUCCESS;
}

realloc 函数

realloc 函数用于调整已分配内存块的大小。

函数原型

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

参数和返回值

  • 参数 - ptr:指向先前分配内存的指针,size:新的内存大小(以字节为单位)
  • 返回值 - 成功时返回指向调整后内存的指针,失败时返回 NULL

内存调整机制

  • 如果 ptrNULLrealloc 等同于 malloc(size)
  • 如果 size 为 0,realloc 等同于 free(ptr) 并返回 NULL
  • 如果新大小小于原大小,可能会截断内存(数据可能丢失)
  • 如果新大小大于原大小,可能会:
    • 在原内存块后面扩展(如果有足够空间)
    • 分配新的内存块,复制原数据,释放原内存块

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
// 分配初始内存
int *ptr = (int *)malloc(5 * sizeof(int));
if (ptr == NULL)
{
perror("内存分配失败");
return EXIT_FAILURE;
}

// 初始化内存
for (int i = 0; i < 5; i++)
{
ptr[i] = i + 1;
}

// 打印初始值
printf("初始数组:");
for (int i = 0; i < 5; i++)
{
printf("%d ", ptr[i]);
}
printf("\n");

// 调整内存大小
int *new_ptr = (int *)realloc(ptr, 10 * sizeof(int));
if (new_ptr == NULL)
{
perror("内存重新分配失败");
free(ptr);
return EXIT_FAILURE;
}
ptr = new_ptr;

// 初始化新分配的内存
for (int i = 5; i < 10; i++)
{
ptr[i] = i + 1;
}

// 打印新数组
printf("调整后数组:");
for (int i = 0; i < 10; i++)
{
printf("%d ", ptr[i]);
}
printf("\n");

// 释放内存
free(ptr);
ptr = NULL;

return EXIT_SUCCESS;
}

free 函数

free 函数用于释放由 malloccallocreallocaligned_alloc 分配的内存。

函数原型

1
void free(void *ptr);

参数

  • 参数 - ptr:指向要释放内存的指针

使用注意事项

  • 只能释放由动态内存分配函数分配的内存
  • 释放后应将指针设置为 NULL,避免悬空指针
  • 不要重复释放同一块内存
  • 不要释放 NULL 指针(free(NULL) 是安全的,不会执行任何操作)

使用示例

1
2
3
4
5
6
7
8
void *ptr = malloc(size);
if (ptr != NULL)
{
// 使用内存

free(ptr); // 释放内存
ptr = NULL; // 避免悬空指针
}

aligned_alloc 函数

aligned_alloc 函数用于分配指定大小和对齐方式的内存块(C11 标准)。

函数原型

1
void *aligned_alloc(size_t alignment, size_t size);

参数和返回值

  • 参数 - alignment:内存对齐要求(必须是 2 的幂),size:要分配的内存大小(必须是 alignment 的倍数)
  • 返回值 - 成功时返回指向分配内存的指针,失败时返回 NULL

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
// 分配 16 字节对齐的内存
void *ptr = aligned_alloc(16, 1024);
if (ptr == NULL)
{
perror("内存分配失败");
return EXIT_FAILURE;
}

// 检查对齐
printf("分配的内存地址:%p\n", ptr);
printf("地址是否 16 字节对齐:%s\n", ((uintptr_t)ptr % 16 == 0) ? "是" : "否");

// 使用内存

// 释放内存
free(ptr);
ptr = NULL;

return EXIT_SUCCESS;
}

常见内存问题

内存泄漏

内存泄漏是指程序分配了内存但没有释放,导致内存使用量不断增加,最终可能导致程序崩溃或系统资源耗尽。

内存泄漏的原因

  • 忘记释放内存 - 分配内存后没有调用 free
  • 释放路径不完整 - 在某些代码路径中没有释放内存
  • 异常处理不当 - 异常发生时跳过了内存释放代码
  • 循环引用 - 数据结构中的循环引用导致内存无法释放

内存泄漏的危害

  • 程序性能下降 - 内存使用量不断增加,导致频繁的垃圾回收或页面交换
  • 程序崩溃 - 最终可能因为内存不足而崩溃
  • 系统不稳定 - 占用过多系统内存,影响其他程序的运行

内存泄漏示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 简单内存泄漏
void leak_memory(void)
{
int *ptr = (int *)malloc(100 * sizeof(int));
// 没有调用 free(ptr);
}

// 条件分支中的内存泄漏
void conditional_leak(int condition)
{
int *ptr = (int *)malloc(sizeof(int));
if (condition)
{
printf("条件为真\n");
free(ptr);
return;
}
// 条件为假时,没有释放 ptr
printf("条件为假\n");
}

// 异常处理中的内存泄漏
void exception_leak(void)
{
int *ptr1 = (int *)malloc(sizeof(int));
int *ptr2 = (int *)malloc(sizeof(int));

if (ptr1 == NULL || ptr2 == NULL)
{
// 只释放了 ptr1,没有释放 ptr2
free(ptr1);
return;
}

// 使用内存

free(ptr1);
free(ptr2);
}

int main(void)
{
for (int i = 0; i < 1000; i++)
{
leak_memory(); // 每次调用都会泄漏内存
}
return 0;
}

悬空指针

悬空指针是指指向已释放内存的指针,使用悬空指针可能导致程序崩溃或数据损坏。

悬空指针的原因

  • 释放内存后没有将指针置为 NULL
  • 函数返回局部变量的地址
  • 指针指向的内存被其他代码释放

悬空指针的危害

  • 程序崩溃 - 访问已释放的内存可能导致段错误
  • 数据损坏 - 写入已释放的内存可能修改其他变量的数据
  • 安全漏洞 - 攻击者可能利用悬空指针执行恶意代码

悬空指针示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 释放内存后未置为 NULL
int *ptr = (int *)malloc(sizeof(int));
*ptr = 10;
free(ptr);
// ptr 现在是悬空指针
*ptr = 20; // 错误!访问已释放的内存

// 函数返回局部变量的地址
int *get_local_address(void)
{
int local_var = 10;
return &local_var; // 错误!返回局部变量的地址
}

int main(void)
{
int *ptr = get_local_address();
// ptr 现在是悬空指针
*ptr = 20; // 错误!访问已释放的内存
return 0;
}

重复释放

重复释放是指对同一块内存多次调用 free 函数,可能导致程序崩溃或内存损坏。

重复释放的原因

  • 释放内存后没有将指针置为 NULL
  • 代码逻辑错误导致多次释放
  • 多个指针指向同一块内存,都调用了 free

重复释放的危害

  • 程序崩溃 - 多次释放同一块内存可能导致段错误
  • 内存损坏 - 可能破坏内存分配器的内部数据结构

重复释放示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 简单重复释放
int *ptr = (int *)malloc(sizeof(int));
free(ptr);
free(ptr); // 错误!重复释放内存

// 多个指针指向同一块内存
int *ptr1 = (int *)malloc(sizeof(int));
int *ptr2 = ptr1;
free(ptr1);
free(ptr2); // 错误!重复释放内存

// 释放后未置为 NULL 导致的重复释放
int *ptr = (int *)malloc(sizeof(int));
if (ptr != NULL)
{
free(ptr);
// 没有将 ptr 置为 NULL
}

// 后续代码可能再次释放
if (ptr != NULL)
{
free(ptr); // 错误!重复释放内存
}

缓冲区溢出

缓冲区溢出是指写入的数据超过了分配的内存空间,可能导致程序崩溃、数据损坏或安全漏洞。

缓冲区溢出的原因

  • 字符串操作不当 - 使用 strcpy 等不安全的字符串函数
  • 数组访问越界 - 索引超出数组范围
  • 输入验证不足 - 没有检查用户输入的长度

缓冲区溢出的危害

  • 程序崩溃 - 覆盖栈上的返回地址或其他重要数据
  • 数据损坏 - 覆盖相邻的变量或内存区域
  • 安全漏洞 - 攻击者可能利用缓冲区溢出执行恶意代码

缓冲区溢出示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 字符串缓冲区溢出
char buffer[10];
strcpy(buffer, "This string is too long for the buffer"); // 错误!缓冲区溢出

// 数组访问越界
int array[5];
for (int i = 0; i <= 5; i++) // 错误!索引 5 超出范围
{
array[i] = i;
}

// 输入验证不足
void vulnerable_function(void)
{
char buffer[100];
printf("请输入字符串:");
gets(buffer); // 错误!gets 不检查输入长度
}

内存碎片

内存碎片是指频繁分配和释放小块内存导致的内存空间浪费,可能导致虽然总内存充足但无法分配大块连续内存的情况。

内存碎片的类型

  • 内部碎片 - 分配的内存块大于实际需要的大小
  • 外部碎片 - 内存中存在许多小的空闲块,无法满足大块内存的分配请求

内存碎片的危害

  • 内存利用率下降 - 大量小的空闲块无法使用
  • 分配失败 - 无法分配大块连续内存,即使总内存充足
  • 程序性能下降 - 内存分配和释放的时间增加

内存碎片示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 频繁分配和释放小块内存
void create_fragmentation(void)
{
void *ptrs[100];

// 分配 100 个小块内存
for (int i = 0; i < 100; i++)
{
ptrs[i] = malloc(16); // 16 字节的小块内存
}

// 释放奇数索引的内存
for (int i = 1; i < 100; i += 2)
{
free(ptrs[i]);
}

// 现在内存中存在许多 16 字节的空闲块
// 但可能无法分配一个 1000 字节的连续内存块
void *large_block = malloc(1000);
if (large_block == NULL)
{
printf("无法分配大块内存,尽管总内存充足\n");
}
else
{
free(large_block);
}

// 释放剩余的内存
for (int i = 0; i < 100; i += 2)
{
free(ptrs[i]);
}
}

内存管理的最佳实践

1. 始终检查内存分配是否成功

内存分配可能失败,尤其是在内存不足的情况下。因此,每次调用内存分配函数后,都应该检查返回值是否为 NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void *ptr = malloc(size);
if (ptr == NULL)
{
// 处理内存分配失败
perror("内存分配失败");
exit(EXIT_FAILURE);
}

// 对于函数内部的内存分配
void function(void)
{
void *ptr = malloc(size);
if (ptr == NULL)
{
perror("内存分配失败");
return;
}

// 使用内存

free(ptr);
ptr = NULL;
}

2. 始终释放已分配的内存

内存分配后必须释放,否则会导致内存泄漏。使用完动态分配的内存后,应立即调用 free 函数释放,并将指针设置为 NULL

1
2
3
4
5
6
7
8
9
10
11
void *ptr = malloc(size);
if (ptr == NULL)
{
// 处理内存分配失败
return;
}

// 使用内存

free(ptr);
ptr = NULL; // 避免悬空指针

3. 使用合适的内存分配函数

根据不同的需求,选择合适的内存分配函数:

  • malloc - 分配未初始化的内存,适用于需要快速分配内存且不需要初始化的场景
  • calloc - 分配并初始化为 0 的内存,适用于需要初始化内存的场景(如数组)
  • realloc - 调整已分配内存的大小,适用于需要动态调整内存大小的场景
  • aligned_alloc - 分配对齐的内存,适用于需要特定内存对齐的场景(如 SIMD 指令)

4. 避免使用已释放的内存

释放内存后,应立即将指针设置为 NULL,并在使用指针前检查其是否为 NULL

1
2
3
4
5
6
7
8
free(ptr);
ptr = NULL; // 将指针设置为 NULL

// 检查指针是否为 NULL
if (ptr != NULL)
{
// 使用指针
}

5. 避免缓冲区溢出

使用安全的字符串函数和边界检查,避免缓冲区溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 安全的字符串复制
char buffer[10];
snprintf(buffer, sizeof(buffer), "%s", "Hello");

// 或者使用 strncpy
strncpy(buffer, "Hello", sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';

// 数组访问时检查边界
int array[10];
for (int i = 0; i < 10; i++) // 确保索引不超出范围
{
array[i] = i;
}

6. 使用 RAII 风格的内存管理

在 C 语言中,可以使用 goto 语句或函数封装来实现类似 RAII(Resource Acquisition Is Initialization)的内存管理,确保内存在任何情况下都能被正确释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 使用 goto 语句确保内存释放
void function(void)
{
void *ptr1 = malloc(size1);
if (ptr1 == NULL)
goto error1;

void *ptr2 = malloc(size2);
if (ptr2 == NULL)
goto error2;

// 使用内存

free(ptr2);
error2:
free(ptr1);
error1:
return;
}

// 使用函数封装
void *safe_malloc(size_t size)
{
void *ptr = malloc(size);
if (ptr == NULL)
{
perror("内存分配失败");
exit(EXIT_FAILURE);
}
return ptr;
}

void safe_free(void **ptr)
{
if (*ptr != NULL)
{
free(*ptr);
*ptr = NULL;
}
}

7. 使用内存池管理频繁分配的小内存

对于频繁分配和释放的小内存块,使用内存池可以减少内存碎片和提高性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 简单内存池实现
typedef struct Node {
struct Node *next;
char data[16];
} Node;

Node *pool = NULL;

Node *allocate_node(void)
{
if (pool == NULL)
{
// 分配一批节点
Node *new_nodes = (Node *)malloc(10 * sizeof(Node));
if (new_nodes == NULL)
return NULL;

// 将新节点链接到内存池
for (int i = 0; i < 9; i++)
new_nodes[i].next = &new_nodes[i + 1];
new_nodes[9].next = NULL;

pool = new_nodes;
}

// 从内存池获取一个节点
Node *node = pool;
pool = pool->next;
return node;
}

void free_node(Node *node)
{
// 将节点返回内存池
node->next = pool;
pool = node;
}

8. 使用内存泄漏检测工具

使用内存泄漏检测工具可以帮助发现和修复内存泄漏问题:

Valgrind Memcheck

Valgrind 是一个强大的内存调试和内存泄漏检测工具。

1
2
3
4
5
# 安装 Valgrind(Ubuntu/Debian)
sudo apt install valgrind

# 使用 Valgrind 运行程序
valgrind --leak-check=full ./program

AddressSanitizer

AddressSanitizer(ASan)是 GCC 和 Clang 提供的内存错误检测器。

1
2
3
4
5
# 使用 AddressSanitizer 编译程序
gcc -fsanitize=address -g program.c -o program

# 运行程序
./program

Dr. Memory

Dr. Memory 是一个跨平台的内存泄漏检测工具,适用于 Windows、Linux 和 macOS。

1
2
# 使用 Dr. Memory 运行程序
drmemory -- ./program

9. 内存优化技术

减少内存分配次数

  • 批量分配 - 一次性分配足够的内存,而不是频繁小批量分配
  • 预分配策略 - 根据预期需求预分配内存,避免频繁的 realloc

优化内存使用

  • 使用合适的数据类型 - 选择最小的合适数据类型
  • 压缩数据结构 - 优化数据结构的内存布局
  • 使用位域 - 对于布尔值和小整数,使用位域节省内存

内存对齐

  • 使用 aligned_alloc - 分配对齐的内存以提高访问速度
  • 结构体对齐 - 合理安排结构体成员顺序,减少内存空洞

10. 动态内存管理的设计模式

引用计数

引用计数是一种跟踪内存使用情况的技术,当引用计数为 0 时释放内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
typedef struct {
int ref_count;
// 其他数据
} RefCounted;

RefCounted *create_object(void)
{
RefCounted *obj = (RefCounted *)malloc(sizeof(RefCounted));
if (obj != NULL)
{
obj->ref_count = 1;
}
return obj;
}

void retain_object(RefCounted *obj)
{
if (obj != NULL)
{
obj->ref_count++;
}
}

void release_object(RefCounted *obj)
{
if (obj != NULL && --obj->ref_count == 0)
{
free(obj);
}
}

智能指针模拟

在 C 语言中,可以使用宏和函数模拟智能指针的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#define SMART_PTR(type) \
typedef struct { \
type *ptr; \
} type##_Ptr; \
\ntype##_Ptr create_##type##_ptr(size_t size) { \
type##_Ptr smart_ptr; \
smart_ptr.ptr = (type *)malloc(size * sizeof(type)); \
return smart_ptr; \
} \
\nvoid free_##type##_ptr(type##_Ptr *smart_ptr) { \
if (smart_ptr->ptr != NULL) { \
free(smart_ptr->ptr); \
smart_ptr->ptr = NULL; \
} \
}

// 使用示例
SMART_PTR(int);

int main(void)
{
int_Ptr arr = create_int_ptr(10);
if (arr.ptr != NULL)
{
// 使用 arr.ptr
free_int_ptr(&arr);
}
return 0;
}

内存分配的高级技巧

内存池

内存池是一种预分配内存的技术,用于频繁分配和释放小内存块的场景。内存池可以显著减少内存碎片和提高内存分配效率。

内存池的设计原理

  1. 预分配 - 一次性分配较大的内存块作为内存池
  2. 内存块管理 - 将内存池分割成固定大小的小内存块
  3. 空闲块链表 - 使用链表管理空闲的内存块
  4. 快速分配/释放 - 从空闲链表中获取内存块,释放时将内存块归还链表

内存池的优点

  • 减少内存碎片 - 预分配固定大小的内存块
  • 提高分配速度 - 避免频繁调用系统内存分配函数
  • 减少系统调用 - 降低系统开销
  • 可预测的性能 - 内存分配时间相对稳定

内存池实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// 通用内存池实现
typedef struct MemoryBlock {
struct MemoryBlock *next;
} MemoryBlock;

typedef struct {
MemoryBlock *free_list;
size_t block_size;
size_t pool_size;
char *pool_start;
} MemoryPool;

// 初始化内存池
int init_memory_pool(MemoryPool *pool, size_t block_size, size_t pool_size)
{
// 确保 block_size 至少能容纳一个指针
if (block_size < sizeof(MemoryBlock))
block_size = sizeof(MemoryBlock);

// 分配内存池
pool->pool_start = (char *)malloc(pool_size);
if (pool->pool_start == NULL)
return -1;

pool->block_size = block_size;
pool->pool_size = pool_size;
pool->free_list = NULL;

// 初始化空闲块链表
size_t num_blocks = pool_size / block_size;
for (size_t i = 0; i < num_blocks; i++)
{
MemoryBlock *block = (MemoryBlock *)(pool->pool_start + i * block_size);
block->next = pool->free_list;
pool->free_list = block;
}

return 0;
}

// 从内存池分配内存
void *pool_alloc(MemoryPool *pool)
{
if (pool->free_list == NULL)
return NULL; // 内存池已满

// 从空闲链表中获取一个块
MemoryBlock *block = pool->free_list;
pool->free_list = block->next;

return block;
}

// 释放内存到内存池
void pool_free(MemoryPool *pool, void *ptr)
{
if (ptr == NULL)
return;

// 确保指针在内存池范围内
if (ptr < pool->pool_start || ptr >= pool->pool_start + pool->pool_size)
return;

// 将块归还到空闲链表
MemoryBlock *block = (MemoryBlock *)ptr;
block->next = pool->free_list;
pool->free_list = block;
}

// 销毁内存池
void destroy_memory_pool(MemoryPool *pool)
{
free(pool->pool_start);
pool->pool_start = NULL;
pool->free_list = NULL;
pool->block_size = 0;
pool->pool_size = 0;
}

// 使用示例
int main(void)
{
MemoryPool pool;
if (init_memory_pool(&pool, 64, 4096) != 0)
{
perror("内存池初始化失败");
return EXIT_FAILURE;
}

// 分配内存
void *ptr1 = pool_alloc(&pool);
void *ptr2 = pool_alloc(&pool);

// 使用内存

// 释放内存
pool_free(&pool, ptr1);
pool_free(&pool, ptr2);

// 销毁内存池
destroy_memory_pool(&pool);

return EXIT_SUCCESS;
}

对齐内存分配

对齐内存分配是指分配的内存地址是特定值的倍数,用于提高内存访问效率,特别是对于需要特殊对齐的硬件指令(如 SIMD 指令)。

内存对齐的重要性

  • 提高内存访问速度 - 对齐的内存访问可以减少 CPU 访问内存的次数
  • 支持特殊硬件指令 - 某些 SIMD 指令要求内存地址必须对齐
  • 避免缓存行冲突 - 合理的对齐可以减少缓存行冲突

对齐内存分配实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 对齐内存分配实现
void *aligned_malloc(size_t size, size_t alignment)
{
// 确保 alignment 是 2 的幂
if ((alignment & (alignment - 1)) != 0)
return NULL;

void *ptr = NULL;
void *aligned_ptr = NULL;
size_t offset = alignment - 1 + sizeof(void *);

// 分配足够的内存,包括额外的空间用于存储原始指针
ptr = malloc(size + offset);
if (ptr == NULL)
return NULL;

// 计算对齐的内存地址
aligned_ptr = (void *)(((size_t)ptr + offset) & ~(alignment - 1));

// 存储原始指针,以便后续释放
*((void **)((size_t)aligned_ptr - sizeof(void *))) = ptr;

return aligned_ptr;
}

// 释放对齐的内存
void aligned_free(void *aligned_ptr)
{
if (aligned_ptr != NULL)
{
// 获取原始指针并释放
void *ptr = *((void **)((size_t)aligned_ptr - sizeof(void *)));
free(ptr);
}
}

// 使用示例
int main(void)
{
// 分配 16 字节对齐的内存
void *ptr = aligned_malloc(1024, 16);
if (ptr == NULL)
{
perror("内存分配失败");
return EXIT_FAILURE;
}

// 检查对齐
printf("分配的内存地址:%p\n", ptr);
printf("地址是否 16 字节对齐:%s\n", ((uintptr_t)ptr % 16 == 0) ? "是" : "否");

// 使用内存

// 释放内存
aligned_free(ptr);

return EXIT_SUCCESS;
}

自定义内存分配器

在某些情况下,可能需要实现自定义内存分配器以满足特定需求,例如:

  • 特定的性能要求 - 需要更快的内存分配速度
  • 特殊的内存管理策略 - 需要特定的内存回收机制
  • 内存使用跟踪 - 需要跟踪内存的分配和释放
  • 嵌入式系统 - 需要适应有限的内存资源

自定义内存分配器示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// 简单的自定义内存分配器
#define POOL_SIZE 1024 * 1024 // 1MB 内存池

static char memory_pool[POOL_SIZE];
static size_t pool_used = 0;
static void *allocation_list[1024];
static size_t allocation_count = 0;

// 自定义 malloc
void *my_malloc(size_t size)
{
if (pool_used + size > POOL_SIZE)
return NULL;

void *ptr = &memory_pool[pool_used];
pool_used += size;

// 记录分配
if (allocation_count < 1024)
{
allocation_list[allocation_count++] = ptr;
}

return ptr;
}

// 自定义 free
void my_free(void *ptr)
{
// 简单实现,不做实际释放
// 在实际应用中,可能需要更复杂的内存管理策略
// 例如使用空闲块链表
(void)ptr; // 避免未使用变量警告
}

// 获取内存使用情况
size_t get_memory_used(void)
{
return pool_used;
}

// 重置内存池
void reset_memory_pool(void)
{
pool_used = 0;
allocation_count = 0;
}

// 使用示例
int main(void)
{
// 分配内存
int *arr = (int *)my_malloc(100 * sizeof(int));
if (arr != NULL)
{
// 使用内存
for (int i = 0; i < 100; i++)
{
arr[i] = i;
}

printf("内存使用量:%zu 字节\n", get_memory_used());

// 释放内存
my_free(arr);
}

// 重置内存池
reset_memory_pool();
printf("重置后内存使用量:%zu 字节\n", get_memory_used());

return EXIT_SUCCESS;
}

内存映射

内存映射是一种将文件或设备映射到进程地址空间的技术,可以用于:

  • 大文件处理 - 无需一次性加载整个文件到内存
  • 共享内存 - 实现进程间通信
  • 设备访问 - 直接访问设备内存

内存映射示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
int fd;
void *addr;
off_t file_size;

// 打开文件
fd = open("example.txt", O_RDONLY);
if (fd == -1)
{
perror("打开文件失败");
return EXIT_FAILURE;
}

// 获取文件大小
file_size = lseek(fd, 0, SEEK_END);
if (file_size == -1)
{
perror("获取文件大小失败");
close(fd);
return EXIT_FAILURE;
}
lseek(fd, 0, SEEK_SET);

// 映射文件到内存
addr = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED)
{
perror("内存映射失败");
close(fd);
return EXIT_FAILURE;
}

// 使用映射的内存
printf("文件内容:\n%.*s\n", (int)file_size, (char *)addr);

// 解除映射
if (munmap(addr, file_size) == -1)
{
perror("解除映射失败");
}

// 关闭文件
close(fd);

return EXIT_SUCCESS;
}

内存调试工具

内存调试工具是检测和修复内存问题的重要助手,它们可以帮助开发者发现内存泄漏、缓冲区溢出、悬空指针等问题。

Valgrind

Valgrind 是一个强大的内存调试和内存泄漏检测工具,它通过模拟 CPU 执行来检测内存问题。

Valgrind 的主要工具

  • Memcheck - 检测内存泄漏、缓冲区溢出、悬空指针等内存错误
  • Addrcheck - 检测地址相关的错误
  • Cachegrind - 分析缓存使用情况
  • Callgrind - 分析函数调用和CPU使用情况
  • Massif - 分析堆内存使用情况

安装 Valgrind

1
2
3
4
5
6
7
8
# Ubuntu/Debian
sudo apt install valgrind

# CentOS/RHEL
sudo yum install valgrind

# macOS
brew install valgrind

使用 Valgrind Memcheck

1
2
3
4
5
6
7
8
# 基本用法
valgrind --leak-check=full ./program

# 详细输出
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./program

# 输出到文件
valgrind --leak-check=full --log-file=valgrind.log ./program

Valgrind 输出解读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
==12345== Memcheck, a memory error detector
==12345== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==12345== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==12345== Command: ./program
==12345==
==12345== Invalid write of size 4
==12345== at 0x40053F: main (program.c:10)
==12345== Address 0x5204048 is 0 bytes after a block of size 8 alloc'd
==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x400531: main (program.c:8)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 8 bytes in 1 blocks
==12345== indirectly lost: 0 bytes in 0 blocks
==12345== possibly lost: 0 bytes in 0 blocks
==12345== still reachable: 0 bytes in 0 blocks
==12345== suppressed: 0 bytes in 0 blocks
==12345==
==12345== For counts of detected and suppressed errors, rerun with: -v
==12345== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

AddressSanitizer

AddressSanitizer(ASan)是 GCC 和 Clang 提供的内存错误检测器,它使用编译时插桩和运行时库来检测内存错误。

AddressSanitizer 的优点

  • 运行速度快 - 比 Valgrind 快 2-5 倍
  • 内存开销小 - 通常只增加 2x-3x 的内存使用
  • 检测范围广 - 可以检测缓冲区溢出、使用已释放内存、双重释放等问题

使用 AddressSanitizer

1
2
3
4
5
6
7
8
# 使用 GCC 编译
cc -fsanitize=address -g program.c -o program

# 使用 Clang 编译
clang -fsanitize=address -g program.c -o program

# 运行程序
./program

AddressSanitizer 输出解读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000018 at pc 0x00000040073f bp 0x7ffc9f8a8d20 sp 0x7ffc9f8a8d10
WRITE of size 4 at 0x602000000018 thread T0
#0 0x40073e in main program.c:10
#1 0x7f5b1c8a183f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2083f)
#2 0x400568 in _start (program+0x400568)

0x602000000018 is located 0 bytes to the right of 8-byte region [0x602000000010,0x602000000018)
allocated by thread T0 here:
#0 0x7f5b1cd6a730 in __interceptor_malloc (/usr/lib/x86_64-linux-gnu/libasan.so.4+0xdeb30)
#1 0x400721 in main program.c:8
#2 0x7f5b1c8a183f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2083f)

SUMMARY: AddressSanitizer: heap-buffer-overflow program.c:10 in main
Shadow bytes around the buggy address:
0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c047fff8000: fa fa 00 00 00 00 00 00[fa]fa 00 00 00 00 00 00
0x0c047fff8010: fa fa 00 00 00 00 00 00 fa fa 00 00 00 00 00 00
0x0c047fff8020: fa fa 00 00 00 00 00 00 fa fa 00 00 00 00 00 00
0x0c047fff8030: fa fa 00 00 00 00 00 00 fa fa 00 00 00 00 00 00
0x0c047fff8040: fa fa 00 00 00 00 00 00 fa fa 00 00 00 00 00 00
0x0c047fff8050: fa fa 00 00 00 00 00 00 fa fa 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==12345==ABORTING

Dr. Memory

Dr. Memory 是一个跨平台的内存泄漏检测工具,适用于 Windows、Linux 和 macOS。

Dr. Memory 的特点

  • 跨平台 - 支持 Windows、Linux 和 macOS
  • 检测范围广 - 可以检测内存泄漏、缓冲区溢出、使用未初始化内存等问题
  • 易于使用 - 不需要修改代码或重新编译(Windows 上)

使用 Dr. Memory

1
2
3
4
5
6
7
8
# Windows 上使用
DrMemory.exe -- program.exe

# Linux 上使用
drmemory -- ./program

# macOS 上使用
drmemory -- ./program

Dr. Memory 输出解读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Dr. Memory version 2.3.1 build 1 built on Dec  5 2022 16:44:46
Dr. Memory results for pid 12345: "program"
Application cmdline: "program"
Recorded 165 suppression(s) from default C:\DrMemory\bin\suppress-default.txt

Error #1: UNINITIALIZED READ: reading register eax
# 0 main+0x123 (program.exe+0x123)
# 1 KERNEL32.dll+0x123456 (C:\Windows\system32\KERNEL32.dll+0x123456)

Error #2: LEAK: 20 direct bytes
# 0 malloc+0x123 (C:\DrMemory\bin\drmemory.dll+0x123456)
# 1 main+0x456 (program.exe+0x456)
# 2 KERNEL32.dll+0x123456 (C:\Windows\system32\KERNEL32.dll+0x123456)

No single-threaded leaks or GDI objects or handle leaks found.
Elapsed run time: 0.123 seconds

内存调试最佳实践

  1. 尽早使用调试工具 - 在开发初期就使用内存调试工具,而不是等到出现问题时
  2. 结合使用多种工具 - 不同工具检测的问题可能不同,结合使用可以发现更多问题
  3. 分析输出信息 - 仔细阅读工具的输出信息,理解问题的根本原因
  4. 修复所有警告 - 不要忽略工具报告的任何警告,它们可能是潜在的问题
  5. 编写测试用例 - 为内存操作编写专门的测试用例,确保内存操作的正确性

常见内存错误类型及修复

内存泄漏

症状:程序运行时间越长,内存使用量越大
修复方法:确保每个 malloc/calloc/realloc 都有对应的 free

缓冲区溢出

症状:程序崩溃或行为异常
修复方法:使用安全的字符串函数(如 snprintfstrncpy),检查数组访问边界

悬空指针

症状:程序崩溃或数据损坏
修复方法:释放内存后将指针设置为 NULL,使用指针前检查是否为 NULL

重复释放

症状:程序崩溃
修复方法:释放内存后将指针设置为 NULL,避免对同一个指针多次调用 free

使用未初始化内存

症状:程序行为不确定
修复方法:确保所有变量在使用前都已初始化

内存对齐错误

症状:程序崩溃(特别是在使用 SIMD 指令时)
修复方法:使用 aligned_alloc 分配对齐的内存,确保数据结构正确对齐

示例:动态数据结构

链表

链表是一种常用的动态数据结构,它由一系列节点组成,每个节点包含数据和指向下一个节点的指针。链表的特点是可以在任意位置高效地插入和删除元素。

单链表实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
#include <stdio.h>
#include <stdlib.h>

// 链表节点结构
typedef struct Node {
int data;
struct Node *next;
} Node;

// 创建新节点
Node *create_node(int data)
{
Node *new_node = (Node *)malloc(sizeof(Node));
if (new_node == NULL)
{
perror("内存分配失败");
exit(EXIT_FAILURE);
}
new_node->data = data;
new_node->next = NULL;
return new_node;
}

// 插入节点到链表头部
Node *insert_at_head(Node *head, int data)
{
Node *new_node = create_node(data);
new_node->next = head;
return new_node;
}

// 插入节点到链表尾部
Node *insert_at_tail(Node *head, int data)
{
Node *new_node = create_node(data);

if (head == NULL)
{
return new_node;
}

Node *current = head;
while (current->next != NULL)
{
current = current->next;
}
current->next = new_node;
return head;
}

// 在指定位置插入节点
Node *insert_at_position(Node *head, int data, size_t position)
{
if (position == 0)
{
return insert_at_head(head, data);
}

Node *new_node = create_node(data);
Node *current = head;

for (size_t i = 0; i < position - 1 && current != NULL; i++)
{
current = current->next;
}

if (current == NULL)
{
fprintf(stderr, "位置超出链表范围\n");
free(new_node);
return head;
}

new_node->next = current->next;
current->next = new_node;
return head;
}

// 删除链表头部节点
Node *delete_at_head(Node *head)
{
if (head == NULL)
{
return NULL;
}

Node *temp = head;
head = head->next;
free(temp);
return head;
}

// 删除链表尾部节点
Node *delete_at_tail(Node *head)
{
if (head == NULL)
{
return NULL;
}

if (head->next == NULL)
{
free(head);
return NULL;
}

Node *current = head;
while (current->next->next != NULL)
{
current = current->next;
}

free(current->next);
current->next = NULL;
return head;
}

// 删除指定值的节点
Node *delete_by_value(Node *head, int value)
{
if (head == NULL)
{
return NULL;
}

if (head->data == value)
{
Node *temp = head;
head = head->next;
free(temp);
return head;
}

Node *current = head;
while (current->next != NULL && current->next->data != value)
{
current = current->next;
}

if (current->next != NULL)
{
Node *temp = current->next;
current->next = current->next->next;
free(temp);
}

return head;
}

// 查找指定值的节点
Node *find_node(Node *head, int value)
{
Node *current = head;
while (current != NULL && current->data != value)
{
current = current->next;
}
return current;
}

// 计算链表长度
size_t list_length(Node *head)
{
size_t length = 0;
Node *current = head;
while (current != NULL)
{
length++;
current = current->next;
}
return length;
}

// 反转链表
Node *reverse_list(Node *head)
{
Node *prev = NULL;
Node *current = head;
Node *next = NULL;

while (current != NULL)
{
next = current->next;
current->next = prev;
prev = current;
current = next;
}

return prev;
}

// 打印链表
void print_list(Node *head)
{
Node *current = head;
while (current != NULL)
{
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}

// 释放链表内存
void free_list(Node *head)
{
Node *current = head;
while (current != NULL)
{
Node *next = current->next;
free(current);
current = next;
}
}

int main(void)
{
Node *head = NULL;

// 插入节点
head = insert_at_head(head, 3);
head = insert_at_head(head, 2);
head = insert_at_head(head, 1);
head = insert_at_tail(head, 4);
head = insert_at_tail(head, 5);
head = insert_at_position(head, 6, 2);

// 打印链表
printf("链表:");
print_list(head);
printf("链表长度:%zu\n", list_length(head));

// 查找节点
Node *node = find_node(head, 4);
if (node != NULL)
{
printf("找到节点:%d\n", node->data);
}
else
{
printf("未找到节点\n");
}

// 删除节点
head = delete_at_head(head);
printf("删除头部节点后:");
print_list(head);

head = delete_at_tail(head);
printf("删除尾部节点后:");
print_list(head);

head = delete_by_value(head, 3);
printf("删除值为 3 的节点后:");
print_list(head);

// 反转链表
head = reverse_list(head);
printf("反转链表后:");
print_list(head);

// 释放链表内存
free_list(head);

return 0;
}

双向链表实现

双向链表是一种更复杂的链表结构,每个节点包含指向前一个节点和后一个节点的指针,使得链表的遍历和操作更加灵活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#include <stdio.h>
#include <stdlib.h>

// 双向链表节点结构
typedef struct DNode {
int data;
struct DNode *prev;
struct DNode *next;
} DNode;

// 创建新节点
DNode *create_dnode(int data)
{
DNode *new_node = (DNode *)malloc(sizeof(DNode));
if (new_node == NULL)
{
perror("内存分配失败");
exit(EXIT_FAILURE);
}
new_node->data = data;
new_node->prev = NULL;
new_node->next = NULL;
return new_node;
}

// 插入节点到链表头部
DNode *insert_dhead(DNode *head, int data)
{
DNode *new_node = create_dnode(data);
if (head != NULL)
{
new_node->next = head;
head->prev = new_node;
}
return new_node;
}

// 插入节点到链表尾部
DNode *insert_dtail(DNode *head, int data)
{
DNode *new_node = create_dnode(data);

if (head == NULL)
{
return new_node;
}

DNode *current = head;
while (current->next != NULL)
{
current = current->next;
}

current->next = new_node;
new_node->prev = current;
return head;
}

// 打印链表(正向)
void print_dlist_forward(DNode *head)
{
DNode *current = head;
while (current != NULL)
{
printf("%d <-> ", current->data);
current = current->next;
}
printf("NULL\n");
}

// 打印链表(反向)
void print_dlist_backward(DNode *tail)
{
DNode *current = tail;
while (current != NULL)
{
printf("%d <-> ", current->data);
current = current->prev;
}
printf("NULL\n");
}

// 释放链表内存
void free_dlist(DNode *head)
{
DNode *current = head;
while (current != NULL)
{
DNode *next = current->next;
free(current);
current = next;
}
}

int main(void)
{
DNode *head = NULL;

// 插入节点
head = insert_dhead(head, 3);
head = insert_dhead(head, 2);
head = insert_dhead(head, 1);
head = insert_dtail(head, 4);
head = insert_dtail(head, 5);

// 打印链表
printf("正向遍历:");
print_dlist_forward(head);

// 找到尾部节点
DNode *tail = head;
while (tail->next != NULL)
{
tail = tail->next;
}

printf("反向遍历:");
print_dlist_backward(tail);

// 释放链表内存
free_dlist(head);

return 0;
}

动态数组

动态数组是一种可以根据需要自动调整大小的数组结构,它结合了数组的随机访问特性和链表的动态调整大小特性。

动态数组实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
#include <stdio.h>
#include <stdlib.h>

// 动态数组结构
typedef struct {
int *data;
size_t size;
size_t capacity;
} DynamicArray;

// 初始化动态数组
DynamicArray *create_array(size_t initial_capacity)
{
DynamicArray *array = (DynamicArray *)malloc(sizeof(DynamicArray));
if (array == NULL)
{
perror("内存分配失败");
exit(EXIT_FAILURE);
}

array->data = (int *)malloc(initial_capacity * sizeof(int));
if (array->data == NULL)
{
perror("内存分配失败");
free(array);
exit(EXIT_FAILURE);
}

array->size = 0;
array->capacity = initial_capacity;
return array;
}

// 调整动态数组大小
void resize_array(DynamicArray *array, size_t new_capacity)
{
int *new_data = (int *)realloc(array->data, new_capacity * sizeof(int));
if (new_data == NULL)
{
perror("内存分配失败");
exit(EXIT_FAILURE);
}

array->data = new_data;
array->capacity = new_capacity;
}

// 添加元素到动态数组
void add_element(DynamicArray *array, int element)
{
if (array->size >= array->capacity)
{
// 扩展数组容量(通常扩展为原来的 2 倍)
resize_array(array, array->capacity * 2);
}

array->data[array->size] = element;
array->size++;
}

// 在指定位置插入元素
void insert_element(DynamicArray *array, size_t index, int element)
{
if (index > array->size)
{
fprintf(stderr, "索引越界\n");
return;
}

if (array->size >= array->capacity)
{
resize_array(array, array->capacity * 2);
}

// 移动元素
for (size_t i = array->size; i > index; i--)
{
array->data[i] = array->data[i - 1];
}

array->data[index] = element;
array->size++;
}

// 删除指定位置的元素
void delete_element(DynamicArray *array, size_t index)
{
if (index >= array->size)
{
fprintf(stderr, "索引越界\n");
return;
}

// 移动元素
for (size_t i = index; i < array->size - 1; i++)
{
array->data[i] = array->data[i + 1];
}

array->size--;

// 如果数组大小小于容量的 1/4,缩小容量
if (array->size > 0 && array->size <= array->capacity / 4)
{
resize_array(array, array->capacity / 2);
}
}

// 获取动态数组元素
int get_element(DynamicArray *array, size_t index)
{
if (index >= array->size)
{
fprintf(stderr, "索引越界\n");
exit(EXIT_FAILURE);
}
return array->data[index];
}

// 设置动态数组元素
void set_element(DynamicArray *array, size_t index, int element)
{
if (index >= array->size)
{
fprintf(stderr, "索引越界\n");
return;
}
array->data[index] = element;
}

// 查找元素的索引
size_t find_element(DynamicArray *array, int element)
{
for (size_t i = 0; i < array->size; i++)
{
if (array->data[i] == element)
{
return i;
}
}
return (size_t)-1; // 未找到
}

// 清空动态数组
void clear_array(DynamicArray *array)
{
array->size = 0;
// 可以选择缩小容量以节省内存
resize_array(array, 1);
}

// 打印动态数组
void print_array(DynamicArray *array)
{
printf("动态数组:");
for (size_t i = 0; i < array->size; i++)
{
printf("%d ", array->data[i]);
}
printf("\n");
printf("大小:%zu, 容量:%zu\n", array->size, array->capacity);
}

// 释放动态数组内存
void free_array(DynamicArray *array)
{
free(array->data);
free(array);
}

int main(void)
{
DynamicArray *array = create_array(2);

// 添加元素
add_element(array, 1);
add_element(array, 2);
add_element(array, 3);
add_element(array, 4);
add_element(array, 5);

// 打印数组
print_array(array);

// 插入元素
insert_element(array, 2, 6);
printf("插入元素后:");
print_array(array);

// 删除元素
delete_element(array, 4);
printf("删除元素后:");
print_array(array);

// 获取元素
printf("索引 2 的元素:%d\n", get_element(array, 2));

// 设置元素
set_element(array, 0, 10);
printf("设置元素后:");
print_array(array);

// 查找元素
size_t index = find_element(array, 3);
if (index != (size_t)-1)
{
printf("元素 3 的索引:%zu\n", index);
}
else
{
printf("元素 3 不存在\n");
}

// 清空数组
clear_array(array);
printf("清空数组后:");
print_array(array);

// 释放数组内存
free_array(array);

return 0;
}

二叉树

二叉树是一种重要的树形数据结构,每个节点最多有两个子节点:左子节点和右子节点。二叉树在搜索、排序等领域有广泛的应用。

二叉搜索树实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#include <stdio.h>
#include <stdlib.h>

// 二叉树节点结构
typedef struct TreeNode {
int data;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;

// 创建新节点
TreeNode *create_node(int data)
{
TreeNode *new_node = (TreeNode *)malloc(sizeof(TreeNode));
if (new_node == NULL)
{
perror("内存分配失败");
exit(EXIT_FAILURE);
}
new_node->data = data;
new_node->left = NULL;
new_node->right = NULL;
return new_node;
}

// 插入节点到二叉搜索树
TreeNode *insert_node(TreeNode *root, int data)
{
if (root == NULL)
{
return create_node(data);
}

if (data < root->data)
{
root->left = insert_node(root->left, data);
}
else if (data > root->data)
{
root->right = insert_node(root->right, data);
}
// 数据已存在,不做处理

return root;
}

// 查找节点
TreeNode *find_node_tree(TreeNode *root, int data)
{
if (root == NULL || root->data == data)
{
return root;
}

if (data < root->data)
{
return find_node_tree(root->left, data);
}
else
{
return find_node_tree(root->right, data);
}
}

// 查找最小节点
TreeNode *find_min_node(TreeNode *root)
{
TreeNode *current = root;
while (current != NULL && current->left != NULL)
{
current = current->left;
}
return current;
}

// 删除节点
TreeNode *delete_node(TreeNode *root, int data)
{
if (root == NULL)
{
return NULL;
}

if (data < root->data)
{
root->left = delete_node(root->left, data);
}
else if (data > root->data)
{
root->right = delete_node(root->right, data);
}
else
{
// 找到要删除的节点

// 情况 1:叶子节点
if (root->left == NULL && root->right == NULL)
{
free(root);
return NULL;
}
// 情况 2:只有一个子节点
else if (root->left == NULL)
{
TreeNode *temp = root->right;
free(root);
return temp;
}
else if (root->right == NULL)
{
TreeNode *temp = root->left;
free(root);
return temp;
}
// 情况 3:有两个子节点
else
{
// 找到右子树中的最小节点
TreeNode *temp = find_min_node(root->right);
// 用最小节点的值替换当前节点的值
root->data = temp->data;
// 删除右子树中的最小节点
root->right = delete_node(root->right, temp->data);
}
}

return root;
}

// 前序遍历
void preorder_traversal(TreeNode *root)
{
if (root != NULL)
{
printf("%d ", root->data);
preorder_traversal(root->left);
preorder_traversal(root->right);
}
}

// 中序遍历
void inorder_traversal(TreeNode *root)
{
if (root != NULL)
{
inorder_traversal(root->left);
printf("%d ", root->data);
inorder_traversal(root->right);
}
}

// 后序遍历
void postorder_traversal(TreeNode *root)
{
if (root != NULL)
{
postorder_traversal(root->left);
postorder_traversal(root->right);
printf("%d ", root->data);
}
}

// 计算树的高度
int tree_height(TreeNode *root)
{
if (root == NULL)
{
return 0;
}

int left_height = tree_height(root->left);
int right_height = tree_height(root->right);

return (left_height > right_height) ? left_height + 1 : right_height + 1;
}

// 释放树的内存
void free_tree(TreeNode *root)
{
if (root != NULL)
{
free_tree(root->left);
free_tree(root->right);
free(root);
}
}

int main(void)
{
TreeNode *root = NULL;

// 插入节点
root = insert_node(root, 50);
root = insert_node(root, 30);
root = insert_node(root, 20);
root = insert_node(root, 40);
root = insert_node(root, 70);
root = insert_node(root, 60);
root = insert_node(root, 80);

// 遍历树
printf("前序遍历:");
preorder_traversal(root);
printf("\n");

printf("中序遍历:");
inorder_traversal(root);
printf("\n");

printf("后序遍历:");
postorder_traversal(root);
printf("\n");

// 计算树的高度
printf("树的高度:%d\n", tree_height(root));

// 查找节点
TreeNode *node = find_node_tree(root, 40);
if (node != NULL)
{
printf("找到节点:%d\n", node->data);
}
else
{
printf("未找到节点\n");
}

// 删除节点
root = delete_node(root, 30);
printf("删除节点 30 后中序遍历:");
inorder_traversal(root);
printf("\n");

// 释放树的内存
free_tree(root);

return 0;
}

小结

本章深入介绍了 C 语言的内存管理,涵盖了从基础概念到高级技术的全面内容。内存管理是 C 语言编程的核心部分,直接影响程序的正确性、性能和安全性。

主要内容总结

  1. 内存区域 - 详细解释了 C 程序的内存布局,包括代码区、全局/静态区(.data 和 .bss 段)、常量区、栈区和堆区,以及它们的特点和用途。

  2. 静态内存和自动内存 - 分析了静态内存(全局变量、静态变量)和自动内存(局部变量、函数参数)的分配机制、生命周期和作用域。

  3. 动态内存分配 - 详细介绍了 malloccallocreallocfreealigned_alloc 等内存分配函数的使用方法、参数含义和返回值处理。

  4. 常见内存问题 - 深入分析了内存泄漏、悬空指针、重复释放、缓冲区溢出和内存碎片等常见内存问题的原因、危害和预防措施。

  5. 内存管理最佳实践 - 提供了一系列内存管理的最佳实践,包括检查内存分配结果、及时释放内存、使用合适的内存分配函数、避免使用已释放的内存、避免缓冲区溢出等。

  6. 内存分配高级技巧 - 介绍了内存池、对齐内存分配、自定义内存分配器和内存映射等高级内存管理技术,以及它们的应用场景和实现方法。

  7. 内存调试工具 - 详细讲解了 Valgrind、AddressSanitizer 和 Dr. Memory 等内存调试工具的使用方法、输出解读和最佳实践。

  8. 动态数据结构 - 通过链表(单链表、双向链表)、动态数组和二叉搜索树等示例,展示了如何使用动态内存管理实现复杂的数据结构。

学习成果

通过学习本章内容,你应该能够:

  • 理解内存布局 - 掌握 C 程序的内存布局和各个区域的特点
  • 熟练使用动态内存 - 正确使用内存分配和释放函数,避免常见的内存错误
  • 识别和解决内存问题 - 能够识别和修复内存泄漏、缓冲区溢出等内存问题
  • 应用高级内存管理技术 - 根据需要使用内存池、对齐内存分配等高级技术
  • 使用内存调试工具 - 熟练使用 Valgrind、AddressSanitizer 等工具检测内存问题
  • 实现动态数据结构 - 能够使用动态内存管理实现链表、动态数组、二叉树等数据结构
  • 优化内存使用 - 能够优化程序的内存使用,提高程序性能

实践建议

  1. 编写内存安全的代码 - 始终检查内存分配结果,及时释放内存,避免使用已释放的内存
  2. 使用内存调试工具 - 在开发过程中使用内存调试工具检测内存问题,不要等到问题出现后再调试
  3. 学习内存管理模式 - 掌握引用计数、内存池等内存管理模式,根据具体场景选择合适的管理方式
  4. 优化内存使用 - 关注程序的内存使用情况,优化数据结构和算法,减少内存消耗
  5. 阅读优秀代码 - 学习开源项目中内存管理的最佳实践,提高自己的编程水平

内存管理是 C 语言编程的重要挑战,也是区分优秀程序员和普通程序员的关键因素之一。通过不断学习和实践,你将能够掌握内存管理的精髓,编写出更加高效、稳定和安全的 C 语言程序。

在后续章节中,我们将学习文件输入/输出,这是 C 语言中另一个重要的主题,它与内存管理密切相关,需要合理使用内存来处理文件数据。