动态内存管理

在了解和学习之前,我们需要了解为什么要有动态内存管理?

  1. 程序运行时需要动态分配内存,而系统提供的内存有限
  2. 程序运行时需要频繁地分配和释放内存,降低系统的内存利用率
  3. 程序运行时需要处理大量的内存,如字符串、数组、结构体等

malloc 和 free 函数

#include
void* malloc(size_t size)

我们可以通过如此的操作来分配内存。参数的单位是字节(size_t),如果申请内存成功返回起始地址,反之则返回NULL。
注:如果参数size为0,malloc的行为标准是未定义的,这主要取决于编译器,不同编译器的结果不同。

malloc 和 free 函数的使用

int main()
{
	//申请10个整型的空间
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		//空间申请失败
		perror("malloc failed");
		return 1;
	}
	else{}//可以使用这40个空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i + 1; //给申请的空间赋值
	}
	free(p);
	p = NULL; 
	return 0;
}

free(p)是通过代码的方式释放内存,如果不释放,程序结束后系统会自动释放。
释放后 p指向的空间不属于当前程序,但依然可以找到这个函数。此时p就成了野指针。
因此,我们需要p = NULL;来释放p所指向的空间,防止造成内存泄漏。
malloc 和 free 函数需要配套使用。

malloc函数申请的空间和数组的空间的区别:

1.动态内存大小是可以调整的
2.开辟空间的位置不同,malloc函数申请的空间是在堆区,数组是在栈区
3.malloc 函数申请的空间需要手动释放,数组使用完毕后自动释放

补充:C/C++中程序内存区域划分(计算机语言中)

内核空间(用户代码不能读写)
栈区(向下增长):进入函数创建,出函数销毁。栈内存分配运算内置与处理器的指令集中,效率高,但内存容量有限。
内存映射段(文件映射、动态库、匿名映射)
堆区(向上增长):一般由程序员释放,若程序员不释放,程序结束时会由OS(操作系统)回收。分配方式类似于链表
数据段(全局数据、静态数据)即静态区:程序结束后由系统释放
代码段(可执行代码/只读常量)数据不可修改:存放函数体(类成员函数和全局函数)的【二进制】代码

局部变量,形式参数放在 栈区
如:int* str1; int str2; char str3;

动态申请的内存,都放在 堆区
如:(int*)malloc(100);

全局变量,静态变量放在 数据段
如:int a; static int b;(全局变量中的) static int c;(局部变量中的)

常量放在 代码段
如:“abcedefg” 1234567890

calloc 函数和 realloc 函数

calloc 函数

void* calloc(size_t num, size_t size)分配内存并初始化为0
参数num是分配的元素个数
参数size是每个元素的大小
申请空间的大小为num * size
申请失败返回NULL
申请成功返回申请空间的指针

int main()
{
	//申请10个整型的空间
	//malloc(10 * sizeof(int));不进行初始化
	int* p = (int*)calloc(10, sizeof(int));//进行初始化,并初始化为0
	if (p == NULL)
	{
		perror("calloc failed");
		return 1;
	}
	//可以使用空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));//p[i]
	}
	//释放
	free(p);
	p == NULL;

	return 0;
}

realloc函数

void* realloc(void* ptr, size_t size)调整内存大小
参数ptr是指向原内存的指针的起始位置
参数size是新的内存大小
调整成功后返回调整后的内存地址,失败则返回NULL

int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc failed");
		return 1;
	}
	//可以使用空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));//p[i]
	}
	//调整空间 -- 希望变成20个整型空间
	int* tmp = realloc(p, 20 * sizeof(int));
	if (tmp != NULL)
	{
		p = tmp;
	}//调整时不要直接使用p接收,可能失败使p变成空指针
	//使用
	free(p);
	p == NULL;

	return 0;
} 

realloc函数在调整空间时有两种情况:
1.原内存足够,只需调整大小
2.原内存不够,分配新的内存,并拷贝原数据到新内存,同时释放旧的空间,返回新的地址
使用realloc函数时,需要注意指针的变化
realloc 函数不仅能调整空间,也可以申请空间

int main()
{
	int* p = (int*)realloc(NULL, 40);//==malloc(40)
	if (p == NULL)
	{ }
	return 0;
}

常见的动态内存的错误

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

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	//使用
	int i = 0;
	//if (p != NULL)
	//{
	//	perror("malloc");
	//	return 1;
	//}
	for (i = 0; i < 10; i++)
	{
		p[i] = i;//*(p + i) = i;
	}

	free(p);
	p = NULL;

	return 0;
}

malloc函数可能返回空指针,对空指针解引用会产生错误
需要判断指针是否为空,避免解引用错误

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

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

int main()
{
	int a = 10;
	int* p = &a;
	//......
	//......
	free(p);
	p = NULL;
	//此时运行时会报错

	return 0;
}

4.使用free函数释放内存的一部分

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	int i = 0;
	if (p != NULL)
	{
		perror("malloc");
		return 1;
	}
	for (i = 0; i < 5; i++)
	{
		*p = i;
		p++;
	}

	free(p);//错误写法
	p = NULL;

	return 0;
}

会报错,因为此时p指向的不是内存的起始位置
正确的释放方式应该是释放p指向的内存的起始位置
不能从中间某个值开始释放一部分动态内存

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

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	//使用
	int i = 0;
	if (p != NULL)
	{
		perror("malloc");
		return 1;
	}
	free(p);


	free(p);//错误写法
	p = NULL;

	return 0;
}

会报错,此时p指向的内存已经被释放
相当于对野指针进行释放
但如果上一次释放时将p置为NULL(空指针),则不会报错

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

void test()
{
	int flag = 1;
	int* p = (int*)malloc(100);
	if (p != NULL)
	{
		return  1;
	}

	if (flag)
	{
		return;//提前返回,内存没有释放
	}

	free(p);
	p = NULL;
}//内存泄漏
int main()
{
	test();
	//......

	//......

	return 0;
}

总结:

动态内存管理是一把双刃剑,它能够提供灵活的内存管理方式,但同时也会带来风险。

柔性数组

C99中,结构体最后一个元素是【未知大小的数组】,这就是【柔性数组】的成员。

struct S
{
	int n;
	char c;
	double d;
	int arr[];//未知大小数组 - arr就是柔性数组成员
};

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

创建柔性数组的结构体变量
不能直接struct S s;
必须先声明数组的大小,再声明结构体变量

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
	if (ps == NULL)
	{
		perror("malloc()");
		return 1;
	}
	//使用这些空间
	ps->n = 100;
	for (int i = 0; i < 20; i++)
	{
		ps->arr[i] = i + 1;
	}
	//调整ps指向空间的大小
	struct S* tmp = (struct S*)realloc(ps, sizeof(struct S) + 30 * sizeof(int));
	if (tmp != NULL)
	{
		ps = tmp;
	}
	else
	{
		return 1;
	}//防止调整失败,导致所有数组都销毁
	//使用
	for (int i = 0; i < 30; i++)
	{
		printf("%d ", ps->arr[i]);//前20个元素确定,后面的为随机值
	}
	//释放
	free(ps);
	ps = NULL;
	return 0;
}

我们也可以采用另一种方式实现柔性数组的内存分配

struct A
{
	int n;
	int* arr;
};
int main()
{
	struct A* ps = (struct A*)malloc(sizeof(struct A));
	if (ps == NULL)
	{
		perror("malloc()");
		return 1;
	}
	int* p = (int*)malloc(20 * sizeof(int));
	if (p != NULL)
	{
		ps->arr = p;
	}
	else
	{
		return 1;
	}
	ps->n = 100;
	int i = 0;
	//给arr中的20个元素赋值为1~20
	for (i = 0; i < 20; i++)
	{
		ps->arr[i] = i + 1;
	}
	//调整空间
	int* tmp = (int*)realloc(ps->arr, 40 * sizeof(int));
	if (tmp != NULL)
	{
		ps->arr = tmp;
	}
	else
	{
		perror("realloc()");
		return 1;
	}
	for (i = 0; i < 40; i++)
	{
		printf("%d ", ps->arr[i]);//前20个元素确定,后面的为随机值
	}
	//释放
	free(ps->arr);//先释放arr中开辟的内存空间
	ps->arr = NULL;//可写可不写
	
	free(ps);
	ps = NULL;
	return 0;
}

对比:
第一种使用柔性数组的方案更好,尽量使用第一种
优点:1.方便内存释放
2.有利于内存访问速度(影响有限),减少内存碎片
第二种易出现内存泄漏的问题

你可能感兴趣的:(算法,c语言,开发语言)