【C语言】动态内存管理

一. 为什么存在动态内存管理

因为在之前我们学习的开辟空间,具有一定的局限性,空间的大小是固定的,在很多的时候达不到我们想要的目的。静态的开辟空间只能开辟多少就是多少,并没有动态开辟内存灵活,在动态开辟空间中,可以空间不够了,继续申请,空间开辟多了,可以释放,更加的灵活。动态内存管理开辟的是一段连续的空间。

二.动态内存的函数介绍

1.malloc

在这里插入图片描述
在这里我们可以看到malloc的参数是无符号整型,而它的返回类型是void*,返回的是它开辟空间的起始地址,为什么是void?因为它只知道开辟多少空间,而并不知道它要赋给什么类型,所以才用void

#include<stdlib.h>//malloc,free,calloc,realloc的头文件

下面我们就来看一下它是如何来使用的

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int arr[10] = {0};//我们之前学习的在栈上开辟空间
	void* p = malloc(40);
	//malloc就是void的类型,所以用p接收没问题

	int* str = (int*)malloc(40);//开辟40字节的空间
	
	return 0;
}

malloc也可能出现内存开辟失败的情况,然后其返回为空指针,这是一个危险的情况,所以我们每次在开辟内存之后,要验证一下开辟的空间是否成功。

int* str = (int*)malloc(INT_MAX);//开辟最大空间,肯定会报错
	if (str == NULL)
	{
		perror(str);
		return 1;
	}

输出结果:
在这里插入图片描述
malloc的使用

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* str = (int*)malloc(40);//开辟40字节的空间
	if (str == NULL)
	{
		perror(str);
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		*(str + i) = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d",*(str+i));
	}
	free(str);
	str=NULL;
	return 0;
}

2.free

在这里插入图片描述

free的作用也就是对开辟的动态内存空间进行释放,动态开辟内存并不像函数那样,在栈区,运行结束就自动释放内存空间,动态内存空间开辟在堆区需要自己释放它的空间。
在这里插入图片描述

继续看上面的代码

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* str = (int*)malloc(40);//开辟40字节的空间
	if (str == NULL)
	{
		perror(str);
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		*(str + i) = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d",*(str+i));
	}
	free(str);
	str=NULL;
	return 0;
}

在这里插入图片描述
释放完空间要对指针变量置为NULL,否则或如果后面使用到该指针变量,就会出现野指针的错误,定义指针变量未初始化

3.calloc

在这里插入图片描述
calloc是和malloc差不多的,区别在于calloc在开辟空间的同时,对内存进行了初始化,还有就是参数上的差别。
也可以理解为 malloc+memset=calloc

下面看一下calloc是如何应用的

#include<stdio.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
	}
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述

malloc和free,calloc和free都是两两成对出现的,如果没有进行free的话,就属于内存泄漏的问题,开辟了空间并没有用,别人也不能用。
但是它们两两成对也并不能代表就不会出现错误,来看下面的例子:

int test()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 1;
	}
	if (1)//在还没有到free就提前返回了
	{
		return 2;
	}
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}//内存泄露
int main()
{
	test();
	return 0;
}

4.realloc

realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,
那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。
那 realloc 函数就可以做到对动态开辟内存大小的调整
在这里插入图片描述

下面就来看一下它是怎么用的

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d", *(p + i));
	}
	int* ptr = (int*)realloc(p, 80);//增加空间
	//realloc开辟空间失败的话也是返回NULL
	if (ptr != NULL)
	{
		p = ptr;
		ptr = NULL;
	}

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

再增容的时候,也会有一些细节问题,接下来我们来看一下:
在这里插入图片描述

接下来我们在编译器中看一下扩容时改变地址的问题

在下面我们可以看到ptr和p的地址是不一样的,realloc开辟了18000的空间,也就说明了扩容太大的话可能会改变地址。
在这里插入图片描述
下面这个图片是realloc开辟80字节的空间,p的后面内容够用,所以就没有改变地址。
在这里插入图片描述

int main()
{
	realloc(NULL, 40);//也就等于malloc(40)
	return 0;
}

三.常见的动态内存错误

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

在动态内存开辟空间的时候,开辟完之后要验证一下是否开辟成功,如果没有开辟成功,会返回NULL
所以也就会出现对NULL指针的解引用的错误。

int main()
{
	int* p = (int *)malloc(40);
	//*p = 5;//这样直接赋值是错误的,需要验证一下
	if (p == NULL)
	{
		return 1;
	}
	else
	{
		*p = 5;
	}
	free(p);
	p=NULL;
	return 0;
}

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

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i <= 10; i++)
	{
		*(p + i) = i;//i=10时是越界访问
	}
	free(p);
	p = NULL;

	return 0;
}

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

非动态内存当中并不需要用free来释放空间,它是出了函数就自动释放空间了

int main()
{
	int a = 0;
	int* p = &a;
	free(p);
	p = NULL;
	return 0;
}

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

意思也就是开辟空间之后,在使用过程中指向起始地址的指针,并没有指向起始地址,而最后释放这个没有指向起始地址指针,就会出现错误。所以我们在使用动态开辟内存时一定要记录起始位置。

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		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");
		return 0;
	}
	free(p);
	free(p);//重复释放
	return 0;
}

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

下面就属于内存泄漏,开辟了空间却忘记释放

int test()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	return 0;
}
int main()
{
	test();
	return 0;
}

四.经典笔试题

第一题:

void GetMemory(char* p)//p接收的为NULL,并非str地址
{
	p = (char*)malloc(100);//开辟完空间并没有返回
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");//这里的str仍为NULL
	printf(str);
	//没有进行内存释放
}
int main()
{
	Test();
	return 0;
}

改正后的代码

void GetMemory(char** p)//传过来str地址
{
	*p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
	free(str);//释放内存
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

也可以改为这样,不过下面这种方法,传参就没有任何意义了

char* GetMemory(char* p)
{
	p = (char*)malloc(100);
	return p;
}
void Test(void)
{
	char* str = NULL;
	str=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;//当出了这个函数p的内容就销毁了
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();//str为野指针
	printf(str);
}
int main()
{
	Test();
	return 0;
}

第三题

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

改正后的代码

void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	free(str);
	str=NULL;
}
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;
}

改正后的代码

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

五.C/C++程序的内存开辟

C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
    束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
    分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
    回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
    配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序
结束才销毁,所以生命周期变长。

六.柔性数组

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

struct S
{
	int a;
	char b;
	int arr[];//柔性数组成员
	//有一些其他编译器的写法是
	//int arr[0];两种方法都可以
};

1.柔性数组的特点

结构中的柔性数组成员前面必须至少一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大
小,以适应柔性数组的预期大小

2.柔性数组的使用

方法一

struct S
{
	int a;
	char b;
	int arr[];
};
int main()
{
	struct S *ptr=(struct S *)malloc(sizeof(struct S) + 4 * sizeof(int));
	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}
	ptr->a = 100;
	ptr->b = 'a';
	for (int i = 0; i < 4; i++)
	{
		ptr->arr[i] = i;
	}
	printf("%d %c\n",ptr->a,ptr->b);
	for (int i = 0; i < 4; i++)
	{
		printf("%d",ptr->arr[i]);
	}
	struct S* ps = (struct S*)realloc(ptr,sizeof(struct S)+10*sizeof(int));
	if (ps == NULL)
	{
		perror("realloc");
		return 2;
	}

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

不用柔性数组,也可以达到同样的目的

方法二

struct S
{
	int a;
	char b;
	int *arr;
};
int main()
{
	struct S* ptr = (struct S*)malloc(sizeof(struct S));
	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}
	ptr->a = 100;
	ptr->b = 'a';
	int* p = (int*)malloc(4 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	else
	{
		ptr->arr = p;
	}
	for (int i = 0; i < 4; i++)
	{
		ptr->arr[i] = i;
	}
	printf("%d %c\n", ptr->a, ptr->b);
	for (int i = 0; i < 4; i++)
	{
		printf("%d", ptr->arr[i]);
	}
	int* ps= (int*)realloc(ptr->arr, sizeof(int) * 10);
	if (ps == NULL)
	{
		perror("realloc");
		return 2;
	}
	else
	{
		ptr->arr = ps;
	}
	free(ptr);
	ptr = NULL;
	free(ptr->arr);
	ptr->arr = NULL;
	return 0;
}

虽然两种方法都能实现同样的功能,但是还是方法一相对来说更好

第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给
用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你
不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好
了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正
你跑不了要用做偏移量的加法来寻址)

最后:文章有什么不对的地方或者有什么更好的写法欢迎大家在评论区指出

  • 19
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 21
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值