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语言】深度剖析动态内存管理 -> 正文阅读

[C++知识库]【C语言】深度剖析动态内存管理

0. 前言

在平常开辟数组的时候,你是否为空间不足、空间浪费、空间无法调整而烦恼?如果对此头疼不已,相信看完这篇博客,你的问题就能迎刃而解。没错,本篇博客就是对动态内存管理的讲解。博客中,对于动态内存的相关函数、使用动态内存时经常出现的问题,和几道经典笔试题做了详细讲解。话不多说,我们这就开始。

1. 为什么存在动态内存分配

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

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

但是上述的开辟空间的方式有两个缺点:

  1. 空间开辟大小是固定的,无法扩容或减容,可能会空间不足或空间浪费。
  2. 数组在定义的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了(数组需要提前指定好大小,因为编译的时候需要确定函数栈空间大小,遇到运行位置才能确定大小的情况就不太适合了),那么这时不如试试动态内存开辟!

2. 动态内存函数

2.1 malloc

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

void* malloc (size_t size);

malloc是一个开辟动态内存的函数,参数size为开辟空间的内存大小,单位是字节。函数返回值为void*的指针,开辟成功返回开辟空间的地址,失败返回NULL空指针。

2.1.1 申请空间成功

例如,开辟一个四十个字节的空间:

#include <stdlib.h>//所需头文件
int main()
{
	void* p = malloc(40);
	return 0;
}

但是这样使用还是不够准确的,因为p的类型是void*void*的指针也不知道步长,也不能解引用,也不能±,不如我们直接将p强制类型转换成对应类型。就比如我们想申请一个10个整形元素的空间。

int* p = (int*)malloc(40);

当malloc成功申请到空间,返回这块空间的起始地址。

2.1.2 申请空间失败

倘若我们申请空间,失败了。例如我内存只有4个G,但是我要申请1个T的空间,这时就会返回NULL空指针。

所以当空间开辟失败这是很危险的,所以在每次开辟空间后最好来一个判断:

int main()
{
	int* p = (int*)malloc(INT_MAX);//21亿多,整形的最大值
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));//打印错误码,了解错误
		return 1;//异常返回
	}//使用
	return 0;
}

运行结果:

image-20220923163711368

这里也可以用断言,断言为直接将程序奔溃,雷厉风行;而if语句则是一个委婉的处理,让我们看到对应的错误。一般在传参时参数检查使用断言,malloc等开辟空间的函数使用if语句判断是否为空指针。

int main()
{
	int* p = (int*)malloc(INT_MAX);//21亿多
	assert(p);//断言
	return 0;
}

运行结果:

image-20220923164401800

2.1.3 总结

void* malloc (size_t size);

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

  • size是需要动态开辟空间的内存大小。
  • 如果开辟成功,则返回一个指向开辟空间起始处的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候自己来决定。
  • 如果参数size为0,malloc的行为是标准未定义的,取决于编译器。

2.2 free

2.2.1 free的重要性

我们平常创建的局部变量,出作用域自动销毁,它在栈区上开辟。

而动态内存分配,如malloc等函数是在堆区上开辟空间的,它并不会自动销毁,需要自己回收,或者程序结束时自动回收。

但是程序结束时自动回收有个缺点,当这个程序不结束时,这块空间就会一直存在。试想一下,如果运行一个大规模的程序,程序运行的周期很长,但是动态内存一直在开辟空间,也不释放,最后会不会因为内存不足,导致内存耗干?导致电脑卡死?

然后就会出现某些灵异现象,程序一跑起来就很卡,过一会程序结束就没了,或者没有关掉程序,然后电脑越来越卡,只能重启,重启完毕又好了的事情,如果内存没有及时释放,你说多恐怖?这件就是典型的"吃内存"现象。

所以C语言提供了另外一个参数free,专门用来做动态内存的释放和回收:

void free (void* ptr);

free用来释放动态内存开辟的空间,参数ptr为指向开辟空间的首地址处的指针。函数没有返回值。若参数为动态开辟的起始地址,释放空间。若参数为NULL空指针,则不进行操作。

2.2.2 free的使用

例如:

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));//打印错误码,了解错误
		return 1;//异常返回
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;//改变指向,p最后指向空间的结尾后面位置
	}
    //释放
    free(p);//ok?
	return 0;
}

这样做可不可行?答案是不行的,因为使用动态开辟的空间时,p被修改了,这时p释放的不是我们开辟的空间,这样就出问题了。

我们应该额外保存一份p的拷贝,用拷贝进行使用,最后再释放p的空间:

int main()
{
	int* p = (int*)malloc(40);
    int* ptr = p;
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));//打印错误码,了解错误
		return 1;//异常返回
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*ptr = i;
		ptr++;//改变指向,p最后指向空间的结尾后面位置
	}
    //释放
    free(p);
    p = NULL;//及时置空
    ptr = NULL;//最好这样,防止把ptr误用
	return 0;
}

注意:

p需要及时置为空指针。当对p进行释放时,p对应的空间被置为随机值,但是p本身的地址还没有改变。这是很麻烦的,万一有人不知道,又使用了之前开辟的空间,这块空间我们已经还给操作系统无法使用了,这时访问了就属于非法访问,所以要及时置为空指针,让它无法被访问。

image-20220924095546648

设想一下,如果我们把p释放了,但是没有置空,那它是不是个野指针,对应着指针的指向被释放。野指针很危险,现在拿NULL把它限制住了,我们不就安全了?

但是这里最好把ptr也置为空指针,因为ptr当前指向了不属于我们当前程序的空间,为防止误用,还是置空。但是ptr不用free,因为ptr指向的空间不是我们动态开辟的。

2.2.3 总结

void* free (void* ptr);
  • ptr是值为动态内存开辟的起始地址的指针。

  • free释放的是动态开辟的指针ptr指向的空间,而不是ptr本身,指针需指向开辟空间的首地址处。

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

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

  • free释放空间后,需要将ptr置为空指针,防止野指针问题(指针指向空间被释放),造成非法访问。

2.3 calloc

倘若我们已经有了明确的目的我们要开辟多大的空间,类型是什么。那么我们就可以使用calloc函数。

calloc和malloc一样,也是由C语言提供,用来动态内存分配:

void* calloc (size_t num, size_t size);

calloc也是动态内存开辟空间的一个函数,参数num为开辟空间的元素个数,参数size为开辟空间元素的大小,单位是字节。函数返回值为void*,开辟成功返回开辟空间的地址,失败返回NULL空指针。

2.3.1 calloc的使用

和malloc一样,calloc返回值也是void*,所以我们在使用时需要强制类型转换。

例如开辟一个40字节,用来存储整形的空间:

int main()
{
    int* p = (int*)calloc(10, sizeof(int));
    if (p == NULL)
    {
        perror("calloc");//打印错误信息
        return 1;
    }
    int i = 0;
    //使用
    for (i = 0; i < 10; i++)
    {
     	*(p + i) = i;//不改变指向   
    }
    //释放
    free(p);
    p = NULL;
    return 0;
}

calloc开辟空间失败也会返回NULL,所以需要判断。并且需要释放开辟的空间,这里由于p并没有改变指向,p还是指向原来的位置,所以直接释放p置空即可。

2.3.2 malloc和calloc的区别

  1. malloc传参时直接传递开辟空间的大小,calloc传参时传元素个数和元素的大小。
  2. malloc开辟的空间默认值为随机值,calloc开辟的空间默认值为0。

image-20220923175144448

calloc相当于把开辟的空间每个元素设置为0,然后返回起始地址。相当于calloc = malloc + memset(内存设置为0)。

总结:

开辟的空间需要初始化,使用calloc,不需要初始化,使用malloc。但是malloc不初始化效率会更高,calloc效率较malloc会比较低。

2.3.3 总结

void* calloc (size_t num, size_t size);
  • num是开辟空间的元素个数,size为开辟空间元素的大小。
  • 函数的功能是开辟num个大小为size的空间,并且把空间的每个字节初始化为0.
  • 与函数malloc的区别在于calloc会在返回地址之前把申请空间的每个字节初始化为全0。

2.4 realloc

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

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

void* realloc (void* ptr, size_t size);

realloc是调整动态开辟内存大小的函数,ptr为指向动态内存开辟空间的指针,size为调整过后这块空间的大小,单位是字节。函数返回值为void*,调整成功返回指向调整之后的内存块,失败返回NULL空指针。

2.4.1 realloc调整空间的两种情况

  1. 当前内存空间大小充足,则跟着原先开辟的空间继续向后开辟,返回原来的空间的起始地址。

image-20220924110627681

  1. 当前内存空间大小不够,重新寻找内存,单独开辟一块全新的空间,空间大小满足调整大小。将原先空间的数据先拷贝到当前空间,再释放掉原先的空间,返回新开辟空间的起始地址。

image-20220924111537777

  1. realloc调整后的空间比原先空间小,直接在原先空间的基础上缩短空间大小,返回原来空间的起始地址。

2.4.2 realloc的使用

对于realloc调整内存,还是要着重强调一下前两种情况:

  1. 内存足够在原有内存之后追加空间,返回原先空间的起始地址。
  2. 内存不足重新开辟调整大小的空间,先拷贝数据,在释放原先空间,返回新空间起始地址。

例如,一个realloc的正常使用:

int main()
{
    //动态开辟
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 1;
	}
	//使用
	int  i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	//打印
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	//增加空间
	int* ptr = (int*)realloc(p, 80);
    //判断
	if (ptr != NULL)
	{
		p = ptr;
		ptr = NULL;//防止ptr误使用
	}
	//扩容后使用
	for (i = 10; i < 20; i++)
	{
		*(p + i) = i;
	}
    //释放
	free(p);
	p = NULL;
	return 0;
}

这里有几个注意点,需要重点提一下。

2.4.2.1 注意点 1

一定要接收realloc的返回值。

首先,得了解函数调整内存的情况。不要不知所云就认为realloc不管什么情况都是以原先空间的基础上向后延伸.

一定要返回值接收,否则当开辟空间足够大,返回新空间的地址时,如果我们不用返回值接收,就像这样:

int main()
{
	int* p = (int*)malloc(8000);
	if (p == NULL)
	{
		return 1;
	}
	//使用
	int  i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	realloc(p, 80);//无返回值接收
	//扩容后使用
	for (i = 10; i < 20; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

运行一下:

image-20220924115541942

分析:

程序直接奔溃了,因为realloc调整空间时,发现空间不足,只能找一块全新的位置开辟,将原先的空间释放掉了,而我们并没有用返回值接收调整p,那么就用p非法访问了内存。

2.4.2.2 注意点 2

用全新的指针接收realloc的返回值,而不是直接用动态开辟内存的指针接收。

我们知道realloc调整空间失败返回NULL空指针。

如果将NULL赋给原先指向开辟空间的p指针。比如,p原本指向40个字节的空间,但是空间调整失败了,直接给我弄成了空指针。这不是偷鸡不成蚀把米嘛!连原本的空间都没了,你说realloc这个老六干的什么事情!

所以我们需要用一个全新的指针来接收,比如这样:

int* ptr = (int*)realloc(p, 80);

当然仅仅用返回值接收肯定不够,当然还要赋给我们之前的指针。当然在这时要对返回值做出判断,并且及时将ptr置空。因为ptr被赋值,以后这块空间就由先开始的指针进行管理并释放,为了保险起见,不让ptr影响p的操作,于是把ptr置空,防止误操作。

int main()
{
    int* p = (int*)malloc(40);
    if (p == NULL)
    {
        return 1;
    }
    int* ptr = (int*)realloc(p, 80);
    if (ptr == NULL)//判断
    {
        p = ptr;
        ptr = NULL;//置空
    }
    return 0;
}

2.4.2.3 注意点 3

当第一个参数为NULL空指针时,realloc起到和malloc/calloc一样的作用。

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

2.4.3 总结

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

  • size 是调整之后新大小

  • 返回值为调整之后的内存起始位置。

  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

  • realloc在调整内存空间的存在主要两种情况:

    • 情况1:原有空间之后有足够大的空间。
    • 情况2:原有空间之后没有足够大的空间。
  • 一定要接收realloc的返回值。

  • 用全新的指针接收realloc的返回值,而不是直接用动态开辟的指针接收。

  • ptr为NULL空指针时,realloc起到和malloc/calloc一样的作用。

2.5 malloc/calloc和free的问题

malloc和free的次数相同,不能开辟空间不释放,会造成内存泄漏。也不能多次释放,同样的对于calloc也是这样。

malloc/calloc不成对出现代码一定错误,但是malloc/calloc成对出现也可能写不出正确的代码。

举个例子:

int test()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		//...
		return 1;
	}
	//使用
	if (1)//某个条件满足
	{
		return 2;//条件满足返回
	}
	//释放
	free(p);//没有释放
	p = NULL;
	return 0;
}
int main()
{
	test();
	return 0;
}

分析:

这里malloc和free成对出现,但是由于满足条件,函数提前结束了,然后p指向的空间就没有释放,依然错误。

而p又是在函数中创建的,等函数结束,p也销毁,也并没有返回值来记住p,p在函数中指向的那块空间是被开辟的,但是出了函数就没人知道这块空间在哪里,这就造成了内存泄漏。

3. 常见的动态内存错误

动态内存虽然好用,但是使用不当就会让人十分苦恼,下面列出几个常见的错误。

3.1 对NULL指针进行解引用操作

int main()
{
	int* p = (int*)malloc(INT_MAX);
	if (p == NULL)//判断
	{
		perror("malloc");
		return 1;
	}
	else
	{
		*p = 5;
	}
	free(p);
	p = NULL;
	return 0;
}

分析:

如果开辟空间过大,malloc开辟空间失败,返回NULL空指针,这时对指针解引用操作,程序就会奔溃。

最好的方法是对p是否为空指针进行判断,如果为空指针则打印错误信息,并退出函数。

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

int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 20; i++)//把20当做元素个数了
	{
		*(p + i) = i;//严重越界
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

分析:

malloc开辟了20个字节的空间,但是我误以为20为元素个数,造成了严重的越界访问,导致程序奔溃。

一定要搞懂函数的意思,在对指针进行操作时候看清楚!!!

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

int main()
{
	int num = 10;
	int* p = &num;
	
	//...
	free(p);
	p = NULL;

	return 0;
}

分析:

平时创建的局部变量在栈空间上开辟,当作用域结束,变量会自动销毁。而free只作用于在堆区上开辟的空间,如果将平常开辟的内存进行释放,程序会奔溃。编译器会很凌乱,表示这届程序员真难带!!!

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

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 1;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*p = i;
		p++;//p改变了
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

分析:

p在使用过程中,进行了调整,p不再指向原来动态内存开辟的空间的起始位置。这块空间可能是动态开辟内存的一部分,也可能完全不适于开辟的空间。这时运行程序,程序依然会奔溃。

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

int main()
{
	int* p = malloc(40);
	if (p == NULL)
	{
		return 1;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}
	free(p);//已经释放过了
    //p = NULL//加上这个就不会奔溃
	//...继续写代码

	free(p);//忘记已经释放过了
	return 0;
}

分析:

当我们对一块动态内存进行释放后,接着写代码,然后忘记自己已经对这块空间进行释放。于是我们继续释放,当程序运行起来时,程序会奔溃。

要牢记一个malloc/calloc对应一个free。

如果我们在这里把p = NULL,就不会有问题了。因为free对空指针时不会操作的。

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

//函数会返回动态开辟空间的地址,记得在使用之后释放
int* get_memory()
{
	int* p = (int*)malloc(40);
	//...
	return p;
}
int main()
{
	int* ptr = getmemory();
	//使用

	//没释放
	return 0;
}

分析:

函数返回了动态内存开辟的空间,我们可以对其进行使用。但是一定要释放,否则就会出现内存泄漏,也就是"吃内存"的情况。

在我们设计这个函数时就应该写好相应注释,提醒使用者。使用者也应该养成良好的习惯,对动态内存开辟的空间进行释放。

4. 几个经典的笔试题

4.1 题目1

下列程序运行结果是什么?

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;
}

运行结果:

image-20220924215350281

分析:

程序奔溃了。这里有两个问题。

第一个问题:

GetMemory函数传参传str本身,p是str的一份临时拷贝。在函数中使用p开辟一块100个字节的动态内存的空间。也就相当于p改变了,但是str本身并没有改变。

回到Test函数中,str依然是NULL空指针。这时对str进行字符串拷贝。会对空指针进行解引用操作。程序奔溃。

值得一提的是这里的printf(str)并没有问题。可以通过一个简单的例子来证明:我们平时可以通过printf("hello")把hello打印出来,同样的我们也可以把字符串的首元素地址放入指针中,通过指针打印出字符串。因为printf("hello")是把h的地址传给了printf,这样打印没问题,那么我把其他部分省略,我的意思也是把地址传给printf,然后直接打印字符串。

第二个问题:

malloc开辟的空间没有释放。但是如果我们想释放也无法释放,因为在GerMemory函数中存放开辟空间地址的指针由于退出函数被释放了。返回Test函数后没人知道这块空间在哪里,也没法释放。

所以这个函数实际上是存在着很严重的问题的,所以我们接下来就将其改对。

正确写法:

  • 传址调用,直接改变str
void 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;
}

分析:

我们要改变str的值,那么就把str的地址传入。str是一级指针,那么&str就需要用二级指针接收。通过解引用,找到str,将动态开辟空间的首地址放入p中。在使用完之后对空间进行释放。

  • 参数无意义,返回值改变str
char* GetMemory(char* p)
{
    p = (char*)malloc(100);
    return p;
}
void Test(void)
{
    char* str = NULL;
    str = GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
    //释放
    free(str);
    str = NULL;
}
int main()
{
    Test();
    return 0;
}

分析:

这种写法也行。但是这里的参数其实没有实际的意义,我完全可以省略参数,在函数体内创建变量,开辟动态空间,然后返回起始地址。这种方法也行,但是我不是很推荐。

以上两种方法运行结果:

image-20220924230758549

4.2 题目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;
}

运行结果:

image-20220925123418176

分析:

str里存放的是空指针,然后调用GetMemory函数,在函数中在栈空间上开辟一个数组,返回p(指向数组首元素位置)。但是出了函数,在函数中开辟的数组就会被销毁。当str接收返回值时就会接收被销毁空间的地址,当我对str进行打印时,这块空间已经还给操作系统了,这块空间可能被更改,也可能没更改。当前我们对其进行打印是乱码。

这就是典型的返回栈空间地址的问题!!!

一个小细节:

刚刚说返回函数栈空间的地址不对,那么这个函数对不对?

int test()
{
	int a = 10;
	return a;
}

int main()
{
	int ret = test();
	printf("%d\n", ret);
	return 0;
}

分析:

这个函数是完全正确的。当局部变量a返回时,会把a放到寄存器中,假设我们这个寄存器为eax,然后a销毁。再通过eax把返回值带回。

那么这个呢?

int* test()
{
	int a = 10;
	return &a;
}

int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

分析:

这就是典型的返回函数栈空间的地址。

我主函数中的p指向的空间a已经被释放,属于野指针,如果通过指针去访问,就是非法访问。

但是大家可能会有疑惑,那我这个运行结果怎么解释:

image-20220924235100478

这个其实是巧合。a所在的空间恰好没有被修改,如果我们坚信这个是对的,以后肯定是会翻车的!!!

如果我稍加改变,在打印*p之前打印一句话,例如这样:

int main()
{
	int* p = test();
    printf("hello\n");
	printf("%d\n", *p);
	return 0;
}

image-20220924235344336

这里仅仅是增加了一句话就改变了*p的值,这是为什么?

看过我之前函数栈帧博客的,可能好理解些,接下来简单说一下原理:

当我们调用test函数时,在main函数上方需要开辟test函数的函数栈帧。栈空间使用习惯是从高地址向低地址使用。首先在栈帧最下方开辟a变量所需空间,当返回时则将*p放入寄存器中,将值带回,test函数栈帧被销毁。这一时刻很巧,a的值也没有改变。但是如果我们在打印*p前再使用了printf函数来打印一句话。这个printf函数可能就会在原先被释放的test函数栈帧的基础上开辟栈帧空间,这时a空间中的数据可能就会被修改,这就是6的来源。

4.3 题目3

下列程序运行结果是什么?

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;
}

运行结果:

image-20220925003823533

分析:

str起始为NULL,将&str和开辟空间大小传给GetMemory函数,函数在内部通过*p找到str空间,将动态开辟空间的起始地址放入str中,在通过strcpy进行拷贝,拷贝也成功了,最后打印也没问题。

这个过程看似一气呵成,但是缺了释放动态开辟的空间!!!及时释放非常重要!!!

正确写法:

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;
}

运行结果:

image-20220925005214872

4.4 题目4

下列程序运行结果是什么?

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;
}

运行结果:

image-20220925182504291

分析:

malloc开辟了一块100个字节的空间,放入str中,strcpy也将"hello"放入了str中,然后我就对str空间进行释放了。但是我没有置为空指针。所以下面的if语句是会执行的,这时我使用了被释放的str,str为野指针,为非法访问。在对str进行strcpy将world放入str中,再进行打印。

虽然跑出了结果,但是它本质上是错的,只能说明编译器大意了,没有闪(doge)。我们还是要发挥主观能动性,自己发现错误,毕竟我们是程序员。

正确写法:

这个代码其实槽点挺多的,首先它释放空间后没有置空。其次它也没有开辟完空间就对str是否为空指针进行判断,所以我们不妨对它进行一个大整改。

我们在释放完空间之后直接将str置为空指针。让下面的if语句起到作用,就达到了我们原本的目的。

void Test(void)
{
	char* str = (char*)malloc(100);
	if (str == NULL)
	{
		return;
	}
	strcpy(str, "hello");
	free(str);
	str = NULL;

	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

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

运行结果:

image-20220925123734312

5. 结语

到这里,本篇博客就到此结束了。相信大家对动态内存管理也有了一定的了解。动态内存管理在C语言中是一块非常重要的知识,还是希望大家可以熟练掌握。

如果觉得anduin写的还不错的话,还请一键三连!如有错误,还请指正!

我是anduin,一名C语言初学者,我们下期见!

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-25 23:05:06  更:2022-09-25 23:05:22 
 
开发: 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年5日历 -2024/5/19 16:01:18-

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