【C语言-动态内存开辟】灵活自由总伴着错误狂出啊!

本文详细介绍了内存函数memcpy、memmove、memcmp和memset的用法,重点讲解了动态内存管理的malloc、calloc、realloc以及free,包括它们的原理、正确使用方法和常见错误预防。此外,还讨论了内存泄漏、越界访问等问题及解决方案。
摘要由CSDN通过智能技术生成

前言

动态开辟…灵活!

1.内存函数的模拟

1.1 memcpy

把源空间对应内存的全部数据拷贝到目标空间

在这里插入图片描述

实践起来就是 按字节拷贝

void* MyMemcpy(void* dest, const void* src, size_t num)
{
	assert(dest && src);

	void* ret = dest;

	while (num--)
	{
		*(char*)dest = *(char*)src;
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}

	return ret;
}

int main()
{
	int arr1[20] = { 0 };
	int arr2[] = { 1,2,3,4,5 };

	MyMemcpy(arr1, arr2, 5 * sizeof(int));

	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", arr1[i]);
	}
	return 0;
}

不妨再试试对同一块数据使用memcpy

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

	MyMemcpy(arr+2, arr, 5 * sizeof(int));

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

1 2 1 2 1 2 1 8 9 10

哟?这结果…

哦哦!原来是把待拷贝的数据都给改了:memcpy对同一块内存空间拷贝的结果是未定义的
(库函数的memcpy是能做到, 但是这样的操作仍旧是标准未定义的)

想要对同一块空间的数据进行拷贝操作,可以用memmove…

1.2 memmove

将一块数据移动到另一块

在这里插入图片描述

那么memmove是怎么避免待拷贝数据被修改的呢?

  1. dest > src:如果从 2-5 依次拷贝,就会覆盖掉待拷贝的数据
    如何解决?倒着拷贝:从 5-2 依次拷贝

    在这里插入图片描述
  2. dest < src:从前到后依次拷贝
    在这里插入图片描述
  3. dest == src 不必多说
void* MyMemove(void* dest, const void* src, size_t num)
{
	assert(dest && src);

	void* ret = dest;

	if (dest < src)
	{
		while (num--)
		{
			*(char*)dest = *(char*)src;
			dest = (char*)dest + 1;
			src = (char*)src + 1;
		}
	}
	else
	{
		while (num--)
		{
			*((char*)dest + num) = *((char*)src + num);
		}
	}
	
	return ret;
}


int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };

	MyMemove(arr + 2, arr, 5 * sizeof(arr[0]));

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

1 2 1 2 3 4 5 8 9 10

1.3 memcmp

逐个字节比较数据

在这里插入图片描述

int MyMemcmp(void* buf1, void* buf2)
{
	assert(buf1 && buf2);

	while (*(char*)buf1 != '\0' && *(char*)buf2 != '\0' && *(char*)buf1 == *(char*)buf2)
	{
		buf1 = (char*)buf1 + 1;
		buf2 = (char*)buf2 + 1;
	}

	if (*(char*)buf1 > *(char*)buf2)
		return 1;
	else if (*(char*)buf1 < *(char*)buf2)
		return -1;
	else
		return 0;
}

int main()
{
	char str1[] = "abcdfe";
	char str2[] = "abcdef";

	printf("%d\n", MyMemcmp(str1, str2));
	return 0;
}

1

1.4 memset

在这里插入图片描述
:按字节设置内存空间的内容

  • dest - 目标空间
  • c - 要设置的character
  • count - character的个数(记得是字节)

在这里插入图片描述
在这里插入图片描述

使用的时候要记住:memset是按字节设置的!


2.动态内存开辟

动态内存的开辟都是在堆区上

开辟主要是三个开辟函数

释放主要是一个释放函数

2.1 malloc

开辟一块连续可用的空间,并返回这块空间的地址

在这里插入图片描述

拆解:

  • size 是要开辟的空间的大小,字节作单位

要点:

  1. malloc 的返回值是void*,用什么指针维护这块内存,就要强转成什么类型的指针
  2. malloc 开辟成功就返回其开辟的内存空间的地址,开辟失败返回NULL

使用:

开辟成功:

int main()
{
	//开辟
	
	//1.malloc的返回值是void*,我们用int*的指针维护,就需要强转
	int* p = (int*)malloc(10 * sizeof(int));

	//2.由于malloc不一定开辟成功(若开辟得太大),需要检查返回值(开辟失败返回NULL)
	if (NULL == p)
	{
		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]);
	}
	
	//释放
	
	free(p);
	p = NULL;
	
	return 0;
}0 1 2 3 4 5 6 7 8 9

再来看看开辟失败的情况:

int main()
{
	//开辟
	
	//1.malloc的返回值是void*,我们用int*的指针维护,就需要强转
	int* p = (int*)malloc(INT_MAX);

	//2.由于malloc不一定开辟成功(若开辟得太大),需要检查返回值(开辟失败返回NULL)
	if (NULL == p)
	{
		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]);
	}
	
	//释放
	
	free(p);
	p = NULL;
	
	return 0;
}

:
Not enough space

根据错误码打印了“空间不足”的信息

2.2 free

释放动态开辟的内存空间

在这里插入图片描述

拆解:

  • memblock 是要释放的内存块的起始地址

要点:

  1. 只能用于释放动态开辟的内存空间
  2. free掉 pointer 指向的空间(这时的pointer是野指针),要将 pointer 置空…
    不然还是可以用这个 pointer,太危险
  3. 释放时不能只释放一部分(开辟了一大块就释放一大块)
  4. 有动态开辟的地方就有free(开辟不释放——内存泄漏

使用:

int main()
{
	//开辟
	
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		printf("%s\n", strerror(errno);
		return 1;
	}
	
	//使用...

	//释放
	
	free(p);
	p = NULL;
	
	return 0;
}

注意:

  1. 传过去的指针,一定是动态开辟内存的起始位置
  2. 传 NULL 时,free什么都不做

2.3 calloc

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

在这里插入图片描述
好像…

calloc ~= malloc + memset?

拆解:

  • num 是要开辟的空间所存放的元素个数
  • size 是元素大小
  • 直接这样理解:为 num 个 size 大小的元素开辟一块空间

为什么malloc的参数和calloc的不一样?

:malloc的参数只有一个size(元素大小);而calloc加了个初始化就必须要num(元素个数)和size(元素大小),calloc难道不能直接一个一个字节初始化吗?为什么要传多一个参数?

解惑

:如果说我想开辟 854个 int类型的空间
如果直接传字节数,这么写:calloc( 854*sizeof(int) )
传元素个数和大小就这么写:calloc(854, sizeof(int))

写法不同罢了

要点:

和malloc无异

使用:

int main()
{
	//开辟
	int* p = (int*)calloc(10, sizeof(int));
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}

	//使用
	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

动态管理动态管理,讲半天咋还没看见动态的影子?!

别急别急,这不就来了嘛…

2.4 realloc

调整动态开辟的空间的大小

在这里插入图片描述

拆解:

  • memblock 是要调整的内存块的起始地址
  • size 是调整后的新大小(byte)

要点:

  1. 返回值的接收要用临时指针接收(以防开辟失败返回NULL,导致原指针被误置空)
  2. 扩容有两种可能:
    (1)原内存空间后的空间充足,可以直接扩容
    (2)原内存空间后的空间不足,realloc会在堆区上另找一块连续可用的空间开辟,然后返回新空间的地址

在这里插入图片描述
在这里插入图片描述
改正:在堆区上新找的另一块连续可用空间

使用:

int main()
{
	//开辟10个整型的空间
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		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]);
	}
	printf("\n");

	//业务需要:将p指向的空间扩容至20个整型
	p = (int*)realloc(p, 20 * sizeof(int));
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}

	//业务处理
	for (i = 0; i < 20; i++)
	{
		p[i] = i;
	}
	for (i = 0; i < 20; i++)
	{
		printf("%d ", p[i]);
	}
	
	//释放
	free(p);
	p = NULL;
	
	return 0;
}0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

嗯… 很不错…吗?

不,我们漏了一处:对realloc的返回值疏忽了

用原指针接收realloc的返回值可以吗?
不行,如果开辟失败返回NULL,那原指针指向的内存也找不到了!
所以用一个临时指针接收并检查,确保没问题了再把临时指针赋给原指针

	int* tmp = (int*)realloc(p, 20 * sizeof(int));
	if (NULL == tmp)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	p = tmp;

3.常见错误

3.1 对NULL的解引用

int main()
{
	//开辟
	int* p = (int*)malloc(INT_MAX);

	//使用
	*p = 0;

	//释放
	free(p);
	p = NULL;

	return 0;
}

分析:

如果开辟失败返回了空指针,那不就是对NULL解引用了嘛…

解决办法:

对这些内存函数的返回值检查

int main()
{
	//开辟
	int* p = (int*)malloc(INT_MAX);
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	
	//使用
	*p = 0;

	//释放
	free(p);
	p = NULL;

	return 0;
}

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

int main()
{
	//开辟
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}

	//使用
	int i = 0;
	for (i = 0; i <= 10; i++)
	{
		p[i] = 0//当i == 0,越界访问
	}

	//释放
	free(p);
	p = NULL;

	return 0;
}

在这里插入图片描述

分析:

没有清楚自己开辟的空间大小,导致使用时越界访问

解决方案:

明了自己开辟了多大的空间,合理使用

3.3 free非动态开辟的空间

int main()
{
	int arr[10];

	free(arr);

	return 0;
}

分析:

脑子卡了

解决办法:

没什么解决方案,一般这个情况是脑子卡了,重启一下脑子

3.4 不完全free

用free释放动态开辟的空间的一部分/指针动了

int main()
{
	//开辟
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}

	//使用
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*p = 0;
		p++;
	}

	//释放
	free(p);
	p = NULL;

	return 0;
}

分析:

只释放了后五个整形元素

解决办法:

对于指向动态开辟的内存空间的指针使用慎重:不乱改

3.5 对同一块空间多次释放

int main()
{
	int* p = (int*)malloc(5 * sizeof(int));
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}

	free(p);
	free(p);

	p = NULL;
	return 0;
}

分析:

脑子卡了

解决办法:

留心

3.6 内存泄漏

来个狠的看看严重性…

int main()
{
	int* p;

	while (1)
	{
		p = (int*)malloc(1);
	}

	return 0;
}

在这里插入图片描述
好在现在的操作系统都做了很好的优化,放在以前真是要死机

再看一个:


int main()
{
	int* p = (int*)malloc(40);
	if (NULL == p)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}


	//...


	//...



	//...



	//...


	//总算把程序写完了!

	return 0;
}

总算把程序写完了…吗?

在这种情况下更容易忘记释放了

分析:

动态开辟的内存没有释放

解决办法:

哪里有动态开辟哪里就有free!!!

3.7 返回栈空间地址

将局部数据(栈空间)的地址返回会怎样?


int* test()
{
	int a = 10;
	return &a;
}


int main()
{
	int* p = test();

	printf("%d\n", *p);

	return 0;
}10

好像不会怎样?

不不不,你再耐心测一下

int* test()
{
	int a = 10;
	return &a;
}


int main()
{
	int* p = test();

	printf("bacon\n");

	printf("%d\n", *p);

	return 0;
}

:
bacon
6

分析:

将局部变量的地址返回

出了test函数,a就销毁,此时的p指向的是已经不属于我们的空间,一旦有任何数据把a原来的空间覆盖,那结果又随缘了

其实就是函数栈帧的事儿:

在这里插入图片描述

如果没有别的数据来捣乱,咱们的p还是能找到10并打印

但这种代码终究是有问题的,来找出它的病根

在这里插入图片描述
咱们已经知道栈区上的空间使用是从高地址向低地址使用的,所以原来a的位置上的数据就很容易被覆盖掉了

4. 典题

(1)

程序运行的结果是?

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

int main()
{
	test();
	return 0;
}
;
烫烫烫烫烫烫烫烫麿

分析:

返回栈空间地址,解引用找到的空间内的数据可能已经被覆盖

(2)

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;
}

分析:

malloc开辟的空间由局部变量p维护,出了函数就销毁,没人维护就造成内存泄漏,而str也并没有获得能够维护的空间,仍然是NULL

要拷贝字符串到NULL上,程序肯定崩溃

在这里插入图片描述

(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);
}

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

分析:

大体上没问题,但是malloc开辟的空间没有释放

(4)

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;
}

分析:

对于动态开辟的空间,释放后没有对其指针置空,而后又在strcpy、printf中对野指针解引用,危险啊!!!


5.汲取

内存函数:

(1)malloc

开辟 size个字节

(2)calloc

开辟 num个大小为size的元素的空间

(3)realloc

将 memoryblock 处的空间调整为 size个字节

动态内存管理:

(1)开辟

  • 返回值要检查强转
  • 开辟完一定要记得释放,避免内存泄漏

(2)使用

  • 明确自己开辟了多大空间,避免越界访问
  • 不要轻易改动指向开辟空间的指针

(3)释放

  • 释放后要置空

本期分享就到这啦,不足之处望请斧正

培根的blog,和你共同进步!

  • 15
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 20
    评论
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周杰偷奶茶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值