鹏哥C语言复习——动态内存开辟

本文详细介绍了动态内存管理的必要性,以及malloc、free、calloc和realloc等动态内存函数的用法、注意事项,包括内存泄漏的概念和柔性数组的使用。同时列出了动态内存管理中的常见错误和优化建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一.动态内存管理的存在原因:

二.动态内存函数的介绍:

1.malloc

2.free

3.calloc

4.realloc

三.常见的动态内存错误汇总:

四.柔性数组介绍


一.动态内存管理的存在原因:

动态内存管理让我们得以自己来维护自己内存空间的大小(即申请空间大小由自己来定)

首先,我们需要知道在计算机中,数据是连续存放的,即存放的地址是连续的;比如数组,假设第一个元素放在地址1上,那么第二个元素就放在了地址2上,以此类推……。而需要开辟的空间时大时小,那么具体类型申请内存(如int型、double型、数组等等)时,会有申请空间小于需要空间或大于需要空间的情况发生,此时就出现了动态内存管理来解决这个问题

内存被划分为了三个区域,分别是栈区、堆区和静态区

栈区主要存放局部变量、形式参数等临时开辟的数据,在此申请的空间,大小无法随意调整,作用域是函数内部

静态区主要存放全局变量、静态变量,静态变量作用域是整个文件,全局变量作用域是整个可执行程序

堆区主要是用malloc()、calloc()、realloc()和free()来进行内存开辟与释放,空间大小可以进行调整在,整个文件中,只有当程序结束了动态内存才会自动销毁、释放和回收

二.动态内存函数的介绍:

动态内存函数共有4种函数:

1.malloc

以下是cplusplus网站对malloc函数的解释

由于malloc函数开辟空间以后,使用者是要使用整型数据还是浮点型数据malloc函数无法判断,因此返回的是无类型指针,使用者在后续使用中需要强制转换成自己需要的类型

有时也会出现malloc函数开辟失败(开辟空间过大)的情况,此时返回的是空指针(NULL)

malloc函数使用前需要引用头文件<stdlib.h>

相关函数(了解即可):strerror()函数和errno

strerror(errno)是将错误代码的错误原因转换成错误信息打印出来,使用前需要引用<string.h>、<errno.h>两个头文件

如下代码便能实现将错误信息打印出来的功能

printf("%s", strerror(errno));

当动态内存开辟失败时,需要在主函数里用 return 1 这条语句作为结束标志

return 0return 1 实际上都是可以的,但是为了更好的区分,人们把 return 0 看作是正常返回,把 return 1 看作是异常返回

下面是开辟成功和开辟失败的两种代码演示:

1.成功版本:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
	int* p = (int*)malloc(40);  
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;  //指针用法与数组相同,后续会详细介绍
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d", *(p + i));
	}
	return 0;
}

输出结果:

0123456789

2.失败版本:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
	int*p = (int*)malloc(INT_MAX);  //INT_MAX叫做整型最大,代表了 2147483647 这个整数
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d", *(p + i));
	}
	return 0;
}

输出结果: 

Not Enough Space
//笔者只有在X86环境下才会出现内存不够

对于 0 这个参数,malloc(0)的结果是未被标准化的,因此malloc(0)的结果取决于编译器

2.free

在讲解free()函数以前,首先需要知道内存泄漏的概念

根据知乎上的一个定义:

内存泄漏是指你向系统申请分配内存进行使用,可是使用完了以后却不归还,结果你申请到的那块内存你自己也不能再访问(也就是你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序

联想记忆:

我借给小明一本书,小明放在家里不看,我让他还回来他又不肯还,最终导致了我想看却看不上,而小明又不看。(“我”指的是电脑,“小明”指的是代码编写者,“书”指的是内存空间)

free()函数就是专门用来释放和回收内存空间的一种函数,然而在使用完free()函数以后,指针所指向的地址并没有被消除,因为free()函数只会释放内存空间,不会消除指针所指向的地址。因此在执行完free语句后,指针就变成了一个野指针

例:假设现在有一个指针*p,且*p所指向的地址为0x011e9940,在执行完free(p)这一条语句后,*p所指向的地址依然为0x011e9940,此时访问*p,*p就作为一个野指针存在。因此需要在执行完free语句后加上p = NULL 让指针p变成一个空指针

因此如果要对*p进行内存释放操作,代码如下:

free(p);
p = NULL;

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
	int* p = (int*)malloc(40);  
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;  //指针用法与数组相同,后续会详细介绍
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d", *(p + i));
	}
	return 0;
}

对于讲解malloc()函数时所出现的示范代码,没有free()函数会不会造成内存泄漏

答案是不会,没有free()函数并不是说内存空间就不回收了,当程序退出时,系统会自动回收内存空间

free()函数注意事项:

1.如果函数参数指向的空间不是动态开辟的,那free函数的行为是未定义的

2.如果函数参数是NULL指针,则函数什么事情也不做

3.free声明在<stdlib.h>文件中

3.calloc

以下是cplusplus网站对calloc函数的解释

size_t num:要开辟几个元素

size_t size:每个元素的字节大小

特殊点:calloc会在返回值前,将calloc函数所操作的空间初始化

例如开辟了10个整型空间赋给了指针*p,那么此时直接输出p得到的结果为

0 0 0 0 0 0 0 0 0 0

除此以外,calloc函数与malloc函数并无二异

4.realloc

以下是cplusplus网站对realloc函数的解释

void* ptr:指向一个先前由malloc、calloc或者realloc开辟的内存空间

size_t size:希望调整成多大的空间大小

realloc函数的特点:realloc(NULL, 40) = malloc(40)

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
	int*p = (int*)malloc(40);  
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//使用
	//0123456789
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	//扩容
	realloc(p, 80);	//*p所指向的空间大小从40个字节修改为了80个字节
	return 0;
}

realloc函数的工作原理:

realloc函数开辟新空间共有2种情况

1.对于realloc所指向的需要修改的空间,开辟了新的空间以后会影响其他已申请使用的空间

:p为指针*p,40个字节红色框为*p指向的空间,绿色框为已申请使用的空间,蓝色框为新开辟的空间,绿色数字80意为需要通过malloc函数将*p指向的空间大小增至80字节

因此此时realloc函数会自动调整扩容的位置,完成扩容的需求

即realloc函数先在内存中开辟一个80字节的空间,然后将原本存放在*p所指向空间的内容复制过来,最后释放回收原本*p所指向的内存空间

2.对于realloc所指向的需要修改的空间,开辟了新的空间以后并未影响其他已申请使用的空间,此时realloc函数返回的首地址依旧是*p所指向的地址,即直接在原内存空间后扩容

:p为指针*p,40byte红框为*p所指向的内存空间,绿色部分是新开辟的40字节空间,其余红色小框是其他已申请使用的空间

由上述的情况1我们可以得知,realloc函数返回的地址不一定是原地址,因此realloc函数再扩容以后不能使用原指针接收,要创建一个新指针接收;并且,当realloc函数扩容以后的内存大小过大,会导致realloc函数扩容失败,返回NULL指针

因此realloc函数的正确使用方式如下:

	//扩容
	int* ptr = realloc(p, 80);	//*p所指向的空间大小从40个字节修改为了80个字节
	if (ptr != NULL)
	{
		p = ptr;
	}
	…… //使用
	free(p);
	p = NULL;
	return 0;

:上述代码中,当p指针释放回收以后,ptr也一同释放回收了,因此无需再对ptr进行释放回收操作;为了让ptr不在指向*p所指向的空间,可以在p = ptr语句后加上一句ptr = NULL,使其成为一个空指针

三.常见的动态内存错误汇总:

1.对NULL指针的解引用操做

代码示例:

	int* p = (int*)malloc(40);
	//……(进行一系列操做使得*p变成了一个空指针)
	*p = 20;

上述代码对空指针进行了解引用操做,所以程序会报错;因此需要在对*p进行解引用操作前,加上一条语句:

	if (p == NULL)
	{
		return 1;
	}

2.对开辟空间的越界访问

代码示例:

	int*p = (int*)malloc(40);  
	if (p == NULL)
    {
		return 1;
	}
	int i = 0;
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;
	}

上述代码中,*p指向的空间只有40个字节的大小,总共可以存入10个整型数据;但是本例在for语句循环中,总共循环了11次,即存放了11个整型数据,超过了开辟的空间大小。因此出现了开辟空间的越界访问

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

代码示例:

int main()
{
	int a = 10;
	int* p = &a;
	//……

	free(p);
	p = NULL;
	return 0;
}

只有动态开辟的内存才能使用free语句!!!要不然会报错

4.使用free释放动态开辟内存的一部分(有时会完全不释放回收)

代码示例:

	int*p = (int*)malloc(40); 
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}
	free(p);
	p = NULL;

上述代码中,*p在执行完for语句以后,就已经指向了原开辟空间以外的地址,此时free语句不进行任何操做;如果*p在执行完for语句后,指向了开辟空间的当中位置,则free语句只对后一半的空间进行释放回收操做

因此,在使用free语句进行释放操作前,应确保*p一直指向初始地址

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

代码示例:

int main()
{
	int* p = (int*)malloc(40);
	//……
	free(p);
		//……
		free(p);
	return 0;
}

上述代码对*p指向的地址进行两次释放回收,因此程序在运行时会报错

可以如下修改:

int main()
{
	int* p = (int*)malloc(40);
	//……
	free(p);
	p = NULL;  //*p重新开辟一个新的空间
		//……
		free(p);
	return 0;
}

 6.动态开辟内存未完成释放(内存泄漏)

代码示例:

void test()
{
	int* p = (int*)malloc(100);
	//……
	int flag = 0;
	scanf("%d", &flag);
	if (flag == 5)
	{
		return;
	}
	free(p);
	p = NULL;
}

int main()
{
	test();
	//……



	return 0;
}

上述代码中,当用户输入5时,free(p)与p = NULL两条语句就直接跳过了,此时造成了内存泄漏

注意事项:

1.p(NULL) = (char*)malloc(100) 是在创建一个空间赋给空指针p,而不是在对空指针p进行解引用

2.动态开辟的内存出了某个函数依然存在,但是形式参数(指针类型)自动销毁

3.动态开辟内存赋给指针以外的操作,大多可以叫做指针的解引用

例如:*p = 20         strcpy(*p, "hello world")

4.字符串的打印可以直接是printf(某个字符串变量)

假设str = "hello world",那么要想打印hello world就直接printf(str)即可

5.当我们返回栈区空间的地址时,由于栈区空间存放的都是临时变量,因此很容易就被其他内容所覆盖;也有一定概率会出现返回后的临时变量还能用的情况,但只需几句代码就会被覆盖取代

四.柔性数组介绍

概念介绍:结构体中的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员

柔性数组的初始化方式:a[ ]  或者 a[0](具体用哪个看编译器)

柔性数组的特点:

1.结构体中的柔性数组成员前必须至少一个其他成员

2.sizeof返回的这种结构体大小不包括柔性数组的内存

3.包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小

4.一个结构体里,柔性数组只能出现一个

柔性数组的使用:

1.柔性数组的内存开辟:

struct S
{
	int n;
	int arr[];
};

int main()
{
	//……
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
    if (ps == NULL)
{
	return 1;
}
	//……
	return 0;
}

上述代码,ps指向的空间先是存在了一个整型变量,占据4字节空间;然后再加上40字节的内存空间,作为柔性数组的使用;同时,ps指向的依旧是这44个字节的首地址,当44字节不够时,可以使用realloc函数更改大小

realloc函数的使用如下:

struct S
{
	int n;
	int arr[];
};

int main()
{
	//……
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
    if (ps == NULL)
    {
	return 1;
    }
	//……
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 80);
	if (ptr != NULL)
	{
		ps = ptr;
	}
	//……
	return 0;
}

即使是结构体中的柔性数组,依然需要释放回收

因此完整代码如下:

int main()
{
	//……
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
    if (ps == NULL)
    {
	return 1;
    }
	//……
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 80);
	if (ptr != NULL)
	{
		ps = ptr;
	}
	//……
	free(ps);
	ps = NULL;
	return 0;
}

2.*arr 的柔性数组开辟方法:

*arr的柔性数组开辟具体含义如下

struct S
{
	int n;
	int* arr;
};

对于这种创建方式,即构造了一个结构体,其中存放了整型变量n,以及一个整型指针变量arr,这两个变量可以看作是不相干的,详见下图

而对于结构体中出现了*arr,可以看成是一个数组首地址的指针变量,我们可以通过在mian函数中,加上一条 ps ->arr = (int*)malloc(40) ,等同于开辟了40字节的柔性数组

然后由于*arr与n是不相干的,所以malloc函数应该使用两次,一次是指向结构体S整体的指针,使用malloc函数,开辟 struct S 字节大小的空间;然后指针中的指针,即*arr,对其再次使用malloc函数,开辟所需要的柔性数组的内存空间大小。第二次开辟的内存空间与第一次开辟的内存空间通过上图不难看出,应该是没有联系的

最后,就是对两个指针的内存释放

首先我们需要知道*ps包含了*arr,*ps指向整个结构体开辟的空间,*arr只是柔性数组的开辟空间;因此,应该先对*arr进行内存释放,再对*ps进行内存释放,先后关系不能颠倒,具体代码如下:

free(ps->arr);
free(ps);
ps = NULL;

*arr被包含在了*ps里,因此在将*ps变成空指针以后,arr也就变成了空指针的一部分,因此不需要再对其进行这一步操作

扩容时依旧使用malloc函数,假设要扩容成80字节大小,依旧用int* ptr接收,则代码如下

int* ptr = (int*)realloc(ps->arr, 80);

该方法的问题:malloc函数用了较多,断断续续访问会降低访问速度,并会导致大量内存碎片;同时,也更容易导致内存泄漏,代码编写者的思考量增加。因此,笔者在此推荐使用方法1开辟柔性数组空间更加好。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值