动态内存分配(2)——经典例题的讲解

前言:

        在前面我们已经学习动态分配内存,今天我们就来做一做它的几道经典例题,加深巩固我们所学的知识。

知识复习:动态内存管理(1)_从前慢,现在也慢的博客-CSDN博客


题目1:

        下面代码存在什么问题,请你指出问题并修改。

#include
#include
#include

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

分析:

        1、程序从main开始,运行之后调用Test函数;

        2、进入Test函数,①先创建了一个str的字符指针,并赋初值为NULL;②调用GetMemory函数,以值传递的方式将str传给GetMemory(值传递,形参只是实参的一份临时拷贝,形参的改变不影响实参);

        3、进入GetMemory函数,①p的初始值为NULL(值传递);②malloc在堆区申请开辟100个字节的连续空间,并将malloc的返回值赋值给p(①忘记判断malloc是否开辟成功;②没有自己free释放申请的动态内存,如果程序一直运行造成内存泄漏);因为函数类型是void,所以代码执行完,直接返回到test函数中的GetMemory调用;

        4、GetMemory调用完继续向下执行,调用strcpy函数,但是目标空间的实参为NULL(值传递形参不影响实参),造成了strcpy形参的非法访问,所以程序出错,不再向下执行。

图示:

动态内存分配(2)——经典例题的讲解_第1张图片

问题归纳:

问题1: 

       GetMemory是传值调用,str传给p的时候,p是str的临时拷贝,有自己独立的空间,当GetMemory函数内部申请完空间后,申请空间的起始地址放在p中,str依然是NULL。当GetMemory函数返回之后。执行strcpy函数拷贝时,strcpy的目标空间形参为NULL,造成非法访问内存。

解决方案:

        ①传址调用(形参影响实参);②通过函数返回值得到。

问题2:

        GetMemory函数内部只动态申请了内存,在Test函数中使用,①但是并没有将malloc的返回值传给Test,②在使用动态内存前没有判断开辟是否成功,③使用完并没有释放空间,如果程序一直运行,会内存泄漏。

解决方案:

        ①使用动态开辟的内存前,判断是否开辟成功;②使用完之后记得free释放。

代码修改1: 通过传址调用,可以间接的得到malloc开辟空间的起始地址,使用前判断是否开辟成功,使用完记得释放

#include
#include
#include

//GetMemory内部进行了malloc操作,在Test函数中使用这块空间,
//通过地址传递,可以得到malloc开辟空间的起始地址,记得在Test函数释放
void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}

void Test(void)
{
	char* str = NULL;
	//调用GetMemory,动态开辟100个字节的空间(传址调用,形参影响实参)
	GetMemory(&str);
	//判断malloc是否开辟成功
	if (NULL == str)
	{
		//开辟失败,打印错误信息,并退出
		perror("malloc");
		return ;
	}
	//将hello world拷贝到malloc开辟的空间
	strcpy(str, "hello world");
	//打印拷贝后的内容
	printf(str);
	//使用完动态开辟的内存,free释放
	free(str);
	//free不会改变str的值,防止非法访问内存,将其设置为NULL
	str = NULL;
}

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

代码修改2:通过函数的返回值得到malloc开辟空间的起始地址,使用完记得释放

#include
#include
#include

//GetMemory内部进行了malloc操作,在Test函数中使用这块空间,
//通过返回其malloc的返回值可以得到malloc开辟空间的起始地址,记得在Test函数释放
char* GetMemory()
{
	char* p = (char*)malloc(100);
	return p;
}

void Test(void)
{
	char* str = NULL;
	//调用GetMemory,动态开辟100个字节的空间
	str = GetMemory();
	//判断malloc是否开辟成功
	if (NULL == str)
	{
		//开辟失败,打印错误信息,并退出
		perror("malloc");
		return;
	}
	//将hello world拷贝到malloc开辟的空间
	strcpy(str, "hello world");
	//打印拷贝后的内容
	printf(str);
	//使用完动态开辟的内存,free释放
	free(str);
	//free不会改变str的值,防止非法访问内存,将其设置为NULL
	str = NULL;
}

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

题目2:

        下面代码存在什么问题,请你指出问题并修改。

#include

char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}

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

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

运行结果:

打印结果为什么不是hello world呢?(小知识:0xcccc打印就是烫)

分析:

        1、程序从main开始,运行之后调用Test函数;

        2、进入Test函数,①先创建一个字符指针变量str,并赋初值为NULL;②调用GetMemory函数,(因为形参为void,所以不传参),将其返回值赋值给str;

        3、进入GetMemory函数,定义了一个局部变量的数组(因为在栈区申请的空间),返回数组的数组名,即返回数组首元素地址(因为数组在该函数的函数栈帧创建,函数执行结束后会自动释放,所以返回了的数组首元素地址已经没有意义了,再通过该地址去访问空间就会造成非法访问内存问题。);函数执行完,直接回到Test函数中的GetMemory调用。

        4、GetMemory调用完将返回值赋值给str,继续向下执行,调用printf,但是传的实参str指向的那块空间已经还给操作系统,所以造成非法访问内存,打印的就是烫烫……

图示:

动态内存分配(2)——经典例题的讲解_第2张图片

问题归纳:

问题:

        return语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。(自动销毁就是把空间还给操作系统,空间返回给了操作系统,再使用空间就会造成非法访问内存,所以返回的指针或者引用就没有实际意义了。)。

解决方案:

        1、将局部变量改存在静态区(存在静态区的数据,创建好后,直到程序结束才释放):

               (1)关键字static修饰局部变量,被static修饰的局部变量,改变了局部变量的存放位置,静态局部变量存放在静态区。

                (2)使用字符指针指向常量字符串,常量字符串存储在静态区。

        2、将局部变量拷贝到堆区(存在堆区的数据,创建好后,①程序结束后系统回收;②free自己回收) 

代码修改1:关键字static修饰局部变量

#include

//创建一个静态局部数组变量,返回其首地址
char* GetMemory(void)
{
	//静态局部变量:存放在静态区,它的生命周期变长,直到程序结束才释放
	static char p[] = "hello world";
	//返回字符串的首地址
	return p;
}

void Test(void)
{
	char* str = NULL;
	//调用GetMemory函数,得到字符串的首地址
	str = GetMemory();
	//打印字符串
	printf(str);
}

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

代码修改2:使用字符指针指向常量字符串

#include

//创建一个常量字符指针,指向一个常量字符串,返回这个字符指针
const char* GetMemory(void)
{
	//常量字符串存储在静态区,直到程序结束才释放
	//常量字符串出现在表达式中,这个常量字符串的值就是首字符的地址
	const char* p = "hello world";
	//返回字符串的首地址
	return p;
}

void Test(void)
{
	const char* str = NULL;
	//调用GetMemory函数,得到字符串的首地址
	str = GetMemory();
	//打印字符串
	printf(str);
}

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

代码修改3:将局部变量拷贝到堆区

#include
#include
#include

//GetMemory内部进行malloc操作,并赋初值“hello world”,之后在Test中打印
//通过返回其malloc的返回值得到“hello world”的首地址,记得在Test中释放
char* GetMemory(void)
{
	
	char p[] = "hello world";
	//计算数组p的大小
	int sz = sizeof(p);
	//开辟sz个字节的空间
	char* ptr = (char*)malloc(sz);
	//判断是否开辟成功
	if (NULL == ptr)
	{
		//打印错误信息
		perror("malloc");
		exit(0);
	}
	//将数组p拷贝到动态内存中
	strcpy(ptr, p);
	//返回malloc的返回值
	return ptr;
}

void Test(void)
{
	char* str = NULL;
	//调用GetMemory函数,得到malloc的返回值
	str = GetMemory();
	//打印字符串
	printf(str);
	//释放
	free(str);
	str = NULL;
}

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

题目3:

        下面代码存在什么问题,请你指出问题并修改。

#include
#include
#include


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

分析:

        1、程序从main开始,运行之后调用Test函数;

        2、进入Test函数,①先创建一个字符指针变量str并为其赋初值为NULL;②调用GetMemory函数第一个参数为地址传递(形参影响实参,解引用可以间接操控实参,得到开辟空间的起始地址),第二个参数为值传递(形参不影响实参,决定开辟空间的大小);

        3、进入GetMemory函数,GetMemory函数内部开辟好空间,在Test中使用开辟的空间,记得释放,因为函数类型为void,所以执行完之后直接回到Test函数中的GetMemory调用;

        4、GetMemory调用完,继续向下执行,①调用strcpy将“hello”拷贝到malloc开辟的空间(使用malloc开辟的空间前完了判断是否开辟成功);②调用printf打印开辟空间的内容,打印完后直接回到main函数中(忘记使用完动态空间之后,free释放,如果程序一直运行,内存泄漏)。

问题归纳:

问题1:

        动态开辟的空间,使用前,忘记判断是否开辟成功,造成非法访问。

解决方案:

        使用前使用if语句判断其返回值是否为空指针。

问题2:

        动态开辟的空间,忘记了释放内存(如果程序一直运行),造成内存泄漏。

解决方案:

        1、动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则就会有错误。(使用完动态开辟的内存使用完一定要释放,并且释放完还要将指针设置为NULL,防止非法访问内存。)。

        2、使用动态开辟的空间,最好注释空间是否释放

代码修改:

#include
#include
#include

//GetMemory函数通过第一个参数间接返回开辟空间的地址,
// 通过第二个参数确定开辟空间的大小。
// 在GetMemory函数内部创建好空间,在Test中使用,记得释放空间
void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}

void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	//判断malloc是否开辟成功
	if (NULL == str)
	{
		return;
	}
	//使用
	strcpy(str, "hello");
	printf(str);
	//释放
	free(str);
	str = NULL;
}

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

题目4:

        下面代码存在什么问题,请你指出问题并修改。

#include
#include
#include

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

分析:

        1、从main开始,运行后调用Test函数;

        2、进入Test函数,①malloc动态申请100个字节的空间,并将返回值赋给str;②调用strcpy函数将“hello”拷贝到动态申请的空间中(使用前未判断malloc是否申请空间成功);③使用完free释放malloc的空间(free释放完空间之后,不会改变指针str的值,未将其设置为NULL,后面使用到str还能找到释放的那块空间,造成非法访问内存);④free释放完,str的值不为空,条件为真,执行if控制的语句,调用strcpy和printf,使用到了已经释放的空间,造成非法访问空间。

问题归纳:

问题1:

        动态开辟的空间,使用前,忘记判断是否开辟成功,造成非法访问。

解决方案:

        使用前使用if语句判断其返回值是否为空指针。

问题2:

        free释放了内存,却仍然继续使用它(free释放完内存,不会改变指针的指向),造成非法访问内存。

解决方案:

        free释放完内存,将指针设置为NULL,防止非法访问内存。

代码修改:

#include
#include
#include

void Test(void)
{
	char* str = (char*)malloc(100);
	//使用前判断是否开辟成功
	if (NULL == str)
	{
		return;
	}
	//使用
	strcpy(str, "hello");
	//释放
	free(str);
	//free释放并不会改变指针指向,防止非法访问内存,将其设置为NULL
	str = NULL;
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

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

加油站:C/C++程序内存分配的区域:

内存图:

动态内存分配(2)——经典例题的讲解_第3张图片

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

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

        2、堆区(heap):动态内存分配。一般由程序员分配释放,生存期由我们决定,非常灵活,但问题也最多。若程序员自己不释放,程序结束时可能由OS回收。分配方式类似于链表。

        3、数据段(静态区):内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,程序结束后由系统释放。例如全局变量、static变量。

        4、代码段(静态区):存放函数体(类成员函数和全局函数)的二进制代码。也可能包含一些只读的常数变量,如常量字符串。

在之前我们就学过了static关键字修饰的局部变量,现在我们重新回顾:

        1、普通的局部变量是放在栈区上的,这种局部变量进入作用域创建,出了作用域释放。

        2、但是局部变量被static修饰后,这种变量就放在静态区,放在静态区的变量,创建好后,直到程序结束才释放。

       3、本质上:static的修改改变了局部变量的存储位置,因为存储位置的差异,使得执行效果不一样。

        注意:被static修饰是不影响作用域的!!!但是生命周期发生了变化,变长了。

1、栈区:变量空间进入作用域创建,出作用域就释放。

2、堆区:变量空间由程序员自己决定,或者程序结束由系统回收。

3、静态区:变量空间创建好之后,直到程序结束才释放。

你可能感兴趣的:(C语言进阶,算法,c语言,c++)