C语言动态内存管理详解:从malloc到柔性数组
2026/9/24 14:47:58 网站建设 项目流程

1. 引言

在C语言的学习过程中,内存管理始终是一个绕不开的核心话题。很多初学者在编写程序时,往往只关注逻辑是否正确,却忽略了内存的分配与释放。实际上,动态内存管理是C语言中极为重要的一环,它直接关系到程序的稳定性、性能和资源利用率。

本文将基于经典教材内容,系统梳理C语言动态内存管理的核心知识点,包括malloc、free、calloc、realloc四大函数的使用方法,常见的内存错误,经典笔试题分析,以及柔性数组这一进阶概念。希望通过本文,能够帮助读者建立起完整的内存管理知识体系。

2. 为什么要有动态内存分配

在接触动态内存之前,我们已经掌握的内存开辟方式主要有以下两种:

int val = 20; //在栈空间上开辟四个字节 char arr[10] = {0}; //在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个特点:

  • 空间开辟大小是固定的:一旦确定,无法在程序运行过程中改变。
  • 数组在声明的时候,必须指定数组的长度:数组空间一旦确定了大小就不能调整。

然而,对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。例如,程序需要根据用户输入的数量来存储数据,这个数量在编译时是无法预知的。

正是为了解决这类问题,C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,这样就比较灵活了。动态内存分配使得程序能够在运行时根据实际需求灵活地调整内存使用,大大提高了程序的适应性和资源利用效率。

3. malloc和free

3.1 malloc函数

C语言提供了一个动态内存开辟的函数:

void* malloc (size_t size);

功能:向内存的堆区申请一块连续可用的空间,并返回指向这块空间的起始地址。

使用malloc函数时,需要注意以下几点:

  • 如果开辟成功,则返回这块空间的起始地址。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数size为0,malloc的行为是标准未定义的,取决于编译器。

3.2 free函数

C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:

void free (void* ptr);

free函数用来释放动态开辟的内存。使用free时需要注意:

  • 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数ptr是NULL指针,则函数什么事都不做。

malloc和free都声明在stdlib.h头文件中。

下面通过一个完整的示例来演示malloc和free的配合使用:

#include <stdio.h> #include <stdlib.h> int main() { int num = 0; scanf("%d", &num); int* ptr = NULL; ptr = (int*)malloc(num*sizeof(int)); if(NULL != ptr) //判断ptr指针是否为空 { int i = 0; for(i=0; i<num; i++) { *(ptr+i) = 0; } } free(ptr); //释放ptr所指向的动态内存 ptr = NULL; //是否有必要? return 0; }

关于free之后是否需要将指针置为NULL,这是一个值得思考的问题。free只是释放了指针所指向的内存空间,但指针本身仍然保存着原来的地址,此时指针变成了"野指针"。为了避免后续误用这个指针,建议在free之后将指针置为NULL。

4. calloc和realloc

4.1 calloc函数

C语言还提供了一个函数叫calloc,calloc函数也用来动态内存分配。原型如下:

void* calloc (size_t num, size_t size);

calloc函数的特点:

  • 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0。
  • 与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。

下面通过一个示例来验证calloc的初始化行为:

#include <stdio.h> #include <stdlib.h> int main() { int *p = (int*)calloc(10, sizeof(int)); if(NULL != p) { int i = 0; for(i=0; i<10; i++) { printf("%d ", *(p+i)); } } free(p); p = NULL; return 0; }

输出结果:

0 0 0 0 0 0 0 0 0 0

所以如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。

4.2 realloc函数

realloc函数的出现让动态内存管理更加灵活。有时我们会发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存大小的调整。

函数原型如下:

void* realloc (void* ptr, size_t size);

参数说明:

  • ptr是要调整的内存地址。
  • size是调整之后新大小,单位是字节。
  • 返回值为调整之后的内存起始位置。

这个函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。realloc在调整内存空间时存在两种情况:

  • 情况1:原有空间之后有足够大的空间。此时要扩展内存就直接在原有内存之后直接追加空间,原来空间的数据不发生变化。
  • 情况2:原有空间之后没有足够大的空间。此时扩展的方法是在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址。

由于上述的两种情况,realloc函数的使用就要注意一些。下面通过代码对比来展示正确的使用方式:

#include <stdio.h> #include <stdlib.h> int main() { int *ptr = (int*)malloc(100); if(ptr != NULL) { //业务处理 } else { return 1; } //扩展容量 //代码1 - 直接将realloc的返回值放到ptr中 ptr = (int*)realloc(ptr, 1000); //这样可以吗?(如果申请失败会如何?) //代码2 - 先将realloc函数的返回值放在p中,不为NULL,再放ptr中 int *p = NULL; p = realloc(ptr, 1000); if(p != NULL) { ptr = p; } //业务处理 free(ptr); return 0; }

代码1存在一个隐患:如果realloc申请失败,会返回NULL,此时直接将返回值赋给ptr,会导致原来的内存地址丢失,造成内存泄漏。而代码2先将返回值暂存在p中,判断不为NULL后再赋给ptr,这样即使申请失败,原来的内存仍然可以通过ptr访问和释放。

4.3 malloc、calloc、realloc对比

为了更直观地理解三个函数之间的区别,下面通过表格进行对比:

对比项malloccallocrealloc
函数原型void* malloc(size_t size)void* calloc(size_t num, size_t size)void* realloc(void* ptr, size_t size)
参数含义size:申请空间的字节数num:元素个数;size:每个元素的大小ptr:原内存地址;size:调整后的新大小
初始化不初始化,内存中为随机值自动将每个字节初始化为0不初始化,保留原内存中的数据
功能申请一块连续的内存空间申请一块连续的内存空间并清零调整已申请内存的大小,并搬运原数据
返回值成功返回起始地址,失败返回NULL成功返回起始地址,失败返回NULL成功返回新地址,失败返回NULL(原内存仍有效)
使用场景需要一块未初始化的内存需要一块已清零的内存需要扩容或缩容已申请的内存

小结:malloc负责申请内存,calloc在申请的同时完成清零初始化,realloc则用于调整已有内存的大小。三者配合free使用,才能构建健壮的动态内存管理代码。

5. 常见动态内存错误

动态内存使用过程中,常见的错误有以下几类,每一类都可能导致程序崩溃或产生难以排查的bug。

5.1 对NULL指针的解引用操作

void test() { int *p = (int *)malloc(INT_MAX/4); *p = 20; //如果p的值是NULL,就会有问题 free(p); }

这段代码的问题在于:malloc可能返回NULL(内存不足时),此时对NULL指针解引用会导致程序崩溃。正确的做法是在使用前判断指针是否为NULL。

5.2 对动态开辟空间的越界访问

void test() { int i = 0; int *p = (int *)malloc(10*sizeof(int)); if(NULL == p) { exit(EXIT_FAILURE); } for(i=0; i<=10; i++) { *(p+i) = i; //当i是10的时候越界访问 } free(p); }

这里申请了10个int大小的空间,但循环访问了11个元素(i从0到10),当i等于10时发生了越界访问。越界访问会破坏堆内存的元数据,可能导致程序崩溃或产生不可预知的行为。

5.3 对非动态开辟内存使用free释放

void test() { int a = 10; int *p = &a; free(p); //ok? }

free只能释放动态开辟的内存。这里p指向的是栈上的变量a,对栈上的内存调用free,行为是未定义的,可能导致程序崩溃。

5.4 使用free释放一块动态开辟内存的一部分

void test() { int *p = (int *)malloc(100); p++; free(p); //p不再指向动态内存的起始位置 }

free要求传入的指针必须指向动态内存的起始位置。这里p自增后不再指向起始位置,此时调用free会导致未定义行为。

5.5 对同一块动态内存多次释放

void test() { int *p = (int *)malloc(100); free(p); free(p); //重复释放 }

同一块内存被释放两次,称为"双重释放"(double free),这会导致堆管理器的元数据被破坏,程序可能崩溃或产生安全问题。

5.6 动态开辟内存忘记释放(内存泄漏)

void test() { int *p = (int *)malloc(100); if(NULL != p) { *p = 20; } } int main() { test(); while(1); return 0; }

忘记释放不再使用的动态开辟的空间会造成内存泄漏。随着程序运行时间的增长,内存泄漏会逐渐耗尽系统内存,最终导致程序崩溃。切记:动态开辟的空间一定要释放,并且正确释放。

6. 动态内存经典笔试题分析

在面试和笔试中,动态内存管理是高频考点。下面通过四道经典题目来深入理解动态内存的使用陷阱。

6.1 题目1:值传递无法修改指针

#include <stdio.h> #include <stdlib.h> void GetMemory(char* p) { p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(str); /*这里传递的是str的数值, 在GetMemory函数中,接收到的是str这个变量的数值 新建了一个变量p 导致没有达到修改str的目的 */ strcpy(str, "hello world"); printf(str); } int main() { Test(); return 0; }

运行结果:程序崩溃。

分析:GetMemory函数的参数是值传递,函数内部对p的修改不会影响到外部的str。因此str仍然是NULL,对NULL指针调用strcpy会导致程序崩溃。正确的做法是传入二级指针,或者通过返回值返回分配的内存地址。

正确修改:

int GetMemory(char** p) // 改为返回 int,0表示成功,-1表示失败 { *p = (char*)malloc(100); if (*p == NULL) { perror("use malloc"); return -1; // 通知调用者失败了 } return 0; } void Test(void) { char* str = NULL; // 检查分配是否成功 if (GetMemory(&str) != 0) { return; // 分配失败,直接退出,防止后续崩溃 } strcpy(str, "hello world"); printf("%s\n", str); // 安全的打印方式 free(str); str = NULL; // 防止悬空指针 } int main() { Test(); return 0; }

6.2 题目2:返回栈空间地址

#include <stdio.h> #include <stdlib.h> char* GetMemory(void) { char p[] = "hello world"; //这里是在栈区开放的空间,函数结束后,p的空间就被释放 return p; } void Test(void) { char* str = NULL; str = GetMemory(); //这个时候str指向的就是已释放的栈空间,未定义行为 //如果p释放的空间没有被占用,可能会正常打印 printf(str); } int main() { Test(); return 0; }

运行结果:输出乱码或程序崩溃。

分析:p是函数内部的局部数组,存储在栈上。函数返回后,栈空间被回收,p指向的内存内容已经失效。此时str指向的是已释放的栈空间,访问它属于未定义行为。正确的做法是使用静态数组或动态内存。

正确修改:

#include <stdio.h> #include <stdlib.h> char* GetMemory(void) { static char p[] = "hello world"; //1.static //2.动态开辟(记得检测和释放) return p; } void Test(void) { char* str = NULL; str = GetMemory(); printf(str); } int main() { Test(); return 0; }

6.3 题目3:二级指针的正确使用

#include <stdio.h> #include <stdlib.h> void GetMemory(char **p, int num) { *p = (char *)malloc(num); } void Test(void) { char *str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); } int main() { Test(); return 0; }

运行结果:输出"hello"。

分析:这里通过二级指针正确修改了外部的str指针,使其指向动态分配的内存。但这段代码存在内存泄漏问题,因为分配的内存没有被释放。正确的做法是在使用完毕后调用free释放内存。

正确修改:

#include <stdio.h> #include <stdlib.h> #include <string.h> // 将返回值改为 int,0表示成功,-1表示失败,方便调用者判断 int GetMemory(char** p, int num) { if (p == NULL || num <= 0) { return -1; // 防御性检查 } *p = (char*)malloc(num); if (*p == NULL) { return -1; // 分配失败 } return 0; // 分配成功 } void Test(void) { char* str = NULL; // 接收返回值并判断 if (GetMemory(&str, 100) != 0) { printf("内存分配失败!\n"); return; // 分配失败,提前退出,防止后续崩溃 } strcpy(str, "hello"); printf("%s\n", str); // 安全的打印方式 free(str); // 释放内存,防止泄露 str = NULL; // 避免悬空指针(好习惯) } int main() { Test(); return 0; }

6.4 题目4:free后未置空指针

#include <stdio.h> #include <stdlib.h> void Test(void) { char* str = (char*)malloc(100); strcpy(str, "hello"); free(str); //这个时候str指向的空间被释放,str变成了野指针 //野指针不为空,下面对野指针进行访问,导致程序崩溃 if (str != NULL) { strcpy(str, "world"); printf(str); } } int main() { Test(); return 0; }

运行结果:输出"world"或程序崩溃,行为未定义。

分析:free(str)后,str变成了野指针,但str的值并不为NULL。此时if(str != NULL)判断为真,继续对已释放的内存进行写操作,属于未定义行为。正确的做法是在free后立即将str置为NULL。

#include <stdio.h> #include <stdlib.h> void Test(void) { char* str = (char*)malloc(100); strcpy(str, "hello"); free(str); str = NULL; if (str != NULL) { strcpy(str, "world"); printf(str); } } int main() { Test(); return 0; }

7. 柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做"柔性数组"成员。

例如:

struct st_type { int i; int a[0]; //柔性数组成员 };

有些编译器会报错无法编译,可以改成:

struct st_type { int i; int a[]; //柔性数组成员 };

7.1 柔性数组的特点

  • 结构中的柔性数组成员前面必须至少一个其他成员。(不包含没办法计算大小)
  • sizeof返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

例如:

typedef struct st_type { int i; int a[0]; //柔性数组成员 }type_a; int main() { printf("%d\n", sizeof(type_a)); //输出的是4 return 0; }

输出结果为4,说明sizeof计算结构大小时不包含柔性数组的内存。

7.2 柔性数组的使用

#include <stdio.h> #include <stdlib.h> // 定义结构体,包含柔性数组 struct S { int n; int arr[]; // C99标准柔性数组写法(原为 int arr[0]) }; int main() { // 1. 动态分配内存:结构体大小 + 5个int的大小 (4 + 5*4 = 24字节) struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int)); if (ps == NULL) { perror("use malloc"); return 1; } // 2. 初始化结构体成员和数组 ps->n = 100; int i = 0; for (i = 0; i < 5; i++) { ps->arr[i] = i + 1; // 填充前5个元素:1, 2, 3, 4, 5 } // 3. 扩容:将数组大小从5个int扩展到10个int // 注意:这里使用临时指针 ptr 接收 realloc 结果,避免 realloc 失败导致原指针 ps 丢失 struct S* ptr = realloc(ps, sizeof(struct S) + 10 * sizeof(int)); if (ptr == NULL) { perror("realloc"); // 严谨的错误处理:扩容失败,释放原内存并退出,防止内存泄漏 free(ps); ps = NULL; return 1; } else { // 扩容成功,将新地址赋给 ps,并置空临时指针 ptr ps = ptr; ptr = NULL; // 填充新扩展的5个元素 for (i = 5; i < 10; i++) { ps->arr[i] = i + 1; // 填充元素:6, 7, 8, 9, 10 } } // 4. 释放动态分配的内存 free(ps); ps = NULL; // 防止野指针 return 0; }

7.3 柔性数组的优势

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

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

立即咨询