动态内存管理

时间:2022-08-30 01:20:37

一、为什么存在动态内存分配

我们常见的开辟空间方式有:

int a = 3;//在栈上开辟4个字节的空间
char b[10] = { 0 };//在栈上开辟10个字节的空间

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

①开辟的空间是固定的。

②在定义数组时,需要指定数组的大小,它所需要的内存在编译时分配。

以上的开辟空间方法在一些情况下无法满足我们的要求。比如说,我们不知道数组需要多大,需要用户输入之后才能确定大小。也就是说在程序运行时才能确定要分配的空间大小。此时用数组的定义无法实现。我们就需要用到动态内存分配的知识了。

二、动态内存函数的介绍

以下4个函数都需要引用头文件#include<stdlib.h>

1、malloc函数与free函数

以下是malloc函数的声明:

动态内存管理

①size:是一个无符号整数,代表的是所需要开辟内存的大小,单位是字节。

②malloc函数会在上开辟一块size字节大小的空间,然后返回指向这块空间的指针,如果过开辟空间失败,则会返回NULL

③如果size的大小为0,这是标准未定义的行为,实现情况根据编译器的不同而不同,可能会报错。

以下是free函数的声明:

动态内存管理

①ptr:是一个指针,指向需要释放的内存。

②free函数能够释放动态开辟的内存。动态开辟的内存不需要时一定要用free函数释放,否则可能会出现内存泄漏等问题

③如果ptr是空指针,则什么都不会发生。

④如果ptr指向的空间不是动态开辟的,这种行为是标准未定义的,实现情况根据编译器的不同而不同,可能会报错。

下面是malloc函数与free函数的实例:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* ptr = (int*) malloc(40);//在堆上动态开辟40个字节的空间
	if (ptr == NULL)//如果ptr接收的是空指针,则开辟失败
	{
		perror("空间开辟失败,原因是:");//提示动态内存开辟失败并打印原因
		return 0;//结束
	}
	for (int i = 0;i < 10;i++)
	{
		ptr[i] = i;//用动态开辟的空间存放数据
		printf("%d ", ptr[i]);
	}
	free(ptr);//释放空间
	return 0;
}

代码运行结果如下:

动态内存管理

如果内存开辟失败:

动态内存管理

2、calloc函数

calloc函数的声明如下:

动态内存管理

①nums:要被分配元素的个数。

②size:元素的大小,单位是字节。

③calloc函数也是能够在堆上开辟num个大小为size的空间,与malloc函数不同的是,它会把空间的每个字节初始化为0。

④返回值是一个指向开辟空间的指针。

实例演示如下:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* pa = (int*) malloc(40);
	int* pb = (int*) calloc(10, 4);//calloc函数的使用
	if ((pa == NULL) || (pb == NULL))
	{
		perror("空间开辟失败,原因是:");
		return 0;
	}
	//在初始化的情况下打印两块空间的数据
	printf("malloc函数开辟的内存未初始化时:\n");
	for (int i = 0;i < 10;i++)
	{
		printf("%d ",pa[i]);
	}
	printf("\n");
	printf("calloc函数开辟的空间未初始化时:\n");
	for (int i = 0;i < 10;i++)
	{
		printf("%d ", pb[i]);
	}
  free(pa);
  free(pb);
	return 0;
}

运行结果如下:

动态内存管理

3、realloc函数

realloc函数的声明如下:

动态内存管理

①realloc函数能够重新调整之前用malloc函数或者calloc函数动态开辟的空间,并返回指向调整后空间的指针,如果调整失败,返回NULL。

②ptr:指向需要调整的空间的指针。

③size:调整后空间的大小,单位是字节。

③该函数在调整空间大小的同时,也会把原来空间中的数据移动到新的内存。

实例演示如下:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* ptr = (int*) calloc(10, 4);//动态开辟10*4个字节大小的空间。
	if (ptr == NULL)
	{
		perror("calloc:Error:");
		return 0;
	}
	//将原本40个字节的空间,调整为100个字节。
	//int *ptr= realloc(ptr, 100);
	//如果调整失败,realloc函数返回NULL,ptr原本指向的空间就找不到了。
	int* p = (int*) realloc(ptr, 100);
	//我们先设置一个中间指针,p不为空指针时才把它赋给ptr。
	if (p == NULL)
	{
		perror("raelloc:Error:");
		return 0;
	}
	ptr = p;//调整空间成功
	p = NULL;
	free(ptr);
	ptr = NULL;
	return 0;
}

realloc函数在调整空间时可能会出现以下情况:

①原本空间之后有足够的空间,就在原本的空间后追加空间,原空间的数据不发生变化。

动态内存管理

动态内存管理

②原本空间之后没有足够的空间,就会在堆上另找一处开辟一块合适的空间,并把原本空间的数据转移过去。

动态内存管理

动态内存管理

动态内存管理

三、动态内存的常见错误

1、对NULL指针的解引用操作

int main()
{
	int* p = (int*) malloc(40);
	*p = 30;//如果p的值是NULL,就会出问题
  //所以,最好对malloc函数的返回值进行判断
  free(p);
  p=NULL;
	return 0;
}

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

int main()
{
	int* p = (int*) maloc(40);
	if (p == NULL)
	{
		perror("malloc:Error:");
		return 1;
	}
	for (int i = 0;i <= 10;i++)
	{
		p[i] = i;//当i=10时越界访问了
	}
	free(p);
  p = NULL;
	return 0;
}

3、对非动态开辟的空间用free函数释放

int main()
{
	int a = 11;
	free(&a);
	return 0;
}

4、对动态开辟空间的一半部分进行释放

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc:Error:");
		return 1;
	}
	for (int i = 0;i < 5;i++)
	{
		*p++ = i;//在循环的过程中p指向的位置不断移动
	}
	free(p);//此时p指向的不是动态开辟空间的起始位置,发生错误
	p = NULL;
	return 0;
}

5、对同一块内存的多次释放

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc:Error:");
		return 1;
	}
	free(p);//第一次释放
	//p=NULL;
	//如果在第一次释放后将p置为空指针,则不会发生错误
	free(p);//再次的释放,发生错误
	p = NULL;
	return 0;
}

6、动态开辟的内存忘记释放(内存泄漏)

动态开辟的空间,如果不需要使用了,需要用free函数释放。

如果不释放,在程序结束后,也会由操作系统回收。

但是如果一个程序需要长时间运行,此时如果不使用free函数释放,操作系统无法回收,就会出现内存泄漏。通俗地说,就是这块内存你不用,也不还,别人也用不了,这块空间就相当于浪费掉了。

void test()
{
	int* p = (int*)malloc(40);
	if (p != NULL)
	{
		*p = 20;
	}
	//没有释放内存
}
int main()
{
	test();
	while (1);
	//程序一直运行,出现内存泄漏
	return 0;
}

四、关于动态内存的题目

题目一

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

请问这段代码的运行结果如下:

动态内存管理

首先进入Test函数,然后定义一个指针str,把str传给GetMemory函数。注意,此时传的是值,而不是地址。是直接把str传过去,而不是把str的地址传过去。所以p就是str的一份临时拷贝,p指向动态开辟的空间,函数结束后,p被销毁。str也不会有任何改变,依旧是一个空指针。strcpy函数调用失败,程序崩溃。所以最后什么也不会打印。

同时还有一个问题,p被销毁后,没有任何指针指向动态开辟的空间,这也就意味着我们无法主动释放动态开辟的空间,可能会出现内存泄漏。

对这段代码稍加修改:

void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void Test()
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

运行结果如下:

动态内存管理

题目二

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

运行结果如下:

动态内存管理

首先进入Test函数,定义一个字符指针str,进入GetMemory函数,定义一个字符串p,存放“hello world\0”,然后返回p的地址,由str接收。此时注意,函数结束后,函数里的数据会被销毁,空间返回操作系统。也就是说,str虽然接收到地址,但是“hello world\0”,已经被销毁了,str就是一个野指针,str指向的空间可能用来存放别的数据了,打印的结果不可预测。

题目三

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

这段代码唯一的错误就是没有把动态开辟的空间释掉。

题目四

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

str指向的空间被释放后,str没有被置为空指针,此时的str就是野指针,此时再用strcpy函数拷贝给str就会非法访问。

五、柔性数组

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

例如:

struct S
{
	int a;
	int b;
	char c[];//这就是柔性数组成员
};

1、柔性数组的特点

①结构体柔性数组成员前面至少有一个其他成员。

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

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

struct S
{
	int a;
	int b;
	char c[];//这就是柔性数组
  //char c[0]  这样写也行
};
int main()
{
	printf("%d",(int)sizeof(struct S));//打印结果应当是8
	return 0;
}

2、柔性数组的使用

#include<stdio.h>
#include<stdlib.h>
struct S
{
	int a;
	int b;
	char c[];//这就是柔性数组
};
int main()
{
	//                               8个字节,给a,b用    10个字节,给c用
	struct S* p = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(char));
	if (p == NULL)
	{
		perror("malloc:Error:");
		return 0;
	}
	p->a = 10;
	p->b = 20;
	for (int i = 0;i < 10;i++)
	{
		p->c[i] = i + 65;//使用柔性数组
	}
	for (int i = 0;i < 10;i++)
	{
		printf("%c ", p->c[i]);//打印出来看看
	}
	free(p);
	p = NULL;
	return 0;
}