一、了解文件操作
这一节需要讲解一个非常重要的板块:文件操作。
在当今网络时代,什么价值最高?那当然就是数据了。
现代黑客,大多都是以钱为出发点的,而最值钱的,就是目标系统里面的资料,比如微软的windows
操作系统源码,就是不公开的,如果能搞到这玩意,它卖出的价格,大部分人应该一辈子都不愁了。
当然,那几乎是不可能的,回到本文,上面的内容只是为了让你明白文件操作的重要性,因为所有操作系统保存数据的方式,基本都是通过文件。
那么什么是文件呢?想来大家都是用过word或者记事本的,没错,这些软件主要的作用就是操作文件:
同样的,我们用VS写代码的时候,同样是在操作文件,唯一不同的可能就是它们似乎长得不一样?
看起来不一样的原因,是因为当我们安装软件时,该软件会向我们的电脑注册指定的后缀名。
如果你看不到这些后缀,如上图的.txt
,.docx
等,是因为你没有开启电脑文件管理器的对应功能,可以参考这篇文章打开:程序员必懂的常识
然后我们可以来到设置中的应用->默认应用
,然后分别搜索.txt
与.docx
,就会发现它们有两个默认的打开软件:
在我这里,.txt
文件默认用记事本打开,.docx
默认用wps打开,其它文件也是同理,因为系统默认将指定后缀名的文件用特定软件打开,所以该后缀名文件的图标也会由该软件所指定。
这也是为什么当你点击这个文件,系统知道用什么软件打开的原因。
如果你将该后缀名的默认应用删除了,你再点击这个文件就会发现已经打不开了,会让你重新选择一个应用打开。
但说到底它也只是个文件而已,比如用记事本打开这个1.docx
文件其实也一样可以!
只不过出现的是乱码而已,那么为什么是乱码呢?
这是因为每个软件是通过特定格式向文件写入数据的,如果你不按这样的格式去读,就会是乱码。
比如一段数据为“YYDS”,熟悉网络用语的人可能会理解它为“永远的神”,但不熟悉网络用语的人完全可以理解它为“永远单身”。
这就产生歧义了,在计算机里面,用错误的方式读取文件数据,就会出现上图所示的乱码。
这也就是为什么有逆向工程师的存在,逆向相关文章可以查看逆向基础。
二、代码中操作文件
既然是操作文件,那就得有操作文件的方法,一般而言,我们对文件的需求无非就两个:读、写。
但对于文件而言,我们还需要两个必要的步骤:打开、关闭。
想来这应该也不难理解,就和你操作电脑一样,操作之前你总得先打开电脑,操作结束之后一般都会合上电脑,或者直接关闭电脑。
所以总结下来就三个步骤:
- 打开文件
- 读写操作
- 关闭文件
你所有对文件的操作,都应该在打开文件与关闭文件之间进行。
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"; //输出字符
}
运行结果:
然后找到我们代码中读写的1.txt文件:
打开文件1.txt:
就可以发现我们确实写进去了,下面开始解释上面各个操作的意思。
首先是代码中定义了一个宏,前面说过,一些函数被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 模式
该函数返回一个文件指针:FILE*
,那么你可能就要问了,这被叫做文件指针的东西有什么用?
它对于我们使用者来说,唯一的作用就是标识文件,说人话就是,以后我们对这个文件指针的任何操作,都基本等效于对该文件的操作。
如果打开文件失败的话,将返回NULL
,这也是一个宏,真实值其实就是个0,所以如果它的返回值不为0,就表示打开成功!
const char *wBuf="Hello world"; //要写入文件的字符串
fwrite(wBuf,1,12,f); //将wBuf指定的内容写入文件中
然后来到fwrite
函数,该函数的作用从名字就能看出来,它有三个参数
- 第一个是我们要写的内容,也就是字符串。
- 第二个是我们写的内容中单个元素的字节大小,因为我们要写入字符串,所以单个元素就是字符,占一个字节。
- 第三个是元素的个数,共有12个元素,你可能会问怎么看的?难道一个一个的数?数当然是可以的,但还有一个更好的方法,那就是将你的鼠标移到这个字符串身上,就会显示。
你可能会问这是怎么算的?明明只有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
。
也正因如此,当我们写入内容后,位置已经到字符串的末尾了,现在读,就是从文本结尾地方读,什么都读不到, 所以就需要手动设置位置。
该函数有三个参数:
- 第一个:文件指针,即
fopen
的返回值 - 第二个:偏移量,我们设置偏移量为
0
- 第三个:我们从哪里开始偏移,
SEEK_SET
表示从文件头开始偏移,同时还有SEEK_CUR
从当前位置偏移,SEEK_END
从文件末尾开始偏移。
通过设置SEEK_SET
并且偏移量为0,就可以将位置调整到文件开头,这样就能读出字符串了。
然后就是fread
函数,该函数的参数与fwrite
函数基本是一样的,只是从写变成读而已。
最后调用fclose
,就可以将文件关闭,然后再将读到的内容输出即可。
其中,fwrite
和fread
的返回值,都是写入与读出的实际字节数,这个以后可能会用到。
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
这些东西是什么玩意:
当你将鼠标放在它的上面时,就可以看到它被定义为了一个数字0
。
当然你可以直接右键这个宏,选择“速览定义”、或者它下面的那个“转到定义”,都可以直接查看定义它的源码:
不仅仅是宏,在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;
}