动态内存管理函数malloc、calloc、realloc、free函数,以及练习,程序的内存开辟,柔性数组

为什么存在动态内存的分配

我们已经学习过了对变量和数组的内存开辟方法,那就是定义变量和定义数组。但是它们是在栈区上开辟空间。函数运行完成后直接会销毁栈空间上的变量。
看下面的代码:

int a = 0; //在栈空间上开辟四个字节的内存空间
char arr[10] = {0}; //在栈空间上开辟十个字节的内存空间

但是上述的开辟空间的方式有两个特点:
1、开辟的空间的大小式不可变的。
2、数组在声明的时候必须指定数组的大小,也就是方括号里的值,它所需要的内存空间在编译时分配。
但是对于上述的情况,我们事先知道要分配多大的内存空间,有的时候我们需要在空间大小在程序运行的时候才能知道,这时候我们就可以引入动态内存开辟了。

动态内存函数的介绍

动态内存管理函数有四个分别是:malloc、calloc、realloc、free函数。

介绍malloc函数的使用

malloc函数是在栈区进行动态开辟一块连续的内存空间的函数,如果没有开辟成功它会返回一个空指针,开辟成功会返回这块地址的起始地址,也就是指向这块地址的指针。因为它会返回一个空指针,所以使用malloc函数一定要进行空指针的检查。返回值是void*指针,malloc函数不知道开辟空间的类型,具体情况按照程序员的决定,可以去把malloc的返回值强制类型转换为自己想要的类型的指针。如果参数size_t size 为0,是标准未定义的,是否开辟内存空间还是报错或者其他情况,这取决于编译器。我们来用一个代码来演示malloc函数的使用。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
	int* a = (int*)malloc(20);
	//进行访问内存空间,按照数组的形式可以访问到这块内存空间的五个元素
	//(20个字节,int类型是4个字节,所以为五个元素)
	//对a指针变量进行判空,排除malloc返回空指针,访问内存空间失败的情况
	//那就是使用野指针的情况。
	if (a == NULL)
	{
		printf("访存失败,原因是:%s", strerror(errno));//strerror函数已经在前面介绍过了
		exit(-1);
	}
	int i = 0;
	int* p = a;
	//赋值
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}
	//打印
	for (i = 0; i < 5; i++)
	{
		//利用数组的方式进行访问存储空间,因为
		//p[i] == *(p+i)
		printf("%d\n", p[i]);
	}
}

使用free函数可以对堆区上动态开辟的内存空间进行释放,不能释放栈区上的内存空间。
如果参数free参数指向的空间不是动态开辟的,free函数的行为是未定义的。参数为NULL指针时,free函数什么也不会做。我们来看下面的代码

int main()
{
	int* a = (int*)malloc(20);
	//进行访问内存空间,按照数组的形式可以访问到这块内存空间的五个元素
	//(20个字节,int类型是4个字节,所以为五个元素)
	//对a指针变量进行判空,排除malloc返回空指针,访问内存空间失败的情况
	//那就是使用野指针的情况。
	if (a == NULL)
	{
		printf("访存失败,原因是:%s", strerror(errno));//strerror函数已经在前面介绍过了
		exit(-1);
	}
	int i = 0;
	int* p = a;
	//赋值
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}
	//打印
	for (i = 0; i < 5; i++)
	{
		//利用数组的方式进行访问存储空间,因为
		//p[i] == *(p+i)
		printf("%d\n", p[i]);
	}
	//释放堆区上动态开辟的内存空间。
	free(a);
	//注意这里,释放a指向的空间后,a就不知道指向哪块内存空间了
	//所以要把它置为空,避免野指针。
	a = NULL;
}

介绍calloc函数的使用

calloc函数也是用来在堆区上进行内存的动态开辟。它相较于malloc函数会把开辟的内存初始化。它的原型是这样的。
void* calloc (size_t num, size_t size);
返回void*类型的指针,指定开辟num个大小为size的元素的空间,并且把每个空间初始化为0。
calloc函数和malloc函数的对比:
1、参数不同。
2、都是在堆区上申请空间,但是malloc不初始化,calloc会初始化为0,如果要初始化,就是用calloc,不需要初始化,使用malloc函数。

int main()
{
	int* a = (int*)calloc(5, 4);
	if (a == NULL)
	{
		printf("%s\n", strerror(errno));
		exit(-1);
	}
	//查看开辟的内存空间是否被初始化
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", *(a + i));
	}

	free(a);
	a = NULL;
}

运行时内存状态
在这里插入图片描述

介绍realloc函数的使用

realloc的出现让动态内存的分配更加灵活,有的时候我们觉得内存空间分配少了,有的时候又觉得大了,这个时候就需要realloc函数出马了,realloc函数可以做到动态内存大小的调整。函数原型如下:
void* realloc (void* ptr, size_t size);
返回void* 的指针,调整ptr指向的内存空间的大小,调整为几个字节,这里需要注意size要包含之前的内存空间的大小。还需要注意,如果ptr所指向的空间后面没有空间了,realloc函数会另寻找一块空间进行分配。并且把原有的空间进行拷贝,返回新空间的地址,然后free掉之前的空间。这里可能会返回空指针。
realloc函数的两种情况:
情况一:原有空间之后有足够大的空间。
情况二:原有空间之后没有足够大的空间。
在这里插入图片描述
情况一:当是情况一的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况二:当时情况二的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
我们来看下面的代码:

int main()
{
	int* ptr = (int*)malloc(20);
	//需要判空
	if (ptr == NULL)
	{
		printf("空间分配失败,原因是:%s\n", strerror(errno));
		return;
	}
	else
	{
		//业务处理
	}
	int* temp = (int*)realloc(ptr, 40);
	//需要进行判空。
	if (temp == NULL)
	{
		printf("空间分配失败,原因是:%s\n", strerror(errno));
		return;
	}
	else
	{
		ptr = temp;
		//业务处理
	}
	//free掉内存空间
	free(ptr);
	ptr = NULL;
	return 0;
}

执行时内存空间状态
在这里插入图片描述

还需注意一点:如果realloc函数ptr接收到的是一个空指针,那么它的功能就相当于malloc函数了。

常见的动态内存错误

对NULL指针的解引用操作

#define MAX 20
void test()
{
	int* p = (int*)malloc(MAX);
	//解决办法,判空
	if (p == NULL) 
	{
		return;
	}
	else
	{
		*p = 20;
	}
	//如果p是空指针,那么会发生对空指针的
	//解引用操作。
	free(p);
}

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

void test()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		exit(-1);
	}
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i; //当i是10的时候越界访问
	}
	free(p);
}

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

void test()
{
	int a = 10;
	int* p = &a;
	free(p); //这是对栈区上的内存空间free,会发生错误
}

使用free释放一块动态开辟内存的一部分

void test()
{
	int* p = (int*)malloc(100);
	p++;
	//p不在指向内存空间的起始位置。
	free(p);
}

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

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

动态开辟内存忘记释放(内存泄露)

void test()
{
	int* p = (int*)malloc(100);
	if (p != NULL)
	{
		*p = 20;
	}
}
int main()
{
	while (1)
	{
		test();
	}
	//如果这个程序不间断的跑下去,会把内存空间全部泄露,别人就不能用了。
}

忘记释放不在使用的动态开辟的空间会造成内存泄漏。所以说动态开辟的空间一定要用free释放,并且正确释放。

练习题讲解

题目一:

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

在这里插入图片描述

题目二:

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

在这里插入图片描述

题目三:

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

在这里插入图片描述

题目四:

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	//对已释放的堆区的内存空间进行访问。
	//程序会崩溃,写出了内存上的错误。
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

c/c++程序的内存开辟

在这里插入图片描述

c/c++ 程序内存分配的几个区域
1、栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存空间容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2、堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
3、数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。
4、代码段:存放函数体(类成员函数和全局函数)的二进制代码。
观察上面的图,我们就可以很好的理解static关键字修饰局部变量的例子了。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就被销毁了。但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,知道程序结束才销毁。
所以声明周期会延长。

柔性数组

在C99标准中,结构体中的最后一个元素允许是未知大小的数组,这就叫做柔性数组的成员。

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

有些编译器报错无法编译可以写成

struct st_type_1
{
	int i;
	int a[];
};

柔性数组的特点

结构中的柔性数组成员前面前面至少有一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
我们来用代码说话:
在这里插入图片描述
包含柔性数组成员的结构用malloc() 函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
我们来用代码说话:

int main()
{
	int i = 0;
	struct st_type* st = (struct st_type*)malloc(sizeof(struct st_type)+100*sizeof(int));
	st->i = 100;
	//判空
	if (st == NULL)
	{
		return;
	}
	for (i = 0; i < 100; i++)
	{
		st->a[i] = 0;
	}
	for (i = 0; i < 100; i++)
	{
		printf("%d ", st->a[i]);
	}
	//调整内存大小
	struct st_type* temp = (struct st_type*)realloc(st, sizeof(struct st_type*) + 200 * sizeof(int));
	if (temp == NULL)
	{
		return;
	}
	else
	{
		st = temp;
	}
	free(st);
	return 0;
}

柔性数组的优势

实现柔性数组功能的两种方案。

//方案一:
int main()
{
	int i = 0;
	struct st_type* st = (struct st_type*)malloc(sizeof(struct st_type)+100*sizeof(int));
	st->i = 100;
	//判空
	if (st == NULL)
	{
		return;
	}
	for (i = 0; i < 100; i++)
	{
		st->a[i] = 0;
	}
	for (i = 0; i < 100; i++)
	{
		printf("%d ", st->a[i]);
	}
	//调整内存大小
	struct st_type* temp = (struct st_type*)realloc(st, sizeof(struct st_type*) + 200 * sizeof(int));
	if (temp == NULL)
	{
		return;
	}
	else
	{
		st = temp;
	}
	free(st);
	return 0;
}

//方案二:
typedef struct st_type
{
	int i;
	int* a //柔性数组成员
}type_a;
int main()
{
	type_a* st = (type_a*)malloc(sizeof(type_a));
	if (st == NULL)
	{
		return;
	}
	int* ptr = (int*)malloc(100 * sizeof(int));
	if (ptr == NULL)
	{
		return;
	}
	else
	{
		st->i = 100;
		st->a = ptr;
	}

	//使用
	
	//调整内存大小
	int* pt = (int*)realloc(st->a, 200 * sizeof(int));
	if (pt == NULL)
	{
		return;
	}
	else
	{
		st->a = pt;
		st->i = 200;
	}
	//使用
	//释放
	free(st->a);
	free(st);
	st = NULL;
}

上述代码都可以实现同样的功能,但是方案一有两个好处:
第一个好处是:方便内存的释放
如果我们的代码是在一个给别人用的函数中,你在里面做了两次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其他成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度。
连续的内存有益于提高访问速度,也有益于减少内存碎片。
在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值