4. 文件操作基础与C++实现

一、了解文件操作

这一节需要讲解一个非常重要的板块:文件操作

在当今网络时代,什么价值最高?那当然就是数据了。

现代黑客,大多都是以钱为出发点的,而最值钱的,就是目标系统里面的资料,比如微软的windows操作系统源码,就是不公开的,如果能搞到这玩意,它卖出的价格,大部分人应该一辈子都不愁了。

当然,那几乎是不可能的,回到本文,上面的内容只是为了让你明白文件操作的重要性,因为所有操作系统保存数据的方式,基本都是通过文件。

那么什么是文件呢?想来大家都是用过word或者记事本的,没错,这些软件主要的作用就是操作文件:

image-20231128081839896

同样的,我们用VS写代码的时候,同样是在操作文件,唯一不同的可能就是它们似乎长得不一样?

看起来不一样的原因,是因为当我们安装软件时,该软件会向我们的电脑注册指定的后缀名。

如果你看不到这些后缀,如上图的.txt.docx等,是因为你没有开启电脑文件管理器的对应功能,可以参考这篇文章打开:程序员必懂的常识

然后我们可以来到设置中的应用->默认应用,然后分别搜索.txt.docx,就会发现它们有两个默认的打开软件:

image-20231128082043379

image-20231128082203473

在我这里,.txt文件默认用记事本打开,.docx默认用wps打开,其它文件也是同理,因为系统默认将指定后缀名的文件用特定软件打开,所以该后缀名文件的图标也会由该软件所指定。

这也是为什么当你点击这个文件,系统知道用什么软件打开的原因。

如果你将该后缀名的默认应用删除了,你再点击这个文件就会发现已经打不开了,会让你重新选择一个应用打开。

但说到底它也只是个文件而已,比如用记事本打开这个1.docx文件其实也一样可以!

image-20231128082443403

只不过出现的是乱码而已,那么为什么是乱码呢?

这是因为每个软件是通过特定格式向文件写入数据的,如果你不按这样的格式去读,就会是乱码。

比如一段数据为“YYDS”,熟悉网络用语的人可能会理解它为“永远的神”,但不熟悉网络用语的人完全可以理解它为“永远单身”。

这就产生歧义了,在计算机里面,用错误的方式读取文件数据,就会出现上图所示的乱码。

这也就是为什么有逆向工程师的存在,逆向相关文章可以查看逆向基础

二、代码中操作文件

既然是操作文件,那就得有操作文件的方法,一般而言,我们对文件的需求无非就两个:读、写。

但对于文件而言,我们还需要两个必要的步骤:打开、关闭。

想来这应该也不难理解,就和你操作电脑一样,操作之前你总得先打开电脑,操作结束之后一般都会合上电脑,或者直接关闭电脑。

所以总结下来就三个步骤:

  1. 打开文件
  2. 读写操作
  3. 关闭文件

你所有对文件的操作,都应该在打开文件关闭文件之间进行。

1.C语言方式

知道了基本的步骤,我们就可以开始写代码了。

首先还是老步骤,解决方案中新建一个day4的控制台空项目,将该项目设置为启动项,然后添加一个main.cpp的源文件,代码如下:

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
int main() {
	FILE *f = fopen("1.txt", "w+"); //用读写模式打开1.txt文件
	const char *wBuf="Hello world"; //要写入文件的字符串
	fwrite(wBuf,1,12,f); //将wBuf指定的内容写入文件中
	char rBuf[13];  //申请一块13字节的字符串数组,用于存字符

	fseek(f, 0, SEEK_SET); //将文件指针移动到文件头部

	fread(rBuf, 1, 12, f); //将文件中的字符读出来到rBuf中
	fclose(f);
	std::cout << rBuf << "\n"; //输出字符
}

运行结果:

image-20231128083222082

然后找到我们代码中读写的1.txt文件:

image-20231128083536449

打开文件1.txt

image-20231128083613234

就可以发现我们确实写进去了,下面开始解释上面各个操作的意思。

首先是代码中定义了一个宏,前面说过,一些函数被VS认为是不安全的,建议咱别用,但我们可以通过定义某些宏来告诉VS:我们就要用,你别管。

#define _CRT_SECURE_NO_WARNINGS

这些宏怎么得到的还记得吗?通过输出窗口或者错误列表窗口中的错误信息中得到,还不会的请参考第二章的内容。

首先就是打开文件,这里用到了函数fopen,还记得查看函数的参数快捷键吗?(Ctrl+Shift+空格

	FILE *f = fopen("1.txt", "w+"); //用读写模式打开1.txt文件

前面说过,一般函数都是有含义的,fopen后面的open很好理解,就是打开,而前面的f,是file的首字母,即文件打开的意思。

后面的操作函数名称都大同小异,可以自己体会一下,不再赘述。

fopen的第一个参数就是要打开的文件名,第二个参数是我们准备用什么方式打开这个文件,可我们咋知道用什么方式打开?

还是老话,用浏览器搜索,能自己总结搜索关键字了吗? C/C++ fopen 模式

image-20231128083955794

该函数返回一个文件指针:FILE* ,那么你可能就要问了,这被叫做文件指针的东西有什么用?

它对于我们使用者来说,唯一的作用就是标识文件,说人话就是,以后我们对这个文件指针的任何操作,都基本等效于对该文件的操作。

如果打开文件失败的话,将返回NULL,这也是一个宏,真实值其实就是个0,所以如果它的返回值不为0,就表示打开成功!

	const char *wBuf="Hello world"; //要写入文件的字符串
	fwrite(wBuf,1,12,f); //将wBuf指定的内容写入文件中

然后来到fwrite函数,该函数的作用从名字就能看出来,它有三个参数

  • 第一个是我们要写的内容,也就是字符串。
  • 第二个是我们写的内容中单个元素的字节大小,因为我们要写入字符串,所以单个元素就是字符,占一个字节。
  • 第三个是元素的个数,共有12个元素,你可能会问怎么看的?难道一个一个的数?数当然是可以的,但还有一个更好的方法,那就是将你的鼠标移到这个字符串身上,就会显示。

image-20231128084211312

你可能会问这是怎么算的?明明只有10个字符,或11个字符啊,咋来的12个?

10个字符的是因为,中间的空格也是一个字符,11个字符的是因为,每一个字符串后面都有一个0作为结尾,这是规定,0作为字符串的结尾,也算到长度里面,这样就是12个字符了

  • 第四个参数,就是文件标识符了,要对哪个文件进行写操作?也就是刚刚我们用fopen打开文件得到的返回值
	char rBuf[12];  //申请一块12字节的字符串数组,用于存字符

写操作完成之后,我们又申请了一个12字节长的字符数组,用于存放读出的数据。

然后我们还调用了一个fseek函数,该函数用于设置我们文件读写的位置。

fseek(f, 0, SEEK_SET); //将文件指针移动到文件头部

那么为什么要调用这个函数?

这我们就得建立一个概念,那就是位置,我们打开的文件,都有一个我们开始读写操作的位置,而这个位置一般在文件开头。

可一旦我们开始读或写,这个位置就会开始变化,保证我们能够顺序的读出内容。

比如Hello World,当写进去一个H,位置就后移动一个,这样你写下一个e时,才不会覆盖掉上次写的H

也正因如此,当我们写入内容后,位置已经到字符串的末尾了,现在读,就是从文本结尾地方读,什么都读不到, 所以就需要手动设置位置。

该函数有三个参数:

  1. 第一个:文件指针,即fopen的返回值
  2. 第二个:偏移量,我们设置偏移量为0
  3. 第三个:我们从哪里开始偏移,SEEK_SET表示从文件头开始偏移,同时还有SEEK_CUR从当前位置偏移,SEEK_END从文件末尾开始偏移。

通过设置SEEK_SET并且偏移量为0,就可以将位置调整到文件开头,这样就能读出字符串了。

然后就是fread函数,该函数的参数与fwrite函数基本是一样的,只是从写变成读而已。

最后调用fclose,就可以将文件关闭,然后再将读到的内容输出即可。

其中,fwritefread的返回值,都是写入与读出的实际字节数,这个以后可能会用到。

2.宏

前面无论是为了让VS不报错,还是fseek函数,我们都用到了宏,没错,要填入fseek第三个参数的三个选项,也叫做宏。

那么宏到底是个什么东西呢?为了解决这个问题,我们先来看一下这个例子:

#include<iostream>
int main() {
	std::cout << 3.1415926 * 30 * 30;
}

上面的式子相当简单,就是输出一下三个数字的乘积,如果你对数学中的Π熟悉的话,想来是知道这是算的半径为30的圆面积。

但如果对于不知道的人呢?或者说那些从来不记Π具体数字的人呢?这时候宏就发挥了作用,我们就可以将上面的代码改为下面的形式:

#include<iostream>
#define Π 3.1415926
int main() {
	std::cout << Π * 30 * 30;
}

现在就一目了然了。

对的,宏的作用就是替换,比如这里,我们用#define语句定义宏,将Π定义为3.1415926

于是当程序编译的时候,编译器就会找程序中所有存在Π的地方,然后将Π换成3.1415926

看到这里,大家有没有一种疯狂的想法,定义一个中文编程语言!比如就像这样:

#include<iostream>
#define 整数 int
#define 小数 double
#define 如果 if
int main() {
	整数 a=10;
	小数 b = 30.11;
	如果(a < b) {
		std::cout << "a<b";
	}
}

现在是不是就觉得宏这玩意相当的神奇?

当然,宏的相关用法还有很多,这里只是其中最简单的一个用法,更多用法可以参考文章

现在再回头看看SEEK_SET这些东西是什么玩意:

image-20231128084857220

当你将鼠标放在它的上面时,就可以看到它被定义为了一个数字0

当然你可以直接右键这个宏,选择“速览定义”、或者它下面的那个“转到定义”,都可以直接查看定义它的源码:

image-20231128085058942

不仅仅是宏,在vs中当你发现任何不认识的东西,都可以这样做,直到找到你认识的东西为止。

现在再来解决另外一个问题,就是我们一直禁止VS报错的宏,为什么后面什么都没写?

这就可以理解为它是一个空宏,也就是凡是遇到它的地方,就用空代替,虽然为空,但VS可以通过一些指令来检查到定没定义这个宏,一旦检测到它,就不会报错。

这里只是文件操作的基础内容,更多的可以参照另外一篇文章:文件操作

3.文件路径

上面fopen的第一个参数我们只填了文件名,那系统应该去哪里找这个文件呢?毕竟就算电脑只有一个C盘,下面都有这么多的目录。

事实上,所有的程序默认都有一个当前路径,一般来说就是.exe文件所在的位置。

比如当你直接点击生成的.exe文件,就会发现,生成的文件就在.exe文件旁边。

但在VS中运行时,源代码所在文件夹会作为默认文件夹,所以文件就会生成在源代码旁边。

而通过右键指定项目,从文件资源管理器中打开项目,就能快速打开文件项目所在的文件夹。

事实上这是在你不指定位置的情况发生的情况,如果你写成:

fopen("C:\\1.txt","w+");

那么文件就会创建到你所指定的位置。

这两种情况都有一个专有名词,相对路径绝对路径,感兴趣的可以看看这篇文章:路径详解

还有一点可能让你感到困惑,那就是我为什么要写两个 \ ?

这是因为\这个符号默认为转义字符,比如我们前面经常用到的换行符 \n,就是将n字母转义为换行的意思。

所以你只添加一个\ ,就不能正确识别,只有添加两个\,才会被当作1个\

4.自己封装文件操作类

通过前面的读写文件操作,我们可以发现,其实C语言读写文件并不方便,其中最麻烦的一点就是,有很多函数需要记住。

当然,只记住函数,其实也不算太难,最难受的是,还需要我们记住函数的参数,比如SEEK_SET等,忘了之后我怎么知道该填什么?

最常见的方式便是,用浏览器搜…或者自己写笔记,忘了的时候看,又或者直接问ChatGPT之类的大模型也可以。

为了解决这一问题,C++就出现了!

不过在使用C++操作文件之前,我们还需要弄明白一些知识点。

前面我讲过类的,大家还记得吧!当时我只说了类的两个特点:属性函数

为了更加深刻的理解C++标准库,我们先来自己封装一个文件操作类试一试!

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
class CFile {
public:
	FILE* file; //文件指针
	//打开文件的函数
	bool Open(const char* fileName,const char* mode) {
		file = fopen(fileName, mode);
		if (file == NULL) {
			return false;
		}
		return true;
	}

	//写数据的函数
	int Write(const char* buf, int size) {
		int byte = fwrite(buf, 1, size, file);
		return byte;
	}
	//读文件函数
	int Read(char* buf, int size) {
		int byte = fread(buf, 1, size, file);
		return byte;
	}
作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux