0. 前置知识

0.1 文件的概念

文件/文件夹的概念是如何出现的?
内存中存放的数据在计算机关机后就会消失。要长久保存数据,就要使用硬盘、光盘、U 盘等设备。为了便于数据的管理和检索,引入了“文件”的概念。
一篇文章、一段视频、一个可执行程序,都可以被保存为一个文件,并赋予一个文件名。操作系统以文件为单位管理磁盘中的数据。成千上万个文件如果不加分类放在一起,用户使用起来显然非常不便,因此又引入了树形目录(文件夹)的机制。
文件的本质是什么?为什么有多种类别?
一般来说,文件可分为文本文件、视频文件、音频文件、图像文件、可执行文件等多种类别,这是从文件的功能进行分类的。从数据存储的角度来说,所有的文件本质上都是一样的,都是由一个个字节组成的,归根到底都是 0、1 比特串。不同的文件呈现出不同的形态(有的是文本,有的是视频等等),这主要是文件的创建者和解释者(使用文件的软件)约定好了文件格式。
所谓“格式”,就是关于文件中每一部分的内容代表什么含义的一种约定。例如,常见的纯文本文件(也叫文本文件,扩展名通常是“.txt”),指的是能够在 Windows 的“记事本”程序中打开,并且能看出是一段有意义的文字的文件。文本文件的格式可以用一句话来描述:文件中的每个字节都是一个可见字符的 ASCII 码。
读写文件的功能本质上是谁提供的?
在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。

0.2 文件和流I/O

文件和流 I/O(输入/输出)是指在存储媒介中传入或传出数据。
文件是一个由字节组成的有序的命名集合,它具有永久存储的性质。 在处理文件时,你将处理目录路径、磁盘、文件和目录名称。
是一个字节序列,可用于对后备存储(磁盘或内存)进行读取和写入操作。存在除文件流之外的多种流(如网络、内存和管道流)。

0.3 文件夹、文件的基本操作

C/C  /C#/Python基本文件操作对比分析 - 图1

1. C语言文件操作

2. C++文件操作

《C++ Primer》第8章 IO库
【C语言中文网】C++文件操作
C/C  /C#/Python基本文件操作对比分析 - 图2

2.1 C++标准库

C++语言并未定义任何输入输出(IO)语句,取而代之,包含了一个全面的标准库(standard library)来提供IO机制(以及很多其他设施):

  • iostream定义了用于读写流的基本类型
  • fstream定义了读写命名文件的类型
  • sstream定义了读写内存string对象的类型。

image.png
标准库使我们能忽略这些不同类型的流之间的差异,这是通过继承机制(inheritance)实现的。

  • 类型 ifstream 和 istringstream 都继承自 istream。因此,我们可以像使用 istream对象一样来使用 ifstream和 istringstream对象。也就是说,我们是如何使用cin的,就可以同样地使用这些类型的对象。
  • 类似的,类型ofstream和ostringstream都继承自ostream。因此,我们是如何使用cout的,就可以同样地使用这些类型的对象。

C/C  /C#/Python基本文件操作对比分析 - 图4
IO对象的特性不能拷贝或赋值:

  1. 不能作为函数参数、返回值。
  2. 进行IO操作的函数通常以引用方式传递和返回流。
  3. 读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。

    2.2 使用文件流对象

    头文件fstream定义了三个类型来支持文件IO:ifstream从一个给定文件读取数据,ofstream向一个给定文件写入数据,以及fstream可以读写给定文件。
    我们可以用IO运算符(<<和>>)来读写文件,可以用getline从一个ifstream读取数据。
    除了继承自iostream类型的行为之外,fstream中定义的类型还增加了一些新的成员来管理与流关联的文件:
    image.png
    当我们想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来。
    每个文件流类都定义了一个名为open的成员函数,它完成一些系统相关的操作,来定位给定的文件,并视情况打开为读或写模式。
    如果调用open失败,failbit会被置位。因为调用open可能失败,进行open是否成功的检测通常是一个好习惯。
    1. #include<fstream>
    2. int main(){
    3. ofstream out;
    4. out.open("test.txt");
    5. if(out){
    6. //文件打开成功
    7. }
    8. return 0;
    9. }
    一旦一个文件流已经打开,它就保持与对应文件的关联。为了将文件流关联到另外一个文件,必须首先关闭已经关联的文件。当一个fstream对象离开其作用域时,与之关联的文件会自动关闭(当一个fstream对象被销毁时,close会自动被调用)。

    2.3 文件模式

    每个流都有一个关联的文件模式(file mode),用来指出如何使用文件。
    image.png
    每个文件流类型都定义了一个默认的文件模式,当我们未指定文件模式时,就使用此默认模式。与ifstream关联的文件默认以in模式打开;与ofstream关联的文件默认以out模式打开;与fstream关联的文件默认以in和out模式打开。
    以out模式打开文件会丢弃已有数据。默认情况下,当我们打开一个ofstream时,文件的内容会被丢弃。阻止一个ofstream清空给定文件内容的方法是同时指定app模式或in模式:
    image.png
    每次调用open时都会确定文件模式。对于一个给定流,每当打开文件时,都可以改变其文件模式。

    2.4 处理输入输出错误

    由于流可能处于错误状态,因此代码通常应该在使用一个流之前检查它是否处于良好状态。确定一个流对象的状态的最简单的方法是将它当作一个条件来使用。while循环检查>>表达式返回的流的状态。如果输入操作成功,流保持有效状态,则条件为真。
    1. while(cin>>word)
    2. {
    3. //读操作成功
    4. }
    发生输入输出错误的可能情况是无限的!但 C++ 将所有可能的情况归结为四类,称为流状态(stream state)。每种流状态都用一个 iostate 类型的标志位来表示。
标志位 意义
badbit 发生了(或许是物理上的)致命性错误,流将不能继续使用。
eofbit 输入结束(文件流的物理结束或用户结束了控制台流输入,例如用户按下了 Ctrl+Z 或 Ctrl+D 组合键。
failbit I/O 操作失败,主要原因是非法数据(例如,试图读取数字时遇到字母)。流可以继续使用,但会设置 failbit 标志。
goodbit 一切止常,没有错误发生,也没有输入结束。

一旦流发生错误,对应的标志位就会被设置,我们可以通过下表列出的函数检测流状态。

检测函数 对应的标志位 说明
good() goodbit 操作成功,没有发生任何错误。
eof() eofbit 到达输入末尾或文件尾。
fail() failbit 发生某些意外情况(例如,我们要读入一个数字,却读入了字符 ‘x’)。
bad() badbit 发生严重的意外(如磁盘读故障)。
  1. //【例1】
  2. int i = 0;
  3. cin >> i;
  4. if(!cin){ //只有输入操作失败,才会跳转到这里
  5. if(cin.bad()){ //流发生严重故障,只能退出函数
  6. error("cin is bad!"); //error是自定义函数,它抛出异常,并给出提示信息
  7. }
  8. if(cin.eof()){ //检测是否读取结束
  9. //TODO:
  10. }
  11. if(cin.fail()){ //流遇到了一些意外情况
  12. cin.clear(); //清除/恢复流状态
  13. //TODO:
  14. }
  15. }
  16. //【例2】
  17. //从 ist 中读入整数到 v 中,直到遇到 eof() 或终结符
  18. void fill_vector(istream& ist, vector<int>& v, char terminator){
  19. for( int i; ist>>i; ) v.push_back(i);
  20. //正常情况
  21. if(ist.eof()) return; //发现到了文件尾,正确,返回
  22. //发生严重错误,只能退出函数
  23. if (ist.bad()){
  24. error("cin is bad!"); //error是自定义函数,它抛出异常,并给出提示信息
  25. }
  26. //发生意外情况
  27. if (ist.fail()) { //最好清除混乱,然后汇报问题
  28. ist.clear(); //清除流状态
  29. //检测下一个字符是否是终结符
  30. char c;
  31. ist>>c; //读入一个符号,希望是终结符
  32. if(c != terminator) { // 非终结符
  33. ist.unget(); //放回该符号
  34. ist.clear(ios_base::failbit); //将流状态设置为 fail()
  35. }
  36. }
  37. }

2.5 close和flush

对于打开的文件,要及时调用 close() 方法将其关闭,否则很可能会导致读写文件失败。

  1. destFile << url;
  2. //程序抛出一个异常
  3. throw "Exception";
  4. //关闭打开的 out.txt 文件
  5. destFile.close();

第 3行会导致文件写入操作失败。执行此程序,会生成 out.txt 文件,但url 字符串并没有成功被写入。
使用 flush() 方法及时刷新输出流缓冲区,也能起到防止写入文件失败的作用。

  1. destFile << url;
  2. //刷新输出流缓冲区
  3. destFile.flush();
  4. //程序抛出一个异常
  5. throw "Exception";
  6. //关闭打开的 out.txt 文件
  7. destFile.close();

2.6 文本文件和二进制读写

1) 以文本形式读/写文件:就是直白地将文件中存储的字符(或字符串)读取出来,以及将目标字符(或字符串)存储在文件中。
2) 以二进制形式读/写文件:操作的对象不再是打开文件就能看到的字符,而是文件底层存储的二进制数据。
C++ 标准库中,提供了 2 套读写文件的方法组合,分别是:
使用 >> 和 << 读写文件:适用于以文本形式读写文件;
使用 read() 和 write() 成员方法读写文件:适用于以二进制形式读写文件。

write方法写入二进制数据

将内存中 buffer 指向的 count 个字节的内容写入文件。

  1. ostream & write(char* buffer, int count);

buffer 用于指定要写入文件的二进制数据的起始位置;count 用于指定写入字节的个数。同时,该方法会返回一个作用于该函数的引用形式的对象。举个例子,obj.write() 方法的返回值就是对 obj 对象的引用。
需要注意的一点是,write() 成员方法向文件中写入若干字节,可是调用 write() 函数时并没有指定这些字节写入文件中的具体位置。事实上,write() 方法会从文件写指针指向的位置将二进制数据写入。所谓文件写指针,是是 ofstream 或 fstream 对象内部维护的一个变量,文件刚打开时,文件写指针指向的是文件的开头(如果以 ios::app 方式打开,则指向文件末尾),用 write() 方法写入 n 个字节,写指针指向的位置就向后移动 n 个字节。

  1. #include <iostream>
  2. #include <fstream>
  3. using namespace std;
  4. class CStudent
  5. {
  6. public:
  7. char szName[20];
  8. int age;
  9. };
  10. int main()
  11. {
  12. CStudent s;
  13. ofstream outFile("students.dat", ios::out | ios::binary);
  14. while (cin >> s.szName >> s.age)
  15. outFile.write((char*)&s, sizeof(s));
  16. //s的地址就是要写入文件的内存缓冲区的地址,但 &s 不是 char * 类型,因此要进行强制类型转换;
  17. outFile.close();
  18. return 0;
  19. }

read方法读取二进制数据

从文件中读取 count 个字节的数据。

  1. istream & read(char* buffer, int count);

buffer 用于指定读取字节的起始位置,count 指定读取字节的个数。同样,该方法也会返回一个调用该方法的对象的引用。
和 write() 方法类似,read() 方法从文件读指针指向的位置开始读取若干字节。所谓文件读指针,可以理解为是 ifstream 或 fstream 对象内部维护的一个变量。文件刚打开时,文件读指针指向文件的开头(如果以 ios::app 方式打开,则指向文件末尾),用 read() 方法读取 n 个字节,读指针指向的位置就向后移动 n 个字节。因此,打开一个文件后连续调用 read() 方法,就能将整个文件的内容读取出来。

  1. #include <iostream>
  2. #include <fstream>
  3. using namespace std;
  4. class CStudent
  5. {
  6. public:
  7. char szName[20];
  8. int age;
  9. };
  10. int main()
  11. {
  12. CStudent s;
  13. ifstream inFile("students.dat",ios::in|ios::binary); //二进制读方式打开
  14. if(!inFile) {
  15. cout << "error" <<endl;
  16. return 0;
  17. }
  18. //read()方法会一直读取到文件的末尾,将所有字节全部读取完毕,while 循环才会终止。
  19. while(inFile.read((char *)&s, sizeof(s))) {
  20. cout << s.szName << " " << s.age << endl;
  21. }
  22. inFile.close();
  23. return 0;
  24. }

get和put方法读取和写入字符

  1. #include <iostream>
  2. #include <fstream>
  3. using namespace std;
  4. int main()
  5. {
  6. char c;
  7. //以二进制形式打开文件
  8. ofstream outFile("out.txt", ios::out | ios::binary);
  9. if (!outFile) {
  10. cout << "error" << endl;
  11. return 0;
  12. }
  13. while (cin >> c) {
  14. //将字符 c 写入 out.txt 文件
  15. outFile.put(c);
  16. }
  17. outFile.close();
  18. return 0;
  19. }

操作系统在接收到 put() 方法写文件的请求时,会先将指定字符存储在一块指定的内存空间中(称为文件流输出缓冲区),等刷新该缓冲区(缓冲区满、关闭文件、手动调用 flush() 方法等,都会导致缓冲区刷新)时,才会将缓冲区中存储的所有字符“一股脑儿”全写入文件。

  1. #include <iostream>
  2. #include <fstream>
  3. using namespace std;
  4. int main()
  5. {
  6. char c;
  7. //以二进制形式打开文件
  8. ifstream inFile("out.txt", ios::out | ios::binary);
  9. if (!inFile) {
  10. cout << "error" << endl;
  11. return 0;
  12. }
  13. while ( (c=inFile.get())&&c!=EOF ) //或者 while(inFile.get(c)),对应第二种语法格式
  14. {
  15. cout << c ;
  16. }
  17. inFile.close();
  18. return 0;
  19. }

getline方法读取一行字符串

  1. #include <iostream>
  2. #include <fstream>
  3. using namespace std;
  4. int main()
  5. {
  6. char c[40];
  7. //以二进制模式打开 in.txt 文件
  8. ifstream inFile("in.txt", ios::in | ios::binary);
  9. //判断文件是否正常打开
  10. if (!inFile) {
  11. cout << "error" << endl;
  12. return 0;
  13. }
  14. //从 in.txt 文件中读取一行字符串,最多不超过 39 个,遇到\n停止
  15. inFile.getline(c, 40);
  16. //遇到字符c就会停止
  17. inFile.getline(c,40,'c');
  18. cout << c ;
  19. inFile.close();
  20. return 0;
  21. }

读取多行字符串

  1. #include <iostream>
  2. #include <fstream>
  3. using namespace std;
  4. int main()
  5. {
  6. char c[40];
  7. ifstream inFile("in.txt", ios::in | ios::binary);
  8. if (!inFile) {
  9. cout << "error" << endl;
  10. return 0;
  11. }
  12. //连续以行为单位,读取 in.txt 文件中的数据
  13. while (inFile.getline(c, 40)) {
  14. cout << c << endl;
  15. }
  16. inFile.close();
  17. return 0;
  18. }

2.7 移动和获取文件读写指针

在读写文件时,有时希望直接跳到文件中的某处开始读写,这就需要先将文件的读写指针指向该处再进行读写。
ifstream 类和 fstream 类有 seekg 成员函数,可以设置文件读指针的位置;
ofstream 类和 fstream 类有 seekp 成员函数,可以设置文件写指针的位置。
所谓“位置”,就是指距离文件开头有多少个字节。文件开头的位置是 0。

  1. istream & seekg (int offset, int mode);
  2. ostream & seekp (int offset, int mode);

mode 代表文件读写指针的设置模式,有以下三种选项:

  • ios::beg:让文件读指针(或写指针)指向从文件开始向后的 offset 字节处。offset 等于 0 即代表文件开头。在此情况下,offset 只能是非负数。
  • ios::cur:在此情况下,offset 为负数则表示将读指针(或写指针)从当前位置朝文件开头方向移动 offset 字节,为正数则表示将读指针(或写指针)从当前位置朝文件尾部移动 offset字节,为 0 则不移动。
  • ios::end:让文件读指针(或写指针)指向从文件结尾往前的 |offset|(offset 的绝对值)字节处。在此情况下,offset 只能是 0 或者负数。

此外,我们还可以得到当前读写指针的具体位置:
ifstream 类和 fstream 类还有 tellg 成员函数,能够返回文件读指针的位置;
ofstream 类和 fstream 类还有 tellp 成员函数,能够返回文件写指针的位置。

  1. int tellg();
  2. int tellp();

要获取文件长度,可以用 seekg 函数将文件读指针定位到文件尾部,再用 tellg 函数获取文件读指针的位置,此位置即为文件长度。
案例:在 students.dat 文件中用折半查找的方法找到姓名为 Jack 的学生记录,并将其年龄改为 20(假设文件很大,无法全部读入内存)

  1. #include <iostream>
  2. #include <fstream>
  3. #include <cstring>
  4. using namespace std;
  5. class CStudent
  6. {
  7. public:
  8. char szName[20];
  9. int age;
  10. };
  11. int main()
  12. {
  13. CStudent s;
  14. fstream ioFile("students.dat", ios::in|ios::out);//用既读又写的方式打开
  15. if(!ioFile) {
  16. cout << "error" ;
  17. return 0;
  18. }
  19. ioFile.seekg(0,ios::end); //定位读指针到文件尾部,
  20. //以便用以后tellg 获取文件长度
  21. int L = 0,R; // L是折半查找范围内第一个记录的序号
  22. // R是折半查找范围内最后一个记录的序号
  23. R = ioFile.tellg() / sizeof(CStudent) - 1;
  24. //首次查找范围的最后一个记录的序号就是: 记录总数- 1
  25. do {
  26. int mid = (L + R)/2; //要用查找范围正中的记录和待查找的名字比对
  27. ioFile.seekg(mid *sizeof(CStudent),ios::beg); //定位到正中的记录
  28. ioFile.read((char *)&s, sizeof(s));
  29. int tmp = strcmp( s.szName,"Jack");
  30. if(tmp == 0) { //找到了
  31. s.age = 20;
  32. ioFile.seekp(mid*sizeof(CStudent),ios::beg);
  33. ioFile.write((char*)&s, sizeof(s));
  34. break;
  35. }
  36. else if (tmp > 0) //继续到前一半查找
  37. R = mid - 1 ;
  38. else //继续到后一半查找
  39. L = mid + 1;
  40. }while(L <= R);
  41. ioFile.close();
  42. return 0;
  43. }

3. C#文件操作

文件系统和注册表(C# 编程指南)
在 .NET 中,System.IO 命名空间包含允许以异步方式和同步方式对数据流和文件进行读取和写入操作的类型。 这些命名空间还包含对文件执行压缩和解压缩的类型,以及通过管道和串行端口启用通信的类型。

3.1 文件和目录

下面是一些常用的文件和目录类:

  • File - 提供用于创建、复制、删除、移动和打开文件的静态方法,并可帮助创建 FileStream 对象。
  • FileInfo - 提供用于创建、复制、删除、移动和打开文件的实例方法,并可帮助创建 FileStream 对象。
  • Directory - 提供用于创建、移动和枚举目录和子目录的静态方法。
  • DirectoryInfo - 提供用于创建、移动和枚举目录和子目录的实例方法。
  • Path - 提供用于以跨平台的方式处理目录字符串的方法和属性。

    3.2 流

    抽象基类 Stream 支持读取和写入字节。 所有表示流的类都继承自 Stream 类。 Stream 类及其派生类提供数据源和存储库的常见视图,使程序员不必了解操作系统和基础设备的具体细节。
    流涉及三个基本操作:

  • 读取 - 将数据从流传输到数据结构(如字节数组)中。

  • 写入 - 将数据从数据源传输到流。
  • 查找 - 对流中的当前位置进行查询和修改。

根据基础数据源或存储库,流可能只支持这些功能中的一部分。 例如,PipeStream 类不支持查找。 流的 CanReadCanWriteCanSeek 属性指定流支持的操作。
下面是一些常用的流类:

  • FileStream - 用于对文件进行读取和写入操作。
  • IsolatedStorageFileStream - 用于对独立存储中的文件进行读取或写入操作。
  • MemoryStream - 用于作为后备存储对内存进行读取和写入操作。
  • BufferedStream - 用于改进读取和写入操作的性能。
  • NetworkStream - 用于通过网络套接字进行读取和写入。
  • PipeStream - 用于通过匿名和命名管道进行读取和写入。
  • CryptoStream - 用于将数据流链接到加密转换。

    3.3 读取器和编写器

    System.IO 命名空间还提供用于在流中读取和写入已编码字符的类型。 通常,流用于字节输入和输出。 读取器和编写器类型处理编码字符与字节之间的来回转换,以便流可以完成操作。 每个读取器和编写器类都与流关联,可以通过类的 BaseStream 属性进行检索。
    下面是一些常用的读取器和编写器类:

  • BinaryReaderBinaryWriter - 用于将基元数据类型作为二进制值进行读取和写入。

  • StreamReaderStreamWriter - 用于通过使用编码值在字符和字节之间来回转换来读取和写入字符。
  • StringReaderStringWriter - 用于从字符串读取字符以及将字符写入字符串中。
  • TextReaderTextWriter - 用作其他读取器和编写器(读取和写入字符和字符串,而不是二进制数据)的抽象基类。

    3.4 异步 I/O 操作

    3.5 压缩

    压缩是指减小文件大小以便存储的过程。 解压缩是提取压缩文件的内容以使这些内容采用可用格式的过程。 System.IO.Compression 命名空间包含用于对文件和流进行压缩或解压缩的类型。
    在对文件和流进行压缩和解压缩时,经常使用以下类:

  • ZipArchive - 用于在 zip 存档中创建和检索条目。

  • ZipArchiveEntry - 用于表示压缩文件。
  • ZipFile - 用于创建、提取和打开压缩包。
  • ZipFileExtensions - 用于创建和提供压缩包中的条目。
  • DeflateStream - 用于使用 Deflate 算法对流进行压缩和解压缩。
  • GZipStream - 用于采用 gzip 数据格式对流进行压缩和解压缩。

    4. Python读写文件