【C语言程序设计】第32篇:动态内存管理

发布时间:2026/7/28 2:56:40

【C语言程序设计】第32篇:动态内存管理 1 引言考虑这样一个需求程序需要处理用户输入的一组整数但事先不知道有多少个。如果用数组必须事先指定大小要么浪费空间要么空间不足。cint arr[1000]; /* 可能太大浪费也可能太小不够 */动态内存分配可以解决这个问题c#include stdio.h #include stdlib.h int main(void) { int n; printf(请输入要存储的数字个数); scanf(%d, n); /* 根据输入动态分配内存 */ int *arr (int*)malloc(n * sizeof(int)); if (arr NULL) { printf(内存分配失败\n); return 1; } /* 使用内存 */ for (int i 0; i n; i) { arr[i] i * i; } /* 释放内存 */ free(arr); return 0; }这就是动态内存管理的基本模式分配 → 使用 → 释放。本章我们将深入探讨这些操作背后的机制。2 堆与栈的区别2.1 两个重要的内存区域C程序的内存布局主要分为几个区域其中与动态内存最相关的是栈和堆特性栈Stack堆Heap分配方式自动分配和释放手动分配和释放malloc/free存放内容局部变量、函数参数、返回地址程序员动态分配的内存大小限制较小通常几MB较大可达几GB受系统内存限制分配速度非常快移动栈指针较慢需要查找空闲块生命周期函数执行期间从malloc到free碎片问题无长时间运行可能产生碎片2.2 栈的特点cvoid func(void) { int a 10; /* 栈上分配 */ int b 20; /* 栈上分配 */ char str[100]; /* 栈上分配固定大小 */ } /* 函数结束a、b、str 自动释放 */栈内存由系统自动管理分配释放速度极快大小固定过大的局部变量可能导致栈溢出2.3 堆的特点cvoid func(void) { int *p (int*)malloc(100 * sizeof(int)); /* 堆上分配 */ if (p ! NULL) { /* 使用内存 */ free(p); /* 必须手动释放 */ } } /* 如果忘记freep指向的内存仍然存在内存泄漏 */堆内存需要程序员手动管理可以分配任意大小受系统限制需要手动释放否则造成内存泄漏2.4 什么时候用堆数据大小在编译时未知需要分配大块内存数据需要在函数返回后继续存在需要动态调整内存大小3 malloc 函数3.1 函数原型c#include stdlib.h void *malloc(size_t size);功能在堆上分配size字节的内存参数要分配的字节数返回值成功返回指向分配内存的指针失败返回NULL分配的内存内容未初始化包含垃圾值3.2 基本使用c#include stdio.h #include stdlib.h int main(void) { /* 分配能存放10个int的内存 */ int *p (int*)malloc(10 * sizeof(int)); /* 总是检查分配是否成功 */ if (p NULL) { printf(内存分配失败\n); return 1; } /* 使用内存 */ for (int i 0; i 10; i) { p[i] i; /* 注意这里p[i]的值是垃圾值先赋值再使用 */ } /* 释放内存 */ free(p); return 0; }3.3 常见错误c/* 错误1忘记检查返回值 */ int *p malloc(1000000000); /* 可能分配失败 */ p[0] 10; /* 如果p为NULL程序崩溃 */ /* 错误2分配大小错误 */ int *p malloc(10); /* 只分配了10字节但需要40字节10*sizeof(int) */ /* 错误3使用未初始化的内存 */ int *p malloc(10 * sizeof(int)); printf(%d\n, p[0]); /* 垃圾值必须先初始化 */4 calloc 函数4.1 函数原型c#include stdlib.h void *calloc(size_t nmemb, size_t size);功能分配nmemb * size字节的内存并将所有位初始化为 0参数nmemb元素个数size每个元素的大小返回值成功返回指针失败返回 NULL4.2 基本使用c#include stdio.h #include stdlib.h int main(void) { /* 分配能存放10个int的内存并全部初始化为0 */ int *p (int*)calloc(10, sizeof(int)); if (p NULL) { printf(内存分配失败\n); return 1; } /* 内存已经初始化为0 */ for (int i 0; i 10; i) { printf(%d , p[i]); /* 全部输出0 */ } printf(\n); free(p); return 0; }4.3 malloc 与 calloc 对比对比malloccalloc参数一个总字节数两个个数和每个大小初始化不初始化垃圾值初始化为0性能稍快不需要清零稍慢需要清零使用场景马上要赋值的数组需要初始化为0的情况c/* 两种方式等价分配并清零 */ int *p1 malloc(10 * sizeof(int)); memset(p1, 0, 10 * sizeof(int)); int *p2 calloc(10, sizeof(int)); /* 更简洁 */5 realloc 函数5.1 函数原型c#include stdlib.h void *realloc(void *ptr, size_t new_size);功能调整之前分配的内存块的大小参数ptr之前分配的内存指针new_size新的大小字节返回值成功返回指向新内存的指针失败返回 NULL原内存不变5.2 行为规则情况行为ptr为 NULL等价于malloc(new_size)new_size为 0等价于free(ptr)返回 NULL扩大内存可能移动内存位置原有数据保留缩小内存多余部分被释放剩余数据保留5.3 基本使用c#include stdio.h #include stdlib.h int main(void) { /* 初始分配5个int */ int *arr (int*)malloc(5 * sizeof(int)); if (arr NULL) return 1; for (int i 0; i 5; i) { arr[i] i; } /* 需要扩大到10个int */ int *new_arr (int*)realloc(arr, 10 * sizeof(int)); if (new_arr NULL) { /* 分配失败原内存还在需要处理错误 */ free(arr); return 1; } /* 更新指针 */ arr new_arr; /* 继续使用 */ for (int i 5; i 10; i) { arr[i] i; } free(arr); return 0; }5.4 重要注意事项c/* 错误用法直接使用原指针 */ arr realloc(arr, 20 * sizeof(int)); /* 如果失败arr 变成 NULL原内存丢失 */ /* 正确用法使用临时变量 */ int *tmp realloc(arr, 20 * sizeof(int)); if (tmp ! NULL) { arr tmp; /* 成功才更新指针 */ } else { /* 处理失败arr 仍然指向原内存 */ }6 free 函数6.1 函数原型c#include stdlib.h void free(void *ptr);功能释放之前动态分配的内存参数ptr必须是之前由 malloc/calloc/realloc 返回的指针返回值无6.2 基本使用cint *p (int*)malloc(10 * sizeof(int)); /* 使用 p */ free(p); /* 释放内存 */ p NULL; /* 好习惯置空防止野指针 */6.3 重要规则c/* 规则1只能释放动态分配的内存 */ int a; free(a); /* 错误不是动态分配的 */ /* 规则2不能重复释放 */ int *p malloc(100); free(p); free(p); /* 错误重复释放 */ /* 规则3释放后不能使用 */ int *p malloc(100); free(p); *p 10; /* 错误悬空指针未定义行为 */7 内存泄漏7.1 什么是内存泄漏内存泄漏Memory Leak是指程序动态分配的内存不再使用后没有释放导致这部分内存无法被回收和重用。cvoid leak_example(void) { int *p (int*)malloc(100 * sizeof(int)); /* 忘记调用 free(p) */ } /* 函数返回p 丢失100*sizeof(int) 字节的内存泄漏 */7.2 内存泄漏的后果程序占用的内存不断增长系统可用内存减少长期运行的程序可能耗尽内存而崩溃对于服务器程序尤其致命7.3 常见的内存泄漏场景场景1忘记释放cvoid func(void) { int *p malloc(1000); /* 使用 p但最后没有 free */ }场景2丢失指针cint *p malloc(100); p malloc(200); /* 第一个100字节的指针丢失无法释放 */ free(p); /* 只释放了第二个 */场景3异常路径cint *p malloc(100); if (some_condition) { return; /* 直接返回忘记释放 */ } free(p);场景4循环内分配cfor (int i 0; i 1000; i) { int *p malloc(1000); /* 使用 p但忘记释放 */ } /* 循环1000次泄漏1MB */7.4 如何避免内存泄漏原则1谁分配谁释放明确每个动态内存的责任人在分配后立即考虑释放时机原则2成对出现c/* malloc 和 free 应该成对出现 */ int *p malloc(...); /* ... */ free(p);原则3使用后立即释放c{ int *p malloc(...); /* 使用 p */ free(p); /* 用完就释放 */ }原则4异常路径处理cint *p malloc(100); if (p NULL) return -1; if (some_condition) { free(p); /* 释放后返回 */ return -1; } /* 正常处理 */ free(p); return 0;原则5释放后置空cfree(p); p NULL; /* 防止悬空指针 */8 内存泄漏检测8.1 人工检查代码审查检查 malloc/calloc/realloc 与 free 是否成对静态分析工具如 cppcheck, splint8.2 使用 ValgrindLinuxValgrind 是 Linux 下强大的内存调试工具bash# 编译程序建议加 -g 选项包含调试信息 gcc -g program.c -o program # 用 valgrind 运行 valgrind --leak-checkfull ./program示例输出text12345 100 bytes in 1 blocks are definitely lost in loss record 1 of 1 12345 at 0x4C2B800: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) 12345 by 0x40054E: main (test.c:5)8.3 使用 AddressSanitizerGCC/ClangAddressSanitizer 是编译器的内存错误检测工具bash# 编译时添加 -fsanitizeaddress 选项 gcc -g -fsanitizeaddress program.c -o program # 运行程序会直接报告内存错误 ./program8.4 简单的手动追踪c#include stdio.h #include stdlib.h /* 简单的调试宏 */ #define DEBUG_MALLOC(size) \ ({ void *p malloc(size); \ printf(malloc %zu bytes at %p\n, size, p); \ p; }) #define DEBUG_FREE(p) \ do { printf(free %p\n, p); free(p); } while(0) int main(void) { int *p DEBUG_MALLOC(100 * sizeof(int)); /* ... */ DEBUG_FREE(p); return 0; }9 综合示例动态数组c#include stdio.h #include stdlib.h typedef struct { int *data; int size; int capacity; } DynamicArray; /* 初始化动态数组 */ void array_init(DynamicArray *arr, int initial_capacity) { arr-data (int*)malloc(initial_capacity * sizeof(int)); arr-size 0; arr-capacity arr-data ? initial_capacity : 0; } /* 添加元素 */ int array_push(DynamicArray *arr, int value) { if (arr-size arr-capacity) { /* 需要扩容 */ int new_capacity arr-capacity 0 ? 4 : arr-capacity * 2; int *new_data (int*)realloc(arr-data, new_capacity * sizeof(int)); if (new_data NULL) { return -1; /* 扩容失败 */ } arr-data new_data; arr-capacity new_capacity; } arr-data[arr-size] value; return 0; } /* 释放数组 */ void array_destroy(DynamicArray *arr) { free(arr-data); arr-data NULL; arr-size arr-capacity 0; } /* 打印数组 */ void array_print(const DynamicArray *arr) { printf([); for (int i 0; i arr-size; i) { printf(%d , arr-data[i]); } printf(]\n); } int main(void) { DynamicArray arr; array_init(arr, 4); if (arr.data NULL) { printf(初始化失败\n); return 1; } /* 添加10个元素会触发自动扩容 */ for (int i 0; i 10; i) { array_push(arr, i * i); } array_print(arr); printf(size%d, capacity%d\n, arr.size, arr.capacity); /* 释放内存 */ array_destroy(arr); return 0; }10 本章小结本章系统介绍了动态内存管理1. 堆与栈的区别栈自动管理速度快大小有限堆手动管理灵活大小几乎不限2. 动态内存函数函数功能特点malloc分配内存不初始化速度快calloc分配并清零初始化为0稍慢realloc调整大小可能移动内存free释放内存必须配对使用3. 使用规则总是检查返回值是否为 NULL分配大小要正确计算n * sizeof(type)释放后置空指针不要重复释放只释放动态分配的内存4. 内存泄漏忘记释放不再使用的内存丢失指向内存的指针异常路径未释放长期运行程序危害严重5. 检测方法代码审查ValgrindLinuxAddressSanitizer手动追踪6. 最佳实践谁分配谁释放释放后置空异常路径处理封装管理逻辑如动态数组

相关新闻