【C语言】动态内存管理

1. 为什么要有动态内存分配

在平常的时候,如果我们想要去开辟一块内存都是直接创建一个变量

int i = 1;
char arr[20] = { 0 };

但是上述的开辟空间的⽅式有两个特点:

1.空间开辟大小是固定的

2.数组在申明的时候,必须指定数组的⻓度,数组空间⼀旦确定了大小不能调整

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间⼤⼩在程序运⾏的时候才能知 道,那数组的编译时开辟空间的⽅式就不能满⾜了。

C语⾔引⼊了动态内存开辟,让程序员⾃⼰可以申请和释放空间,就⽐较灵活了。

2. malloc和free

C语言提供了一个动态内存开辟的函数malloc函数:

这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。

1. 如果开辟成功,则返回⼀个指向开辟好空间的指针。

2.如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。

3.返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃ ⼰来决定。

4.如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。

C语⾔提供了另外⼀个函数free,专⻔是⽤来做动态内存的释放和回收的:

free函数⽤来释放动态开辟的内存。

1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的。

2.如果参数 ptr 是NULL指针,则函数什么事都不做。

malloc和free都声明在 stdlib.h 头⽂件中。

举个例子:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = NULL;
	p = (int*)malloc(20);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	int i = 1;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i + 1;
		printf("%d\n", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

3. calloc和realloc

C语⾔还提供了一个函数叫 calloc , calloc 函数也⽤来动态内存分配。

函数的功能是为 num 个⼤⼩为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。

举个例子:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)calloc(5,sizeof(int));//calloc与malloc的区别是calloc函数会初始化开辟的空间的
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	else
	{
		int i = 0;
		for (i = 0; i < 5; i++)
		{
			*(p + i) = i + 1;
			printf("%d\n", *(p + i));
		}
	}
	free(p);
	p = NULL;
	return 0;
}

当我们开始调试查看内存的时候,发现p往下的20个字节的空间全部初始化为0。而malloc函数却不会初始化函数所开辟的空间。

有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时 候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。

1.ptr 是要调整的内存地址

2.size 调整之后新⼤⼩

3.返回值为调整之后的内存起始位置。

4.这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到 新 的空间。

5.realloc在调整内存空间的是存在两种情况:

情况1:原有空间之后有⾜够⼤的空间

情况2:原有空间之后没有⾜够⼤的空间

步骤:

a.在堆栈的内存中找一个新的空间,并且符合新的大小的要求。

b.将原来空间的数据拷贝一份到新的空间。

c.释放旧的空间。

d.返回新的空间的起始地址。

情况3:调整失败,返回空指针

举个例子:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = NULL;
	p = (int*)malloc(5 * sizeof(int));
	if (p == NULL)
	{
		perror("molloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i + 1;
		printf("%d\n", *(p + i));
	}
	int* ptr = realloc(p, 4000);//避免开辟失败导致前面开辟的空间也被销毁
	if (ptr == NULL)
	{
		perror("realloc");
		free(ptr);
		ptr = NULL;
	}
	else
	{
		int i = 0;
		for (i = 5; i < 10; i++)
		{
			*(ptr + i) = i + 1;
			printf("%d\n", *(ptr + i));
		}
		free(ptr);
		ptr = NULL;
	}
	return 0;
}

4. 常见的动态内存的错误

1.未检查动态内存空间是否创建成功就使用

void test()
{
	int* p = (int*)malloc(INT_MAX / 4);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
}

2.对动态内存空间的越界访问

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

3.使用free释放部分动态内存开辟的空间

void test()
{
	int* p = (int*)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
}

4.动态开辟的内存忘记释放

void test()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}//忘记释放会造成信息泄露的问题
}

5. 动态内存经典笔试题分析

题目1:

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

这段代码有什么问题呢?

1.没有使用free函数来释放动态内存开辟的空间,容易造成内存信息的泄露。

2.在Test函数中,GetMemory函数传参参数是一个指针变量,只要是变量,就是值传递。所以将指针变量p传过去运行完之后,GetMemory函数栈帧销毁会将p指针销毁,然后就会导致代码崩溃。

那么我们应该怎样改进这段代码呢?

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
void GetMemory(char** p)
{
	*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;
}

题目2:

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

问题:

1.等GetMemory函数返回后,使用str指针去访问p数组,就是非法访问,因为p数组的内存已经还给了操作系统。(就相当于假设有一天张三去某个酒店的211房间住了一晚上,第二天退房离开后推荐自己的朋友李四某酒店211房间,说211房间住的非常舒服,但张三已经把房间退了,这时候李四再去居住就是非法的)所以str就成了野指针。

6. 柔性数组

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

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

柔性数组的特点:

1.结构中的柔性数组成员前面必须至少一个其他成员,并且柔性数组未指定大小。

2.sizeof 返回的这种结构大小不包括柔性数组的内存。

例如:

#define _CRT_SECURE_NO_WARNINGS 1
typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;
int main()
{
	printf("%d\n", sizeof(type_a));
	return 0;
}

3.包含柔性数组成员的结构⽤malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

柔性数组的使⽤:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
struct S
{
	int n;
	int arr[];
};
int main()
{
	struct S* p=(struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	p->n = 0;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		p->arr[i] = i+1;
	}
	struct S* ptr = (struct S*)realloc(p, sizeof(struct S) + 10 * sizeof(int));
	if (ptr == NULL)
	{
		perror("mealloc");
		return 2;
	}
	for (i = 5; i < 10; i++)
	{
		ptr->arr[i] = i + 1;
	}
	free(ptr);
	ptr = NULL;
	return 0;
}

在这个数组中我们可以看到在使用malloc函数开辟了一块空间之后,可以设置柔性数组的长度,同时我们还可以使用realloc函数改变柔性数组的长度,最后一个数组的长度是可以随意改变的,因此,它被称为柔性数组。

我们还有一种方法不使用柔性数组的前提下就可以做到柔性数组的功能,随意改变结构体变量最后一个成员数组的大小:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
struct S
{
	int n;
	int* arr;
};
int main()
{
	struct S* p = (struct S*)malloc(sizeof(struct S));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	p->arr = (int*)malloc(5 * sizeof(int));
	if (p->arr == NULL)
	{
		perror("malloc");
		return 1;
	}
	p->n = 100;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		p->arr[i] = i + 1;
	}
	int* arr1 = (int*)realloc(p->arr, 10 * sizeof(int));
	if (arr1 == NULL)
	{
		perror("realloc");
		return 1;
	}
	for (i = 5; i < 10; i++)
	{
		arr1[i] = i + 1;
	}
	free(p->arr);
	p->arr = NULL;
	free(p);
	p = NULL;
	return 0;
}

那么,这两种方法有什么区别呢?

第一个好处是:

方便内存释放 如果我们的代码是在⼀个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调⽤free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能 指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返 回给用户⼀个结构体指针,用户做⼀次free就可以把所有的内存也给释放掉。

第二个好处是:这样有利于访问速度. 连续的内存有益于提高访问速度,也有益于减少内存碎片。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值