IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 谈谈C语言中的动态内存分配函数使用(malloc calloc realloc free)和动态分配的错误分析 -> 正文阅读

[C++知识库]谈谈C语言中的动态内存分配函数使用(malloc calloc realloc free)和动态分配的错误分析

前言

在C语言中,如果要使用堆空间的内容的话,可以使用C语言提供的动态内存函数去开辟堆空间,供程序员去使用,在C语言中,提供 4个函数malloc realloc calloc realloc free,其实free是为了释放分配的堆空间的空间,其他三个是申请堆空间;这里分享这些函数的基本使用,和一些常见的动态分配的错误分析!!!


为什么要有动态内存分配堆空间

我的理解:

首先我们内存本来就会划分好多个分区,就是为了方便内存管理,数据管理的,而在平时我们使用的局部变量全局变量都是存放在不同的空间的,我们不可能一直使用局部变量开辟的空间都是放在栈空间上,因为栈空间在设计时候给它是非常少的,通常只有几M左右,而此时,当我们频繁的使用栈空间,当达到超过了栈的最大容量时候,就会内存溢出。
而对于我们来说:我们使用内存空间很多时候都是不确定的,不确定我要多少空间,不确定我要在哪里开开辟,所以当我们在栈空间开辟空间时候,并不够自由,而在动态内存分配时候,当我们写程序写着写着时候,觉得需要分配内存时候,就可以自由的分配内存,并且按自己的意愿分配大小。

在C语言的动态内存函数中,一直都会被人诟病,动态分配后的内存,不自己手动free,就会导致内存泄漏; ,其实从另一个方面来讲,既然C语言都给你提供了内存分配的使用权,当然也是会给你自己释放的使用权,它可以让你的内存使用更加自由,对内存的使用权完全掌握在自己的手里,你不觉得这样很“自由”嘛


看看在栈开辟一定大小的空间,仅仅就1M发生了栈溢出(vs2013编译器),所以我们很需要动态内存分配堆空间来使用

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

int main()
{
char a[1024 * 1024]; //就简单的1M空间,程序就崩溃了

system("pause");
return 0;
}

在这里插入图片描述


malloc 的使用 和 free

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

void* malloc (size_t size);

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的起始地址。

malloc 这个函数仅仅开辟内存空间,并不做初始化操作。

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

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

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

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


C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:

void free (void* ptr);

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

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

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

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


使用案例:

#include <stdio.h>
int main()
{	
	//malloc 里的参数是字节为单位:这里是40个字节
	int* ptr = (int*)malloc(10*sizeof(int));
	
	//ptr == NULL 表示分配失败	
	if(ptr == NULL) 
		exit(1);
	//来到这里表示分配成功		
	//这是给分配的40个字节初始化为0
	for(int i=0; i<num; i++)
	{
		*(ptr+i) = 0}
	//当你认为后面的程序不再使用 ptr的指针指向的空间时候就free
	free(ptr);//释放ptr所指向的动态内存
	ptr = NULL;//是否有必要?
	return 0;
}

如何正确理解:free(ptr) ?

free(ptr) :释放ptr指向的动态内存的空间;
可能总有人误解:是释放ptr指针,然后ptr指针的生命周期就结束了,ptr这个指针就是销毁了;其实不然;即使free了ptr,ptr还是指向动态分配的堆空间的其实地址,这个ptr指针的生命周期并没有结束,但是我们却不能够再使用ptr指向的内存空间了。


为什么 free(ptr) 后,就不可以再使用ptr所指向的空间了?

因为free ptr后,ptr指向的动态分配空间就被系统回收掉了,就表明,这段空间不属于你的了,系统收回去了,虽然你还有ptr指针指向那段空间,但是没用,你用ptr试图访问那段空间就会出错,内存访问失败,这是不被允许的行为的,所以,很多人就有一种做法,再free调用ptr指向的内存空间时候,为了防止别人再次使用 ptr 这个指针去访问被回收的空间,就会把它置为 ptr = NULL;


free (ptr) 后,ptr置为空,即 ptr = NULL ;是必要的行为嘛?

其实不是必要的,因为ptr指向的内存已经被系统回收了,系统就已经让你不能够再使用ptr了,系统规定你不能使用了,但是你还是要强制使用,那就是你的问题了,就像明知道国家法律,你要去犯法一样,后面的事情就是你要承担了的。


free(ptr),我们只知道ptr指向动态内存的起始地址ptr,但是我们却可以释放完动态分配的内存空间大小,换句话说,free 是如何知道动态内存分配的空间是多大的,它是如何根据起始地址就知道了要释放的内存大小?

其实,当我们进行动态内存分配的时候,并不是仅仅分配了你所需要的字节数的内存空间,malloc这个函数,还还会分配多一部分空间,这一部分空间是用来记录分配的字节数或其他一定量的信息。
可以理解:malloc分配空间时候,不仅仅分配了程序员所需要的空间,于此同时也分配了一段空间是用来存放内存信息的空间的。只不过这个用来存放内存信息的空间我们是不需要知道。
所以当free(ptr)时候,free就可以根据存放内存信息的空间内容,去精准的释放空间大小啦。

至于这个malloc用来存放信息空间是什么信息由于又是一个大话题,估计等我有缘再探讨它了,到时候,有源再分享这部分内容。


calloc函数

calloc函数也是动态内存开辟函数;看看它的原型

void* calloc (size_t num, size_t size);
  • 数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0

  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0;

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//分配10个空间,每个空间为int类型的4个字节大小;
	int *p = (int*)calloc(10, sizeof(int)); 
	if(NULL != p)
	{
		//使用空间
	}
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述


realloc 函数

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

函数原型如下:

void* realloc (void* ptr, size_t size);
  • ptr 是要调整的内存地址;size 调整之后新大小;

  • 返回值为调整之后的内存起始位置;这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间

  • realloc在调整内存空间的是存在两种情况:
    情况1:原有空间的之后有足够大的空间
    当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
    在这里插入图片描述

    • 情况2:原有空间之后没有足够大的空间
      当是情况2 的时候,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,并把旧空间内容复制到新的空间内容上去,同时释放旧空间,这样函数返回的是一个新的内存地址。

对于 realloc函数来说,可能返回的指针地址和原来不一样,这点要注意。


常见的动态内存分配的错误使用

对空指针的使用

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

上面的情况可能会出问题:因为malloc分配内存时候,假如分配失败,就会返回空指针,所以在malloc分配完内存后,必须有一个操作,那就是对分配的指针做一个判断。


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

void test()
{
	int i = 0;
	int *p = (int *)malloc(10*sizeof(int)); //分配40个字节空间
	if(NULL == p)
	{
		exit(0); //分配失败就退出
	}
	for(i=0; i<=10; i++) 
	{
		*(p+i) = i;//当i是10的时候越界访问
	}
		free(p);
	}

这种情况很明显是对动态分配得到的内存越界访问,也会出现很大问题,要避免。


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

void test()
{
	int a = 10;
	int *p = &a;
	free(p);//ok?
}

这种情况也是错误的:千万不要对不是动态内存分配的空间指针使用free;


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

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

对于动态内存分配的起始地址指针,不要随便乱动它,它是有用的,用来记录内存的起始位置,方便下次不要时候可以用起始地址指针去释放改内存,如果你移动了起始地址的指针,那么就会发生错误,因为起始地址不见了,free就找不到正确释放内存的大小了。


void test()
{
	int *p = (int *)malloc(100);
	if(NULL != p)
	{
		*p = 20;
	}
	//忘记释放p,一旦函数调用结束,p的生命周期结束,
	//但动态内存分配的内存没有释放,就会导致内存泄漏
}
	int main()
	{
		test();
		return 0;
	}

上面是最常见的错误了,忘记释放动态内存分配的空间,这会导致内存泄漏,内存泄漏就是使用完的内存没有释放;所以一定要记住,使用完的动态内存要记得释放,且要正确释放。


经典错误使用动态内存分配的代码分析

题目1

问:请问运行Test 函数会有什么样的结果?

先好好分析一下,然后去编译器验证你的想法,同时给出你的分析理由,假如可以的话,如何修改正确,你是否做得到呢?

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指针指向该堆空间,然后对该str指针指向的堆空间赋值。

但是它这个想法却因为错误的使用的动态内存分配,导致了很严重的错误!

对于Test函数里的 GetMemory(str)函数调用,这里是值传递方式,值传递方式就是直接把str的NULL值拷贝一份给 GetMemory(char* p)函数里面形参p,所以p的值为NULL,在GetMemory函数里面的形参p申请了一份内存100个字节,p指针就指向了该100个字节的堆空间。当在Test函数里面调用完后GetMemory函数后,str的指向并没有发生改变,还是指向NULL,所以当指向strcpy(str, "hello world");这句代码时候,就相当于给NULL拷贝,然而对NULL指针是不可以访问的,一旦访问就会发生错误!
对于 GetMemory函数来说,还有一很严重的问题,那就是内存泄漏,一旦调用完GetMemory函数后,形参指针p就销毁了,p销毁后,你想找到动态分配的起始地址都找不到了,此时就会发生内存泄漏了。

在这里插入图片描述

所以总体描述上面的错误:
1. str给p传参时候,是值传递,值传递的方式就是str的一份拷贝数据给p,p和str是没有关系的,所以p申请得到的malloc空间只是对p有效,而对str是无效的,str依旧是NULL;一旦执行strcpy时候,就相当于给NULL拷贝数据,然后对NULL指针访问时不被允许的,所以程序就会发生崩溃错误!
2. 还有一个问题就是内存泄漏的,GetMemory函数执行完后,p形参就销毁了,而p执行的堆空间且没有被释放,就会发生内存泄漏;


所以假如我们想修改上面的程序正确的话,那么就可以抓住上面的两个问题修改
3. 值传递该为地址传递:由于实参是一级指针,所以传递一级指针实参地址给形参p时候,形参p的指针要设计是二级指针的形式
4. 释放堆空间GetMemory函数的堆空间:在使用使用完堆空间后(在这里就是strcpy函数执行完后),free(str)即可;重点错误做法:在GetMemory函数里面free§,这是错的,因为,你申请完的堆空间,都还没有使用就free了,那也会发生错误!

修改后正确的代码:正常打印 hello world;

void GetMemory(char **p) 
{
	p = (char *)malloc(100);
}
void Test(void)
{
	char *str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	free(str); //要在这里释放堆空间,防止内存泄漏;由于是地址传递,
	str =NULL;	//所以str也是指向堆空间,即使形参p已经销毁了,
				//我依旧还有能力找到堆空间,这也是传地址的一个魅力吧
	printf(str);
}

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);
}
int main()
{
	Test();
	return 0;
}

你们分析完后,我们看看编译器的结果:
在这里插入图片描述
我们看到了随机值!!!!并且还有一个警告?返回局部变量的地址;没错这个问题真的很常见就是返回局部变量的地址的问题。

分析过程:

GetMemory 函数 里面的定义了一个局部变量 p数组,并且给p数组初始化了 “hello world”;
return p时候,由于数组名就是首元素的地址,就相当于return 了 hello world 首字符 h的地址回去给Test函数的str指针;一旦 GetMemory函数执行完后,局部变量的数组p就会被销毁,销毁的意思是:局部变量p的空间使用权权给了操作系统,而不属于程序本身,这意味这程序无法使用该局部变量p的空间了,而系统回收的这段空间他就会使用,所以访问它的话就不是你原本的想要的结果了
即使我们 GetMemory 函数返回了该内存的地址,调用完后 在Test函数也有 str指针接收该地址,即表明str指针是有资格有能力找到被系统回收的局部变量p的空间的,这个本身是没有问题的,但是但是:虽然str指针是有资格有能力找到被系统回收的局部变量p的空间的,但是str却没有使用该空间的权力,这就类比在酒店住房到期一样,虽然你到期了还可以找到房间在哪里,但是却没有住的权力了。


题目3

运行结果是什么?

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函数里面free(str)后,str就是野指针,就是野指针啦!后面还是继续使用str指针,很明显free后就是把str指向的堆空间还给操作系统,你却还要strcpy 拷贝world给 str指针,很明显是在尝试访问不属于你的空间的内存;这个肯定是会发生错误!

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-10-03 16:54:12  更:2021-10-03 16:54:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/29 4:06:05-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计