【动态内存管理】malloc,calloc,realloc的使用方法以及常见错误

目录

malloc

注意点1

注意点2

注意点3 

calloc

realloc 

realloc分配空间的规则 

 使用内存函数时的常见错误

对空指针的解引用

对动态内存的越界访问

对非动态内存的释放 

释放开辟的动态内存的一部分 

返回栈空间地址的问题

样例1

 样例2

 样例3

 区分返回栈空间的值


        malloc、calloc、realloc都是动态内存函数,它们都是在堆区上面开辟内存,其头文件都是<stdlib.h>。这三个函数在使用的时候有细微差别,值得注意。

malloc

malloc函数的声明: void* malloc(size_t  size);

从中可以知道的信息是:

1、malloc函数的参数只有一个,代表了要开辟的内存大小。(单位是字节)

2、malloc函数返回一个指针 ,指向已分配大小的内存。(如果请求失败,则返回 NULL。)

        比如下方代码,malloc开辟了20个字节的空间,然后对这20个字节的空间进行操作。

注意点1

        在malloc函数前面加一个强制类型转换的原因是:malloc开辟的空间并没有指定用来存储什么类型的数据,其返回的指针是void*类型的,必须要强制类型转换成某个确定类型的指针。在这里,接收malloc函数返回值的是p,p是int*类型的指针,所以把malloc函数返回的指针转化成int*类型。calloc和realloc也类似,下面不多赘述。

注意点2

        在结尾处有free(p); 的操作,free也是内存函数,只不过它的作用是把动态开辟的内存释放,在这段代码就是把p指向的空间(20个字节)还给操作系统,这样就成为了随机值。与此同时,这块空间还给了操作系统,那么我们就失去了对这块空间的使用权限,再访问这块空间就是非法访问,但是这时候p指针并没有什么改变,所以p指针依然可以指向这块空间,为了避免非法访问,所以把p置为NULL。下面每一段代码都是如此,也不多赘述。

        在开辟内存之后,第一件事是判断malloc返回的指针是否为空,因为可能存在开辟失败的情况。

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(20);
    if (p==NULL)
    {
      printf("malloc fail!");
      exit(-1);
    }
	for (int i = 0;i < 5;i++)
		p[i] = i;
	for (int i = 0;i < 5;i++)
		printf("%d ", p[i]);
    free(p);
    p=NULL;
	return 0;
}

 

注意点3 

         内存开辟失败的情况:如下,要开辟INT_MAX大小的空间,INT_MAX指的是int类型数据能存储的最大值,即2^32-1,这个空间太大了,堆区没有那么大的空间,所以开辟失败。但是这里开辟失败如果没有对应的提示,则比较难找到错误。相反,有了提示则一目了然:

        当然,calloc和realloc同样会存在这种问题,所以下面不再过多赘述。

#include<stdio.h>
#include<stdlib.h>
#include <vcruntime.h>
int main()
{
	int* p = (int*)malloc(INT_MAX);
	if (p == NULL)
	{
		printf("malloc fail!");
		exit(-1);
	}
	for (int i = 0;i < 5;i++)
		p[i] = i;
	for (int i = 0;i < 5;i++)
		printf("%d ", p[i]);
	free(p);
    p=NULL;
	return 0;
}

 

calloc

calloc函数的声明: void* calloc(size_t nitems,size_t size);

从声明也可以看出一些信息:
1、calloc函数有两个参数,nitems表示要开辟的元素个数,size表示每个元素的大小(单位是字节)。

2、malloc函数返回一个指针 ,指向已分配大小的内存。(如果请求失败,则返回 NULL。)

calloc和malloc的区别就是:calloc开辟空间的同时,会设置分配的内存为0,而malloc不会,malloc会设置分配的内存为随机值。

        比如下方代码,calloc分配了5个元素的空间,每个元素占据四个字节的内存,所以一共是20个字节的内存,然后直接打印,全是0。 

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)calloc(5,4);
    if (p==NULL)
    {
      printf("calloc fail!");
      exit(-1);
    }
	for (int i = 0;i < 5;i++)
		printf("%d ", p[i]);
    free(p);
    p=NULL;
	return 0;
}

 

realloc 

realloc函数的声明:void* realloc(void* ptr , int size);

其中的信息有:

1、ptr -- 指针指向一个要重新分配内存的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果为空指针,则会分配一个新的内存块,且函数返回一个指向它的指针。

2、size -- 内存块的新的大小,以字节为单位。如果大小为 0,且 ptr 指向一个已存在的内存块,则 ptr 所指向的内存块会被释放,并返回一个空指针。

         比如下方代码,首先malloc除了4个int类型数据的空间(16个字节),但是想要存放更多数据,所以realloc空间,此时想要存储10个int类型的数据,然后对没有填入数据的空间进行操作,最后输出结果如下。

        在realloc的时候,用一个tmp指针接收的原因是:如上的注意点3,可能存在开辟失败的情况。如果realloc开辟失败,那就返回NULL。此时如果用p指针接收,那么p指针也就成了NULL,再想要访问原本malloc出来的空间,就访问不到了,那么原有数据就会丢失。所以先用一个临时的指针接收,再判断,最后赋给p。

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(4 * sizeof(int));
    if (p==NULL)
    {
      exit(-1);
      return;
    }

	for (int i = 0;i < 4;i++)
		p[i] = i;

	int* tmp = (int*)realloc(p, 10 * sizeof(int));
    if (tmp==NULL)
    {
      exit(-1);
      return;
    }

	p = tmp;
	for (int i = 4;i < 10;i++)
		p[i] = i;
	for (int i = 0;i < 10;i++)
		printf("%d ", p[i]);

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

realloc分配空间的规则 

         不过,在使用malloc函数的时候,值得注意的是,其分配空间的规则。

        比如下方图片,现在已经有一块空间是malloc或者calloc出来的,但是这块空间不够,我想要再多一些空间,于是realloc。

        情况1:如果这块空间后面的内存足够大,那么realloc函数就在这块空间的基础上,在它后面扩容,增加一些存储空间,原有的数据不变,如情况1的图片。

        情况2:如果这块空间后面的空间大小,不够realloc之后新增的空间,那么就再堆区新找一片空间,如情况2的红色框,然后把之前的空间里的内容拷贝到新空间(如情况2里面,红色框中的黑色框),然后把原有空间free掉。

 使用内存函数时的常见错误

对空指针的解引用

        比如下方代码,由最开始malloc处的注意点可知,malloc不出INT_MAX大小的空间,所以返回NULL,故p是NULL,但此时又对p解引用,完全是错误的。程序运行会崩溃。

#include<stdio.h>
#include<stdlib.h>
#include <vcruntime.h>

//对NULL的指针的解引用操作
int main()
{
	int* p = (int*)malloc(INT_MAX);
	*p = 5;

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

        正确写法如下,要先判断开辟的空间是否为空,不为空才对这片空间进行操作:

#include<stdio.h>
#include<stdlib.h>
#include <vcruntime.h>

//对NULL的指针的解引用操作
int main()
{
	int* p = (int*)malloc(INT_MAX);
	if (p == NULL)
	{
		exit(-1);
		return;
	}
	*p = 5;
	//应该这样子写

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

对动态内存的越界访问

        如下,malloc开辟了20个字节,可以存放5个int类型的数据,但是在下面却想要存放20个int类型的数据,造成了动态内存的越界访问,程序会崩溃。所以在使用动态内存的时候,一定要注意大小,不可以越界访问。

#include<stdio.h>
#include<stdlib.h>
#include <vcruntime.h>

int main()
{
	int* p = (int*)malloc(20);//开辟了20个字节,可以存放5个int类型的数据
	if (p == NULL)
	{
		exit(-1);
		return;
	}
	for (int i = 0;i < 20;i++)
		p[i] = i;
	//越界访问了,程序会崩溃

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

对非动态内存的释放 

        有的时候,看到指针就想释放,但是却会造成错误,如下代码。a是局部变量,在栈区,不在堆区,而定义一个指针变量p,指向a,后面却想释放p。p是a的指针,释放p就是释放a所在的空间,但是a的空间是在栈区free函数只能释放堆区的空间,所以这就造成了对非动态内存的释放。下面的图片是报错信息。

        要记住,我们要释放的是堆区的空间,而不是释放指针。指针只是指向这某块空间,通过指向堆区的指针,我们才能够释放堆区的内存,并不是所有指针都可以释放。

#include<stdio.h>
#include<stdlib.h>
#include <vcruntime.h>

int main()
{
	int a = 20;
	int* p = &a;
	if (p == NULL)
	{
		exit(-1);
		return;
	}

	free(p);//对非动态内存的释放
	p = NULL;
	return 0;
}

释放开辟的动态内存的一部分 

         如下代码,开辟了40个字节的空间p最开始指向这块空间的起始位置。p是int*类型的指针,所以每次++,都要跳过4个字节的空间,在赋值的时候,p++一共指行了5次,所以赋值完毕后,p指向这块空间中的第21个字节的空间。此时free(p),也会造成报错,如下图。

        解决方法一般分为两种。第一种是在移动p指针之前,定义一个同类型的指针,把开辟的空间起始位置存起来,最后释放直接释放该指针指向的空间即可。第二种是,p--,++了多少次就--多少次,这样就回到了最开始的位置,然后释放。  一般情况下建议第一种方法。

#include<stdio.h>
#include<stdlib.h>

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

	free(p);//这时候是错误的,因为p以经不指向最初开辟的起始位置了
	p = NULL;
}

返回栈空间地址的问题

         栈空间地址,顾名思义,是某个函数返回栈区的某一块空间的地址。

样例1

        如下的代码,str指针接收的是GetMemory函数的返回内容,再看GetMemory函数内部,定义了一个字符数组,然后返回该数组的起始位置。乍一看这个代码貌似没有什么问题。

        但是,仔细思考就会发现大问题!!!栈区,栈区,就如同客栈一样,当一个函数的生命周期结束,其内部在栈区开辟的空间就会被还给操作系统。对应本段代码,就是GetMemory函数内部,其 char p[] = "hello world";  这是在栈区开辟空间存储的,所以在这个函数调用完之后,这块空间就会还给操作系统了,我们就没有使用权限,那么我们也不知道这块空间被操作系统更改没有。但是依然返回了p,p依然是指向这块空间,所以str也指向了这块空间,那么打印出来的结果未知,如下图,可以看出,在本段代码,这块空间被操作系统更改了,所以打印一段乱码。

#include<stdio.h>
#include<stdlib.h>

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

         但是有人通过别的例子发现,返回栈空间地址依然可以得到想要的结果,如下代码和运行结果,运行后依然打印出来10。根据上面标红的文字所说,被还给操作系统的这块空间,我们并不知道操作系统有没有更改它,在这里仅仅是操作系统恰好没有修改这块空间。但是并不是每一次操作系统都不会改这块空间,比如上面的情况。所以保险起见,还是不能返回栈空间地址。

#include<stdio.h>
#include<stdlib.h>

int* test()
{
	int a = 10;
	return &a;//这里恰好没被改
}

int main()
{
	int *ret = test();
	printf("%d ", *ret);
	return 0;
}

 样例3

         在打印返回的栈空间地址内容之前,创建新的函数战争,覆盖先前的地址,就会导致函数返回的栈空间地址被修改,打印出来的值不同。由函数栈帧的创建和销毁知识,test函数的栈帧销毁了,然后printf的栈帧又创建,所以a的地址的内容被改了,打印出来的内容不是10。如下代码和运行结果。通过流程图可以简易理解。:

#include<stdio.h>
#include<stdlib.h>

int* test()
{
	int a = 10;
	return &a;//这里恰好没被改
}

int main()
{
	int* ret = test();
	printf("hehe\n"); 
	printf("%d ", *ret);
	return 0;
}

 

 区分返回栈空间的值

        如下代码,test函数不是返回栈空间的地址,返回的是一个值,这样子是可以的,就是正常的写法,不要和返回栈空间地址搞混了。在这里不免会提出疑问,a变量所在空间不是已经还给操作系统了吗,为什么可以这样呢?这是由于,这种情况下,a的值先被存在寄存器中,然后由ret变量接收,并不是把a所在空间的内容返回给ret,所以可行。

#include<stdio.h>
#include<stdlib.h>

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

int main()
{
	int ret = test();
	printf("%d ", ret);
	return 0;
}

         关于动态内存管理的内容就到这里了,如果有错误欢迎多多指正!!!

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

努力努力再努力.xx

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

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

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

打赏作者

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

抵扣说明:

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

余额充值