19. 【C语言】动态内存分配
2026/7/5 14:56:45 网站建设 项目流程

前面的文章里,我们用的数组、字符串、结构体,都有一个共同特点:大小在编译时就确定了。比如int arr[100];,这 100 个元素的数组在程序运行前就已经被分配好了空间,不管你实际用了 5 个还是 80 个,内存都占那么多。

但现实中的程序往往不知道运行时会有多少数据——用户可能输入 3 个成绩,也可能输入 300 个;日志文件可能一行,也可能十万行。如果总是“多预留一些”,内存会大量浪费;“预留少了”又会溢出。这就需要一种能在程序运行时按需申请内存的机制。

C 语言把这把钥匙交给了你:动态内存分配。它强大,但也危险——申请了要记得还,还了就别再用。今天我们就来掌握这套“手动内存管理”的艺术。


一、程序的内存布局:栈、堆与静态区

在正式学malloc之前,先搞清楚程序运行时内存的几个主要区域,这对理解“变量住哪里”很有帮助。

一个典型的 C 程序,其内存布局(从高地址到低地址)大致是:

内存区域存放内容特点
栈(Stack)局部变量、函数参数、返回地址自动管理,函数调用时分配,返回时释放;空间有限(通常几 MB)
堆(Heap)动态分配的内存手动管理,程序通过malloc申请,用free释放;空间较大
静态数据区全局变量、static局部变量、字符串字面量整个程序运行期存在
代码区程序的机器指令只读

以前我们写的局部变量都在栈上,来去匆匆,不需要你操心。但动态分配的内存来自——这块区域的大小只受系统物理内存和虚拟内存的限制,你可以按需索取。代价是:你必须自己管理它的生命周期。忘了归还,它就一直占着,这就是内存泄漏


二、malloc:申请一块内存

malloc(memory allocation)是最基础的动态内存分配函数,定义在<stdlib.h>里:

void*malloc(size_tsize);
  • 参数size:要申请的字节数。
  • 返回值:一个void*指针,指向申请到的内存块的首地址;如果分配失败(比如内存不足),返回NULL

使用流程

#include<stdlib.h>int*p=(int*)malloc(sizeof(int));// 申请一块 int 大小的内存if(p==NULL){// 处理分配失败printf("内存分配失败!\n");return1;}*p=42;// 正常使用free(p);// 用完后释放

几个要点:

  1. 为什么用sizeof不同类型大小不同,用sizeof让编译器自动计算,不依赖手动猜测。
  2. 为什么强制类型转换?malloc返回void*,在 C 里可以隐式转换成任意指针类型,强转主要是为了代码清晰,以及兼容 C++(如果以后被 C++ 编译器使用的话)。C 社区有争议,但写了更清晰。
  3. 为什么检查NULL内存请求可能失败(尤其是申请大块内存时),不检查就使用会因空指针解引用而崩溃。

三、动态分配数组

malloc最常见的用途是创建运行时才能确定大小的数组:

#include<stdio.h>#include<stdlib.h>intmain(void){intn;printf("请输入学生数量:");scanf("%d",&n);int*scores=(int*)malloc(n*sizeof(int));if(scores==NULL){printf("内存分配失败!\n");return1;}// 像普通数组一样使用for(inti=0;i<n;i++){scores[i]=i*10;// 赋值}for(inti=0;i<n;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,全部为 0

malloc分配到的内存里是垃圾值,如果忘了初始化就直接读,很容易出问题。calloc自动清零,代价是稍微慢一点(因为要擦写内存)。给结构体数组、或者需要初始化为 0 的场景,推荐用calloc


五、realloc:给已分配的内存“扩容”

有时候之前申请的 100 个位置用满了,想扩容到 200 个。realloc可以调整动态内存的大小:

void*realloc(void*ptr,size_tnew_size);
  • ptr:之前通过malloc/calloc/realloc返回的指针。
  • new_size:新的大小(字节)。
  • 返回值:新内存块的地址(可能和原来一样,也可能不一样)。

重要:realloc可能搬移数据!

如果原地址后面没有足够的连续空闲空间,realloc会:

  1. 在别处找一块够大的新区域。
  2. 把旧数据复制过去。
  3. 自动释放旧区域。
  4. 返回新地址。

所以,永远用返回值更新原指针,而且先用临时变量接住,防止失败时丢失原指针:

int*arr=(int*)malloc(5*sizeof(int));// ... 使用 arr,满了int*tmp=(int*)realloc(arr,10*sizeof(int));if(tmp==NULL){// 扩容失败,但 arr 仍然有效!可以继续用原来的printf("扩容失败\n");}else{arr=tmp;// 用新地址}

如果realloc第一个参数是NULL,它的行为等同于malloc(new_size)


六、free:归还内存

free把通过malloccallocrealloc申请的内存归还给堆,以供后续分配。

free(ptr);ptr=NULL;// 好习惯:释放后置空,防止误用

规则

  • 只能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));*p=10;free(p);*p=20;// 未定义行为!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);#endif

darray.c

#include<stdlib.h>#include"darray.h"DArray*darray_create(intinitial_capacity){DArray*da=(DArray*)malloc(sizeof(DArray));if(da==NULL)returnNULL;da->data=(int*)malloc(initial_capacity*sizeof(int));if(da->data==NULL){free(da);returnNULL;}da->capacity=initial_capacity;da->size=0;returnda;}voiddarray_append(DArray*da,intvalue){if(da->size>=da->capacity){// 扩容为原来的 2 倍intnew_cap=da->capacity*2;int*tmp=(int*)realloc(da->data,new_cap*sizeof(int));if(tmp==NULL)return;// 扩容失败,保持现状da->data=tmp;da->capacity=new_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

#include<stdio.h>#include"darray.h"intmain(void){DArray*da=darray_create(4);if(da==NULL){printf("创建动态数组失败\n");return1;}for(inti=0;i<20;i++){darray_append(da,i*i);}printf("动态数组内容: ");for(inti=0;i<da->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。这个“动态数组”就是很多高级语言中vectorArrayList的底层原理。


九、小结

动态内存分配让程序摆脱了编译时固定大小的桎梏。你今天学到了:

  • 堆与栈的本质区别:栈自动管理,堆手动管理。
  • malloccallocrealloc的使用场景和差异。
  • free的规则:配对使用,释放后置 NULL。
  • 五大常见动态内存错误及其防范方法。
  • 一个完整的动态数组实现,体感“自动扩容”的原理。

现在你已经有了指针的深厚功底,又能自由操控内存——这几乎是 C 语言最具威力的组合。下一步,我们将用这些知识构建真正灵活的数据结构:链表。数组再能扩容,也逃不开“插入中间要移动大量元素”的宿命。而链表能让你在 O(1) 时间内完成插入和删除,它是动态数据结构的真正起点。


课后小练习

  1. 写一个程序,用malloc创建一个包含 N 个double的数组(N 由用户输入),然后读取 N 个数存入数组,计算平均值并输出。最后free
  2. calloc实现和上题同样的功能,然后解释callocmalloc的区别。
  3. 下面的代码有 bug,找出并修复:
    int*create_array(intsize){intarr[size];returnarr;}intmain(void){int*p=create_array(10);p[0]=5;free(p);return0;}
  4. (小挑战)在“简易动态数组”的基础上,增加一个darray_remove_last函数,删除最后一个元素(size 减 1 即可,不需缩容)。再加一个darray_insert函数,在指定索引处插入一个元素(后面的元素要后移)。如果空间不够,自动扩容。

我们下期见!

💡获取本系列示例代码请访问 GitCode 仓库。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询