首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C语言】动态内存管理(详解版)

【C语言】动态内存管理(详解版)

作者头像
用户11915063
发布2025-11-20 09:09:27
发布2025-11-20 09:09:27
1400
举报

一、为什么要有动态内存分配

我们已经掌握的内存开辟方式有:

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

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

  • 空间开辟的大小是固定的
  • 数组在声明的时候,必须指定数组的长度,数组的长度大小一旦确定,数组空间的大小也就确定,不能调整大小

动态内存分配的原因:

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知 道,那数组的编译时开辟空间的方式就不能满足了。

动态内存分配的意义:

C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。


二、malloc和free

1、malloc函数

首先给大家介绍一下C语言提供的一个动态内存开辟的函数:

void* malloc (size_t size);

返回值: 指向要申请空间的指针,由于不确定要给什么类型的数据开辟空间,所以这里指针类型是void*类型,具体在使用的时候由用户自己来确定。

函数参数:size_t类型的参数由于开辟的空间大小不可能是负数,所以这里用size_t类型(无符号整型),size为要开辟空间的大小,例如:我要开辟10个字节的大小,函数参数就可以表示为10*sizeof(int)。

功能:函数可向堆区申请一片连续可用的空间。

注意事项:

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

2、free函数

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

void free (void* ptr);

返回值:无。

函数参数:指向动态内存开辟的那片空间的指针。

功能:释放动态开辟的内存。

注意事项:

  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptr 是NULL指针,则函数什么事都不做。
  • malloc和free都声明在 <stdlib.h> 头文件中。

3、malloc函数和free函数的使用

给大家举个例子:

代码语言:javascript
复制
#include<stdio.h>
#include<stdlib.h>
//malloc
int main()
{
	//申请20个字节空间,存放5个整型
	int* p = (int*)malloc(5 * sizeof(int));
	//判断返回值
	//开辟失败
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//开辟成功
	//使用这块空间
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p+i) = i + 1;
	}
	for (i = 0; i < 5; i++)
	{
		printf("%d\n", p[i]);
	}
	//当不再使用这块空间,就主动还回去
	free(p);//p必须是上面原来开闭空间所用到的指针,如果是别的指针,没反应
	p = NULL;//释放过后必须置为空,要不就变成了野指针
	return 0;

}

运行结果:


三、calloc函数和realloc函数

1、calloc函数

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

void* calloc (size_t num, size_t size);

返回值: 指向要动态内存开辟的那片空间。

函数参数:num:为个数 size:为大小。

例如:我要开辟10个整形大小的空间函数参数为(10 , sizeof(int))

功能:为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0

malloc函数与calloc函数的比较:

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

例子:

代码语言:javascript
复制
//calloc
int main()
{
	//int* p = (int*)malloc(10 * sizeof(int));
	int* p=(int*)calloc(10, sizeof(int));//会初始化为0
	//caolloc=malloc+memset	
    //判断是否开辟成功
    if(p==NULL)
    {
        perror("calloc");
        return 1;
    }
    //开辟成功后使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d\n", p[i]);
	}
    //释放
    free(p);
    p=NULL;
	return 0;
}

运行结果:

结论: 如果我们要动态内存开辟一片空间并且要全部初始化为0,我们就可以使用calloc函数来完成,如果不想要全部初始化为0,那么就用malloc函数。

2、realloc函数

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

返回值: 调整过后空间的起始地址

函数参数:ptr:要调整空间的地址,size:要调整空间新的大小

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

注意事项:这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

2.1 realloc在调整内存空间的是存在两种情况:
  • 情况1:原有空间之后有足够大的空间
  • 情况2:原有空间之后没有足够大的空间

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

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

如果大家还没有理解的话,这里我给大家画个图更直观的感受一下:

运行代码:由于不容易看到这两种情况使用情况一我们用X86环境,情况二用X64环境

代码语言:javascript
复制
//realloc
int main()
{
	//开辟空间,存放1-5
	int* p = (int*)malloc(5 * sizeof(int));
	//开辟失败
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//开辟成功
	//使用
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		p[i] = i + 1;
	}
	//继续存放6-10,需要增容
	int* p2 = realloc(p, 10 * sizeof(int));
	if (p2 == NULL)
	{
		perror("realloc");
		free(p);
		p = NULL;
		return 1;
	}
	p = p2;
	for (i = 5; i < 10; i++)
	{
		p[i] = i + 1;
	}
	free(p);
	p = NULL;
	return 0;
}

图一: 这种情况为情况一,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化,我们也可以观察到二者地址相同。

图二: 这种情况为情况二,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找⼀个合适大小的连续空间来使用。这样函数返回的是⼀个新的内存地址。我们也可以观察到二者地址不相同。


四、常见的动态内存的错误

4.1对NULL指针的解引用操作
代码语言:javascript
复制
#include<stdio.h>
#include<stdlib.h>
int main()
 {
 int *p = (int *)malloc(INT_MAX/4);
 *p = 20;//如果p的值是NULL,就会有问题 
 free(p);
 return 0;
 }

解释:这里开辟空间后,我们没有判断开辟空间是否成功就对p进行解引用操作,如果这里开辟失败,p就是个空指针,对空指针解引用操作就会报错。

改进:在使用之前,要先对malloc,realloc,calloc这种函数的返回值进行判断,如果为NULL,将它的错误信息打印出来,并直接返回,避免了对空指针的使用。

4.2对动态开辟内存空间的越界访问
代码语言:javascript
复制
//2.对动态开辟内存空间的越界访问
int main()
{
	int* p = (int*)malloc(5 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i <= 5; i++)
	{
		p[i] = i + 1;//当i=5就越界了
	}
	return 0;
}

解释: 这里我们动态内存开辟了5个整型的空间,但我们在使用的时候i=5的时候越界了,这样就会导致错误

4.3对非动态开辟内存使用free释放
代码语言:javascript
复制
//3.对非动态开辟内存使用free释放
int main()
{
	int arr[] = { 1,2,3,4 };
	int* p = arr;
	//使用
	//.....
	free(p);//只能释放malloc,calloc,realloc申请的空间
	return 0;
}

解释: free只能释放malloc,realloc,calloc函数在堆区申请的空间,栈区上的空间会自己销毁的。

4.4使用free释放一块动态开辟内存的一部分
代码语言:javascript
复制
//4. 使用free释放一块动态开辟内存的一部分
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 5; i++)
	{
		/**p = i + 1;
		p++;*/  //err
		*(p + i) = i + 1;//正确的

	}
	//free(p);//free释放空间时,必须要给这块空间的起始地址,不能给中间地址
	p = NULL;
	return 0;
}

解释: 在释放p时,我们要看p的地址是否为起始地址,这里我们使用p++的情况,指针就会往后走,从而导致指针指向的不是起始位置。

改进:可以使用*(p+i),这样就不会导致p指针往后走,p指向的一直是起始地址.

4.5对同一块动态内存多次释放
代码语言:javascript
复制
//5.对同一块内存多次释放
int* test()
{
	int* p = (int*)malloc(5 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	free(p);
}
int main()
{
	int*ptr=test();
	//....
	free(ptr);//二次释放
	return 0;
}

解释: 对同一块动态内存开辟的空间多次释放也会报错,所以要避免。

4.6动态开辟内存忘记释放(内存泄漏)
代码语言:javascript
复制
//6.动态开辟内存忘记释放(内存泄露)
int* test()
{
	int* p = (int*)malloc(5 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
}
int main()
{
	test();
	//....
	return 0;
}

注意:

  • 忘记释放不再使用的动态开辟的空间会造成内存泄漏。
  • 动态开辟的空间一定要释放,并且正确释放。
  • 谁申请的空间,谁释放
  • 函数1不方便释放的空间要告诉函数2释放

五、动态内存经典笔试题分析

5.1-->题目一:

代码语言:javascript
复制
//经典笔试题一
#include<string.h>

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}

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

int main()
{
	Test();
	return 0;
}

运行结果:

解析: 1. 程序崩溃 原因是: 1.str传递给GetMemory函数的时候,采用的值传递形参变量p其实是str的一份拷贝 当我们把malloc申请的空间的起始地址存放在p中时不会修改str,str依然为NULL所以当GetMemory函数返回后,再去调用strcpy函数需要将“hello world”拷贝到str指向的空间时,程序崩溃。 2. 因为malloc申请的空间没有free释放,存在内存泄露

修改后:

代码语言:javascript
复制
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;
}

int main()
{
	Test();
	return 0;
}

5.2-->题目二:

代码语言:javascript
复制
//经典笔试题二
char* GetMemory(void)
{
	char p[] = "hello world";
	return p;//出了函数就销毁了
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

运行结果:

解析: p是数组名,也就是首元素地址,也就是h的地址,p是临时变量,是在栈区中存储的,而p出来GetMemory()函数就会销毁,地址中存放的就不一定是原来数据了,所以就会打印出来随机值 改进: 因为像malloc、calloc、realloc这类在堆区开辟空间的函数,我们就可以在变量前加static来修饰变量,即把存储位置改为静态区,这样就不会导致临时变量出了函数销毁的情况了

改进:

代码语言:javascript
复制
//改进
char* GetMemory(void)
{
	static char p[] = "hello world";//存放在静态区了
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

5.3-->题目三:

代码语言:javascript
复制
//经典笔试题三
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使用过后没有释放

改进:

代码语言:javascript
复制
//改进
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;
}

int main()
{
	Test();
	return 0;
}

5.4-->题目四:

代码语言:javascript
复制
//经典笔试题四
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);//成为野指针
	if (str != NULL)
	{
		strcpy(str, "world");//非法访问内存
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

打印结果:

解析: 1.这里str使用过后释放,但是没有将str置为NULL,所以这时的str就是野指针。 2.calloc函数开辟空间后,也没有进行判断str是否为空

改进:

代码语言:javascript
复制
//改进
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;
}

改进后的打印结果:

解释:释放后置为NULL,判断是否为NULL,不进行srtcpy,所以什么也不打印

六、柔性数组

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

接下来我给大家解释一下柔性数组的概念:C99中,结构中的最后⼀个元素允许是未知大小的数组,这就叫做柔性数组成员。

给大家举个最简单的例子:

struct s { int i; char c; int a[];//柔性数组前面至少要有1个成员 };

6.1柔性数组的特点:

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

给大家看一段代码:

代码语言:javascript
复制
#include<stdio.h>

struct s
{
	int i;
	char c;
	int a[];//柔性数组前面至少要有1个成员
};

int main()
{
	printf("%zu", sizeof(struct s));//8---不计算柔性数组
	return 0;
}

解释: sizeof返回的这种结构大小不包括柔性数组的内存。

6.2柔性数组的使用:

代码语言:javascript
复制
#include<stdlib.h>
struct S
{
	int n;
	int arr[];//柔性数组
};
int main()
{
	struct S*ps=(struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	//使用
	ps->n = 100;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i + 1;
	}
	//调整
	struct S*tmp=realloc(ps, sizeof(struct S) + 10 * sizeof(int));
	ps = tmp;
	if (tmp == NULL)
	{
		perror("realloc");
		return 1;
	}
	//释放
	free(ps);
	ps = NULL;
	return 0;
}

解释:创建一个struct S的结构体,结构体成员包含了一个整型n和一个整型数组arr,malloc为结构体动态内存开辟空间,首先开辟一个整型n的大小的空间,再创建柔性数组的空间大小,然后判断开辟是否成功,把n的值赋为100,用for循环,将柔性数组的元素分别赋为1,2,3,4,5,再用realloc函数将结构体中柔性数组的空间扩大为10个整型,然后再进行判断.

6.3柔性数组的优势:

上面的代码也可以用另外一种方法实现:

代码语言:javascript
复制
struct S
{
	int n;
	int* arr;
};
int main()
{
	struct S*ps=(struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->n = 100;
	int* ptr = (int*)malloc(5 * sizeof(int));
	if (ptr == NULL)
	{
		perror("malloc2");
		return 1;
	}
	ps->arr = ptr;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i + 1;
	}
	//空间不够,扩容
	ptr = realloc(ps->arr, 10 * sizeof(int));
	if (ptr == NULL)
	{
		perror("realloc");
		return 1;
	}
	//释放两次
	free(ps->arr);
	free(ps);
	ps = NULL;
	return 0;
}

第一个好处是:方便内存释放 如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。 第二个好处是:这样有利于访问速度. 连续的内存有益于提高访问速度,也有益于减少内存碎片。其实,这样觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

七、总结C/C++中程序内存区域划分

C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时 这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内 存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统) 回收。分配方式类似于链表。
  3. 数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

总结:这篇文章给大家总结了malloc、calloc、realloc、free函数的使用以及注意事项,同时给大家总结了常见的动态内存的错误,以及动态内存经典笔试题和柔性数组的概念。

如果这篇文章让大家对动态内存管理这方面知识有扩展和新的认识,希望大家给博主一键三连,你们的支持是我最大的动力,感谢大家的支持🌹🌹🌹

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-07-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、为什么要有动态内存分配
  • 二、malloc和free
    • 1、malloc函数
    • 2、free函数
    • 3、malloc函数和free函数的使用
  • 三、calloc函数和realloc函数
    • 1、calloc函数
    • 2、realloc函数
      • 2.1 realloc在调整内存空间的是存在两种情况:
  • 四、常见的动态内存的错误
    • 4.1对NULL指针的解引用操作
    • 4.2对动态开辟内存空间的越界访问
    • 4.3对非动态开辟内存使用free释放
    • 4.4使用free释放一块动态开辟内存的一部分
    • 4.5对同一块动态内存多次释放
    • 4.6动态开辟内存忘记释放(内存泄漏)
  • 五、动态内存经典笔试题分析
    • 5.1-->题目一:
    • 5.2-->题目二:
    • 5.3-->题目三:
    • 5.4-->题目四:
  • 六、柔性数组
    • 6.1柔性数组的特点:
    • 6.2柔性数组的使用:
    • 6.3柔性数组的优势:
  • 七、总结C/C++中程序内存区域划分
    • C/C++程序内存分配的几个区域:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档