C++重温笔记(十一): C++文件操作

1. 写在前面

c++在线编译工具,可快速进行实验: https://www.bejson.com/runcode/cpp920/

这段时间打算重新把c++捡起来, 实习给我的一个体会就是算法工程师是去解决实际问题的,所以呢,不能被算法或者工程局限住,应时刻提高解决问题的能力,在这个过程中,我发现cpp很重要, 正好这段时间也在接触些c++开发相关的任务,所有想借这个机会把c++重新学习一遍。 在推荐领域, 目前我接触到的算法模型方面主要是基于Python, 而线上的服务全是c++(算法侧, 业务那边基本上用go),我们所谓的模型,也一般是训练好部署上线然后提供接口而已。所以现在也终于知道,为啥只单纯熟悉Python不太行了, cpp,才是yyds。

和python一样, 这个系列是重温,依然不会整理太基础性的东西,更像是查缺补漏, 不过,c++对我来说, 已经5年没有用过了, 这个缺很大, 也差不多相当重学了, 所以接下来的时间, 重温一遍啦 😉

资料参考主要是C语言中文网光城哥写的C++教程,然后再加自己的理解和编程实验作为辅助,加深印象,当然有些地方我也会通过其他资料进行扩充。 关于更多的细节,还是建议看这两个教程。

今天开始学习文件操作, 内存中存放的数据在计算机关机后就消失了,如果想长期保存,就需要把数据保存到硬盘里面进行持久化, 为了便于管理和检索,就引入了"文件"的概念。为了便于分类,还引入了文件夹(目录)。 操作系统以文件为单位管理磁盘中的数据。 从文件的功能角度,文件大致可分为文本文件,视频文件,音频文件,图像文件,可执行文件等, 但从数据存储角度,他们本质上都一样, 都是由字节组成(0,1比特串)。不同的文件之所以呈现出不同形态(文本,视频,图像),是因为文件创建的时候就事先约定好了格式(每一部分代表什么的约定), 比如纯文本文件(每个字节是一个可见的ASCII码), 二进制文件(包括图像,视频,可执行文件)。这俩的区别本质上是格式上的区别。

文件的读写在实际使用中非常重要,毕竟我们往往需要把数据进行持久化,读取数据的时候也不能总是手动黑窗口输入,一般我们都是会通过文件的方式读取数据,然后再把最后的结果写入文件,所以不管学习哪一门语言,文件的读写一般都会涉及到,而这篇文章,就是想比较系统的把C++的文件读写操作过一遍。

主要内容:

  • C++的文件流类及用法
  • C++文件的打开及不同打开方式的区别
  • C++关闭文件方法
  • C++文本文件读写操作
  • C++二进制文件读写(read()和write())
  • C++get()和put()读写文件
  • C++ getline()从文件中读取一行
  • C++移动或获取文件读写指针
  • 小总

Ok, let’s go!

2. C++文件流类及用法

cin和cout通过重定向的方式可用于读取文件中的数据和写入数据到文件。其实,C++还提供了3个类用于实现文件操作,统称为文件流类:

  • ifstream: 专门从文件中读取数据
  • ofstream: 专用于向文件中写入数据
  • fstream: 既可以从文件中读取数据,又可以向文件中写入数据

根据流类的派生关系:
在这里插入图片描述
ifstream和fstrem类都是从istream中派生来的,所以ifstream类拥有istream类全部成员方法,同理ofstream和fstream也有用ostream类的全部成员方法。 即istream和ostream中提供的cin和cout的成员调用方法,同样适用于文件流。 比如常用的 operator <<()、operator >>()、peek()、ignore()、getline()、get() 等。

<iostream> 头文件中定义有 ostream 和 istream 类的对象 cin 和 cout 不同,<fstream> 头文件中并没有定义可直接使用的 fstream、ifstream 和 ofstream 类对象。因此,如果我们想使用该类操作文件,需要自己创建相应类的对象。

为啥C++标准库不提供现成的类似fin或者fout的对象? 这是因为文件输入流和文件输出流设备都是硬盘中文件, 而硬盘上很多文件, 到底使用哪一个没有办法写死。 所以C++标准库就把创建文件流对象的任务交给了用户。

fstream类拥有ifstream和ofstream类所有成员方法,常用的如下:
在这里插入图片描述
详细的看手册

读写文件的基本流程:

  1. 创建fstream类对象
  2. 调用open()成员方法将文件与文件流关联起来,就相当于把一个管子接到了文件上
  3. 向文件写入数据
  4. 调用close()成员方法关闭文件, 相当于拔掉管子

比如:

const char *s = "hello world";
// 第一步
fstream fs;   

// 第二步
fs.open("hello.txt", ios::out);

// 第三步
fs.write(s, 12);

// 第四步
fs.close()

3 C++ 打开文件

在对文件读写之前,要先打开文件,这样就相当于在程序和文件之间架起了桥梁, 数据就可以在桥梁上飞奔过去啦。哈哈,当然没有这么动感, 主要是下面两个目的:

  • 通过制定文件名,建立了文件与文件流对象的关联,后面要对文件进行操作时, 可以通过与之关联的流对象进行
  • 指明文件的使用方式。主要包括只读,只写,读写,文件末尾添加数据,以文本,二进制方式使用等

打开文件的两种常用方式:

  • 调用流对象的open成员函数
  • 定义文件流对象时,通过构造函数打开

3.1 open函数打开文件

open成员函数在ifstream, ofstream, fstream中皆有定义, 原型如下:

void open(const char* szFileName, int mode)

第一个参数是指向文件名的指针,第二个参数是文件打开模式标记。这个东西代表了文件的使用方式。主要有以下几种:

模式标记适用对象作用
ios::inifstream、fstream打开文件读取数据。如果文件不存在,在打开出错
ios::outofstream、fstream打开文件写入数据。如果文件不存在,则新建该文件,如果文件原来存在,打开时清除原来内容
ios::appofstream、fstream打开文件,在其尾部添加数据。如果文件不存在,则新建该文件
ios::ateifstream、fstream打开一个已有的文件,并将文件读指针指向文件末尾,如果文件不存在,报错
ios::truncofstream打开文件时会清空内部存储的所有数据,单独使用时和ios::out相同
ios::binaryifstream、ofstream、fstream二进制方式打开文件。 如果不指定这个,默认是文本模式打开

ios::binary可以和其他模式标记组合使用:

  • ios::in | ios::binary表示用二进制模式,以读取的方式打开文件。
  • ios::in | ios::binary表示用二进制模式,以读取的方式打开文件。

在流对象上执行 open 成员函数,给出文件名和打开模式,就可以打开文件。判断文件打开是否成功,可以看“对象名”这个表达式的值是否为 true,如果为 true,则表示文件打开成功。

ifstream inFile;
inFile.open("c:\\tmp\\test.txt", ios::in);
if (inFile)  //条件成立,则说明文件打开成功
    inFile.close();
else
    cout << "test.txt doesn't exist" << endl;
ofstream oFile;
oFile.open("test1.txt", ios::out);
if (!oFile)  //条件成立,则说明文件打开出错
    cout << "error 1" << endl;
else
    oFile.close();

3.2 使用流类的构造函数打开文件

定义流对象时, 在构造函数中给出文件名和打开模式。比如ifstream, 它有带参的构造函数:

ifstream::ifstream (const char* szFileName, int mode = ios::in, int);

第一个参数是指向文件名的指针;第二个参数是打开文件的模式标记,默认值为ios::in; 第三个参数是整型的,也有默认值,一般极少使用。

ifstream inFile("c:\\tmp\\test.txt", ios::in);
if (inFile)
    inFile.close();
else
    cout << "test.txt doesn't exist" << endl;
ofstream oFile("test1.txt", ios::out);
if (!oFile)
    cout << "error 1";
else
    oFile.close();

3.3 文本打开方式和二进制打开方式

这里来简单总结下文本文件和二进制文件的区别:

  • 文本文件,通常保存肉眼可见的字符,比如.txt文件,.c文件,.dat文件等,文本编辑器打开,我们能看得懂。

  • 二进制文件通常保存视频,图片,程序等不可阅读的内容, 文本编辑器打开,会是一堆乱码。

但从物理上讲, 这两种文件并没有区别,都是二进制形式保存在磁盘上。

  • 我们之所以能看懂文本文件的内容,是因为文本文件中采用的是 ASCII、UTF-8、GBK 等字符编码,文本编辑器可以识别出这些编码格式,并将编码值转换成字符展示出来。

  • 而二进制文件使用的是 mp4、gif、exe 等特殊编码格式,文本编辑器并不认识这些编码格式,只能按照字符编码格式胡乱解析,所以就成了一堆乱七八糟的字符,有的甚至都没见过。

总的来说,不同类型的文件有不同的编码格式,必须使用对应的程序(软件)才能正确解析,否则就是一堆乱码,或者无法使用。

所以呢? 文本方式和二进制方式打开或写入文件并没有本质的区别, 只是对换行符的处理不同。

  • Linux平台, 文本方式和二进制方式打开文件没有任何区别, 因为文本文件以\n作为换行符
  • 但windows平台,文本文件以连在一起的\r\n作为换行符。
    • 如果以文本方式打开文件,当读取文件时, 程序狐疑将所有\r\n转换成一个字符\n。 即如果文本中连续两个字符是\r\n, 则程序会丢弃前面的\r,只读入\n。
    • 写入文件时, 程序会将\n转换为\r\n写入。 所以windows上用文本方式打开二进制文件进行读写,读写内容有可能有出入。

总的来说,Linux 平台使用哪种打开方式都行;Windows 平台上最好用 ios::in | ios::out 等打开文本文件,用 ios::binary 打开二进制文件。但无论哪种平台,用二进制方式打开文件总是最保险的。

4. C++关闭文件

open()方法打开文件, 是文件流对象和文件之间建立关联的过程, 而close()方法关闭已打开的文件,把管子拔掉, 切断文件流对象与文件之间的关联。 注意:close()方法的功能是仅切断文件流与文件之间的关联,该文件流并未被销毁, 后续还可以关联其他文件。

void close()

这个使用非常简单:

const char *s = "hello world";
ofstream outFile("test.txt", ios::out);
outFile.write(s, 12);
outFile.close();

// 这里也可以通过输入输出错误看看是否close失败
// 当文件流对象未关联任何文件时, 调用close方法会失败
if (outFile.fail()){
	cout << "文件操作过程发生了错误" << endl;
}

虽然我们不调用close()方法, 也可以成功写入字符串,这是因为,文件流对象生命周期结束,会自行调用析构函数, 该函数在内部销毁对象之前, 先调用close()方法切断它与任何文件的关联,再销毁, 但还是建议open it, then close do

那么问题来了, 既然文件流对象自行销毁时会隐式调用close()方法, 那么为啥我们对于打开的文件,还需要手动调用close方法将其关闭呢?这不是多此一举了嘛!

其实并没有,原因我理解是写入文件的时候, 当真正调用close的方法时, 数据才从缓冲区写入到文件。 而如果在close方法之前出现了一个异常, 那么此时程序退出, 缓冲区的数据并没有写入到文件。

这个也是看了下面例子感觉到的:

coust char *s = "hello world";
ofstream outFile("out.txt", ios::out);
if (!outFile){cout << "打开文件失败" << endl;  return 0;}

// 向out.txt中写入hello world, 此时并没有真正写入文件,而是写到了缓冲区里面暂存
outFile << s;

// 假设此时抛出了一个异常, 这时候,如果没有捕捉,程序就崩溃了,缓冲区中的字符没法写到文件中
throw "Exceptrion";

// 这句话并不起作用了
outFile.close();

对于已经打开的文件,如果不及时关闭,一旦程序出现异常,则很可能会导致之前读写文件的所有操作失效。所以,解决这个的第一个方法, 就是即使close, 比如上面后两句话换一下。

当然, 很多场景中, 肯定不会只进行一次写操作, 可能后续还会执行其他写操作,而并不像频繁的打开/关闭文件,此时使用flush()方法及时刷新输出流缓冲区, 也可以起到防止写入文件失败的作用

coust char *s = "hello world";
ofstream outFile("out.txt", ios::out);
if (!outFile){cout << "打开文件失败" << endl;  return 0;}

// 向out.txt中写入hello world, 此时并没有真正写入文件,而是写到了缓冲区里面暂存
outFile << s;

// 写入之后,及时刷新输出流缓冲区
outFile.flush();

// 假设此时抛出了一个异常, 这时候,如果没有捕捉,程序就崩溃了,缓冲区中的字符没法写到文件中
throw "Exceptrion";

// 这句话并不起作用了
outFile.close();

总之,C++ 中使用 open() 打开的文件,在读写操作执行完毕后,应及时调用 close() 方法关闭文件,或者对文件执行写操作后及时调用 flush() 方法刷新输出流缓冲区

5. C++读写文件

将数据存储在文件中时, 可以将其存储为文本格式或二进制格式。 当然从文件读数据,也会有相应的这两种格式。

  • 文件格式指的是所有内容(甚至是数字)都存储成文本, 就是直白的将文件中存储的字符或者字符串读取或者写入到文件中。
    • 比如, 以文本格式存储值0.375时, 将存储该数字包含的5个字符。 这需要将浮点数的计算机内部表示转化成字符格式, 这个正是<<插入运算符完成的工作。我们打开文件看的时候,就直接看到0.375这个字符串了。
  • 二进制的格式是直接存储值的计算机内部表示(底层存储的二进制数据)。
    • 比如上面这个, 存储的是0.375浮点数对应的二进制数据, 即0011|1110|1100|0000|0000|0000|0000|0000。如果直接将上面二进制数据转成float类型,仍然得到0.375, 但对于文件来说, 它只会存储二进制数据根据既定的编码格式转换得到的字符。此时如果打开文件,看到的是一堆乱码了。
  • 对于字符来说, 二进制表示和文本表示是一样的,即字符的ASCII码的二进制表示。但对于数字来讲, 二进制表示和文本表示有很大的区别。
    • 比如上面0.375, 二进制表示是0|0111110|110000000000000000000000,对应符号位|指数位|二进制分数位, 而文本表示00110000|00101110|00110011|00110111|00110111, 对应着5个字符的编码

文本格式便于读取,可以使用编译器或字处理器读取和编辑,可以很方便将本文文件从一个计算机系统传输到另一个计算机系统。

二进制格式对数字比较精确, 传输速度快,占用较少空间, 但可能无法直接从一个系统传到另一个系统(毕竟这是内部表示,不同系统之间内部结构布局可能不同)。

各有优劣, 下面分别看看这两个格式的具体读写方式。

5.1 C++读写文本文件(>>和<<)

当fstream或者ifstream类对象打开文件(通常ios::in),就可以直接借助>>输入流运算符,读取文件中存储的字符或字符串;同理, 当fstream或ofstream类对象打开文件(通常ios::out)后,可直接借助<<输出流运算符向文件写入字符或字符串。

看个例子, 由于我是在上面那个网站上进行演示,所以这里没法直接从文件中读数据,所以我这里是先将屏幕上的数据输出到文件,然后再从文件中读的数据。

#include <iostream>
#include <fstream>
using namespace std;
 
int main()
{
    int x, sum = 0;
    ofstream outFile("out.txt", ios::out);   // 文本模式打开out.txt
    if (!outFile){
        cout << "打开out文件失败" << endl;
        return 0;
    }
    
    // 这里我先从屏幕上读入数值
    while (cin >> x){
        sum += x;
        // 写出到文件out.txt中
        outFile << x << " ";
    }
    outFile << sum << endl;
    outFile.close();
    
    // 文本模式打开out.txt
    ifstream srcFile("out.txt", ios::in); 
    if (!srcFile){
        cout << "打开out失败" << endl;
        return 0;
    }
    // 从文本中读数据,然后输出到屏幕
    while (srcFile >> x){
        cout << x + 10 << endl;
    }
    srcFile.close();
    
    return 0;
}

结果如下:
在这里插入图片描述
这个程序的执行过程,首先会把屏幕上的输入,通过cin的方式读入到程序(经过了'10' -> 10的转换), 然后求和, 再通过outFile的方式写入到out.txt(经过了10->'10'的转换), 当然又写了和sum(150->'150)。

再从out.txt,通过srcFile的方式读取字符(经过了'10'->10), 然后在原来数字基础上加10,得到了最终的输出结果。

5.2 C++二进制文件读写(read和write)

直观上,先理解下二进制读写文件的好处。

比如,要做一个学籍管理程序, 其中一个重要工作是记录学生学号、姓名、年龄等信息,这意味着需要用一个类表示学生:

class CStudent{
	char szName[20];  //假设学生姓名不超过19个字符,以 '\0' 结尾
	char szId[10];   //假设学号为9位,以 '\0' 结尾
	int age;  // 年龄
}; 

如果用文本形式存储学生信息,则最终可能是:

wuzhongqiang 110923412 25
zhangsan 110923413 18
......

这种存储学生信息方式浪费空间,另外不利于查找指定学生信息,因为每个学生的信息所占用的字节数不同。

而如果是二进制形式存储到文件, 可以直接把CStudent对象写入文件, 意味着每个学生的信息只占用sizeof(CStudent)个字节。

实现二进制形式读写文件,<<>>不再适用,需要使用C++标准库提供的read()write()

  • read(): 二进制形式从文件中读取数据
  • write(): 二进制形式将数据写入文件
5.2.1 ostream::write方法写入文件

ofstream 和 fstream 的 write() 成员方法实际上继承自 ostream 类,其功能是将内存中 buffer 指向的 count 个字节的内容写入文件,基本格式如下:

ostream & write(char* buffer, int count);
// buffer是写入文件的二进制数据起始位置, count用于指定字节的个数

需要注意的一点是,write() 成员方法向文件中写入若干字节,可是调用 write() 函数时并没有指定这些字节写入文件中的具体位置。

事实上,write() 方法会从文件写指针指向的位置将二进制数据写入。所谓文件写指针,是 ofstream 或 fstream 对象内部维护的一个变量,文件刚打开时,文件写指针指向的是文件的开头(如果以 ios::app 方式打开,则指向文件末尾),用 write() 方法写入 n 个字节,写指针指向的位置就向后移动 n 个字节。

来个例子:

class CStudent{
public:
    char szName[20];
    int age;
    
};
int main()
{
    CStudent s;
    ofstream outFile("students.dat", ios::out | ios::binary);  // 二进制形式写入
    while (cin >> s.szName >> s.age){
        outFile.write((char*)&s, sizeof(s));
    }
    outFile.close();
    return 0;
}

此时, 会自动生成一个students.dat文件,但记事本打开的时候,就会看到一堆乱码。

wuzhongqiang 烫烫烫烫烫烫烫烫?

显然, 数字没法正确显式出来。毕竟文本文件和二进制文件的编码格式不同。

5.2.2 istream::read方法读取文件

ifstream 和 fstream 的 read() 方法实际上继承自 istream 类,其功能正好和 write() 方法相反,即从文件中读取 count 个字节的数据。该方法的语法格式如下:

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

write() 方法类似,read() 方法从文件读指针指向的位置开始读取若干字节。

所谓文件读指针,可以理解为是 ifstream 或 fstream 对象内部维护的一个变量。文件刚打开时,文件读指针指向文件的开头(如果以 ios::app 方式打开,则指向文件末尾),用 read() 方法读取 n 个字节,读指针指向的位置就向后移动 n 个字节。因此,打开一个文件后连续调用 read() 方法,就能将整个文件的内容读取出来。

下面, 把上面那个students.dat文件的内容,通过read()方法读取查看下。

在这里插入图片描述

6. C++get和put方法读写文件

get方法和put方法在上一篇文章中, 其实整理过,只不过那里是和屏幕进行交互,而这里是和文件交互,但有用法是一样的。

fstream和ofstream文件流对象调用put()方法时, 是向指定文件中写入单个字符, 语法格式如下:

ostream& put (char c);

put() 成员方法的功能相对的是 get() 方法,其定义在 istream 类中,借助 cin.get() 可以读取用户输入的字符。在此基础上,fstream 和 ifstream 类继承自 istream 类,因此 fstream 和 ifstream 类的对象也能调用 get() 方法。

当 fstream 和 ifstream 文件流对象调用 get() 方法时,其功能就变成了从指定文件中读取单个字符(还可以读取指定长度的字符串), 最常用两种方法格式:

int get();   // 返回值是读到字符的ASCII码, 如果到末尾了,返回值为EOF
istream& get(char& c);  // 读取字符给字符变量

下面用一个例子,来演示上面两种方式, 首先是从屏幕上读入一句话,然后通过put方法写入到文件,然后再通过get方法读文件, 把结果再输出到屏幕上。

int main()
{
    char c;
    ofstream outFile("test.txt", ios::out | ios::binary);
    if (!outFile){
        cout << "error" << endl;
        return 0;
    }
    while (cin.get(c)){
        // 将字符写入到文件
        outFile.put(c);
    }
    // 上面其实暂存到了缓冲区里面, 下面这句话才真正持久化
    outFile.close();
    
    // 下面从文件中读入数据,然后输出到屏幕
    ifstream inFile("test.txt", ios::in | ios::binary);
    if (!inFile){
        cout << "error" << endl;
        return 0;
    }
    
    while ((c=inFile.get())&&c!=EOF){ //或者 while(inFile.get(c)),对应第二种语法格式
        cout << c;
    }
    inFile.close();
    
    return 0;
}

输出结果:
在这里插入图片描述
这里说一下缓冲区存在的意义:

由于文件存放在硬盘中,硬盘的访问速度远远低于内存。如果每次写一个字节都要访问硬盘,那么文件的读写速度就会慢得不可忍受。因此,操作系统在接收到 put() 方法写文件的请求时,会先将指定字符存储在一块指定的内存空间中(称为文件流输出缓冲区),等刷新该缓冲区(缓冲区满、关闭文件、手动调用 flush() 方法等,都会导致缓冲区刷新)时,才会将缓冲区中存储的所有字符“一股脑儿”全写入文件。

put() 方法一样,操作系统在接收到 get() 方法的请求后,哪怕只读取一个字符,也会一次性从文件中将很多数据(通常至少是 512 个字节,因为硬盘的一个扇区是 512 B)读到一块内存空间中(可称为文件流输入缓冲区),这样当读取下一个字符时,就不需要再访问硬盘中的文件,直接从该缓冲区中读取即可。

7. C++的getline

和cin对象的方法一样, getline()方法也适用于读取指定文件中的一行数据, 方法定义在了istream类, 由于fstream和ifstream都继承istream类,所以这俩类对象都可以调用这个方法。

// 用于从文件输入流缓冲区中读取 bufSize-1 个字符到 buf,或遇到 \n 为止(哪个条件先满足就按哪个执行),该方法会自动在 buf 中读入数据的结尾添加 '\0'
istream & getline(char* buf, int bufSize);

// 第一个版本是读到 \n 为止,第二个版本是读到 delim 字符为止。\n 或 delim 都不会被读入 buf,但会被从文件输入流缓冲区中取走
istream & getline(char* buf, int bufSize, char delim);

注意,如果文件输入流中 \n 或 delim 之前的字符个数达到或超过 bufSize,就会导致读取失败。

看如何使用:

char c[40];
// 二进制模式打开in.txt
ifstream inFile("in.txt", ios::in | ios::binary);
// 判断文件是否正常打开
if (!inFile){cout << "error" << endl;  return 0;}

// 从in.txt文件中读取一行字符串, 最多不错过39个
// 如果想遇到指定的字符结束 inFile.getline(c, 40, 'c');
inFile.getline(c, 40);
cout << c;
inFile.close();

// 如果想读取文件中多行数据
// 连续以行为单位,读取in.txt文件中的数据
while (inFile.getline(c, 40)){
	cout << c << endl;
}

8. C++移动或获取文件读写指针

在读写文件时, 有时希望直接跳到文件的某个地方开始读写, 这时候需要先将文件的读写指针指向该处,然后进行读写操作。

  • ifstream类和fstream类有seekg成员函数,可设置文件读指针的位置
  • ofstream类和fstream类有seekp成员函数,可设置文件写指针的位置

所谓"位置", 就是值距离文件开头有多少个字节。 文件开头位置是0。两个函数原型如下:

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

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

  • ios::beg: 让文件读指针或写指针指向从文件开始向后的offset字节处。offset为0即文件开头, 这时候, offset是非负数
  • ios::cur: 在此情况下, offset为负数则表示将读指针或写指针从当前位置朝着开头方向移动offset字节,为正数表示从当前位置朝文件尾部移动offset个字节。
  • ios::end: 让文件读指针或写指针指向文件结尾,往前移动。 此时offset是0或者负数。

上面这是定位, 此外我们可以得到当前读写指针的具体位置:

  • ifstream类和fstream类有tellg成员函数,能返回文件读指针的位置
  • ofstream类和fstream类有tellp成员函数, 能返回文件写指针的位置

两个函数原型:

int tellg();
int tellp();

要获取文件长度, 可以用seekg函数将文件读指针定位到文件尾部,再用tellg函数获取文件读指针的位置,此位置即为文件长度。

一直好奇,这里的g和p表示什么? 查了下,就比较好理解, 读取的时候对应g,而写入的时候对应p了, 这里的g代表get, 而p表示put。 所以这样seekp, tellp为啥对应着ofstream, 而seekg和tellg对应istream就了然了。 另外,seek代表寻找寻求寻觅,代表着设置读写位置, 也就是找合适的读写位置, 而tell是告诉,也就是找到位置了,告诉我位置是啥?

这样, 就容易记了, 下面就看怎么用。

假设学生记录文件students.dat是按照姓名排好序的, 编写程序, 在students.dat文件中用折半查找的方法找到姓名为Jack的学生记录,并将年龄改为20(假设文件很大,无法全部读入内存)

class CStudent{
public:
    char szName[20];
    int age;
};

int main()
{
    CStudent s;
    fstream ioFile("students.dat", ios::in | ios::out); // 用既读又写的方式打开
    if (!ioFile){
        cout << "error";
        return 0;
    }
    
    // 定位读指针到文件尾部, 以便用以后的tellg获取文件长度
    ioFile.seekg(0, ios::end);
    
    // 折半查找
    int L = 0, R;
    R = ioFile.tellg() / sizeof(CStudent) - 1;
    
    do{
        int mid = (L+R) / 2;
        ioFile.seekg(mid*sizeof(CStudent), ios::beg); // 定位到正中的记录
        ioFile.read((char *)&s, sizeof(s));
        int tmp = strcmp(s.szName, "Jack");
        // 找到了
        if (tmp == 0){
            s.age = 20;
            ioFile.seekp(mid*sizeof(CStudent), ios::beg);
            ioFile.write((char*)&s, sizeof(s));
            break;
        }else if (tmp > 0){
            R = mid - 1;
        }else{
            L = mid + 1;
        }
    }while(L <= R);
    ioFile.close();
    return 0;
}

9. 小总

下面依然是一张导图,把这篇文章的主要内容拎起来了,这次主要是围绕着文件操作进行展开。

在这里插入图片描述

  • 10
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值