泊头市做网站价格,响应式模板网站建设,学生兼职网站开发,镇江建设网站动态内存管理1. 为什么存在动态内存分配2. 动态内存函数的介绍2.1 malloc 和 freemalloc 函数free 函数2.2内存泄漏2.3 calloc2.4 realloc3. 常见的动态内存错误3.1 对NULL指针的解引用操作3.2 对动态开辟空间的越界访问3.3 对非动态开辟内存使用free释放3.4 使用free释放一块…
动态内存管理1. 为什么存在动态内存分配2. 动态内存函数的介绍2.1 malloc 和 freemalloc 函数free 函数2.2内存泄漏2.3 calloc2.4 realloc3. 常见的动态内存错误3.1 对NULL指针的解引用操作3.2 对动态开辟空间的越界访问3.3 对非动态开辟内存使用free释放3.4 使用free释放一块动态开辟内存的一部分3.5 对同一块动态内存多次释放3.6 动态开辟内存忘记释放内存泄漏4. 几个经典的笔试题4.1 题分析4.2 题分析4.3 题分析4.4 题分析5. 柔性数组Flexible Array5.1 柔性数组的特点5.2 柔性数组的使用5.3 柔性数组的优势1. 为什么存在动态内存分配
已掌握的内存开辟方式及局限 栈上开辟的空间如int val 20;是在栈上分配4个字节char arr[10] {0};是在栈上分配10个字节的连续空间。这些方式有明显局限 空间大小固定比如char arr[10]只能开辟10个字节无法根据程序运行时的需求改变大小而且栈空间通常有限不能开辟过大的空间。数组在声明时必须指定长度像int n; scanf(%d, n); char arr[n];这种在C99之前是不允许的因为数组的长度需要在编译时确定而程序运行时才能知道的长度无法通过这种方式开辟空间。 实际开发中很多场景下空间大小只有在程序运行时才能确定例如根据用户输入的数字来决定需要存储多少个数据这时候静态开辟空间的方式就无法满足需求动态内存分配应运而生。
2. 动态内存函数的介绍
2.1 malloc 和 free
malloc 函数
malloc函数 函数原型void* malloc(size_t size);的作用是向内存的堆区申请一块连续可用的空间。 如果开辟成功则返回一个指向开辟好空间的指针。如果开辟失败则返回一个NULL指针因此malloc的返回值一定要做检查。返回值的类型是 void* 所以malloc函数并不知道开辟空间的类型具体在使用的时候使用者自己来决定。size_t size 表示要分配的内存块的大小以字节为单位。如果参数size 为0malloc的行为是标准是未定义的取决于编译器 示例1正常开辟
// 申请可以存储5个int类型数据的空间int占4字节所以总共申请5*420字节
int* p (int*)malloc(5 * sizeof(int));
// 必须检查开辟是否成功因为当内存不足时malloc会返回NULL
if (p NULL) {// 打印错误信息perror会在字符串后加上具体的错误原因perror(malloc failed);return 1; // 开辟失败退出程序
}
// 成功开辟后使用空间给每个元素赋值
for (int i 0; i 5; i) {p[i] i * 10;
}示例 2开辟失败
// 申请1000000000个int类型的空间可能因内存不足导致失败
int* p (int*)malloc(1000000000 * sizeof(int));
if (p NULL) {perror(malloc failed); // 可能输出malloc failed: Not enough spacereturn 1;
}特性总结 开辟成功返回指向该空间的指针由于返回类型是void*所以需要根据实际存储的数据类型进行强制类型转换比如存储int类型就转为int*。开辟失败返回 NULL 指针所以使用前必须检查返回值是否为 NULL。当size为 0 时C 语言标准没有定义其行为不同的编译器可能有不同的处理方式有的可能返回 NULL有的可能返回一块很小的空间实际开发中应避免这种情况。
free 函数
函数原型void free(void* ptr);专门用于释放动态开辟的内存将内存归还给系统。
示例
int* p (int*)malloc(5 * sizeof(int));
if (p NULL) {perror(malloc failed);return 1;
}
// 使用空间...
free(p); // 释放p指向的动态内存此时这块内存归还给系统不能再使用
p NULL; // 释放后将指针置为NULL避免成为野指针野指针指向的内存已无效使用会导致不可预期的错误特性总结 只能释放动态开辟的内存比如int a 10; int* p a; free(p);这种释放栈上空间的行为是未定义的可能导致程序崩溃。当ptr是 NULL 指针时free 函数什么也不做所以释放后将指针置为NULL是安全的。 malloc和free都声明在 stdlib.h 头文件中
2.2内存泄漏
定义 动态开辟的内存没有通过 free 释放并且指向该内存的指针也丢失了导致系统无法回收这块内存这就是内存泄漏。
示例 1忘记释放
void test() {int* p (int*)malloc(100);// 使用p后没有调用free(p)函数结束后p被销毁再也无法找到这块内存导致内存泄漏
}
int main() {test();// 程序运行期间test函数中申请的100字节内存一直未被释放return 0;
}示例 2指针被修改导致无法释放 int* p (int*)malloc(100);
p; // 指针指向了动态开辟空间的第二个字节不再指向起始位置
free(p); // 错误无法释放因为free需要指向动态开辟空间的起始地址同时原起始地址丢失导致内存泄漏危害内存泄漏不会导致程序立即崩溃但如果程序长期运行如服务器程序、嵌入式程序随着时间的推移泄漏的内存会越来越多最终会耗尽系统内存导致程序运行缓慢甚至崩溃。预防 动态内存使用完毕后及时调用 free 函数释放并将指针置为 NULL。在函数中申请的动态内存要确保在函数返回前释放或者将指针传递出去由外部释放。避免在释放内存前修改指针的指向如果需要移动指针操作先保存起始地址。
2.3 calloc
函数原型void* calloc(size_t num, size_t size);其功能是为num个大小为size的元素开辟一块空间并且会将这块空间的每个字节都初始化为0。与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。示例与malloc对比
// 使用calloc申请3个int类型的空间
int* p1 (int*)calloc(3, sizeof(int));
// 使用malloc申请3个int类型的空间
int* p2 (int*)malloc(3 * sizeof(int));
if (p1 NULL || p2 NULL) {perror(malloc/calloc failed);return 1;
}
// 打印空间中的值
printf(calloc初始化后的值);
for (int i 0; i 3; i) {printf(%d , p1[i]); // 输出0 0 0因为calloc会初始化
}
printf(\nmalloc初始化后的值);
for (int i 0; i 3; i) {printf(%d , p2[i]); // 输出随机值因为malloc不会初始化
}
// 释放空间
free(p1);
p1 NULL;
free(p2);
p2 NULL;输出结果
与 malloc 的区别 参数不同calloc 需要两个参数分别是元素的个数和每个元素的大小malloc 只需要一个参数即总共需要开辟的字节数。初始化不同calloc 会将申请的空间每个字节都初始化为 0malloc 不会初始化空间中的值是随机的取决于内存中之前存储的数据。 适用场景当需要申请一块初始化为 0 的动态内存时使用 calloc 更方便避免了使用 malloc 后再调用 memset 进行初始化的步骤。
2.4 realloc
函数原型void* realloc(void* ptr, size_t size);用于调整已经动态开辟的内存空间的大小ptr是指向原来动态开辟空间的指针size是调整后的新大小以字节为单位。调整内存的两种情况 原有空间之后有足够的空闲空间这种情况下realloc会直接在原有空间的后面追加空间不会移动原有数据返回原来的指针。原有空间之后没有足够的空闲空间这种情况下realloc会在堆区重新找一块大小合适的空间将原来空间中的数据复制到新空间然后释放原来的空间返回新空间的指针。 示例正确使用
// 先申请4个int的空间
int* p (int*)malloc(4 * sizeof(int));
if (p NULL) {perror(malloc failed);return 1;
}
// 给空间赋值
for (int i 0; i 4; i) {p[i] i;
}
// 现在需要将空间调整为8个int用新指针接收realloc的返回值
int* new_p (int*)realloc(p, 8 * sizeof(int));
if (new_p NULL) {perror(realloc failed);// 如果realloc失败原来的p仍然有效需要释放避免内存泄漏free(p);p NULL;return 1;
}
// 调整成功更新指针
p new_p;
// 使用调整后的空间
for (int i 4; i 8; i) {p[i] i;
}
// 释放空间
free(p);
p NULL;注意最好别用要动态修改的指针来接受返回值因为若realloc失败返回NULL会导致指针变为NULL原来的100字节内存无法释放造成内存泄漏
错误示例用原指针接收返回值 int* p (int*)malloc(100);
// 错误若realloc失败返回NULL会导致p变为NULL原来的100字节内存无法释放造成内存泄漏
p (int*)realloc(p, 200);注意事项
realloc 的第一个参数为 NULL 时其功能相当于 malloc即realloc(NULL, size)等价于malloc(size)。调整后的空间大小可以比原来小此时会截断原有数据只保留前面部分数据。使用 realloc 后原来的指针可能会失效当需要移动数据时所以必须使用 realloc 的返回值来访问调整后的空间。
3. 常见的动态内存错误
3.1 对NULL指针的解引用操作
错误原因当malloc、calloc或realloc函数开辟内存失败时会返回NULL指针而NULL指针不指向任何有效的内存空间对其进行解引用操作如赋值、取值会导致程序崩溃。示例错误
int* p (int*)malloc(1000000000); // 申请过大空间可能失败返回NULL
*p 10; // 对NULL指针解引用程序会崩溃避免方法 在使用动态内存函数返回的指针之前必须检查该指针是否为 NULL。
示例正确 int* p (int*)malloc(1000000000);
if (p NULL) {perror(malloc failed); // 打印错误信息return 1; // 不继续使用指针避免解引用NULL
}
*p 10; // 指针非NULL可安全使用3.2 对动态开辟空间的越界访问
错误原因访问动态开辟的内存空间时超出了申请的范围就像数组越界访问一样会导致不可预期的错误可能修改其他内存的数据也可能导致程序崩溃。示例
// 申请3个int的空间共3*412字节有效访问范围是p[0]到p[2]
int* p (int*)malloc(3 * sizeof(int));
if (p NULL) {perror(malloc failed);return 1;
}
// 循环访问到了p[3]和p[4]超出了申请的空间范围属于越界访问
for (int i 0; i 5; i) {p[i] i; // i3、4时越界
}
free(p);
p NULL;危害越界访问可能会修改其他动态开辟的内存数据或者破坏堆区的管理信息导致后续的内存操作如 free出现错误。避免方法访问动态开辟的空间时严格控制访问范围确保不超过申请的大小。比如申请了 n 个 int 类型的空间访问索引就只能在 0 到 n-1 之间。
3.3 对非动态开辟内存使用free释放
错误原因free函数的作用是释放动态开辟的内存堆区的内存而栈上的局部变量、全局变量等非动态开辟的内存其生命周期由系统自动管理不需要也不能用free释放对这些内存使用free会导致程序行为未定义通常会引发程序崩溃。示例错误
int a 10; // 栈上的局部变量
int* p a;
free(p); // 错误释放非动态开辟的内存程序可能崩溃
p NULL;避免方法明确区分动态开辟的内存和非动态开辟的内存只对通过malloc、calloc、realloc函数申请的内存使用 free 释放。
3.4 使用free释放一块动态开辟内存的一部分
错误原因free函数释放动态内存时要求指针必须指向动态开辟内存的起始地址因为内存管理系统需要通过起始地址来回收整个内存块。如果指针指向的是动态开辟内存的中间位置free无法正确回收内存会破坏堆区的内存管理结构导致程序出错。示例错误
int* p (int*)malloc(4 * sizeof(int)); // p指向动态开辟内存的起始地址
if (p NULL) {perror(malloc failed);return 1;
}
p; // p现在指向动态开辟内存的第二个int的位置不再是起始地址
free(p); // 错误释放的是内存的一部分程序可能崩溃避免方法在释放动态内存之前确保指针指向动态开辟内存的起始地址。如果在操作过程中移动了指针需要先保存起始地址。示例正确 int* p (int*)malloc(4 * sizeof(int));
if (p NULL) {perror(malloc failed);return 1;
}
int* q p; // 保存起始地址
p; // 移动指针进行操作
// ... 使用p进行操作
free(q); // 使用保存的起始地址释放内存
q NULL;
p NULL;3.5 对同一块动态内存多次释放
错误原因同一块动态内存被free多次会导致堆区内存管理结构被破坏因为第一次释放后该内存已经归还给系统再次释放时系统无法识别该内存块的状态从而引发程序崩溃。示例错误
int* p (int*)malloc(100);
free(p);
free(p); // 错误对同一块内存多次释放程序可能崩溃避免方法释放内存后立即将指针置为 NULL因为 free 函数对 NULL 指针什么也不做这样即使不小心再次释放也不会出现错误。示例正确 int* p (int*)malloc(100);
free(p);
p NULL; // 释放后将指针置为NULL
free(p); // 安全free对NULL指针无操作3.6 动态开辟内存忘记释放内存泄漏
错误原因动态开辟的内存需要手动通过free释放如果使用完毕后没有释放并且指向该内存的指针也丢失了如指针超出作用域被销毁系统就无法回收这块内存导致内存泄漏。示例1函数中忘记释放
void test() {int* p (int*)malloc(100); // 在函数内部申请动态内存// 使用p进行操作但没有释放
} // 函数结束p被销毁无法再释放申请的100字节内存造成内存泄漏
int main() {test();// 程序运行期间test函数申请的内存一直未被释放return 0;
}示例 2指针被覆盖导致无法释放危害对于短期运行的程序内存泄漏可能不会有明显影响因为程序结束后操作系统会回收所有内存但对于长期运行的程序如服务器程序、后台服务内存泄漏会导致可用内存越来越少最终程序会因内存不足而崩溃。避免方法 动态内存使用完毕后及时调用 free 释放并将指针置为 NULL。在函数中申请的动态内存如果需要在函数外部使用要将指针返回给外部由外部负责释放如果不需要在外部使用一定要在函数返回前释放。避免覆盖指向动态内存的指针如果需要重新赋值先释放原来的内存。
4. 几个经典的笔试题
4.1 题分析
代码实现
void GetMemory(char* p) {p (char*)malloc(100); // 为形参p分配内存
}
void Test(void) {char* str NULL;GetMemory(str); // 传递str的值NULLstrcpy(str, hello world); // 操作NULL指针printf(str);
}运行结果程序崩溃。原因详解 值传递的局限性GetMemory函数的参数p是str的副本值传递p在函数内被赋值为malloc 返回的地址但这不会改变str的值str仍为 NULL。NULL 指针解引用strcpy(str, ...)试图向NULL 指针指向的内存写入数据这是未定义行为会导致程序崩溃。内存泄漏隐患GetMemory中 malloc 分配的内存地址仅存于p函数结束后p被销毁该内存无法释放造成内存泄漏。
4.2 题分析
代码实现
char* GetMemory(void) {char p[] hello world; // 局部数组存于栈区return p; // 返回局部数组的地址
}
void Test(void) {char* str NULL;str GetMemory(); // 接收已销毁的局部数组地址printf(str); // 访问无效内存
}运行结果打印随机值或乱码行为未定义。原因详解 局部变量的生命周期数组p是GetMemory函数的局部变量存储在栈区函数执行结束后栈区内存被释放p的地址变为无效野指针。野指针访问str接收的是无效地址此时访问该地址的内存printf(str)读取到的是栈区残留的随机数据结果不可预期。关键结论不要返回局部变量的地址其指向的内存会随函数结束而失效。
4.3 题分析
代码实现
void GetMemory(char**p, int num) {*p (char*)malloc(num); // 为二级指针指向的指针分配内存
}
void Test(void) {char* str NULL;GetMemory(str, 100); // 传递str的地址二级指针strcpy(str, hello); // 向分配的内存写入数据printf(str); // 打印hello
}运行结果正常打印 hello但存在内存泄漏。原因详解 二级指针的作用GetMemory的参数p是str二级指针*p就是str本身因此*p malloc(...)能正确为str分配内存str指向堆区的 100 字节。内存泄漏问题str指向的堆区内存未通过 free 释放程序结束前该内存一直被占用造成内存泄漏尤其在多次调用时。改进方案使用后添加free(str); str NULL;释放内存。
4.4 题分析
代码实现
void Test(void) {char* str (char*)malloc(100); // 分配堆区内存strcpy(str, hello);free(str); // 释放str指向的内存if (str ! NULL) { // str仍指向已释放的内存野指针strcpy(str, world); // 向已释放的内存写入数据printf(str); // 访问无效内存}
}运行结果可能打印 world也可能崩溃或打印乱码行为未定义。原因详解 free 后的指针状态free(str)释放了内存但str的值并未改变仍指向原地址此时str成为野指针。访问已释放内存strcpy(str, world)向已归还给系统的内存写入数据这会破坏堆区管理结构可能导致后续内存操作出错如再次 malloc 时崩溃。预防措施释放内存后应立即将指针置为 NULL即free(str); str NULL;此时if (str ! NULL)条件不成立避免无效操作。
5. 柔性数组Flexible Array
柔性数组是C99标准引入的特殊数组形式仅能作为结构体的最后一个成员存在其大小在结构体定义时无需指定或指定为0因此也被称为“可变长数组成员”。定义示例及编译器兼容性
// 方式1数组大小指定为0早期C99支持此形式部分编译器如GCC兼容
typedef struct st_type {int len; // 用于记录柔性数组的实际长度int data[0]; // 柔性数组成员必须位于结构体末尾
} type_a;// 方式2不指定数组大小空数组形式是C99推荐写法兼容更多编译器如MSVC
typedef struct st_type {int len;int data[]; // 柔性数组成员同样位于结构体末尾
} type_a;核心约束柔性数组成员前面必须至少有一个其他类型的成员如示例中的int len且不能是结构体的唯一成员。这是因为柔性数组本身不占用结构体的固定内存需要通过前面的成员确定其起始偏移量。
5.1 柔性数组的特点
结构成员的位置约束 柔性数组成员必须是结构体的最后一个成员不能有其他成员跟在其后。错误示例柔性数组后有其他成员
typedef struct wrong_st {int a;int flex[]; // 柔性数组int b; // 错误柔性数组后不能有其他成员
} wrong_type; // 编译器会报错sizeof运算符的计算规则 sizeof计算包含柔性数组的结构体大小时仅计算柔性数组前面所有成员的总大小完全忽略柔性数组的存在。示例基于type_a // type_a中仅int len一个非柔性成员占4字节
printf(sizeof(type_a) %zu\n, sizeof(type_a)); // 输出4不包含data[]的大小原理柔性数组的大小在编译期未知无法纳入结构体的固定大小计算其内存需在运行时动态分配。
内存分配的强制性与计算方式 包含柔性数组的结构体必须通过动态内存分配函数malloc/calloc/realloc创建实例不能在栈上直接定义变量如type_a obj;是错误的因为无法确定柔性数组的大小。分配内存时总大小计算公式为结构体固定大小sizeof(type_a) 柔性数组实际所需字节数。示例为柔性数组分配 10 个int元素的空间 // 计算总大小4len 10*4data 44字节
type_a* p (type_a*)malloc(sizeof(type_a) 10 * sizeof(int));
if (p NULL) {perror(malloc failed);exit(EXIT_FAILURE);
}
p-len 10; // 记录柔性数组的实际长度方便后续访问5.2 柔性数组的使用
基本使用流程动态分配内存→初始化成员→访问柔性数组→释放内存。 完整示例
#include stdio.h
#include stdlib.htypedef struct st_type {int len; // 记录柔性数组元素个数int data[]; // 柔性数组成员
} type_a;int main() {// 1. 分配内存结构体固定大小4字节 5个int20字节 24字节type_a* p (type_a*)malloc(sizeof(type_a) 5 * sizeof(int));if (p NULL) {perror(malloc failed);return 1;}// 2. 初始化设置柔性数组长度并赋值p-len 5;for (int i 0; i p-len; i) {p-data[i] i * 10; // 直接通过结构体指针访问柔性数组}// 3. 访问柔性数组元素printf(柔性数组元素);for (int i 0; i p-len; i) {printf(%d , p-data[i]); // 输出0 10 20 30 40}printf(\n);// 4. 释放内存一次free即可free(p);p NULL; // 避免野指针return 0;
}柔性数组的动态调整体现 “柔性” 通过 realloc 函数可以随时调整柔性数组的大小原数据会自动迁移到新空间若空间地址改变。示例将上述示例中的柔性数组从 5 个int扩展到 8 个
// 原p指向24字节空间扩展为4 8*4 36字节
type_a* new_p (type_a*)realloc(p, sizeof(type_a) 8 * sizeof(int));
if (new_p NULL) {perror(realloc failed);free(p); // 若扩展失败释放原有内存return 1;
}
p new_p;
p-len 8; // 更新长度记录// 为新增的3个元素赋值
for (int i 5; i p-len; i) {p-data[i] i * 10;
}// 验证扩展后的数据
printf(扩展后元素);
for (int i 0; i p-len; i) {printf(%d , p-data[i]); // 输出0 10 20 30 40 50 60 70
}free(p);
p NULL;注意调整大小时realloc的第二个参数必须重新计算sizeof(type_a) 新元素个数*元素大小不能直接基于原有柔性数组的长度累加。
5.3 柔性数组的优势
以“存储一段动态长度的整数序列”为例对比柔性数组与“结构体指针”两种实现方式凸显柔性数组的优势
实现方式对比 柔性数组方式type_a
// 分配一次malloc完成所有内存申请
type_a* fa (type_a*)malloc(sizeof(type_a) 100 * sizeof(int));
fa-len 100;// 使用直接通过fa-data[i]访问
for (int i 0; i fa-len; i) {fa-data[i] i;
}// 释放一次free即可
free(fa);
fa NULL;结构体 指针方式type_b typedef struct ptr_type {int len;int* data; // 用指针指向动态数组
} type_b;// 分配需两次malloc分别申请结构体和数组内存
type_b* pb (type_b*)malloc(sizeof(type_b));
pb-len 100;
pb-data (int*)malloc(pb-len * sizeof(int)); // 二次分配// 使用通过pb-data[i]访问
for (int i 0; i pb-len; i) {pb-data[i] i;
}// 释放需两次free且必须先释放数组再释放结构体
free(pb-data); // 若忘记释放会导致内存泄漏
pb-data NULL;
free(pb);
pb NULL;优势 1内存释放的简洁性与安全性 柔性数组只需一次free操作无需关注内部成员的内存管理尤其在函数返回动态结构体时能避免用户因忘记释放成员内存如type_b中的data而导致的内存泄漏。示例函数返回场景
// 返回柔性数组结构体用户只需一次释放
type_a* create_flex_array(int n) {type_a* p (type_a*)malloc(sizeof(type_a) n * sizeof(int));p-len n;return p;
}// 用户使用
type_a* arr create_flex_array(50);
// ... 使用后
free(arr); // 简单安全无内存泄漏风险优势 2内存连续性与访问效率 柔性数组的所有内存结构体固定部分 柔性数组部分是连续的存储在同一块内存区域中。这种连续性带来两个好处 减少 CPU 缓存失效连续内存更可能被一次性加载到 CPU 缓存中访问时无需频繁从内存中读取速度更快。简化地址计算访问fa-data[i]时编译器只需通过fa的地址 sizeof(int)len的大小即可定位到data的起始地址再加上i*sizeof(int)得到目标元素地址仅需一次地址计算。 结构体 指针方式中结构体与数组内存是离散的访问pb-data[i]时需先从pb中读取data指针的地址再计算i对应的偏移量涉及两次地址计算且离散内存更难被 CPU 缓存优化。 优势 3减少内存碎片 内存碎片指系统中存在大量零散的、无法被有效利用的小内存块。柔性数组通过一次内存分配获取所有所需空间相比两次分配结构体 指针能减少内存碎片的产生尤其在频繁创建和销毁动态数组时效果更明显。