二十二、动态内存管理

👉 欢迎阅读这篇文章 👇

目录

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

我们已经掌握的内存开辟方式有

c 复制代码
int val = 20;//在栈空间上开辟四个字节

char arr[10]={0};//在栈空间上开辟10个字节的连续空间

上述两种开辟方式有两个特点:

  • 空间开辟大小是固定的。
  • 数组在声明的时候,必须要指定数组长度,数组空间一旦确定后大小就不能调整。

有时候我们需要的空间大小在程序运行的时候才能知道,那数组在编译时开辟空间的方式就不能满足了。

C语言引入了动态内存开辟,让程序员可以自己申请和释放空间,就比较灵活了。

2、malloc和free

2.1malloc

函数原型

c 复制代码
void* malloc (size_t size);
  • 功能: 向内存的堆区 申请一块连续可用的空间,并返回指向这块空间的起始地址。
  • 参数: size------要分配的内存块的字节数。
  • 返回值:
    • 如果开辟成功,则返回这块空间的起始地址。
    • 如果开辟失败(如系统内存不足),则返回一个NULL指针,因此malloc的返回值一定要检查。
    • 返回值类型是void*的,所以malloc函数并不知道开辟空间的类型,具体使用者在使用的时候自己决定。

该函数的使用需要包含头文件stdlib.h

注意事项: 如果参数size为0,malloc的行为是标准未定义的,不要传0。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

//malloc函数使用演示

int main()
{
	//int arr[ ] = { 1,2,3,4,5 };//栈区开辟20个字节空间
	int*p = (int*)malloc(20);

	if (p == NULL)//检查是否开辟成功
	{
		perror("use malloc");
		return 1;//结束程序
	}

	//使用空间

	return 0;
}

2.2free

函数原型

c 复制代码
void free (void* ptr);
  • 功能: 释放之前通过动态内存分配函数(如malloccallocrealloc)申请的内存空间。
  • 参数:
    • ptr:指向要释放的指针。
      • 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是为定义的。
      • 如果参数ptr是NULL指针,则函数什么也不做。

该函数的使用需要包含头文件stdlib.h

举例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
//free函数演示

int main()
{
	//在堆区开辟20个字节的空间
	int* p = (int*)malloc(20);

	//判断开辟空间是否成功
	if (p == NULL)
	{
		perror("use malloc");
		return 1;//退出程序
	}

	//使用空间
	int i = 0;
	for (i = 0;i < 5;i++)
	{
		scanf("%d", p + i);
	}

	for (i = 0;i < 5;i++)
	{
		printf("%d", *(p + i));
	}

	//释放空间
	free(p);
	p = NULL;

	return 0;
}

上方代码在使用free的过程是这样的

c 复制代码
    //释放空间
	free(p);
	p = NULL;

为什么要加上一个p = NULL;

我们来调试看一下p指向的地址在开辟和最终释放的变化

刚刚开辟时

释放后

我们可以发现执行完free函数后(释放开辟的空间后)p指向的地址没有改变。由前面指针的知识可知这里时指针指向的空间被释放,那p就变成了野指针,所以我们要把p初始化为NULL,防止它成为野指针。

3、calloc和realloc

3.1calloc

calloc 函数也⽤来动态内存分配。原型如下:

c 复制代码
void* calloc(size_t num,size_t size);
  • 功能:num 个⼤⼩为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。
  • 参数:
    • num------元素的个数。
    • size------每个元素大小
  • 返回值:
    • 如果开辟成功,则返回这块空间的起始地址。
    • 如果开辟失败(如系统内存不足),则返回一个NULL指针,因此calloc的返回值要检查。
    • 返回值类型是void*的,所以calloc函数并不知道开辟空间的类型,具体使用者在使用的时候自己决定。

该函数的使用需要包含头文件stdlib.h

与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。(calloc的功能可以理解为malloc+memset的功能)

我们通过调试来观察一下malloccalloc的区别

malloc开辟的

calloc开辟的

通过监视可以看出calloc开辟完空间后确实会把申请的空间的每个字节初始化为全0。

所以如果我们对申请的内存空间的内容要求初始化,那么可以很⽅便的使⽤calloc函数来完成任务。

3.2relloc

realloc 函数的出现让动态内存管理更加灵活。

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

函数原型:

c 复制代码
void* realloc(void* ptr,size_t size);
  • 功能: 重新调整之前分配的内存块大小,它可以在不丢失原有数据的情况下,扩⼤或缩⼩动态分配的内存块。
  • 参数:
    • ptr------要调整的内存空间的起始地址,如果ptrNULL指针,realloc函数的功能就类似于malloc的功能。
    • size------调整之后新⼤⼩,单位是字节。
  • 返回值:
    • 成功,返回⼀个指向重新分配的内存块的 void * 类型指针。这个指针可能与原来的指针不同。
    • 失败,则返回一个NULL指针,并且原来的内存块保持不变。

注意事项:

  • realloc 在调整内存空间⼤⼩的时候,存在两种情况:
    • 情况1: 原有空间之后有足够大的空间,要扩展的内存就直接在原来的内存之后追加,原空间的数据不发生变化,最终返回的地址还是旧地址。
    • 情况2: 原有空间之后没有足够大的空间,会在内存的堆区寻找新的内存空间,返回新的起始地址,在这个过程会有以下过程发生
      • 寻找新的满足要求的空间
      • 将旧空间的数据拷贝到新空间,保证数据不会丢失
      • 释放旧的空间,返回新空间的起始地址

下面来分析如何接收realloc的返回值。

c 复制代码
//直接将realloc的返回值放到p中
p = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)

直接放的话,假如申请失败了就会返回NULL,那么p就直接变成空指针了,原空间也找不到了,功能就没有实现,所以应该先把它放到一个新的指针。去判断是否成功。

c 复制代码
//realloc演示

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int* p = (int*)malloc(20);

	if (p == NULL)
	{
		perror("use malloc");
		return 1;
	}

	//使用

	//调整动态内存大小(期望存10个整形大小)

	int* ptr = (int*)realloc(p, 10 * sizeof(int));

	//失败了
	if (ptr == NULL)
	{
		perror("use realloc");
		return 1;
	}
	
	//成功了
	else
	{
		//p继续用
		p = ptr;
		ptr = NULL;//避免ptr变成空指针
	}

	//使用


	return 0;
}

4、动态内存的常见错误

4.1对NULL指针的解引用操作

c 复制代码
//动态内存的常见错误1:对NULL指针解引用操作
void test()
{
	int* p = (int*)malloc(INT_MAX / 4);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
	p = NULL;
}

这里没有对malloc的实现成功与否进行判断,有可能p会变成NULL。

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

c 复制代码
//动态内存的常见错误2:对动态开辟空间的越界访问
void test()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		return 1;
	}
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;//当i是10的时候越界访问
	}
	free(p);
	p = NULL;
}

4.3对⾮动态开辟内存使⽤free释放

c 复制代码
//动态内存的常见错误3:非动态内存空间使用free释放空间
void test()
{
	int a = 10;
	int* p = &a;
	free(p);//错误
}

4.4使⽤free释放⼀块动态开辟内存的⼀部分

c 复制代码
//动态内存的常见错误4:使用free释放内存中的一部分
void test()
{
	int* p = (int*)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
}

4.5对同⼀块动态内存多次释放

c 复制代码
//动态内存的常见错误5:对同一块动态内存多次释放
void test()
{
	int* p = (int*)malloc(100);
	free(p);
	free(p);//重复释放
}

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

c 复制代码
//动态内存常见错误6:动态开辟内忘记释放--造成内存泄漏
void test()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}
}
int main()
{
	test();
	while (1);
	return 0;
}

这里调用了test函数开辟了一个空间,下面进入无限循环,开辟的这块空间既没有用上,也无法释放,就会造成内存泄漏。

忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。

切记:动态开辟的空间⼀定要释放,并且正确释放。

如果定义一个函数只是为了开辟空间,那也要交代清楚:这段代码中进行了动态内存分配,记得释放空间。

动态申请的空间,可以通过free直接释放

如果没有使用free释放,在程序结束的时候,操作系统也会将这个内存回收

5、动态内存经典题目

5.1题目1

运行test函数有什么后果

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}
int main()
{
	Test();
	return 0;
}

test函数内部定义了一个指针变量str,又调用了函数GetMemory,将参数str传了过去,这里是传值调用,在函数GetMemory内开辟了空间后将地址传给了p,但是函数结束后,变量p就会销毁,这时开的的空间就找不到了,str仍然是NULL,在strcpy(str, "hello world");直接就会崩溃。

改正:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
}
int main()
{
	Test();
	return 0;
}

现在调用GetMemory是进行了传址调用 ,将str的地址传了过去,GetMemory用二级指针p接收,这时p指向的就是str*p指向的内容就是str指向的内容,这时在函数内部开辟了空间,将开辟的空间的地址赋给 *p,就是相当于赋给了str,函数结束了p销毁了,但是str*p存的地址是一样的,开辟的空间还被记着,就可以使用。

5.2题目2

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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,在GetMemory函数内部创建了一个数组p存放了一个常量字符串,返回了一个临时变量地址,这样会造成野指针的出现,返回后数组p就被销毁了,返回的这个临时变量地址就失效了,它又赋给了str,这时str就会变成野指针,后面打印就会造成非法访问。

不要返回栈空间地址

改正:

我们要想改正就要想办法增长p的生命周期,增常生命周期的方法有:

  • static修饰变量。
  • 利用开辟动态内存的方法开辟需要的空间。
c 复制代码
//更正方法1:用static修饰p,增长p的生命周期
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* GetMemory(void)
{
	static char p[] = "hello world";//p就从栈区到了静态区,生命周期延长,这段函数结束后不会被销毁。
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}
c 复制代码
//更正方法2:用动态内存管理的方法开辟空间存储
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* GetMemory(void)
{
	char* p =(char*)malloc(20);//开辟20个字节的空间

	if (p == NULL)
	{
		perror("use malloc");
		return NULL;
	}
	
	strcpy(p, "hello world");//将常量字符串复制到刚刚开辟的空间内

	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);

	//释放空间
	free(str);
	str = NULL;

}
int main()
{
	Test();
	return 0;
}

5.3题⽬3

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

这段代码只有一个问题,就是没有释放开辟的动态内存。

更正:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

5.4题目4

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

test函数内部定义了一个指针变量str,开辟了一个100字节的堆区空间,然后没有检查开辟是否成功 ,将字符串常量hello复制给了str指向的空间,然后又释放了str指向的空间,这时str就变成了野指针,str不为NULL,满足if条件,又把字符串常量hello复制给了str指向的空间,这里就进行了非法访问。

更正:

c 复制代码
//更正方法一:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Test(void)
{
	char* str = (char*)malloc(100);

	if (str == NULL)
	{
		perror("use malloc");
		return;
	}

	strcpy(str, "hello");
	printf(str);

	free(str);
	str = NULL;

}
int main()
{
	Test();
	return 0;
}
c 复制代码
//更正方法二

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");

	free(str);
	str = NULL;//让str不满足if的条件

	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

6、柔性数组

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

例如:

c 复制代码
struct st_type
{
	int i;
	int arr[];//柔性数组成员
};

6.1柔性数组的特点

  • 结构中柔性数组成员前面必须至少一个其他成员。
  • sizeof返回的这种结构大小不包含柔性数组的内存。
  • 包含柔性数组成员的结构用malloc()函数进行内存分配,分配的内存应该大于结构的大小,以适应柔性数组预期的大小。
c 复制代码
struct st_type
{
	int i;
	int arr[];//柔性数组成员
};

int main()
{
	printf("%zu", sizeof(struct st_type));//4
}

6.2柔性数组的使用

c 复制代码
//柔性数组的使用

#include <stdio.h>

struct ps
{
	int i;
	int arr[];//柔性数组成员,最初期望能够存5个整型数。
};

int main()
{
	//开辟内存
	struct ps* p = (struct ps*)malloc(sizeof(struct ps) + 5 * sizeof(int));

	//检查
	if (p == NULL)
	{
		perror("use malloc");
		return 1;
	}

	printf("扩容前\n");

	//使用
	int i = 0;
	for (i = 0;i < 5;i++)
	{
		p->arr[i] = i + 1;
	}

	for (i = 0;i < 5;i++)
	{
		printf("%d ", p->arr[i]);
	}

	printf("\n");
	printf("扩容后\n");

	//扩容 期望柔性数组能够存放10个整型数
	struct ps* ptr = (struct ps*)realloc(p, sizeof(struct ps) + 10 * sizeof(int));

	//检查
	if (ptr == NULL)
	{
		perror("use realloc");
		return 1;
	}
	else
	{
		p = ptr;
		ptr = NULL;
	}

	//使用
	for (i = 5;i < 10;i++)
	{
		p->arr[i] = i + 1;
	}

	for (i = 0;i < 10;i++)
	{
		printf("%d ", p->arr[i]);
	}

	//释放
	free(p);
	p = NULL;

	return 0;
}

6.3柔性数组的优势

上方柔性数组的使用,也可以使用下方代码实现

c 复制代码
//柔性数组的替代

struct ps
{
	int i;
	int* arr;//结构里存一个指针,用于后续开辟一个arr指向的空间,然后对arr指向的空间进行操作以达到实现柔性数组的作用
};

int main()
{
	//第一次开辟空间,为结构体开辟一个空间
	struct ps* p = (struct ps*)malloc(sizeof(struct ps));//p指向结构体

	//检查
	if (p == NULL)
	{
		perror("use malloc");
		return 1;
	}

	//使用
	p->i = 100;

	//第二次开辟空间(为结构体里的arr指向的内容开辟空间)
	int* ptr = (int*)malloc(5 * sizeof(int));

	if (ptr == NULL)
	{
		perror("use malloc");
		return 1;
	}
	else
	{
		p->arr = ptr;
		ptr = NULL;
	}

	//使用arr指向的内容

	printf("扩容前\n");
	int i = 0;
	for (i = 0;i < 5;i++)
	{
		p->arr[i] = i + 1;
	}

	for (i = 0;i < 5;i++)
	{
		printf("%d ", p->arr[i]);
	}

	//扩容
	int* ptr2 = (int*)realloc(p->arr, 10 * sizeof(int));

	//检查

	if (ptr2 == NULL)
	{
		perror("use realloc");
		return 1;
	}
	else
	{
		p->arr = ptr2;
		ptr2 = NULL;
	}

	//使用扩容后的空间

	printf("\n");
	printf("扩容后\n");


	for (i = 5;i < 10;i++)
	{
		p->arr[i] = i + 1;
	}

	for (i = 0;i < 10;i++)
	{
		printf("%d ", p->arr[i]);
	}

	//第一次释放-对结构体里的arr释放
	free(p->arr);
	p->arr = NULL;

	//第二次释放-对结构体里释放
	free(p);
	p = NULL;

	return 0;
}

上方的代码和柔性数组的使用可以完成同样的功能,但是使用柔性数组有两个好处

  • ⽅便内存释放
  • 有利于访问速度

7、C/C++中程序内存区域划分

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

  • 栈区(stack): 在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
  • 堆区(heap): ⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收 。分配⽅式类似于链表。
  • 数据段(静态区)(static): 存放全局变量、静态数据。程序结束后由系统释放。
  • 代码段: 存放函数体(类成员函数和全局函数)的⼆进制代码。
相关推荐
Black蜡笔小新12 小时前
制造业AI质检工作站/自动化AI算法训练服务器DLTM企业AI算力工作站筑牢制造业品质防线
人工智能·算法·自动化
晚风予卿云月12 小时前
【模拟】多项式输出 & 蛇形方阵 & 字符串展开
c++·算法·模拟算法·随笔·竞赛练习
listhi52012 小时前
基于MATLAB的自适应粒子群算法(APSO)实现大规模分类特征选择
算法·matlab·分类
weixin_4074438712 小时前
基于Sentinel-1/2数据特征优选的冬小麦识别
人工智能·算法·随机森林·机器学习·sentinel
智者知已应修善业12 小时前
【51单片机按键加减1若不释放自动加减】2023-11-24
c++·经验分享·笔记·算法·51单片机
zavoryn12 小时前
大模型入门:从 MHA 到 GQA,一次讲清 KV Cache 为什么能省显存
人工智能·算法
孬甭_12 小时前
栈和队列
c语言·数据结构
骄马之死12 小时前
ThreadLocal 核心原理
java·jvm·算法
周末也要写八哥12 小时前
经典算法题之删列造序(二)
数据结构·算法