当前位置: 首页 > news >正文

C/C++语言动态内存管理宝典

1. 动态内存管理的意义

在C语言中,动态内存管理一直是非常重要的,普通变量和数组虽然可以开辟内存,但它们的大小通常在编译阶段就已经确定,无法灵活适应程序运行时的数据规模变化。

int val = 20; char arr[10] = {0}; struct s{ int num; char arr[10]; }

上述第一行代码会在栈区开辟一块空间,用来存放整型变量val;第二行代码会在栈区开辟一段连续空间,用来存放长度为 10 的字符数组arr。第三行代码开始定义了一个结构体类型 struct s,该结构体中包含一个整型成员 num和一个字符数组成员 arr。

但这些空间大小往往都是固定的,也就是说,普通数组在声明时必须提前指定长度,程序运行过程中无法灵活调整数组大小。在一些简单场景下,这样做没有问题。比如我们明确知道只需要存放 10 个元素,那么直接定义数组即可。但是在实际开发中,很多时候我们并不能提前确定需要多少空间。

例如:

  1. 用户输入多少个数据,程序运行前并不知道;
  2. 文件中有多少内容,程序读取前并不知道;
  3. 通讯录中要保存多少个联系人,程序一开始也无法确定;
  4. 学生成绩管理系统中有多少名学生,可能需要运行时才能决定。

如果我们仍然使用固定大小的数组,就可能出现两个问题:

第一,如果数组开小了,数据放不下,可能会导致越界访问。

int arr[10]; // 如果实际需要存放 100 个数据,arr 就不够用了

第二,如果数组开大了,虽然可以避免空间不够的问题,但会造成内存浪费。

int arr[10000]; // 实际只存放 10 个数据,剩下的空间就被浪费了

因此,固定大小的栈空间并不能很好地应对所有场景。尤其是当程序需要在运行过程中,根据实际情况决定申请多少内存时,就需要一种更加灵活的内存管理方式。

这就是动态内存管理存在的意义!

C 语言提供了malloccallocreallocfree等函数,让程序员可以在程序运行期间主动向堆区申请空间,并在使用完毕后主动释放空间。

简单来说:

  • 普通变量和数组通常是在栈区开辟空间;
  • 栈区空间由系统自动管理,使用方便,但灵活性较差;
  • 动态内存是在堆区申请的;
  • 堆区空间由程序员手动申请和释放,使用更加灵活;
  • 当程序运行时才知道需要多少空间时,就应该考虑使用动态内存管理。

所以,动态内存管理解决的核心问题就是:当程序在运行过程中才知道需要多少内存时,可以按需申请、按需释放,从而更加灵活地使用内存。

2. C/C++ 程序中的内存区域划分

1.栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。

2.堆区(heap:⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配⽅式类似于链表。

3.数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。

4.代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。

3. malloc与free 函数详解

3.1 malloc函数

malloc是C语⾔提供了⼀个动态内存开辟的函数:

void* malloc(size_t size);

这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。

1.如果开辟成功,则返回⼀个指向开辟好空间的指针

2.如果开辟失败,则返回⼀个NULL 指针,因此malloc的返回值⼀定要做检查。返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定。

3.如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。可能分配也可能报错。

3.2 free函数

C语⾔提供了另外⼀个函数free,专⻔是⽤来做动态内存的释放和回收的,函数原型如下:

1 void free (void* ptr);

free函数⽤来释放动态开辟的内存。

如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的。

如果参数 ptr 是NULL指针,则函数什么事都不做。

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

但函数free后,并不一定会将ptr指针后的连续内存置为空,故一般在free后,要将ptr置为NULL,避免空指针。

#include <stdio.h> #include <stdlib.h> int main() { int arr[3]={0}; int*ptr; ptr=(int*)malloc(3*sizeof(arr[0])); for(int i=0;i<3;i++){ *(ptr+i)=i;//为ptr指针所指内存赋值 arr[i]=*(ptr+i);//将ptr内存所指值赋值给arr printf("arr[%d]=%-2d",i,arr[i]); printf("ptr[%d]=%-2d",i,ptr[i]); printf("\n"); } free(ptr); return 0; }

此时ptr未置空,则ptr所指内存视图如下所示(内存所示地址为小端存储,16进制):

可以看到ptr所指不为空,有野指针风险。

#include <stdio.h> #include <stdlib.h> int main() { int arr[3]={0}; int*ptr; ptr=(int*)malloc(3*sizeof(arr[0])); for(int i=0;i<3;i++){ *(ptr+i)=i;//为ptr指针所指内存赋值 arr[i]=*(ptr+i);//将ptr内存所指值赋值给arr printf("arr[%d]=%-2d",i,arr[i]); printf("ptr[%d]=%-2d",i,ptr[i]); printf("\n"); } free(ptr); ptr=NULL; return 0; }

置空后ptr所指内存如下(内存所示地址为小端存储,16进制,Clion2025):

可以看到已被置空

4. calloc函数与realloc函数

4.1 calloc函数

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

1 void* calloc (size_t num, size_t size);

函数的功能是为 num 个⼤⼩为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。

与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0

举个例子:

#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

此时查看内存视窗图,可见p所指内存内容均为0(内存所示地址为小端存储,16进制):

由图可知p所指地址空间存储字节均为0,所以如果我们对申请的内存空间的内容要求初始化,那么可以很⽅便的使⽤calloc函数来完成任务。

4.2 realloc函数

realloc函数的出现让动态内存管理更加灵活。

有时会我们发现过去申请的空间太⼩了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的使

⽤内存,我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc 函数就可以做到对动态开辟内存⼤

⼩的调整。

函数原型如下:

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

ptr是要调整的内存地址

size为 调整之后新⼤⼩

返回值为调整之后的内存起始位置。

这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到新的空间。

realloc在调整内存空间的是存在两种情况:

情况1:原有空间之后⾜够⼤的空间

情况2:原有空间之后没有⾜够⼤的空间

情况1

当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发⽣变化。

int main() { int *ptr = (int*)malloc(20); //代码1 - 直接将realloc的返回值放到ptr中 ptr=(int*)realloc(ptr,1000); //代码2 - 先将realloc函数的返回值放在temp中,不为NULL,在放ptr中 int*temp=NULL; temp=(int*)realloc(ptr,40); if (temp!=NULL) { ptr=temp; } free(ptr); return 0; }

以下是代码1的内存视图:

由此可见ptr所指空间置为空,后面放不下;

情况2

当原空间后面没有足够连续空间时,realloc(ptr, new_size)大致会做这几步:

  1. 在堆上找一块新的、更大的连续空间
  2. 把旧空间中的数据拷贝到新空间(和malloc区别)
对比项mallocrealloc(情况2)
申请新空间
保留旧数据
自动复制旧数据
自动释放旧空间
用于调整已有内存大小
  1. 释放旧空间(和malloc区别)
  2. 返回新空间地

当是情况2 的时候,原有空间之后没有⾜够多的空间时,扩展的⽅法是:在堆空间上另找⼀个合适⼤⼩的连续空间来使⽤。这样函数返回的是⼀个新的内存地址。

以下是代码2运行情况(注意要将代码1给注释掉,否则因为ptr=NULL,temp也会=NULL):

5. 对动态内存的常见错误

5.1 对NULL指针的解引⽤操作

void test(){ int* p=(int*)malloc(INT_MAX) *p=20; freep(p); }

由于INT_MAX过大,导致p指针为NULL,解引用出错。

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

void test(){ int* p=(int*)malloc(40); assert(p);//p不为NULL int i; for(i=0;i<=10;i++){ p[i]=i;}//越界访问 free(p); return 0; }

p空间最大只能40个字节,此时的i访问越界了。

5.3 对⾮动态开辟内存使⽤free释放

void test(){ int a=10; int*p=&a; free(p);//非动态申请的堆内存 }

p为非动态申请的堆内存

5.4 使⽤free释放⼀块动态开辟内存的⼀部分

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

此时的p不再指向动态内存起始位置

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

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

5.6 动态内存忘记释放

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

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

6.1 题目1:

void GetMemory(char *p) { p = (char *)malloc(100); } void Test(void) { char *str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str); }

请问运⾏Test 函数会有什么样的结果?

答案:NULL。

6.2 题目2:

char *GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL; str = GetMemory(); printf(str); }

请问运⾏Test 函数会有什么样的结果?

答案:崩溃

6.3 题目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); }

请问运⾏Test 函数会有什么样的结果?

答案:"hello"

6.4 题目4:

void Test(void) { char *str = (char *) malloc(100); strcpy(str, "hello"); free(str); if(str != NULL) { strcpy(str, "world"); printf(str); } }

请问运⾏Test 函数会有什么样的结果?

答案:结果是未定义

7. 柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。

C99 中,结构中的最后⼀个元素允许是未知⼤⼩的数组,这就叫做柔性数组成员

例如:

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

或:

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

7.1 柔性数组特点

  1. 结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。
  2. sizeof 返回的这种结构⼤⼩不包括柔性数组的内存。
  3. 包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的⼤⼩,以适应柔性数组的预期⼤⼩。

例如:

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

7.2 柔性数组的使用

//代码1 #include <stdio.h> #include <stdlib.h> typedef struct st_type { int i; int a[0];//柔性数组成员 }type_a; int main() { int i = 0; type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int)); //业务处理 p->i = 100; for(i=0; i<100; i++) { p->a[i] = i; } free(p); return 0; }

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

7.3 柔性数组的优劣

上述的 type_a 结构也可以设计为下⾯的结构,也能完成同样的效果:

//代码2 #include <stdio.h> #include <stdlib.h> typedef struct st_type { int i; int *p_a; }type_a; int main() { type_a *p = (type_a *)malloc(sizeof(type_a)); p->i = 100; p->p_a = (int *)malloc(p->i*sizeof(int)); //业务处理 for(i=0; i<100; i++) { p->p_a[i] = i; } //释放空间 free(p->p_a); p->p_a = NULL; free(p); p = NULL; return 0; }

上述 代码1 和 代码2 可以完成同样的功能,但是 ⽅法1 的实现有两个好处:

第⼀个好处是:⽅便内存释放

如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤⼾。⽤户调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给⽤⼾⼀个结构体指针,⽤户做⼀次free就可以把所有的内存也给释放掉。

第⼆个好处是:有利于访问速度

连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实也没多⾼了,反正你跑不了要⽤做偏移量的加法来寻址)

8.总结

动态内存管理是 C 语言中非常重要的一部分,它解决了普通变量和数组空间大小固定、不够灵活的问题。当程序在运行过程中才知道需要多少内存时,就可以通过动态内存管理在堆区按需申请空间。

本文主要介绍了malloc、free、calloc 和 realloc这几个常用函数。

malloc 用来在堆区申请一块指定大小的连续空间,但申请到的空间不会自动初始化;calloc 同样用于动态申请空间,不过它会将申请到的内存初始化为 0;realloc 用来调整已经申请好的动态内存大小,当原空间后面有足够连续空间时,可能会直接在原地扩容;当原空间后面空间不足时,则可能会重新申请一块更大的空间,并将原数据拷贝过去;free 则用于释放动态申请的空间,避免造成内存泄漏

在使用动态内存时,需要特别注意几个问题:申请内存后一定要判断返回值是否为 NULL,避免对空指针进行解引用;访问动态内存时不能越界;不能对非动态申请的空间使用 free;不能释放同一块空间多次;释放空间时必须传入动态内存的起始地址;使用完动态内存后要及时释放,避免内存泄漏。

此外,free 只是释放指针所指向的动态内存,并不会自动把指针变量本身置为 NULL。因此,释放后建议手动写上:

free(ptr); ptr = NULL;

如果你觉得本篇博客对你有帮助,不妨点个赞鼓励一下作者。

http://www.jsqmd.com/news/722986/

相关文章:

  • 17.18.动态规划,背包问题
  • Dify - (一)、本地部署Dify+聊天助手/Agent
  • 解读C++11 原生字符串
  • 路由器1111111111
  • 2025_NIPS_Understanding the Expressive Power and Mechanisms of Transformer for Sequence Modeling
  • C 基础(16) - C 预处理和C库
  • 终极指南:如何用OnStep将普通望远镜升级为智能寻星系统
  • 手把手带你了解C++最小栈
  • 2026年3月靠谱的汽车增压器组件口碑推荐,欧曼增压器/船机增压器/7830增压器/工程机械增压器,汽车增压器供应商推荐 - 品牌推荐师
  • MIMO稀疏信道估计:MOMPnet算法与硬件损伤校准
  • 95%小白选手持喷码机的误区
  • 华硕笔记本性能调校终极指南:G-Helper完全替代Armoury Crate
  • 国网低压侧, 智能融合终端, 微应用基础库
  • 2025_NIPS_Table2LaTeX-RL: High-Fidelity LaTeX Code Generation from Table Images via Reinforced Mu...
  • 出轨小三就会净身出户?告诉你出轨离婚财产分割的5个真相
  • ARM架构异常处理与RAS特性深度解析
  • PHP开发的OA办公系统源码|集成CRM客户管理+ERP订单合同管理(PC端与移动端双平台)
  • 2026年惠州保安公司行业解析,惠州工厂保安公司服务优势与选择要点,帮你判断惠州哪家保安公司好 - 栗子测评
  • Proxmox VE (PVE):虚拟化神器,从0开始踩坑
  • 出海办公效率瓶颈凸显,跨应用AI办公助手如何打通跨境业务孤岛?
  • 如何快速实现老Mac升级:OpenCore Legacy Patcher终极指南
  • 抖音无水印视频下载终极指南:3分钟掌握免费高清资源获取秘籍
  • ARM虚拟化核心:HFGRTR_EL2寄存器详解与应用
  • 石墨烯地暖高频自动化设备哪家好?2026年石墨烯地暖高频自动化设备/医疗袋高频热合机厂家推荐权威盘点:华日金菱领衔 - 栗子测评
  • 2026年怎么挑商用和面机厂家?核心技术看这几点 - 优质品牌商家
  • ARM SPE性能分析:PMSIDR_EL1寄存器详解与实践
  • Coordinate IM 系统 - 企业即时通讯解决方案
  • 【教学类-160-14】20260425 AI视频培训-练习014“豆包AI视频《月下枯蔷(哥特风)》+豆包图片风格:油画”
  • ARMv8/v9异常处理与ESR_EL2寄存器深度解析
  • ContextFlow视频对象编辑技术解析与应用实践