第10章 内存管理 内存的基本概念 内存区域 C 程序在运行时使用的内存通常分为以下几个区域,每个区域都有其特定的用途和特点:
代码区(Text Segment)
存储程序的可执行指令 通常是只读的,防止程序意外修改指令 大小在编译时确定 包含函数体的二进制机器码 全局/静态区(Data Segment)
存储全局变量和静态变量 分为初始化部分(.data)和未初始化部分(.bss) .data 段:存储已初始化的全局变量和静态变量 .bss 段:存储未初始化的全局变量和静态变量,会被自动初始化为 0 大小在编译时确定 常量区(Constant Segment)
存储字符串字面量和其他常量 通常是只读的 例如:"Hello, World!" 存储在常量区 栈区(Stack)
存储局部变量、函数参数和返回地址 自动分配和释放,由编译器管理 后进先出(LIFO)结构 大小通常较小(几兆字节) 栈溢出会导致程序崩溃 堆区(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 int global_var = 10 ;int uninitialized_global;void function (void ) { static int static_var = 20 ; static_var++; printf ("static_var: %d\n" , static_var); } int main (void ) { function(); function(); function(); printf ("uninitialized_global: %d\n" , uninitialized_global); return 0 ; }
自动内存 自动内存是在函数执行时在栈上分配的内存,函数返回时自动释放。自动内存包括:
局部变量 - 在函数内部声明的变量(默认存储类别为 auto)函数参数 - 传递给函数的参数临时变量 - 表达式计算过程中创建的临时变量自动内存的特点 自动管理 - 函数执行时分配,函数返回时自动释放生命周期短 - 仅在函数执行期间存在空间有限 - 栈空间通常较小(几兆字节)高效 - 栈操作(压栈/弹栈)非常快后进先出(LIFO) - 最后分配的内存最先释放无需手动管理 - 由编译器自动处理内存分配和释放1 2 3 4 5 6 7 8 9 10 11 12 void function (int param) { int local_var = 10 ; auto int auto_var = 20 ; 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 ]; printf ("Array address: %p\n" , big_array); } int main (void ) { large_local_variable(); return 0 ; }
静态内存 vs 自动内存 特性 静态内存 自动内存 分配时机 编译时 函数执行时 释放时机 程序结束时 函数返回时 存储位置 全局/静态区 栈区 空间大小 编译时确定,通常较大 运行时确定,通常较小 初始化 自动初始化为 0 未初始化(值不确定) 访问速度 较快 非常快 作用域 全局或局部 局部 管理方式 自动管理 自动管理 适用场景 全局配置、共享状态 临时变量、函数参数
动态内存分配 动态内存是在程序运行时在堆上分配的内存,由程序员手动管理。动态内存分配允许程序根据运行时的需要灵活地分配和释放内存,是 C 语言中实现动态数据结构(如链表、树、图等)的基础。
内存分配函数 C 语言提供了以下动态内存分配函数,它们都在 <stdlib.h> 头文件中声明:
malloc - 分配指定大小的未初始化内存calloc - 分配指定数量和大小的内存,并初始化为 0realloc - 调整已分配内存的大小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 ) { int *ptr = (int *)calloc (5 , sizeof (int )); if (ptr == NULL ) { perror("内存分配失败" ); return EXIT_FAILURE; } 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内存调整机制 如果 ptr 为 NULL,realloc 等同于 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 函数用于释放由 malloc、calloc、realloc 或 aligned_alloc 分配的内存。
函数原型 参数 使用注意事项 只能释放由动态内存分配函数分配的内存 释放后应将指针设置为 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 ) { 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 )); } void conditional_leak (int condition) { int *ptr = (int *)malloc (sizeof (int )); if (condition) { printf ("条件为真\n" ); free (ptr); return ; } printf ("条件为假\n" ); } void exception_leak (void ) { int *ptr1 = (int *)malloc (sizeof (int )); int *ptr2 = (int *)malloc (sizeof (int )); if (ptr1 == NULL || ptr2 == NULL ) { 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 int *ptr = (int *)malloc (sizeof (int ));*ptr = 10 ; free (ptr);*ptr = 20 ; int *get_local_address (void ) { int local_var = 10 ; return &local_var; } int main (void ) { int *ptr = get_local_address(); *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); int *ptr = (int *)malloc (sizeof (int ));if (ptr != NULL ){ free (ptr); } 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++) { array [i] = i; } void vulnerable_function (void ) { char buffer[100 ]; printf ("请输入字符串:" ); gets(buffer); }
内存碎片 内存碎片是指频繁分配和释放小块内存导致的内存空间浪费,可能导致虽然总内存充足但无法分配大块连续内存的情况。
内存碎片的类型 内部碎片 - 分配的内存块大于实际需要的大小外部碎片 - 内存中存在许多小的空闲块,无法满足大块内存的分配请求内存碎片的危害 内存利用率下降 - 大量小的空闲块无法使用分配失败 - 无法分配大块连续内存,即使总内存充足程序性能下降 - 内存分配和释放的时间增加内存碎片示例 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 ]; for (int i = 0 ; i < 100 ; i++) { ptrs[i] = malloc (16 ); } for (int i = 1 ; i < 100 ; i += 2 ) { free (ptrs[i]); } 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 ; 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 (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 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 sudo apt install valgrindvalgrind --leak-check=full ./program
AddressSanitizer AddressSanitizer(ASan)是 GCC 和 Clang 提供的内存错误检测器。
1 2 3 4 5 gcc -fsanitize=address -g program.c -o program ./program
Dr. Memory Dr. Memory 是一个跨平台的内存泄漏检测工具,适用于 Windows、Linux 和 macOS。
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 ) { free_int_ptr(&arr); } 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 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) { 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) { 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 ) { 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 static char memory_pool[POOL_SIZE];static size_t pool_used = 0 ;static void *allocation_list[1024 ];static size_t allocation_count = 0 ;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; } 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 sudo apt install valgrindsudo yum install valgrindbrew 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 cc -fsanitize=address -g program.c -o program 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 DrMemory.exe -- program.exe drmemory -- ./program 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
内存调试最佳实践 尽早使用调试工具 - 在开发初期就使用内存调试工具,而不是等到出现问题时结合使用多种工具 - 不同工具检测的问题可能不同,结合使用可以发现更多问题分析输出信息 - 仔细阅读工具的输出信息,理解问题的根本原因修复所有警告 - 不要忽略工具报告的任何警告,它们可能是潜在的问题编写测试用例 - 为内存操作编写专门的测试用例,确保内存操作的正确性常见内存错误类型及修复 内存泄漏 症状 :程序运行时间越长,内存使用量越大修复方法 :确保每个 malloc/calloc/realloc 都有对应的 free
缓冲区溢出 症状 :程序崩溃或行为异常修复方法 :使用安全的字符串函数(如 snprintf、strncpy),检查数组访问边界
悬空指针 症状 :程序崩溃或数据损坏修复方法 :释放内存后将指针设置为 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) { 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--; 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 { if (root->left == NULL && root->right == NULL ) { free (root); return NULL ; } 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; } 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 语言编程的核心部分,直接影响程序的正确性、性能和安全性。
主要内容总结 内存区域 - 详细解释了 C 程序的内存布局,包括代码区、全局/静态区(.data 和 .bss 段)、常量区、栈区和堆区,以及它们的特点和用途。
静态内存和自动内存 - 分析了静态内存(全局变量、静态变量)和自动内存(局部变量、函数参数)的分配机制、生命周期和作用域。
动态内存分配 - 详细介绍了 malloc、calloc、realloc、free 和 aligned_alloc 等内存分配函数的使用方法、参数含义和返回值处理。
常见内存问题 - 深入分析了内存泄漏、悬空指针、重复释放、缓冲区溢出和内存碎片等常见内存问题的原因、危害和预防措施。
内存管理最佳实践 - 提供了一系列内存管理的最佳实践,包括检查内存分配结果、及时释放内存、使用合适的内存分配函数、避免使用已释放的内存、避免缓冲区溢出等。
内存分配高级技巧 - 介绍了内存池、对齐内存分配、自定义内存分配器和内存映射等高级内存管理技术,以及它们的应用场景和实现方法。
内存调试工具 - 详细讲解了 Valgrind、AddressSanitizer 和 Dr. Memory 等内存调试工具的使用方法、输出解读和最佳实践。
动态数据结构 - 通过链表(单链表、双向链表)、动态数组和二叉搜索树等示例,展示了如何使用动态内存管理实现复杂的数据结构。
学习成果 通过学习本章内容,你应该能够:
理解内存布局 - 掌握 C 程序的内存布局和各个区域的特点熟练使用动态内存 - 正确使用内存分配和释放函数,避免常见的内存错误识别和解决内存问题 - 能够识别和修复内存泄漏、缓冲区溢出等内存问题应用高级内存管理技术 - 根据需要使用内存池、对齐内存分配等高级技术使用内存调试工具 - 熟练使用 Valgrind、AddressSanitizer 等工具检测内存问题实现动态数据结构 - 能够使用动态内存管理实现链表、动态数组、二叉树等数据结构优化内存使用 - 能够优化程序的内存使用,提高程序性能实践建议 编写内存安全的代码 - 始终检查内存分配结果,及时释放内存,避免使用已释放的内存使用内存调试工具 - 在开发过程中使用内存调试工具检测内存问题,不要等到问题出现后再调试学习内存管理模式 - 掌握引用计数、内存池等内存管理模式,根据具体场景选择合适的管理方式优化内存使用 - 关注程序的内存使用情况,优化数据结构和算法,减少内存消耗阅读优秀代码 - 学习开源项目中内存管理的最佳实践,提高自己的编程水平内存管理是 C 语言编程的重要挑战,也是区分优秀程序员和普通程序员的关键因素之一。通过不断学习和实践,你将能够掌握内存管理的精髓,编写出更加高效、稳定和安全的 C 语言程序。
在后续章节中,我们将学习文件输入/输出,这是 C 语言中另一个重要的主题,它与内存管理密切相关,需要合理使用内存来处理文件数据。