智云通CRM:如何让潜在客户换掉合作多年的供应商?

js复制指定内容

  返回  

动态内存管理那些事:malloc、calloc、realloc、free

2021/7/21 6:38:04 浏览:

文章目录

    • 一、为什么存在动态内存分配
    • 二、动态内存函数的介绍
      • 💦 malloc
      • 💦 free
      • 💦 calloc
      • 💦 realloc
    • 三、常见的动态内存错误
      • 💦 对NULL指针的解引用操作
      • 💦 对动态开辟空间的越界访问
      • 💦 使用free释放非动态开辟的空间
      • 💦 使用free释放一块动态开辟内存的一部分
      • 💦 对同一块动态内存多次释放
      • 💦 动态开辟内存忘记释放 (内存泄漏)
      • 💦 C/C++中程序内存区域划分示意图
    • 四、几个经典的笔试题
      • 💦 1.
      • 💦 2.
      • 💦 3.
      • 💦 5.
      • 💦 6.
    • 五、柔性数组
      • 💦 什么是柔性数组
      • 💦 柔性数组的特点

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

🎗 在之前我们都是这样开辟空间的:

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

特点

1️⃣ 开辟的空间大小是固定的

2️⃣ 数组在声明的时候,必需包含常量值 (指定数组长度)

小结
以往开辟空间的方式不够灵活,有很大的局限性 (有时候我们需要的空间大小在程序运行的时候才能知道)
所以这篇文章主要了解在内存堆上开辟空间所使用的函数
在这里插入图片描述

二、动态内存函数的介绍

💦 malloc

⭕ 函数信息
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include<stdio.h>
#include<stdlib.h>
int main()
{
	//假设开辟10个整型的空间:
	
	int arr[10];//1.栈区开辟
	
	int* p = (int*)void* p = malloc(10 * sizeof(int));//2.堆区开辟
	
	/*-----------------分割线-----------------*/
	
	//使用
	//1.开辟失败
	if(p == NULL)
	{
		perror("main");
		return 0;
	}
	//2.开辟成功
	int i = 0;
	for(i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	//打印
	for(i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	//回收空间
	free(p);
	p = NULL;
	return 0;
}

小结

1️⃣ malloc向内存申请一块连续可用的空间,且开辟成功时返回指向那块空间的地址;开辟失败返回NULL

2️⃣ 因为malloc函数的返回值是void*类型,所以在使用某一类型的指针变量接收时,也要强制类型转换为对应的类型

3️⃣ 如果malloc开辟失败,可能会对空指针进行非法解引用操作,所以malloc开辟的空间一定要检查

4️⃣ 使用完malloc开辟的空间,要主动回收空间。因为在回收空间后,那块空间的使用权已经不是自己能控制了,且能通过指针再去寻找到那块空间,所以为了避免非法访问,通常会主动将指向那块空间的指针置为NULL

💦 free

⭕ 函数信息
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include<stdio.h>
int main()
{
	int a = 10;
	int* p = &a;
	free(p);//1.err,回收栈空间
	p = NULL;
	
	free(NULL)//2.等同于-> //free(NULL)
	return 0;
}

小结

1️⃣ 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是标准未定义的

2️⃣ 如果参数ptr是NULL指针,则视为无效代码

3️⃣ 关于回收空间有2种方式:一是main函数结束后,开辟的空间会被动的还给OS,但是对于一个每天24小时不停跑的程序来说,如果不主动回收不用的空间的话,剩余的空间将会越来越少。二就是主动的把不用的空间主动回收掉

💦 calloc

⭕ 函数信息
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include<stdio.h>
#include<stdlib.h>
int main()
{
	//malloc和calloc都未主动初始化
	//1.malloc
	int* p = (int*)malloc(40);
	if(p == NULL)
		return 1;
	int i = 0;
	for(i = 0; i < 10; i++)
	{
		printf("%d\n", *(p + i));
	}
	free(p);
	p = NULL;
	
	//2.calloc
	int* q = (int*)calloc(10, sizeof(int));
	if(q == NULL)
		return 1;
	for(i = 0; i < 10; i++)
	{
		printf("%d\n", *(q + i));
	}
	free(q);
	q = NULL;
	return 0;
}

💨 结果:

在这里插入图片描述

小结

1️⃣ calloc相比malloc来说:calloc会主动初始化开辟的内存空间

💦 realloc

🎗 realloc的出现让动态内存管理更加灵活

在申请空间的时候,有时我们会发现过大了或过小了,需要灵活的调整:而能实现灵活调整的函数其实是realloc,所以malloc、calloc、realloc中realloc是毫无争议的一把手

⭕ 函数信息
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include<stdio.h>
#include<stdlib.h>
int main()
{
	//1.使用calloc开辟10个整形大小
	int* p = (int*)calloc(10, sizeof(int));
	if(p == NULL)
	{
		perror("main");
		return 0;
	}
	//2.使用开辟的空间
	int i = 0;
	for(i = 0; i < 10; i++)
	{
		*(p + i) = 5;
	}

	//3.到了这里还需要10个整型空间,而p所指向的空间已经被使用完了,所以使用realloc调整空间	
	//为什么这样设计,请看正面详解:
	int* pnew = (int*)realloc(p, 20 * sizeof(int));
	if(pnew != NULL)
	{
		p = pnew;	
	}
	//.回收空间
	free(p);
	p = NULL;
	
	return 0;
}

📝详解:

🎗 realloc开辟原理
在这里插入图片描述
❓❔ 思考:如何合适的接收realloc的地址呢

✖ int* p = (int*)realloc(p, 20 * sizeof(int));
如果用旧地址去接收:realloc有可能找不到合适的空间,来调整大小,这时就返回NULL。此时再交给p,不仅空间没开辟好,旧空间的内容也找不到了
—— 偷鸡不成蚀把米

✔ int* pnew = (int*)realloc(p, 20 * sizeof(int));
先用新地址接收,如果开辟成功再把它赋值给旧空间,这样不仅避免了旧空间的丢失,同样也适用场景一、场景二

🎗 realloc单独使用时能实现malloc的效果 (不会初始化)

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)realloc(NULL, 40);//同int* p = (int*)malloc(40);
	if (p == NULL)
		return 1;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d\n", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

💨 结果:
在这里插入图片描述

三、常见的动态内存错误

💦 对NULL指针的解引用操作

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

int main01()
{
	int* p = (int*)malloc(10000000000);
	int i = 0;
	for(i = 0; i < 10; i++)
	{
		*(p + i) = i;//int* p = NULL; 如果开辟失败,就会非法访问内存
	}
	return 0;
}
/*--------------------改正--------------------*/
int main()
{
	int* p = (int*)malloc(10000000000);
	if(p == NULL)
	{
		perror("main");
		return 0;	
	}
	int i = 0;
	for(i = 0; i < 10; i++)
	{
		*(p + i) = i;//int* p = NULL; 如果开辟失败,就会非法访问内存
	}
	return 0;
}

小结

1️⃣ 对于malloc、calloc、realloc的返回值要作判空理

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

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

💦 使用free释放非动态开辟的空间

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for(i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	free(p);//这是err的
	p = NULL;
	return 0;
}

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

#include<stdio.h>
#include<stdlib.h>
int main01()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if(p == NULL)
	{
		return 0;	
	}
	int i = 0;
	for(i = 0; i < 5; i++)
	{
		*p++ = i;
	}
	free(p);//没有完全回收
	p = NULL;
	return 0;
}
/*--------------------改正--------------------*/
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if(p == NULL)
	{
		return 0;	
	}
	//无非就是想让数组的的前5个元素初始化为0 1 2 3 4,只要不让p真实的往后走即可
	int i = 0;
	for(i = 0; i < 5; i++)
	{
		//1.
		p[i] = i;
		//2.
		//*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

小结
在这里插入图片描述

1️⃣ 未完全释放动态开辟的空间是err的

2️⃣ 可能会造成内存泄漏,因为没有人再能记住开辟的起始空间了

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

#include<stdio.h>
#include<stdlib.h>
int main01()
{
	int* p = (int*)malloc(100);
	//使用
	//...
	//释放
	free(p);
	//...
	//...
	free(p);//err
	return 0;
}
/*--------------------改正--------------------*/
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(100);
	//使用
	//...
	//释放
	free(p);
	p = NULL;
	//...
	//...
	free(p);//无意义
	return 0;
}

小结

1️⃣ 在free完malloc、calloc、realloc开辟空间后,要及时置为NULL

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

#include<stdio.h>
#include<stdlib.h>
void test()
{
	int* p = (int*)malloc(100);//p是局部变量
	if(p == NULL)
		return;
	//使用
	//...
}
int main01()
{
	test();
	//...	
	//这里就内存泄漏了:
	//在test内动态开辟了空间,忘了释放。且局部变量p也没留下任何遗言,所以在函数外部也释放不了。只要程序没有死,这块空间就没人能找到	
	return 0;
}

💦 C/C++中程序内存区域划分示意图

在这里插入图片描述

1️⃣ 栈区( stack ):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

2️⃣ 堆区( heap ) :一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。

3️⃣ 数据段(静态区 ) ( static )存放全局变量、静态数据。程序结束后由系统释放。

4️⃣ 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

四、几个经典的笔试题

💦 1.

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char* p)//p是str的一份临时拷贝
{
	p = (char*)malloc(100);//2.动态开辟空间后没有释放;p为局部变量,出了范围就找不到开辟的空间了,所以内存泄漏 	 	
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);//值传递
	strcpy(str, "hello world");//1.同strcpy(NULL, "hello world")
	printf(str);
}
int main01()
{
	Test();
	return 0;
}
/*--------------------改正1--------------------*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char* GetMemory(char* p)
{
	p = (char*)malloc(100); 	
	return p;//在局部变量p销毁前,把指向动态开辟好的空间的地址返回回来
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
}
int main02()
{
	Test();
	return 0;
}
/*--------------------改正2--------------------*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char** p)
{
	*p = (char*)malloc(100); //*p找到str	
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);//传址
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

💦 2.

#include<stdio.h>
char* GetMemory(void)
{
	char p[] = "hello world";//p是局部变量,但是接收的内容是在栈区创建的
	return p;//这里虽然返回p的地址,但是这块空间的内容已经销毁了
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory(); 
	printf(str);//非法访问内存:现在str的空间就不是自己的了,所以打印的时候,就烫烫烫了
}
int main()
{
	Test();
	return 0;
}

💦 3.

#include<stdio.h>
int* f2(void)
{
	int* ptr;
	*ptr = 10;//野指针问题:ptr没有初始化,这时候去解引用就出问题了
	return ptr;
}
int main()
{
	f2();
	return 0;
}

💦 5.

#include<stdio.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 main01()
{
	Test();//没有free
	return 0;
}
/*--------------------改正--------------------*/
#include<stdio.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;
}

💦 6.

#include<stdio.h>
#include<stdlib.h>
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello")
	free(str);
	if(str != NULL)//free不会主动置为NULL
	{
		strcpy(str, "world");//str已经释放了,非法访问内存
		printf(str);
	}
}
int main01()
{	
 	Test();
	return 0;
}
/*--------------------改正--------------------*/
#include<stdio.h>
#include<stdlib.h>
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello")
	free(str);
	str = NULL;//主动置NULL
	if(str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{	
 	Test();
	return 0;
}

五、柔性数组

💦 什么是柔性数组

🎗 想必很多人都未听说过柔性数组 (flexible array) 这个概念,但是它确实存在。C99中,结构体中的最后一个元素允许是未知大小的数组,这就叫做 “柔性数组” 成员。注:不代表所有编译器都能支持

struct S1
{
	int n;
	int arr[];//这就叫做 "柔性数组" 成员
};
struct S2
{
	int n;
	int arr[0];//也可以这样写
};

💦 柔性数组的特点

1️⃣ 结构体中的柔性数组成员前面必须至少一个成员

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

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

4️⃣ 柔性数组的成员都在堆上开辟

#include<stdio.h>
#include<stdlib.h>
struct S
{
	int n;
	int arr[];//1.在之前必须有1个成员以上
};
int main()
{
	struct S s = { 0 };
	printf("%d\n", sizeof(s));//2.4Byte

	//3.期望arr的大小是10个整型
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));
	if(ps == NULL)
	{
		return 0;	
	}
	//赋值
	ps->n = 10;
	int i = 0;
	for(i = 0; i < 10; i++)
	{
		 ps->arr[i] = 0;
	}
	//调整
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20 * sizeof(int));
	if(ptr != NULL)
	{
		ps = ptr;	
	}
	//释放
	free(ps);
	ps = NULL;
	return 0;
}

在这里插入图片描述


🎗 看到这里,是不是觉得柔性数组没必要存在,因为只要让指针指向动态开辟的空间即可 (模拟柔性数组)

#include<stdio.h>
#inlude<stdlib.h>
struct S
{
	int n;
	int* arr;
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if(ps == NULL)
		return 0;
	ps-> n = 10;
	ps->arr = (int*)malloc(10 * sizeof(int))
	if(ps->arr == NULL)
		return 0;
	//赋值
	int i = 0;
	for(i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	//增加
	int* ptr = (int*)realloc(ps->arr, 20 * sizeof(int));
	if(ptr != NULL)
	{
		ps->arr = ptr;
	}
	//释放
	//这里需要回收2个空间,且回收必须有先后
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

对比:柔性数组➰指针模拟柔性数组

1️⃣ 指针模拟的方式需要2次malloc、2次free,也就意味着容易出错;而柔性数组只要1次malloc、1次free即可
在这里插入图片描述

2️⃣ 其次malloc多了,内存碎片相对的也变多了,内存的利用率就降低了
在这里插入图片描述
💨 小结柔性数组的好处:

1️⃣ 方便内存释放

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

2️⃣ 有利于访问速度

连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,也没多高,反正你跑不了要用做偏移量的加法来寻址)

推荐一篇关于柔性数组的文章
C语言结构体里的成员数组和指针

联系我们

如果您对我们的服务有兴趣,请及时和我们联系!

服务热线:18288888888
座机:18288888888
传真:
邮箱:888888@qq.com
地址:郑州市文化路红专路93号