C语言动态内存管理:从入门到精通,掌握内存分配的核心技能

张开发
2026/6/10 2:42:46 15 分钟阅读
C语言动态内存管理:从入门到精通,掌握内存分配的核心技能
前言在C语言编程中动态内存管理是一个既基础又关键的概念。很多初学者在学习过程中常常对malloc、free等函数感到困惑甚至在面试中遇到相关题目时手足无措。今天我就带大家深入浅出地学习C语言动态内存管理让你彻底掌握这一核心技能目录一、为什么需要动态内存分配二、malloc和free基础内存管理2.1 malloc函数2.1.1 malloc函数的基本用法2.2.1.1 输出结果2.1.2 更多有关malloc函数的细节2.2 free函数2.2.1 free函数的基本用法2.2.2 更多有关使用free函数的细节三、calloc和realloc高级内存操作3.1 calloc函数3.1.1 calloc函数的基本用法3.1.1.1 输出结果3.1.2 更多有关malloc函数的细节3.2 realloc函数3.2.1 realloc函数的基本用法3.2.1.1 输出结果3.2.2 更多有关realloc函数的细节四、常见动态内存错误及避免方法4.0 申请内存过大4.0.1 运行结果4.1 对NULL指针的解引用4.1.1 解决方案4.2 对动态开辟空间的越界访问4.2.1 解决方案4.3 对非动态开辟的内存使用free函数释放4.4 使用free函数释放一块动态开辟内存的一部分4.5 对同一块内存多次释放4.6 动态开辟内存忘记释放内存泄漏4.7 总结动态内存管理函数书写的规范五、动态内存管理经典笔试题分析5.1 参数传递问题5.1.1 解决方案5.2 返回栈内存地址5.3 正确使用二级指针和动态内存管理函数5.4 指针被释放后非法访问没有权限的内存空间六、柔性数组C99的神奇特性6.1 什么是柔性数组6.2 柔性数组的特点6.3 柔性数组的使用6.4 柔性数组的优势6.5 拓展阅读C语言结构体里的成员数组和指针七、C/C内存区域划分总结7.1 栈区stack7.2 堆区heap7.3 数据段静态区7.4 代码段结语关键原则一、为什么需要动态内存分配在学习动态内存之前我们先来看看传统的内存分配方式int val 20; // 在栈空间上开辟4个字节 char c w; // 在栈空间上开辟1个字节 char arr[10] {0}; // 在栈空间上开辟10个字节的连续空间 int arr[20] {0}; // 在栈空间上开辟80个字节的连续空间这种方式有两个明显的特点空间开辟大小是固定的数组在声明时必须指定长度一旦确定不能调整问题来了有时候我们需要的空间大小在程序运行时才能确定比如用户输入的数据量、文件大小等。这时传统的静态分配方式就无法满足需求了。动态内存分配的出现正是为了解决这个问题它让程序员可以在程序运行时根据需要申请和释放内存大大提高了程序的灵活性。二、malloc和free基础内存管理2.1 malloc函数malloc函数是C语言中最基础的动态内存分配函数void* malloc(size_t size);函数说明向内存申请一块连续可用的空间并返回指向这块空间的指针函数参数中size_t类型的size单位为字节如果开辟成功返回指向开辟空间的指针如果开辟失败返回NULL指针必须检查返回值返回值类型是void*使用时需要强制类型转换如果参数size为0行为是未定义的2.1.1 malloc函数的基本用法#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h #include stdlib.h int main() { // 开辟20个字节的空间存放5个整数 int* p (int*)malloc(20);// 注意malloc返回值的类型是void*需要强制类型转换为int* // 检查是否开辟成功 if (p NULL) { perror(malloc failed); return 1; } // 使用空间 for (int i 0; i 5; i) { *(p i) i 1;// 存放数据 } //打印数据 for (int i 0; i 5; i) { printf(%d , *(p i)); } return 0; }2.2.1.1 输出结果2.1.2 更多有关malloc函数的细节使用malloc函数需要包含头文件#include stdlib.hmalloc函数强制类型转换的写法int* p (int*)malloc(20);使用malloc函数开辟空间后要检验是否开辟成功malloc函数申请的空间存放在堆区补充概念堆区由程序员手动动态分配和释放内存生命周期由程序员完全控制。2.2 free函数free函数是C语言中用来做动态内存的释放和回收的函数void free(void* ptr);函数说明只能释放动态开辟的内存只能释放malloc、calloc、realloc函数开辟的动态内存如果ptr是NULL指针函数不做任何操作释放后最好将指针置为NULL避免野指针2.2.1 free函数的基本用法// 释放空间 free(p);// 把动态开辟内存的使用权限还给操作系统释放的是内存空间的起始地址p变为野指针 p NULL;// 将p置为空指针避免后续使用野指针2.2.2 更多有关使用free函数的细节free函数的作用是把动态开辟内存的使用权限还给操作系统但是经过free函数操作后p仍然指向malloc函数动态开辟的空间的地址相当于p指向了一块没有使用权限的内存因此p变成了野指针free函数释放的是内存空间的起始地址所以在使用malloc函数时如果使用了*p i; p的操作此时free函数将失去作用编译器也可能会报错在释放了动态申请的内存空间后将p手动置为空指针避免野指针对程序的伤害三、calloc和realloc高级内存操作3.1 calloc函数calloc函数与malloc函数类似但会自动初始化内存void* calloc(size_t num, size_t size);函数说明为num个大小为size的元素开辟空间例如为5个大小为sizeof(int)的元素开辟空间自动将申请的空间每个字节初始化为0适合需要初始化为0的场景3.1.1 calloc函数的基本用法#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h #include stdlib.h int main() { // 开辟20个字节的空间存放5个整数 int* p (int*)calloc(5, sizeof(int));// 注意calloc返回值的类型是void*需要强制类型转换为int* // 检查是否开辟成功 if (p NULL) { perror(calloc failed); return 1; } // 与malloc函数不同calloc函数会将内存空间初始化为0 //检验内存初始化的值 for (int i 0; i 5; i) { printf(%d , *(p i)); } printf(\n); // 使用空间 for (int i 0; i 5; i) { *(p i) i 1;// 存放数据 } // 打印数据 for (int i 0; i 5; i) { printf(%d , *(p i)); } // 释放空间 free(p);// 把动态开辟内存的使用权限还给操作系统释放的是内存空间的起始地址p变为野指针 p NULL;// 将p置为空指针避免后续使用野指针 return 0; }3.1.1.1 输出结果3.1.2 更多有关malloc函数的细节如果我们对申请的内存空间的内容要求初始化为0那么可以很方便的使用calloc函数来完成任务。3.2 realloc函数realloc函数用于调整已分配内存的大小void* realloc(void* ptr, size_t size);函数说明这个函数调整原内存空间大小的基础上还会将原来内存中的数据移动到新的空间realloc函数在调整内存空间存在三种情况情况1原有空间之后有足够大的空间要扩展内存就直接原有内存之后直接追加空间原来空间的数据不发生变化返回的内存空间的地址不变情况2原有空间之后没有足够大的空间①在堆空间上另找连续的符合新的大小要求的空间 ②将原来空间的数据拷贝一份到新的空间 ③释放旧的空间 ④返回新的内存空间的地址情况3内存调整失败返回空指针NULL3.2.1 realloc函数的基本用法#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h #include stdlib.h int main() { // 开辟20个字节的空间存放5个整数 int* p (int*)malloc(20);// 注意malloc返回值的类型是void*需要强制类型转换为int* // 检查是否开辟成功 if (p NULL) { perror(malloc failed); return 1; } // 使用空间 for (int i 0; i 5; i) { *(p i) i 1;// 存放数据 } // 调整空间 int* ptr (int*)realloc(p, 40);// 调整空间大小为10个整数的大小并用临时变量来接收调整后的空间防止调整失败出现空指针的情况 // 检查realloc函数是否成功调整内存空间 if (ptr ! NULL) { p ptr;// 调整成功使用40个字节的空间 for (int i 5; i 10; i) { *(p i) i 1; } for (int i 0; i 10; i) { printf(%d , *(p i)); } free(p); p NULL; } else// 调整失败 { perror(realloc failed); free(p); p NULL; return 1; } return 0; }3.2.1.1 输出结果3.2.2 更多有关realloc函数的细节由于realloc函数存在调成内存失败的情况情况3故需要先使用临时变量ptr来接收realloc函数调整后的地址避免直接使用原地址时出现调整失败将p置为空指针NULL的情况而破坏原有数据的情况。调整后对临时变量ptr进行检验确保ptr不为空指针NULL后才将ptr赋值给p原指针。realloc函数可以完成和malloc函数一样的操作。// realloc函数可以完成和malloc函数一样的操作 int* p (int*)realloc(NULL, 20);// 等价于 int* p (int*)malloc(20);四、常见动态内存错误及避免方法4.0 申请内存过大#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h #include stdlib.h #include assert.h int main() { // 4.0 申请的内存过大 int* p (int*)malloc(10000000000000); assert(p); return 0; }4.0.1 运行结果4.1 对NULL指针的解引用#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h #include stdlib.h #include assert.h int main() { // 4.1 int* p (int*)malloc(INT_MAX); *p 20; // 如果p为NULL程序崩溃 return 0; }4.1.1 解决方案判断在对p解引用前判断p是否为空指针使用assert断言判断p是否为空指针4.2 对动态开辟空间的越界访问#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h #include stdlib.h #include assert.h void test() { int* p (int*)malloc(10 * sizeof(int)); for (int i 0; i 10; i)// i10时越界 { *(p i) i; } free(p); } int main() { // 4.2 test(); return 0; }4.2.1 解决方案严格控制访问范围4.3 对非动态开辟的内存使用free函数释放#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h #include stdlib.h #include assert.h void test2() { int a 10; int* p a; free(p); // 错误p指向栈内存 p NULL; } int main() { // 4.3 test2(); return 0; }4.4 使用free函数释放一块动态开辟内存的一部分#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h #include stdlib.h #include assert.h int main() { // 4.4 int* p (int*)malloc(100); if (p NULL) { perror(malloc failed); return 1; } for (int i 0; i 10; i) { *p i; p; } free(p); p NULL; return 0; }4.5 对同一块内存多次释放#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h #include stdlib.h #include assert.h void test3() { int* p (int*)malloc(100); free(p); free(p); // 重复释放危险 } int main() { // 4.5 test3(); return 0; }4.6 动态开辟内存忘记释放内存泄漏#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h #include stdlib.h #include assert.h void test4() { int* p (int*)malloc(100); if (NULL ! p) { *p 20; } // 忘记free(p)导致内存泄漏 } int main() { // 4.6 test4(); while (1); return 0; }4.7 总结动态内存管理函数书写的规范其实malloc/calloc/realloc申请的内存如果不想使用的时候可以使用free释放。如果没有使用free来说释放当程序运行结束的时候也会由操作系统回收的尽量要做到谁函数申请的空间谁释放malloc/calloc/realloc和free成对出现补充有时即使malloc和free成对出现也可能会出现内存泄漏的问题void test5() { int* p (int*)malloc(100); if (NULL ! p) { *p 20; } if (*p 0) return 1; free(p); }如果不能释放要告诉使用的人记得释放五、动态内存管理经典笔试题分析5.1 参数传递问题void GetMemory(char* p) { p (char*)malloc(100); } void Test(void) { char* str NULL; GetMemory(str); strcpy(str, hello world); // 程序崩溃 printf(str); }分析C语言的函数是值传递函数内修改p不影响str5.1.1 解决方案void GetMemory(char** p) { *p (char*)malloc(100); } void Test(void) { char* str NULL; GetMemory(str); strcpy(str, hello world); printf(str); free(str); str NULL; }5.2 返回栈内存地址char* GetMemory(void) { char p[] hello world; return p; // 返回栈内存地址 } void Test(void) { char* str NULL; str GetMemory(); printf(str); // 未定义行为 }5.3 正确使用二级指针和动态内存管理函数void GetMemory(char** p, int num) { *p (char*)malloc(num); } void Test(void) { char* str NULL; GetMemory(str, 100); strcpy(str, hello); printf(str); // 正确输出 free(str); // 不要忘记释放 str NULL; }5.4 指针被释放后非法访问没有权限的内存空间void Test(void) { char* str (char*)malloc(100); strcpy(str, hello); free(str); if (str ! NULL) { strcpy(str, world); // 野指针使用 printf(str); } }分析free掉str后str并没有被置为空指针而是变成了野指针指向一块没有访问权限的内存空间所以if语句中的条件成立但是下面的strcpy函数执行的操作是非法的六、柔性数组C99的神奇特性6.1 什么是柔性数组C99标准中结构体的最后一个元素可以是未知大小的数组称为柔性数组成员typedef struct st_type { int i; int a[]; // 柔性数组成员 等价于 int a[0]; } type_a;6.2 柔性数组的特点柔性数组成员前面必须至少有一个其他成员sizeof返回的结构大小不包括柔性数组内存需要使用malloc函数进行动态分配内存并且分配的内存应该大于结构的大小以适应柔性数组的预期大小6.3 柔性数组的使用#include stdio.h #include stdlib.h struct S { int n;// 4bytes int arr[0]; }; int main() { printf(%zd\n, sizeof(struct S)); // 使用柔性数组开辟空间 struct S* ps (struct S*)malloc(sizeof(struct S) 5 * sizeof(int)); if (ps NULL) { perror(malloc failed); return 1; } ps-n 100; // 使用空间 for (int i 0; i 5; i) { ps-arr[i] i 1; } // 打印数组内容 for (int j 0; j 5; j) { printf(%d , ps-arr[j]); } printf(\n); // 调整空间 struct S* ptr (struct S*)realloc(ps, sizeof(struct S) 10 * sizeof(int)); if (ptr ! NULL) { ps ptr; // 使用空间 for (int i 5; i 15; i) { ps-arr[i] i 1; } // 打印数组内容 for (int i 0; i 15; i) { printf(%d , ps-arr[i]); } } else { perror(realloc failed); return 1; } // 释放空间 free(ps); ps NULL; return 0; }说明通过malloc函数动态分配的内存空间还可以使用realloc函数灵活调整数组大小实现内存空间的动态伸缩更多方法但不推荐多次使用mallocrealloc等动态开辟内存的函数会增加内存碎片导致内存空间的利用率降低多次使用mallocrealloc等函数可能会造成忘记释放空间的问题#include stdio.h #include stdlib.h struct S { int n; int* arr; }; int main() { struct S* ps (struct S*)malloc(sizeof(struct S)); if (ps NULL) return 1; ps-arr (int*)malloc(5 * sizeof(int)); if (ps-arr NULL) return 1; //使用 ps-n 100; int i 0; for (i 0; i 5; i) { ps-arr[i] i; } //调整数组大小 int* ptr (int*)realloc(ps-arr, 10 * sizeof(int)); if (ptr ! NULL) { ps-arr ptr; } //使用 //... //释放 free(ps-arr); ps-arr NULL; free(ps); ps NULL; return 0; }6.4 柔性数组的优势相比传统指针方式柔性数组有两大优势方便内存释放只需要一次free就能释放所有内存提高访问速度连续内存有利于提高访问速度减少内存碎片6.5 拓展阅读C语言结构体里的成员数组和指针七、C/C内存区域划分总结7.1栈区stack存放局部变量、函数参数、返回地址等函数执行结束自动释放效率高但容量有限7.2 堆区heap程序员手动分配释放程序结束时可能由操作系统回收分配方式类似于链表7.3 数据段静态区存放全局变量、静态变量程序结束后由系统释放7.4 代码段存放函数体的二进制代码结语动态内存管理是C语言编程的核心技能之一。掌握好malloc、calloc、realloc、free等函数的使用理解常见的内存错误并学会柔性数组这样的高级特性将让你的C语言编程水平更上一层楼。关键原则始终检查malloc/calloc/realloc的返回值释放内存后将指针置为NULL避免内存泄漏和野指针理解内存区域划分合理使用不同区域的内存希望这篇文章对你有所帮助如果你有任何疑问或想了解更多细节欢迎在评论区留言讨论。记得点赞收藏分享给更多需要学习C语言的朋友

更多文章