【C进阶】动态内存管理-创新互联

⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C语言进阶
⭐代码仓库:C Advanced
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!

让客户满意是我们工作的目标,不断超越客户的期望值来自于我们对这个行业的热爱。我们立志把好的技术通过有效、简单的方式提供给客户,将通过不懈努力成为客户在信息化领域值得信任、有价值的长期合作伙伴,公司提供的服务项目有:域名注册、虚拟空间、营销软件、网站建设、滨州网站维护、网站推广。

动态内存管理
  • 前言
  • 一、为什么存在动态内存分配
  • 二、动态内存函数的介绍
    • (一)malloc和free
      • 1.malloc函数介绍
      • 2.free函数总结
      • 3.p==NULL效果演示
    • (二)calloc
      • 1、介绍
      • 2、应用
    • (三)malloc和calloc比较
    • (四)realloc
      • 1.介绍
      • 2.应用
      • 3.realloc和malloc互相转化
  • 三、常见的动态内存错误
    • (一)对NULL指针的解引用操作
    • (二)对动态开辟空间的越界访问
    • (三)对非动态开辟内存使用free释放
    • (四)使用free释放一块动态开辟内存的一部分
    • (五)对同一块动态内存多次释放
    • (六)动态开辟内存忘记释放(内存泄漏)
    • (七)动态开辟内存提前返回
  • 四、经典的笔试题
    • (一)题1
        • 小知识
    • (二)题2(返回栈空间的地址)
    • (三)题3
    • (四)题4
  • 五、C/C++程序的内存开辟
  • 六、柔性数组
    • (一)什么是柔性数组
    • (二)柔型数组的特点
    • (三)柔性数组的使用
    • (四)柔性数组的优势
  • 总结


前言

先罗列一下本章节的重点:
本章重点
为什么存在动态内存分配
动态内存函数的介绍
malloc
free
calloc
realloc
常见的动态内存错误
几个经典的笔试题
柔性数组

那在之前我们介绍了可以进行静态内存管理,是可以开辟一个空间供我们进行存放,可是这开辟的空间过于小或者空间过于大呢?是不是不太靠谱,所以就有了动态内存的概念,当我们运用动态内存的时候,是十分靠谱的,它能够根据数量进行开辟合适的空间,所以,接下来跟着我一起看一看吧!


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

我们已经掌握的内存开辟方式有:
在这里插入图片描述
但是上述的开辟空间的方式有两个特点:

  1. 空间开辟大小是固定的。
  2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。这就需要我们引入动态内存开辟的操作了,如下:


二、动态内存函数的介绍

大家看下图,在cpu内核是这么存放的,我们今天要介绍的是堆区里面存放的动态内存管理:
在这里插入图片描述

(一)malloc和free 1.malloc函数介绍

C语言提供了一个动态内存开辟的函数malloc,我们打开MSDN看一看这个函数的介绍:
在这里插入图片描述
那我们根据这个介绍来写一下简单的打印1~10的malloc函数吧:
在之前我们如果要打印1~10的数的函数很简单,是先创建一整个空间去打印,而malloc是在堆区申请空间去进行打印:

#include#includeint main() {//申请40个字节,用来存放10个整型
	int* p = (int*)malloc(40);
	if (p == NULL) {perror("p::malloc");
		return 1;
	}
	//存放1~10
	//空间是连续存放的
	int i = 0;
	for (i = 0; i< 10; i++) {*(p + i) = i + 1;
	}
	//打印
	for (i = 0; i< 10; i++) {printf("%d ", *(p + i));
	}
	return 0;
}

似乎好像是写完了,但大家不要忘记我们开头的那张图,malloc是在堆区进行申请空间的,申请了空间,然后用这块空间,但是你没有还给操作系统,这空间不是浪费了吗,就好比图书馆有一本很热门很好的书,我借去了然后看个几天看完了,一直忘了还,那我一直占有这本书,没有还给人家,是不是就浪费了这本书的价值,但当图书管理员发现这本书咋一直在我这边,强行让我还掉,那我就得被动的还,也就是说这个程序结束了以后,强制性地把空间还给操作系统,那既然这么被强迫,图书管理员还跟我说让我下次注意点,盯着我去还书,那我不是很没面子!?那我就想办法了,我主动去还,我不拖欠书,这不就不会被说了吗?那就引进了一个free函数:

在这里插入图片描述
所以我们进行优化,加入free函数:
单单加上free函数看起来没什么问题,但倘若有人去再次进行访问以后是非法访问,就相当于一家图书馆搬地方了,但是在导航上还是显示有的,所以你吭哧吭哧去这个图书馆了,发现这个图书馆里面早就已经空了,里面装了一堆其他的东西,你进去以后就是非法访问了,所以需要在导航中显示这个地方改成其他了才OK~~~
在这里插入图片描述

2.free函数总结

简单了解完这串代码,我们归纳总结一下free函数:
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的。
free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h 头文件中。
在这里插入图片描述

3.p==NULL效果演示

那再给大家看一下当申请字节过于大的情况:
在这里插入图片描述

(二)calloc 1、介绍

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:
在这里插入图片描述
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

2、应用

那我们根据介绍简单实现一下吧!

#include#includeint main() {int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL) {perror("p::calloc");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i< 10; i++) {printf("%d ", *(p + i));
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}
(三)malloc和calloc比较

不同点1:malloc是只需要一个传递的参数,而calloc是需要两个传递的参数。
不同点2:如下图片:
在这里插入图片描述
在这里插入图片描述
malloc优势在于这个函数更加高效,因为它不需要空间初始化这个步骤,calloc存放的不是随机值,直接就是初始化。两者需要根据实际情况进行使用,没有优劣之分!!!

(四)realloc 1.介绍

1.realloc函数的出现让动态内存管理更加灵活。
2.有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
在这里插入图片描述
ptr 是要调整的内存地址
size 调整之后新大小
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

我们简单来实现一下:
在这里插入图片描述
当我们做到这里的时候,肯定一个字“莽”,直接就写代码:
在这里插入图片描述
看似写的没什么问题,但这里报了两个警告,那我们看一下为什么警告:
在这里插入图片描述
取消对NULL指针p+i的引用,这里需要解释一下,堆区是有一定的空间的,开辟的动态空间既然是个连续存放的空间,空间必定会有不够用的时候,那就需要重新开辟一块更大的空间去存放这些值,开辟的方法就是换个命名的指针。所以就有了以下的概念(详情见应用):

2.应用

在有了上面的概念以后,进行增容的操作也是比较复杂的,因为我们需要真正了解realloc函数的实现过程,那就是分为三种情况:
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间
情况3:增容失败,返回NULL
如下图:
在这里插入图片描述
所以就是说,换个指针去做冤大头,要是增容失败,那这个冤大头指针最后销毁了也好,被浪费了也好,都不管,而p能够完美输出;而如果它增容成功,那就是那个冤大头指针受到p的维护。这个堆区可以比作包租婆,你是p指针,你想在她地界上多上几个房间,而你不确定包租婆肯不肯给你增加地盘,你和包租公说,让包租公和包租婆商量商量能不能去增加房子,包租公就是这个冤大头指针ptr,当他和包租婆商量失败了,它就作为那个冤大头被打了,我不管他,我还是享有我的地盘,但如果我直接和包租婆说增加地盘,那我就会被赶出去,没地址了;当包租公和包租婆商量成功以后,包租公就要回来告诉我,我可以增容了,那我就接受包租公的指令,当我想开辟的空间没有影响到别人,我直接施工开干,当我想要的地盘过于大,我就得申请另开辟一块新的空间去扩容,realloc函数内部是这么做的,我们不需要模拟实现,知道原理即可。

代码如下:

#include#includeint main() {int* p = (int*)malloc(5 * sizeof(int));
	if (p == NULL) {perror("p::malloc");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i< 5; i++) {*(p + i) = 1;
	}
	//增容,增加5个整型空间
	int* ptr = (int*)realloc(p, 10 * sizeof(int));
	//解释为什么是开辟了ptr指针:
	//第二种情况是续的空间不够了,那只能另开辟一块空间
	//而如果硬要头铁返回p的指针的地址,导致返回了NULL
	//p指针已经被修改了,找不到原本的位置
	if (ptr != NULL) {p = ptr;//ptr那块区域用p去维护
		ptr = NULL;
	}
	//继续使用空间
	for (i = 0; i< 10; i++) {printf("%d ", *(p + i));
	}
	//释放空间
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述

3.realloc和malloc互相转化

当realloc在开头出现时,此时不需要扩容,所以就是NULL空指针,相当于malloc了,如下代码:

#include#includeint main() {//int* p = (int*)malloc(10 * sizeof(int));
	int* p = (int*)realloc(NULL, 10 * sizeof(int));
	if (p == NULL) {perror("p::realloc");
	}
	else {int i = 0;
		for (i = 0; i< 10; i++) {	printf("%d ", *(p + i));
		}
	}
	free(p);
	p = NULL;

	return 0;
}

三、常见的动态内存错误 (一)对NULL指针的解引用操作

在这里插入图片描述
上面的代码没有对p进行判断是不是空指针,所以会有警告,而我们加上判断以后就是没有警告了,所以我们需要在每次进行使用的时候加上判断是不是空指针。

在这里插入图片描述

(二)对动态开辟空间的越界访问

当我们进行越界访问的时候,发现是出现错误了,编译器直接给dubug了,所以大家在进行动态开辟空间操作的时候不要进行越界访问。
在这里插入图片描述

//错误代码:
#include#includevoid 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;
}
int main() {test();
	return 0;
}
(三)对非动态开辟内存使用free释放

当我们写了很多动态内存函数,完了,free永远忘不了了,所以就不管三七二十一,都加了free,在静态的变量处也加了个free,那我们看看吧!
在这里插入图片描述
代码继续挂,所以不能在静态内存中用free哦!!!

//错误代码:
#include#includevoid test()
{int a = 10;
	int* p = &a;
	free(p);
}
int main() {test();
	return 0;
}
(四)使用free释放一块动态开辟内存的一部分

当这个指针发生移动变化了以后,那这个指针指向的不是整个空间,是指向的其他位置,以及指向这个空间的一部分,你去给它释放了,这是很危险的。
在这里插入图片描述

//错误代码:
#include#includevoid test(){int* p = (int*)malloc(100);
	if (p == NULL) {return 1;
	}
	int i = 0;
	for (i = 0; i< 25; i++) {*p = i;
		p++;
	}
	//p出循环的时候p指向的是末尾数,不是起始位置
	//p只有指向起始位置的时候才需要被释放
	free(p);//p不再指向动态内存的起始位置
	p = NULL;
}
int main() {test();
	return 0;
}
(五)对同一块动态内存多次释放

在这里插入图片描述
释放了一次空间以后,p是个野指针了,没法进行再释放,是很危险的。

//错误代码:
#include#includevoid test() {int* p = (int*)malloc(100);
	if (p == NULL) {return 1;
	}
	free(p);
	free(p);//重复释放
}

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

//更改代码:
#include#includevoid test() {int* p = (int*)malloc(100);
	if (p == NULL) {return 1;
	}
	free(p);
	p = NULL;
	free(p);//重复释放
}

int main() {test();
	return 0;
}
(六)动态开辟内存忘记释放(内存泄漏)

在这里插入图片描述

忘记释放空间,看似是个小事,但是这块空间就一直存在你释放不了它,它就一直占用,只有把计算机重启才能够释放掉,所以我们在进行使用的时候,要进行释放。

//错误代码:
#include#includevoid test()
{int* p = (int*)malloc(100);
	if (p == NULL) {return 1;
	}
	//使用...
}
int main()
{test();
	return 0;
}

//正确代码:
//函数内部进行了malloc操作,返回了malloc开辟的空间的起始地址
//记得释放
#include#includeint* test()
{int* p = (int*)malloc(100);
	if (NULL == p){return 1;
	}
	return p;
}
int main()
{int* ptr = test();
	free(ptr);
	ptr = NULL;
	return 0;
}
(七)动态开辟内存提前返回

在这里插入图片描述
当成立满足的时候,test1函数直接返回了,即使这个malloc和free同时出现了,也是会有bug的。

所以:
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:
动态开辟的空间一定要释放,并且正确释放 。

四、经典的笔试题 (一)题1

大家觉得下面这串代码出现的问题是什么呢?
在这里插入图片描述
在这里插入图片描述
大家可能看这个有点懵逼,那其实有两个问题:
第一个问题是str无法接收hello world而导致系统的崩溃。
第二个问题就是内存泄露。
我们一一解释吧!!!
在这里插入图片描述
这串代码本来的意思是将str传参到GetMemory函数中,然后利用在GetMemory函数中开辟的空间进行复制“Hello World"字符串,但是我们知道,形参只是实参的一份临时拷贝,p有自己独立的空间,当我们传参到GetMemory这个函数的时候,在内部申请了空间以后,只是p指针指向开辟的动态内存空间,也就是p存放了新开辟动态内存空间的地址,而真正的str指针并没有改变,依旧是NULL,当strcpy进行拷贝的时候,形参被非法访问,所以会发生错误,写入位置为空指针,无法复制字符串。
第二个问题是在GetMemory函数内部,动态申请了内存,但是没有释放,会内存泄露。

正确代码:
取地址传参,接收用二级指针,传址操作。

#include#include#includevoid 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;
}

//或者:
#include#include#includechar* GetMemory()
{char* p = (char*)malloc(100);
	return p;
}
void Test(void)
{char* str = NULL;
	str = GetMemory();
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
}
int main() {Test();
	return 0;
}
小知识
#includeint main() {char* p = "hehe\n";
	printf("hehe\n");
	printf(p);

	return 0;
}

大家可能对这串代码有点不理解,这个p是char*类型的指针,存的是"hehe\n"的首元素地址,所以printf§;就是从首地址直接打印了。

(二)题2(返回栈空间的地址)

大家先看看下面的代码,感觉是打印的是什么!?
在这里插入图片描述
在这里插入图片描述
这怎么是烫烫烫了,哎这个好熟悉,出现这个烫烫烫不就是内存中的栈区有点问题吗???
那我们画图分析一下:在这里插入图片描述
我们根据代码一步一步来分析,当GetMemory函数进行操作的时候,我们看,return p;返回给str了,str记住了这个p的地址,看样子似乎没什么问题,但是当GetMemory函数销毁了以后呢???这个"hello world"字符串已经销毁了,你str找到地址以后里面是什么内容啊!?我们不知道的,是属于非法访问。就举一个例子:我今天去酒店租了一间房,我告诉张三啊,我的房间在豪大大酒店302房间,你明天记得来找我玩游戏,可是我第二天跑路了,回家去了,这个房间我也退了,第二天张三吭哧吭哧跑到302房间发现只有清洁的阿姨,好家伙,我已经回家了。也就是说这串空间早就已经销毁了,也有可能被人家占用了。

//修改
#include#includechar* GetMemory(void){static char p[] = "hello world";
	return p;
}
void Test(void){char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main() {Test();
	return 0;
}


//第二种:
#include#includechar* GetMemory(void){char* p = "hello world";//常量字符串,字符串在内存中本身就存在,*p只不过也是去访问而已,它也是存的是地址,传给str以后,str也是去找地址找到字符串并打印
	return p;
}
void Test(void){char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main() {Test();
	return 0;
}
(三)题3

在这里插入图片描述
大家看看这串代码的问题出现在哪!?看似没有问题,但好像少了个free();这岂不是内存泄露吗,那我们发现问题了就解决问题:
正确代码:

#include#include#includevoid 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;
}
(四)题4

在这里插入图片描述
相信大家第一眼就看出来了这个问题了,那就是提前释放空间了,当我们提前释放空间了以后这个空间就不被str所维护了,就有可能存放别的值了,但要记住的是这个空间被释放以后str并没有变成NULL,而是变为野指针了,所以当下面操作进行以后就是非法访问了,是很危险的,访问的是不确定的地方。

改造:

#include#include#includevoid Test(void)
{char* str = (char*)malloc(100);
	if (str == NULL) {return 1;
	}
	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关键字修饰局部变量的例子:
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁。
所以生命周期变长。

六、柔性数组 (一)什么是柔性数组

柔性数组(flexible array)在C99 之后出现的,在C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
在这里插入图片描述
在这里插入图片描述

(二)柔型数组的特点

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

(三)柔性数组的使用

大家看代码:

#include#includestruct S {int n;
	char arr[];//数组大小是未知的 - 柔型数组成员
};
int main() {//printf("%d\n", sizeof(struct S));
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(char));//开辟动态内存需要比柔型数组实际内存大
	ps->n = 100;
	int i = 0;
	//使用
	for (i = 0; i< 10; i++) {ps->arr[i] = '1';
	}
	//打印
	for (i = 0; i< 10; i++) {printf("%c\n", ps->arr[i]);
	}
	//增容
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20 * sizeof(char));
	if (ptr != NULL) {ps = ptr;
	}
	else {perror("ps::realloc");
		return 1;
	}
	//使用
	int j = 0;
	for (j = 10; j< 20; j++) {ps->arr[j] = 'b';
	}
	//打印
	for (j = 10; j< 20; j++) {printf("%c\n", ps->arr[j]);
	}
	//释放
	free(ps);
	ps = NULL;
	return 0;
}

我们知道的是可以用动态内存去开辟一块较大的空间,要是不够就往后继续增容。

(四)柔性数组的优势

既然说到优势,那就需要进行对比,我们写一串代码,是在结构体内用的字符指针:

#include#includestruct S {int a;
	char* arr;
};
int main() {struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL) {perror("malloc->ps");
		return 1;
	}
	ps->a = 100;
	//为arr开辟10个char
	ps->arr = (char*)malloc(10 * sizeof(char));
	if (ps->arr == NULL) {perror("malloc->arr");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i< 10; i++) {ps->arr[i] = 'w';
	}
	//打印
	for (i = 0; i< 10; i++) {printf("%c\n", ps->arr[i]);
	}
	//增容
	char* ptr = (char*)realloc(ps->arr, 20 * sizeof(char));
	if (ptr != NULL) {ps->arr = ptr;
	}
	else {perror("realloc->ptr");
		return 1; 
	}
	//使用
	int j = 0;
	for (j = 10; j< 20; j++) {ps->arr[j] = 'o';
	}
	//打印
	for (j = 10; j< 20; j++) {printf("%c\n", ps->arr[j]);
	}
	//释放
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

此图是解释开辟空间:(先在结构体中开辟一块动态内存空间,再将arr指针指向另一块开辟的动态内存空间)

大家将这串代码与前面柔型数组使用进行对比:
我们可以看以下几点:
在这里插入图片描述
那我们归纳一下柔型数组的优势:

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


总结

动态内存开辟实在是太重要了,如果我们在以后的写代码过程中使用了动态内存开辟,那是很好的一种方法,因为节省空间,我需要多少空间你就给我开辟多少空间,我们只需要申请空间即可。


客官,码字不易,来个三连支持一下吧!!!关注我不迷路!!!

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


网页名称:【C进阶】动态内存管理-创新互联
分享地址:http://cdiso.cn/article/csggdh.html

其他资讯