19. 【C语言】动态内存分配

📅 2026/7/5 14:56:48
19. 【C语言】动态内存分配
前面的文章里我们用的数组、字符串、结构体都有一个共同特点大小在编译时就确定了。比如int arr[100];这 100 个元素的数组在程序运行前就已经被分配好了空间不管你实际用了 5 个还是 80 个内存都占那么多。但现实中的程序往往不知道运行时会有多少数据——用户可能输入 3 个成绩也可能输入 300 个日志文件可能一行也可能十万行。如果总是“多预留一些”内存会大量浪费“预留少了”又会溢出。这就需要一种能在程序运行时按需申请内存的机制。C 语言把这把钥匙交给了你动态内存分配。它强大但也危险——申请了要记得还还了就别再用。今天我们就来掌握这套“手动内存管理”的艺术。一、程序的内存布局栈、堆与静态区在正式学malloc之前先搞清楚程序运行时内存的几个主要区域这对理解“变量住哪里”很有帮助。一个典型的 C 程序其内存布局从高地址到低地址大致是内存区域存放内容特点栈Stack局部变量、函数参数、返回地址自动管理函数调用时分配返回时释放空间有限通常几 MB堆Heap动态分配的内存手动管理程序通过malloc申请用free释放空间较大静态数据区全局变量、static局部变量、字符串字面量整个程序运行期存在代码区程序的机器指令只读以前我们写的局部变量都在栈上来去匆匆不需要你操心。但动态分配的内存来自堆——这块区域的大小只受系统物理内存和虚拟内存的限制你可以按需索取。代价是你必须自己管理它的生命周期。忘了归还它就一直占着这就是内存泄漏。二、malloc申请一块内存mallocmemory allocation是最基础的动态内存分配函数定义在stdlib.h里void*malloc(size_tsize);参数size要申请的字节数。返回值一个void*指针指向申请到的内存块的首地址如果分配失败比如内存不足返回NULL。使用流程#includestdlib.hint*p(int*)malloc(sizeof(int));// 申请一块 int 大小的内存if(pNULL){// 处理分配失败printf(内存分配失败\n);return1;}*p42;// 正常使用free(p);// 用完后释放几个要点为什么用sizeof不同类型大小不同用sizeof让编译器自动计算不依赖手动猜测。为什么强制类型转换malloc返回void*在 C 里可以隐式转换成任意指针类型强转主要是为了代码清晰以及兼容 C如果以后被 C 编译器使用的话。C 社区有争议但写了更清晰。为什么检查NULL内存请求可能失败尤其是申请大块内存时不检查就使用会因空指针解引用而崩溃。三、动态分配数组malloc最常见的用途是创建运行时才能确定大小的数组#includestdio.h#includestdlib.hintmain(void){intn;printf(请输入学生数量);scanf(%d,n);int*scores(int*)malloc(n*sizeof(int));if(scoresNULL){printf(内存分配失败\n);return1;}// 像普通数组一样使用for(inti0;in;i){scores[i]i*10;// 赋值}for(inti0;in;i){printf(%d ,scores[i]);}printf(\n);free(scores);// 释放return0;}注意n * sizeof(int)计算总字节数。释放后指针scores仍然存着那个地址但内存已经不属于你了再访问它会导致未定义行为。四、calloc申请并自动清零calloc也是申请内存但它会把分配到的所有字节初始化为 0并且参数写法不同void*calloc(size_tnmemb,size_tsize);nmemb元素个数size每个元素的大小int*arr(int*)calloc(10,sizeof(int));// 10 个 int全部为 0malloc分配到的内存里是垃圾值如果忘了初始化就直接读很容易出问题。calloc自动清零代价是稍微慢一点因为要擦写内存。给结构体数组、或者需要初始化为 0 的场景推荐用calloc。五、realloc给已分配的内存“扩容”有时候之前申请的 100 个位置用满了想扩容到 200 个。realloc可以调整动态内存的大小void*realloc(void*ptr,size_tnew_size);ptr之前通过malloc/calloc/realloc返回的指针。new_size新的大小字节。返回值新内存块的地址可能和原来一样也可能不一样。重要realloc可能搬移数据如果原地址后面没有足够的连续空闲空间realloc会在别处找一块够大的新区域。把旧数据复制过去。自动释放旧区域。返回新地址。所以永远用返回值更新原指针而且先用临时变量接住防止失败时丢失原指针int*arr(int*)malloc(5*sizeof(int));// ... 使用 arr满了int*tmp(int*)realloc(arr,10*sizeof(int));if(tmpNULL){// 扩容失败但 arr 仍然有效可以继续用原来的printf(扩容失败\n);}else{arrtmp;// 用新地址}如果realloc第一个参数是NULL它的行为等同于malloc(new_size)。六、free归还内存free把通过malloc、calloc、realloc申请的内存归还给堆以供后续分配。free(ptr);ptrNULL;// 好习惯释放后置空防止误用规则只能free动态分配的内存栈上的变量绝对不能free。不能重复free同一块内存free(NULL)是安全的不会报错。free后指针变成“悬垂指针”再访问就是未定义行为。七、常见动态内存错误避坑手册1. 忘记调用free——内存泄漏voidfunc(void){int*p(int*)malloc(100*sizeof(int));// 使用 p但函数结束前没有 free(p)}// p 本身消失但内存还在堆里没人回收每次func被调用就泄露 400 字节。程序长时间运行内存被慢慢蚕食。对于长期运行的程序服务器、数据库内存泄漏是致命的。2. 使用已释放的内存悬垂指针int*p(int*)malloc(sizeof(int));*p10;free(p);*p20;// 未定义行为p 指向的内存已经不属于你释放后把指针置为NULL至少在解引用NULL时会立即崩溃更容易定位。3. 多次free同一块内存int*p(int*)malloc(sizeof(int));free(p);free(p);// 未定义行为同样free后置NULL可以避免这种情况因为free(NULL)是安全的。4. 越界写入动态数组int*arr(int*)malloc(5*sizeof(int));arr[5]100;// 越界只分配了 0-4 共 5 个元素和普通数组一样动态分配的数组也不检查越界但后果可能更隐蔽——可能覆盖了堆的管理结构导致后续free时崩溃。5. 使用未初始化的动态内存malloc后直接读int*p(int*)malloc(sizeof(int));printf(%d\n,*p);// 垃圾值要么用calloc要么malloc后立即赋值。八、实战简易动态数组我们把动态内存知识整合成一个可用的“动态数组”模块——能自动扩容的整数数组。darray.h#ifndefDARRAY_H#defineDARRAY_Htypedefstruct{int*data;// 指向堆上的数组intcapacity;// 当前容量intsize;// 实际存放的元素个数}DArray;DArray*darray_create(intinitial_capacity);voiddarray_append(DArray*da,intvalue);intdarray_get(DArray*da,intindex);voiddarray_free(DArray*da);#endifdarray.c#includestdlib.h#includedarray.hDArray*darray_create(intinitial_capacity){DArray*da(DArray*)malloc(sizeof(DArray));if(daNULL)returnNULL;da-data(int*)malloc(initial_capacity*sizeof(int));if(da-dataNULL){free(da);returnNULL;}da-capacityinitial_capacity;da-size0;returnda;}voiddarray_append(DArray*da,intvalue){if(da-sizeda-capacity){// 扩容为原来的 2 倍intnew_capda-capacity*2;int*tmp(int*)realloc(da-data,new_cap*sizeof(int));if(tmpNULL)return;// 扩容失败保持现状da-datatmp;da-capacitynew_cap;}da-data[da-size]value;da-size;}intdarray_get(DArray*da,intindex){// 注意实际项目应加边界检查returnda-data[index];}voiddarray_free(DArray*da){free(da-data);// 先释放内部数组free(da);// 再释放结构体本身}main.c#includestdio.h#includedarray.hintmain(void){DArray*dadarray_create(4);if(daNULL){printf(创建动态数组失败\n);return1;}for(inti0;i20;i){darray_append(da,i*i);}printf(动态数组内容: );for(inti0;ida-size;i){printf(%d ,darray_get(da,i));}printf(\n容量: %d, 大小: %d\n,da-capacity,da-size);darray_free(da);return0;}输出动态数组内容: 0 1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361 容量: 32, 大小: 20初始容量为 4插入到第 5 个元素时自动扩容为 8再满扩到 16、32。这个“动态数组”就是很多高级语言中vector或ArrayList的底层原理。九、小结动态内存分配让程序摆脱了编译时固定大小的桎梏。你今天学到了堆与栈的本质区别栈自动管理堆手动管理。malloc、calloc、realloc的使用场景和差异。free的规则配对使用释放后置 NULL。五大常见动态内存错误及其防范方法。一个完整的动态数组实现体感“自动扩容”的原理。现在你已经有了指针的深厚功底又能自由操控内存——这几乎是 C 语言最具威力的组合。下一步我们将用这些知识构建真正灵活的数据结构链表。数组再能扩容也逃不开“插入中间要移动大量元素”的宿命。而链表能让你在 O(1) 时间内完成插入和删除它是动态数据结构的真正起点。课后小练习写一个程序用malloc创建一个包含 N 个double的数组N 由用户输入然后读取 N 个数存入数组计算平均值并输出。最后free。用calloc实现和上题同样的功能然后解释calloc和malloc的区别。下面的代码有 bug找出并修复int*create_array(intsize){intarr[size];returnarr;}intmain(void){int*pcreate_array(10);p[0]5;free(p);return0;}小挑战在“简易动态数组”的基础上增加一个darray_remove_last函数删除最后一个元素size 减 1 即可不需缩容。再加一个darray_insert函数在指定索引处插入一个元素后面的元素要后移。如果空间不够自动扩容。我们下期见获取本系列示例代码请访问 GitCode 仓库。