代码编织梦想

C语言学习之路

第一章 初识C语言
第二章 变量
第三章 常量
第四章 字符串与转义字符
第五章 数组
第六章 操作符
第七章 指针
第八章 结构体
第九章 控制语句之条件语句
第十章 控制语句之循环语句
第十一章 控制语句之转向语句
第十二章 函数基础
第十三章 函数进阶(一)(嵌套使用与链式访问)
第十四章 函数进阶(二)(函数递归)
第十五章 数组进阶
第十六章 操作符(详解及注意事项)
第十七章 指针进阶(1)
第十八章 指针进阶(2)
第十九章 指针进阶(3)
第二十章 指针进阶(4)
第二十一章 数据的存储(秒懂原、反、补码)
第二十二章 指针和数组笔试题详解(1)
第二十三章 指针和数组笔试题详解(2)
第二十四章 字符串函数(1)
第二十五章 字符串函数(2)
第二十六章 内存函数
第二十七章 自定义数据类型之结构体进阶(1)
第二十八章 自定义数据类型之结构体进阶(2)
第二十九章 动态内存管理(1)
第三十章 动态内存管理(2)



前言

前面一章中,我们主要讲解了动态内存管理相关的四个函数:malloc、realloc、calloc、free。本章节中,将针对前面的函数做一些笔试题,同时了解一下柔性数组的概念。


一、笔试题(一)

题目描述

void GetMemory(char *p)
{
 p = (char *)malloc(100);
}
void Test(void)
{
 char *str = NULL;
 GetMemory(str);
 strcpy(str, "hello world");
 printf(str);
}

上述test函数的运行结果是什么?

题目解答

上述代码出现了一个非常严重的错误,
上述代码在运行时,出现了一个访问权限的错误。接下来我们仔细分析一下上面的代码。
在test函数中,我们采用值传递的方式调用了getmemory函数,所以我们并不是将str这个指针传入,而是将str的数据,即NULL进行了传入,即此时形参变量p接收了NULL。(这里不懂的话,请看:第十二章 函数基础),当我们明白了这个代码采用的是值传递的时候,这道题就迎刃而解了。因为str本身依旧是空指针,并没有发生改变,我们对空指针进行了解引用操作,必然会发生错误。

代码调整

那么我们如何将代码写对呢?

方法1:二级指针(地址传递)

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

方法2:修改返回值

char* GetMemory()
{
	char*p = (char*)malloc(100);
	assert(p!=NULL);
	return p;
}
void Test()
{
	char* str = NULL;
	str=GetMemory();
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str=NULL;
}
int main()
{
	Test();
	return 0;
}

二、笔试题(二)

题目描述

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

题目解答

在这里插入图片描述
我们发现,上面的代码虽然解决了题目一中的值传递问题,但是最终的打印结果依然是错误的。这是为什么呢?
在解决这个问题之前,我们首先要回忆一下堆区和栈区的知识。栈区中主要存储的是局部变量。而局部变量的生命周期比较短,当生命周期截至,即代码运行完整个大括号后,会对局部变量进行释放。这里也是同理,我们发现,getmemory函数中是在栈区开辟的内存,所以当这个函数执行结束的时候,这个函数中的局部变量就会被释放。所以存储的字符串也会被释放掉。当这段空间被释放掉后,这段空间依然存在,其地址也不会发生改变,也就是说,我们依旧可以通过一个相同的地址找到这块空间,但是这段空间中存储的内容将是一堆乱码。
因此,当我们将这段空间的地址返回时,我们利用str指针接收,此时打印出来的就是这段空间被释放后所放置的乱码。

代码调整

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
char* GetMemory()
{
	char* p = (char*)malloc(40);
	assert(p != NULL);

	return p;
}
void Test()
{
	char* str = NULL;
	str = GetMemory();
	strcpy(str, "hello world.");
	printf(str);
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

三、笔试题(三)

题目描述

void GetMemory(char **p, int num)
{
 *p = (char *)malloc(num);
}
void Test(void)
{
 char *str = NULL;
 GetMemory(&str, 100);
 strcpy(str, "hello");
 printf(str);
}

题目解答

在这里插入图片描述
我们发现这段代码运行后,最终都得到了我们想要的结果,但是上面的代码就一定正确码?我们发现在我们修改第一二题目的时候,我们有意地释放了动态开辟的内存,同时将指向被释放的动态开辟内存空间的指针设置为空指针。那么这段代码恰恰就缺少了释放动态开辟空间的步骤。所以造成了内存泄漏问题,很多人会想,这段代码很短,当代码执行结束后,系统同样会释放堆区中未被释放的空间,用不着我们释放。但是,在较大工程中,倘若我们每次开辟都不释放,那么内存泄漏是相当严重的,所以我们每次在堆区开辟内存后,都要及时释放。

代码修改

#include<stdio.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);
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

四、笔试题(四)

题目描述

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

题目解答

在这里插入图片描述
我们发现这道题,及时的释放了开辟的空间,但是这道题释放的太过着急,以至于后面依旧通过str指针访问了这段空间。这是一个非常严重和经典的问题,野指针的解引用。
那么这道题,想考察的其实是,当我们将一段空间后释放后,一定要将指向这段空间的指针设置为空指针。
那么问题又来了,虽然出现了野指针解引用的问题,但是编译器依旧编译出了程序结果。但是这不代表这段代码本身没有问题,仅仅是因为编译器没有发现这个错误。

代码修改

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

五、柔性数组

1、什么是柔性数组?

C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。

2、柔性数组的使用

struct type_a
{
	char a;
	int b;
	int arr[];
};

struct type_b
{
	char a;
	int b;
	int arr[0];
};

上述两种柔性数组的方式都是成立的,只是在某些编译器中只有某个柔性数组的定义版本能够通过编译。
那么我们发现,在结构体中,柔性数组的大小是不确定,那么我们应该如何计算这个结构体的大小呢?

printf("%zu",sizeof(struct type_a));

在这里插入图片描述
我们发现,我们打印的结果是8。而利用我们之前学过的内存对齐,简单分析一下,如下图所示:
在这里插入图片描述

我们发现,这个数组的内存开辟并没有将柔性数组的内存大小算进结构体的大小。
那么我们该如何使用柔性数组呢?

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

struct type_a
{
	char a;
	int b;
	int arr[];
};

int main()
{
	struct type_a* ptr = (struct type_a*)malloc(sizeof(struct type_a) + (10 * sizeof(int)));
	struct type_a* p = NULL;
	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}
	else
	{
		p = ptr;
	}
	p->a = 'a';
	p->b = 18;
	for (int i = 0; i < 10; i++)
	{
		p->arr[i] = i;
	}

	free(p);
	p = NULL;
	ptr = NULL;
	return 0;
}

由于sizeof并不会计算柔性数组的大小,其实根本原因是,这个数组的大小是不确定的,所以没法计算。因此,在我们开辟这个空间的时候,需要在形参列表中主动写上,柔性数组所需的内存空间。

3、柔性数组的等效代码

我们发现,我们完全能够不使用柔性数组,然后达到柔性数组的作用。

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

struct type_a
{
	char a;
	int b;
	int* arr;
};

int main()
{
	struct type_a* ptr = (struct type_a*)malloc(sizeof(struct type_a));
	struct type_a* p = NULL;
	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}
	else
	{
		p = ptr;
	}
	int* pp = (int*)malloc(10 * sizeof(int));

	if (pp == NULL)
	{
		perror("malloc");
		return 1;
	}
	else
	{
		p->arr = pp;
	}

	p->a = 'y';
	p->b = 12;
	for (int i = 0; i < 10; i++)
	{
		p->arr[i] = i;
	}

	free(p);
	p = NULL;
	ptr = NULL;
	free(pp);
	pp = NULL;
	p->arr = NULL;

	return 0;
}

我们仔细分析一下上面的代码,我们发现,两者的作用完全等效,那么柔性数组又有什么好处呢?

4、柔性数组的好处

(1)减少内存碎片

我们发现,柔性数组的代码中,结构体和柔性数组所在的空间之间是连续的。但是等效代码中的二者的空间未必是连续的。如下图所示:
在这里插入图片描述
而连续存储的好处在于,减少了内存碎片的存在。那么什么叫内存碎片呢?如下图所示:
在这里插入图片描述
这些内存碎片是指,两段内存空间之间较小的内存区。这些内存区其实几乎不太可能会用到,因为我们开辟的空间一般较大,这些内存碎片不够存储,只能白白浪费。因此,柔性数组的存在就能够减少内存碎片的存在,从而提高内存的利用效率。

(2)提高访问速度

由于利用柔性数组开辟的空间是连续的,所以当我们遍历完前两个元素后,指针直接向后偏移就能够找到柔性数组。
但是,我们的等效代码开辟的空间中,遍历完前面两个元素后,还要不断地向后寻找数组的存储空间,浪费时间。

(3)方便释放内存空间

如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给
用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你
不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好
了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_72060925/article/details/127176458

现代操作系统 第三章 内存管理 习题_marshazheng的博客-爱代码爱编程_如果将fifo页面置换算法用到4个页框

Chapter03 第三章 内存管理 习题 知识点小记 当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作: 1、检查要访问的虚拟地址是否合法 2、查找/分配一个物理页 3、填充物理页内容(读取磁盘,或者直接置0,或者啥也不干) 4、建立映射关系(虚拟地址到物理地址)当发生缺页中断/缺页错误时,操作系统要找一个较少使用的页框,把它的内容写

第四章 内存空间管理---连续_吃饭睡觉打code的博客-爱代码爱编程

第4章 存储器管理 1 程序的装入和链接 2 连续分配存储管理方式 3 分页存储管理方式 4 分段存储管理方式 5 虚拟存储器、请求分页/分段、页面置换算法 2、连续分配方式 为一个用户程序分配一个连续的内存空间 20世

现代操作系统 第三章 内存管理 习题答案_dldldl1994的博客-爱代码爱编程

首先,需要使用特殊的硬件进行比较,而且必须快速,因为它用于每个内存引用。第二,使用4位密钥,一次只能在内存中存储16个程序(其中一个是操作系统)。 这是个意外。基址寄存器是16384,因为程序恰好加载在地址16384。它

《深入理解java虚拟机》第2章 Java内存区域与内存溢出异常-爱代码爱编程

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。 2.1 概述 https://blog.csdn.net/q5706503/article/details/84640762 对于从事C、C++程序开发的开发人员来说,在内存管理领域,他们既是拥有最高权力的“皇帝”又是从事最基础工作的“劳动

11 操作系统第三章 内存管理 内存的基本知识 内存管理 内存空间扩充 连续分配管理方式-爱代码爱编程

文章目录 1 内存概念1.1 内存作用1.2 逻辑地址VS物理地址1.3 装入的三种方式1.3.1 绝对装入1.3.2 可重定位装入1.3.3 动态重定位装入1.4 链接的三种方式1.4.1 静态链接1.4.2 装入时动态链接1.4.3 运行时动态链接1.5 内存的基础知识小结2 内存管理2.1 内存管理的任务2.2 内存保护的两种方法2.3 内存

计算机操作系统 电子科技大学 第三章:存储管理(测试1)-爱代码爱编程

计算机操作系统 电子科技大学 通关攻略 第一章第二章第三章第四章第五章考试操作系统概述2.1进程描述与控制存储管理(测试1)I/O设备管理文件系统期末考试2.2进程调度存储管理(测试2)2.3进程并发2.4死锁与饥饿1.内存管理的主要目的是( ) 编号选项A方便用户B增加内存物理容量C方便用户和提高内存利用率D提高内存利用率2.关于内存管理,下列叙述中

OS知识点汇总(考研用)——第三章:内存管理-爱代码爱编程

OS知识点汇总(考研用)——第三章:内存管理  本文参考于《2021年操作系统考研复习指导》(王道考研),《计算机操作系统教程》思维导图: 文章目录 OS知识点汇总(考研用)——第三章:内存管理3.内存管理3.1 内存管理概念 3.1.1 内存管理的基本原理和要求  1.程序装入和链接  2.逻辑地址空间与物理地址空间  3.内存保护 3.1.

操作系统习题整理 第三章 内存-爱代码爱编程

前言 参考王道考研复习指导 后续会进一步整理。 更新中。。。 跳过目录 目录 一、选择题待整理题目答案模板答案 一、选择题 1、外部碎片最严重的存储管理方式是(  )。   A. 固定分区  B. 可变分区  C. 分页  D. 分段 知识点:【】、【】 答案:B 解析:   在内

Android内存管理机制官方详解文档-爱代码爱编程

很早之前写过一篇《Android内存管理机制详解》点击量已7万+,现把Google官方文档整理输出一下,供各位参考。 一、内存管理概览 Android 运行时 (ART) 和 Dalvik 虚拟机使用分页和内存映射来管理内存。这意味着应用修改的任何内存,无论修改的方式是分配新对象还是轻触内存映射的页面,都会一直驻留在 RAM 中,并且无法换出。要从应用

正点原子的内存管理_正点原子【STM32-F407探索者】第四十二章 内存管理实验-爱代码爱编程

1)资料下载:点击资料即可下载 2)对正点原子Linux感兴趣的同学可以加群讨论:935446741 3)关注正点原子公众号,获取最新资料更新 上一章,我们学会了使用 STM32F4 驱动外部 SRAM,以扩展 STM32F4 的内存,加上 STM32F4 本身自带的 192K 字节内存,我们可供使用的内存还是比较多的。如果我们所用的内 存都像上

王道操作系统 第三章 内存管理-爱代码爱编程

第三章:内存管理 知识框架 3.1 内存管理概念 3.1.1 内存管理的概念 概念 内存管理 (MemoryManagement)是操作系统设计中最重要和最复杂的内容之一。操作系统对内存的划分和动态分配,就是内存管理的概念。 内存管理的功能 内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,使

【维生素C语言】第十三章 - 动态内存管理-爱代码爱编程

分手后还打电话骚扰?ptr=NULL解决!动态内存管理【C语言】 前言: 本章将讲解C语言动态内存管理,由浅到深的讲解动态内存管理。学习完本章后可以做一下动态内存分配的练习加深巩固,降低踩动态内存分配坑的概率: 🚪 传送门:动态内存分配笔试题题目+答案+详解) 一、动态内存分配 0x00 引入 📚 目前我们已经掌握了以下两种开辟

第三章 虚拟化概述(内存虚拟化)-爱代码爱编程

        对内存的访问都是通过处理器来转发的。为了唯一标识,处理器采用统一编址的方式将物理内存映射成为一个地址空间,即所谓的物理地址空间。我们把一根根内存条插到主板上的内存插槽中,每根内存条都需要被映射到物理地址空间中的某个位置。一般来说,每根内存插槽在物理地址空间的起始地址可以在主板制造时就固定下来,也可以通过某种方式由BIOS加电后自动配置。一旦

第4章 内存管理-爱代码爱编程

2-1 在可变式分区分配方案中,某一作业完成后系统收回其主存空间,并与相邻空闲区合并,为此修改空闲区表,造成空闲区数减一的情况是( D)。(2分) A.无上邻空闲区,也无下邻空闲区 B.有上邻空闲区,但无下邻空闲区 C.有下邻空闲区,但无上邻空闲区 D.有上邻空闲区,也有下邻空闲区 2-2 分区式存储器管理方式,每个程序(B )。(2分)

17.第二十三章.测试管理-爱代码爱编程

文章目录 23.1 测试基础23.2 软件测试技术23.3 信息系统测试管理 23.1 测试基础 1、软件测试模型V模型特点: 存在一定的局限性,它将测试过程作为在需求分析、概要设计、详细设计及编码之后的一个阶段,这样会导致需求分析或系统设计阶段隐藏的问题一直到后期的验收测试时才被发现,当在最后测试中发现这些需求错误时,可能已经很难再更改程序

【C++要笑着学】C++动态内存管理 | new/delete底层探索 | new/delete实现原理 | 定位new-爱代码爱编程

 ​​​​​​ 🤣 爆笑教程 👉 《C++要笑着学》 👈 火速订阅 🔥    本篇博客全站热榜排名:4 前言: 是这样的,C语言里的 "动态内存管理" 放到 C++ 里面,用起来不是那么爽,所以C++就对这一块进行了升级,本章我们就探索探索 C++的内存管理,顺便复习一下C语言里讲过的动态内存管理的知识。学完本章,单身的同学不用怕了,以后没有

操作系统王道考研复习——第三章(内存管理/存储器管理)_2024王道操作系统 3.2.8地址翻译-爱代码爱编程

操作系统王道考研复习——第三章(内存管理/存储器管理) 3 内存管理(存储器管理)3.1 内存管理概念3.1.1 内存管理的基本原理和要求1. 可执行程序的形成2. 程序的链接1)静态链接2)装入时动态链接3)

操作系统第三章:内存管理_对换区和文件区-爱代码爱编程

内存的基本知识 知识总览 内存作用 内存可存放数据。程序执行前需要先放到内存中才能被CPU处理——缓和CPU与硬盘之间的速度矛盾 补充单位 指令的工作原理 写代码之后,经过编译之后会形成指令,然后建立对