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

news2024/11/14 21:46:40

前言:

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

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


题目1:

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

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

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形参的非法访问,所以程序出错,不再向下执行。

图示:

问题归纳:

问题1: 

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

解决方案:

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

问题2:

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

解决方案:

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

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

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

//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<stdio.h>
#include<stdlib.h>
#include<string.h>

//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<stdio.h>

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指向的那块空间已经还给操作系统,所以造成非法访问内存,打印的就是烫烫……

图示:

问题归纳:

问题:

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

解决方案:

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

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

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

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

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

#include<stdio.h>

//创建一个静态局部数组变量,返回其首地址
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<stdio.h>

//创建一个常量字符指针,指向一个常量字符串,返回这个字符指针
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<stdio.h>
#include<stdlib.h>
#include<string.h>

//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<stdio.h>
#include<stdlib.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 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<stdio.h>
#include<stdlib.h>
#include<string.h>

//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<stdio.h>
#include<stdlib.h>
#include<string.h>

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<stdio.h>
#include<stdlib.h>
#include<string.h>

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++程序内存分配的区域:

内存图:

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

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

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

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

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

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

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

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

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

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/758074.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

福利!打造自己的ChatGPT聊天小程序,前后端代码全开源

简介 本文分享一个我前几个月实现的一个智能聊天系统小项目&#xff0c;包含了java后端&#xff0c;微信小程序端&#xff0c;web页面端三个子工程。 代码已经全部开源&#xff0c;地址放在了文末。 最近一年&#xff0c;chatGPT的火爆程度&#xff0c;已经不需要我再多说了…

使用docker简单创建一个python容器

/root/docker_python目录结构&#xff1a; . |-- demo | -- main.py -- docker-compose.ymlmain.py内容&#xff1a; # codingutf-8 # -*- coding: utf-8 -*-if __name__ __main__:print("hello world")docker-compose.yml内容&#xff1a; version: "3&q…

Spark高级特性

spark shuffle 中 map 和 reduce 是一个相对的概念&#xff0c;map是产生一批数据&#xff0c;reduce是接收一批数据&#xff0c;前一个任务是map&#xff0c;后一个任务是reduce。 hashShuffle&#xff1a;hash分组&#xff0c;一个task里面按hash值的不同&#xff0c;分到不…

7.Java 运算符

运算符分成以下几组 算术运算符关系运算符位运算符逻辑运算符赋值运算符其他运算符 1.算术运算符 public class Test {public static void main(String[] args) {int a 10;int b 20;int c 25;int d 25;System.out.println("a b " (a b) );System.out.print…

Gitlab 多重构建镜像上传私有 Harbor与 Dockerhub

文章目录 1. 预备条件2. 安装 docker2.1 安装 docker buidx2.2 docker 配置2.3 安装 Buildx2.4 安装模拟器 3. 安装 git4. 安装 gitlab5. 部署 gitlab-runner6. 搭建 harbor7. 开发应用8. 配置 BuildKit8.1 Registry mirror8.2 设置镜像仓库正式 9. 编写 .gitlabs-ci.yaml 1. 预…

Java Stream流对多个字段进行排序

谈起Java 8&#xff0c;不少熟悉它的人&#xff0c;都会知道有一个对我们帮助很大的新特性&#xff0c;没错&#xff0c;就是我们在项目中经常用到的stream&#xff0c;它对我们处理数据的过程中提供了很多的便利&#xff0c;而这边文章主要讲述stream的便利之一&#xff1a;对…

聊一聊Java抽象同步队列AQS

抽象同步队列AQS概述 AQS是锁的底层支持 AQS类图 由该图可以看到,AQS是一个FIFO的双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。其中Node中的thread变量用来存放进入AQS队列里面的线程;Node节点内部的SHARED用来标记该线程是获取共享资源时…

考核:QTableWidget开发[折叠/展开单元格QTableWidgetItem]

目录 效果要求一、功能概述二、功能三、关系FATable 表NTable 表CTable 表 实现infos.hmain.cppcomplextablewidget.hcomplextablewidget.cppschemedialog.hschemedialog.cpp 源码模糊知识点 效果 要求 一、功能概述 二、功能 三、关系 FATable 表 CREATE TABLE fatable (idF…

UE4从零开始制作数字孪生道路监测平台

UE4从零开始制作数字孪生道路监测平台 UE4集成Cesium for Unreal和WebSocket&#xff0c;后端使用NodeJs搭建服务器进行数据模拟和真实数据实时转发。 1&#xff1a;新建UE4项目并集成Cesium for Unreal Cesium for UE4插件解锁了虚幻引擎中的3D地理空间生态系统。通过将高精…

基于FPGA的softmax函数优化及实现

文章目录 前言优化方案测试数据产生及Matlab结果处理流程工程说明功耗与面积标准softmax函数功耗与面积总结前言 FPGA异构计算是一个趋势,在AI推理、深度学习中广泛使用FPGA进行加速,减小系统延迟。而AI推理中有一个组件被广泛使用,各种网络模型中都有其身影,那就是激活函…

【雕爷学编程】Arduino动手做(152)---BMI160 六轴陀螺仪模块2

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

走进Linux世界【三、Linux文件与路径】

第三章 Linux文件与路径 1、文件结构 ​ Windows和Linux文件系统区别 ​ 在windows平台下&#xff0c;打开“此电脑”&#xff0c;我们可以看到盘符分区 ​ 每个驱动器都有自己的根目录结构&#xff0c;这样形成了多个树并列的情形 ​ 但是在 Linux 下&#xff0c;我们是看…

创建和分析二维桁架和梁结构研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

03_008内存映射原理_虚拟内存区域vm_area_struct详解,和mmap系统钓调用完全分析

前言 上一个记录中的 虚拟地址里的虚拟内存区域没有说的很完全 这次补充一下 同时记录一些 物理地址空间 内存映射原理 最后直接通过进程使用函数完成虚拟空间到物理空间的映射 物理地址空间 物理地址是处理器在系统总线上看到的地址。使用RISC的处理器通常只实现一个物理地…

递归函数(详解+实战)

目录 递归函数介绍 递归函数的作用 案例&#xff1a;实现10以内阶乘 递归思想 递归的编写 斐波那契数列(实战) 循环实现 递归实现 递归函数介绍 递归函数是指在函数的定义中调用函数本身的过程。递归函数可以用于解决那些可以通过将大问题拆分为更小的相似子问题来解决的…

窗口看门狗 WWDG

窗口看门狗介绍 Q: 什么是窗口看门狗&#xff1f; A: 窗口看门狗用于监测单片机程序运行时效是否精准&#xff0c;主要检测软件异常(独立看门狗检测的是硬件异常)&#xff0c;一般用于需要精准检测&#xff08;独立看门狗不太精准&#xff09;程序运行时间的场合。 窗口看门狗…

MySQL数据库操作篇3(聚合函数分组查询)

通过MySQL提供的聚合函数&#xff0c;可以很方便的进行一些计算来辅助查询&#xff0c;所谓聚合函数就是将表中的数据统计后进行的某种处理 分组查询可以理解成将一张表按照某个属性分成多张表&#xff0c;属性值相同的在一张表里 比如说学生表&#xff0c;按照性别这个属性分组…

java+大数据实战 短链项目

一.前言 1.1课程大致包含技术 首先选这套课的目的是 包含了我所学的大部分技术 比如springboot ssm redis kafka flink clickhouse 等 1.2 外界客观原因 就业环境一般 目前来看暂时还没但是有后续潜在的毕业 或者 离职 1.3技术追求 个人的技术追求暂时是在技术总监 技术…

vue3功能实现

在vue2中&#xff0c;要实现一些方法&#xff08;增删改查&#xff09;一般都是写在一起的。如下图所示&#xff1a; 但是在vue3中&#xff0c;实现一个方法需要用到很多文件。 方法定义方法如下&#xff1a; export function classSign(phone: string) {return sign_reques…