第11章 C++文件操作总结
关于文件操作,虽然在 C++ 程序中可以继续沿用 C 语言的那套文件操作方式,但更推荐使用适当的文件流类来读写文件。
计算机文件到底是什么?
- 一般来说,文件可分为文本文件、视频文件、音频文件、图像文件、可执行文件等多种类别,这是从文件的功能进行分类的。
- 从数据存储的角度来说,所有的文件本质上都是一样的,都是由一个个字节组成的,归根到底都是 0、1 比特串。
不同的文件呈现出不同的形态(有的是文本,有的是视频等等),这主要是文件的创建者和解释者(使用文件的软件)约定好了文件格式。
所谓“格式”,就是关于文件中每一部分的内容代表什么含义的一种约定。
- 例如,常见的纯文本文件(也叫文本文件,扩展名通常是“.txt”),指的是能够在 Windows 的“记事本”程序中打开,并且能看出是一段有意义的文字的文件。文本文件的格式可以用一句话来描述:文件中的每个字节都是一个可见字符的 ASCII 码。(个人:不准确,这个文本文件还有可能包含中文,使用GBK或者utf-8编码)
- 除了纯文本文件外,图像、视频、可执行文件等一般被称作“二进制文件”。二进制文件如果用“记事本”程序打开,看到的是一片乱码。
所谓“文本文件”和“二进制文件”,只是约定俗成的、从计算机用户角度出发进行的分类,并不是计算机科学的分类。因为从计算机科学的角度来看,所有的文件都是由二进制位组成的,都是二进制文件。文本文件和其他二进制文件只是格式不同而已。
- 实际上,只要规定好格式,而且不怕浪费空间,用文本文件一样可以表示图像、声音、视频甚至可执行程序。简单地说,如果约定用字符 '1'、'2'、...、'7' 表示七个音符,那么由这些字符组成的文本文件就可以被遵从该约定的音乐软件演奏成一首曲子。
- 下面再看一个用文本文件表示一幅图像的例子:一幅图像实际上就是一个由点构成的矩阵,每个点可以有不同的颜色,称为像素。有的图像是 256 色的,有的是 32 位真彩色(即一 个像素的颜色用一个 32 位的整数表示)的。以 256 色图像为例,可以用 0~255 这 256 个数代表 256 种颜色,那么每个像素就可以用一个数来表示。再约定文件开始的两个数代表图像的宽度和高度(以像素为单位),则以下文本文件就可以表示一幅宽度为 6 像素、高度为 4 像素的 256 色图像:
这个“文本图像”文件的格式可以描述为:第一行的两个数分别代表水平方向的像素数目和垂直方向的像素数目,此后每行代表图像的一行像素,一行中的每个数对应于一个像素,表示其颜色。理解这一格式的图像处理软件就可以把上述文本文件呈现为一幅图像。视频是由每秒24幅图像组成的,因此用文本文件也可以表示视频。
上面用文本文件表示图像的方法是非常低效的,浪费了太多的空间。
- 文件中大量的空格是一种浪费。
- 另外,常常要用 2 个甚至 3 个字符来表示一个像素,也造成大量浪费,因为用一个字节就足以表示 0~255 这 256 个数。
因此,可以约定一个更节省空间的格式来表示一个 256 色的图像,此种文件格式的描述如下:
- 文件中的第 0 和第 1 个字节是整数 n,代表图像的宽度(2 字节的 n 的取值范围是 0~65 535,说明图像最多只能是 65 535 个像素宽),
- 第 2 和第 3 个字节代表图像的高度。
- 接下来,每 n 个字节表示图像的一行像素,其中每个字节对应于一个像素的颜色。
用这种格式存储 256 色图像,比用上面的文本格式存储图像能够大大节省空间。在“记事本”程序中打开它,看到的就会是乱码,这个图像文件也就是所谓的“二进制文件”。 真正的图像文件、音频文件、视频文件的格式都比较复杂,有的还经过了压缩,但只要文件的制作软件和解读软件(如图像查看软件,音频、视频播放软件)遵循相同的格式约定,用户就可以在文件解读软件中看到文件的内容。
C++文件类(文件流类)及用法详解
C++ 标准库中还专门提供了 3 个类用于实现文件操作,它们统称为文件流类,这 3 个类分别为:
- ifstream:专用于从文件中读取数据;
- ofstream:专用于向文件中写入数据;
- fstream:既可用于从文件中读取数据,又可用于向文件中写入数据。
值得一提的是,这 3 个文件流类都位于 <fstream> 头文件中,因此在使用它们之前,程序中应先引入此头文件。
这 3 个文件流类的继承关系,如图 1 所示。
可以看到,
- ifstream 类和 fstream 类是从 istream 类派生而来的,因此 ifstream 类拥有 istream 类的全部成员方法。
- 同样地,ofstream 和 fstream 类也拥有 ostream 类的全部成员方法。
这也就意味着,istream 和 ostream 类提供的供 cin 和 cout 调用的成员方法,也同样适用于文件流。
值得一提的是,和 <iostream> 头文件中定义有 ostream 和 istream 类的对象 cin 和 cout 不同,<fstream> 头文件中并没有定义可直接使用的 fstream、ifstream 和 ofstream 类对象。因此,如果我们想使用该类操作文件,需要自己创建相应类的对象。
fstream 类拥有 ifstream 和 ofstream 类中所有的成员方法,表 2 罗列了 fstream 类一些常用的成员方法。
成员方法名 | 适用类对象 | 功 能 |
---|---|---|
open() | fstream ifstream ofstream |
打开指定文件,使其与文件流对象相关联。 |
is_open() | 检查指定文件是否已打开。 | |
close() | 关闭文件,切断和文件流对象的关联。 | |
swap() | 交换 2 个文件流对象。 | |
operator>> | fstream ifstream |
重载 >> 运算符,用于从指定文件中读取数据。 |
gcount() | 返回上次从文件流提取出的字符个数。该函数常和 get()、getline()、ignore()、peek()、read()、readsome()、putback() 和 unget() 联用。 | |
get() | 从文件流中读取一个字符,同时该字符会从输入流中消失。 | |
getline(str,n,ch) | 从文件流中接收 n-1 个字符给 str 变量,当遇到指定 ch 字符时会停止读取,默认情况下 ch 为 '\0'。 | |
ignore(n,ch) | 从文件流中逐个提取字符,但提取出的字符被忽略,不被使用,直至提取出 n 个字符,或者当前读取的字符为 ch。 | |
peek() | 返回文件流中的第一个字符,但并不是提取该字符。 | |
putback(c) | 将字符 c 置入文件流(缓冲区)。 | |
operator<< | fstream ofstream |
重载 << 运算符,用于向文件中写入指定数据。 |
put() | 向指定文件流中写入单个字符。 | |
write() | 向指定文件中写入字符串。 | |
tellp() | 用于获取当前文件输出流指针的位置。 | |
seekp() | 设置输出文件输出流指针的位置。 | |
flush() | 刷新文件输出流缓冲区。 | |
good() | fstream ofstream ifstream |
操作成功,没有发生任何错误。 |
eof() | 到达输入末尾或文件尾。 |
表 2 中仅列举的了部分常用的成员方法,更详细的介绍,读者可查看 C++标准库手册。
C++ open 打开文件(含打开模式一览表)
打开文件可以通过以下两种方式进行:
- 调用流对象的 open 成员函数打开文件。
- 定义文件流对象时,通过构造函数打开文件。
使用 open 函数打开文件
先看第一种文件打开方式。以 ifstream 类为例,该类有一个 open 成员函数,其他两个文件流类也有同样的 open 成员函数:
void open(const char* szFileName, int mode)
第一个参数是指向文件名的指针,第二个参数是文件的打开模式标记。
文件的打开模式标记代表了文件的使用方式,这些标记可以单独使用,也可以组合使用。
(个人:对于ofstream,似乎我们也可以指定in模式,此时文件如果文件如果不存在,就会打开出错)
#include <iostream> #include <fstream> using namespace std; int main() { ofstream app2("out.txt", ios::in); if(app2){ app2<<"hello world!aaaa"<<endl; } else cout<<"open error"<<endl; app2.close(); return 0; }
文件不存在时,
文件存在时,
文本方式与二进制方式打开文件的区别其实非常微小,一般来说,如果处理的是文本文件,那么用文本方式打开会方便一些。但其实任何文件都可以以二进制方式打开来读写。
在流对象上执行 open 成员函数,给出文件名和打开模式,就可以打开文件。判断文件打开是否成功,可以看“对象名”这个表达式的值是否为 true,如果为 true,则表示文件打开成功。
- 调用 open 成员函数时,给出的文件名可以是全路径的,如c:\\tmp\\test.txt, 指明文件在 c 盘的 tmp 文件夹中;
- 也可以只给出文件名,如test1.txt,这种情况下程序会在当前文件夹(也就是可执行程序所在的文件夹)中寻找要打开的文件。
使用流类的构造函数打开文件
定义流对象时,在构造函数中给出文件名和打开模式也可以打开文件。以 ifstream 类为例,它有如下构造函数:
ifstream::ifstream (const char* szFileName, int mode = ios::in, int);
第一个参数是指向文件名的指针;第二个参数是打开文件的模式标记,默认值为ios::in; 第三个参数是整型的,也有默认值,一般极少使用。
文件的文本打开方式和二进制打开方式的区别
在 UNIX/Linux 平台中,文本文件以\n(ASCII 码为 0x0a)作为换行符号;而在 Windows 平台中,文本文件以连在一起的\r\n(\r的 ASCII 码是 0x0d)作为换行符号。
- 在 UNIX/Linux 平台中,用文本方式或二进制方式打开文件没有任何区别。
- 在 Windows 平台中,如果以文本方式打开文件,
- 当读取文件时,系统会将文件中所有的\r\n转换成一个字符\n,如果文件中有连续的两个字节是 0x0d0a,则系统会丢弃前面的 0x0d 这个字节,只读入 0x0a。
- 当写入文件时,系统会将\n转换成\r\n写入。 也就是说,如果要写入的内容中有字节为 0x0a,则在写人该字节前,系统会自动先写入一个 0x0d。
因此,如果用文本方式打开二进制文件进行读写,读写的内容就可能和文件的内容有出入。 因此,用二进制方式打开文件总是最保险的。
C++ close()关闭文件方法详解
调用 close() 方法关闭已打开的文件,就可以理解为是切断文件流对象和文件之间的关联。注意,close() 方法的功能仅是切断文件流与文件之间的关联,该文件流并会被销毁,其后续还可用于关联其它的文件。
close() 方法的用法很简单,其语法格式如下:
void close( )
程序中不调用 close() 方法,也能成功向文件中写入字符串。这是因为,当文件流对象的生命周期结束时,会自行调用其析构函数,该函数内部在销毁对象之前,会先调用 close() 方法切断它与任何文件的关联,最后才销毁它。 强烈建议使用 open() 方法打开的文件,一定要手动调用 close() 方法关闭,这样可以避免程序发生一些奇葩的错误!
值得一提的是,4 种流状态,它们也同样适用于文件流。当文件流对象未关联任何文件时,调用 close() 方法会失败,其会为文件流设置 failbit 状态标志,该标志可以被 fail() 成员方法捕获。例如:
#include <iostream> #include <fstream> using namespace std; int main() { const char *url="https://www.abc.def/computer/programme/c++"; ofstream outFile; outFile.close(); if (outFile.fail()) { cout << "文件操作过程发生了错误!"; } return 0; }
C++打开的文件一定要用close()方法关闭!
既然文件流对象自行销毁时会隐式调用 close() 方法,是不是就不用显式调用 close() 方法了呢? 当然不是。在实际进行文件操作的过程中,对于打开的文件,要及时调用 close() 方法将其关闭,否则很可能会导致读写文件失败。
#include <iostream> //std::cout #include <fstream> //std::ofstream using namespace std; int main() { const char * url = "http://www.abc.def/computer/programme/c++"; //以文本模式打开out.txt ofstream destFile("out.txt", ios::out); if (!destFile) { cout << "文件打开失败" << endl; return 0; } //向out.txt文件中写入 url 字符串 destFile << url; //程序抛出一个异常 throw "Exception"; //关闭打开的 out.txt 文件 destFile.close(); return 0; }
第 17 行添加了抛出异常的语句。由于程序中没有对抛出的异常进行处理,因此当程序执行到此行时会崩溃。 更重要的是,第 17 行会导致文件写入操作失败。执行此程序,同样会生成 out.txt 文件,但字符串并没有成功被写入。
也就是说,对于已经打开的文件,如果不及时关闭,一旦程序出现异常,则很可能会导致之前读写文件的所有操作失效。
如果将第 17 行代码和第 19 行代码互换,再次执行程序会发现,虽然程序执行仍会崩溃,但字符串可以被成功写入到 out.txt 文件中。(个人:运行之,确实如此)
在很多实际场景中,即便已经对文件执行了写操作,但后续还可能会执行其他的写操作。对于这种情况,我们可能并不想频繁地打开/关闭文件,可以使用 flush() 方法及时刷新输出流缓冲区,也能起到防止写入文件失败的作用。 前面的程序之所以写入文件失败,是因为 << 写入运算符会先将 url 字符串写入到输出流缓冲区中,待缓冲区满或者关闭文件时,数据才会由缓冲区写入到文件中。但直到程序崩溃,close() 方法也没有得到执行,且 destFile 对象也没有正常销毁,所以 url 字符串一直存储在缓冲区中,没有写入到文件中。
总之,C++ 中使用 open() 打开的文件,在读写操作执行完毕后,应及时调用 close() 方法关闭文件,或者对文件执行写操作后及时调用 flush() 方法刷新输出流缓冲区。
C++使用 >>和<<实现以文本形式读写文件
对文件的读/写操作又可以细分为2类,分别是以文本形式读写文件和以二进制形式读写文件。(个人:这里的说的是以二进制或者文本方式读写,而不是我们在使用open函数时指定的文本方式或者二进制方式,那里是指的以二进制或者文本方式打开,而不是读写)
- 我们知道,文件中存储的数据并没有类型上的分别,统统都是01序列。所谓以文本形式读/写文件,就是直白地将文件中存储的字符(或字符串)读取出来,以及将目标字符(或字符串)存储在文件中。
- 而以二进制形式读/写文件,操作的对象不再是打开文件就能看到的字符,而是文件底层存储的二进制数据。更详细地讲,当以该形式读取文件时,读取的是该文件底层存储的二进制数据;同样,当将某数据以二进制形式写入到文件中时,写入的也是其对应的二进制数据。
举个例子,假设我们以文本形式将浮点数 19.625 写入文件,则该文件会直接将 "19.625" 这个字符串存储起来。当我们双击打开此文件,也可以看到 19.625。值得一提的是,由非字符串数据(比如这里的浮点数 19.625)转换为对应字符串(转化为 "19.625")的过程,C++ 标准库已经实现好了,不需要我们操心。
但如果以二进制形式将浮点数 19.625 写入文件,则该文件存储的不再是 "19.625" 这个字符串,而是 19.625 浮点数对应的二进制数据。(个人:存储的是这个浮点数在内存中的表示,原封不动),以 float 类型的 19.625 来说,文件最终存储的数据如下所示:
显然,如果直接将以上二进制数据转换为 float 类型,仍可以得到浮点数 19.625。但对于文件来说,它只会将存储的二进制数据根据既定的编码格式(如 utf-8、gbk 等)转换为一个个字符。这也就意味着,如果我们直接打开此文件,看到的并不会是 19.625,往往是一堆乱码。
C++ 标准库中,提供了 2 套读写文件的方法组合,分别是:
- 使用 >> 和 << 读写文件:适用于以文本形式读写文件;
- 使用 read() 和 write() 成员方法读写文件:适用于以二进制形式读写文件。
本节先讲解如何用 >> 和 << 实现以文本形式读写文件,至于如何实现以二进制形式读写文件,下一节会做详细介绍。
#include <iostream> #include <fstream> using namespace std; int main() { int x,sum=0; ifstream srcFile("in.txt", ios::in); //以文本模式打开in.txt备读 if (!srcFile) { //打开失败 cout << "error opening source file." << endl; return 0; } ofstream destFile("out.txt", ios::out); //以文本模式打开out.txt备写 if (!destFile) { srcFile.close(); //程序结束前不能忘记关闭以前打开过的文件 cout << "error opening destination file." << endl; return 0; } //可以像用cin那样用ifstream对象 while (srcFile >> x) { sum += x; //可以像 cout 那样使用 ofstream 对象 destFile << x << " "; } cout << "sum:" << sum << endl; destFile.close(); srcFile.close(); return 0; }
通过分析程序的执行结果不难理解,
- 对于 in.txt 文件中的 "10 20 30 40 50" 字符串,srcFile 对象会依次将 "10"、"20"、"30"、"40"、"50" 读取出来,将它们解析成 int 类型的整数 10、20、30、40、50 并赋值给 x,同时完成和 sum 的加和操作。
- 同样,对于每次从 in.txt 文件读取并解析出的整形 x,destFile 对象都会原封不动地将其再解析成对应的字符串(如整数 10 解析成字符串 "10"),然后和 " " 空格符一起写入 out.txt 文件。(个人:也就是说,此时,C++会先将要写入的内容转换为字符串,然后再写入,实际写入文件应该是字符串的ASCII对应的整数的二进制表示)
此程序中分别采用 ios::in 和 ios::out 打开文件,即以文本模式而非二进制模式打开文件。在其基础上添加 ios::binary,即以二进制模式打开文件,程序依旧会正常执行。这是因为,以文本模式打开文件和以二进制模式打开文件,并没有很大的区别。 (个人:实际运行之,确实如此)
C++ 使用read()和write()以二进制形式读写文件
介绍具体的实现方法前,先给读者介绍一下相比以文本形式读写文件,以二进制形式读写文件有哪些好处。
举个例子,现在要做一个学籍管理程序,其中一个重要的工作就是记录学生的学号、姓名、年龄等信息。这意味着,我们需要用一个类来表示学生,如下所示:
class CStudent { char szName[20]; //假设学生姓名不超过19个字符,以 '\0' 结尾 char szId[l0]; //假设学号为9位,以 '\0' 结尾 int age; //年龄 };
以文本形式读写文件,存储学生的信息,则最终的文件中存储的学生信息可能是这个样子:
要知道,这种存储学生信息的方式不但浪费空间,而且后期不利于查找指定学生的信息(查找效率低下),因为每个学生的信息所占用的字节数不同。
这种情况下,以二进制形式将学生信息存储到文件中,是非常不错的选择,因为以此形式存储学生信息,可以直接把 CStudent 对象写入文件中,这意味着每个学生的信息都只占用 sizeof(CStudent) 个字节。
值得一提的是,要实现以二进制形式读写文件,<< 和 >> 将不再适用,需要使用 C++ 标准库专门提供的 read() 和 write() 成员方法。其中,
- read() 方法用于以二进制形式从文件中读取数据;
- write() 方法用于以二进制形式将数据写入文件。
C++ ostream::write()方法写文件
ofstream 和 fstream 的 write() 成员方法实际上继承自 ostream 类,其功能是将内存中 buffer 指向的 count 个字节的内容写入文件,基本格式如下:
ostream & write(char* buffer, int count);
其中,buffer 用于指定要写入文件的二进制数据的起始位置;count 用于指定写入字节的个数。
也就是说,该方法可以被 ostream 类的 cout 对象调用,常用于向屏幕上输出字符串。同时,它还可以被 ofstream 或者 fstream 对象调用,用于将指定个数的二进制数据写入文件。
需要注意的一点是,write() 成员方法向文件中写入若干字节,可是调用 write() 函数时并没有指定这些字节写入文件中的具体位置。事实上,write() 方法会从文件写指针指向的位置将二进制数据写入。所谓文件写指针,是 ofstream 或 fstream 对象内部维护的一个变量,文件刚打开时,文件写指针指向的是文件的开头(如果以 ios::app 方式打开,则指向文件末尾),用 write() 方法写入 n 个字节,写指针指向的位置就向后移动 n 个字节。
下面的程序演示了如何将学生信息以二进制形式写入文件:
#include <iostream> #include <fstream> using namespace std; 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 文件,其内部存有 72 字节的数据,如果用“记事本”打开此文件,可能看到如下乱码:
- 值得一提的是,程序中第 13 行指定文件的打开模式为 ios::out | ios::binary,即以二进制写模式打开。在 Windows平台中,以二进制模式打开文件是非常有必要的,否则可能出错.。
- 另外,第 15 行将 s 对象写入文件。s 的地址就是要写入文件的内存缓冲区的地址,但是 &s 不是 char * 类型,因此要进行强制类型转换;
- 第 16 行,文件使用完毕一定要关闭,否则程序结束后文件的内容可能不完整。
C++ istream::read()方法读文件
ifstream 和 fstream 的 read() 方法实际上继承自 istream 类,其功能正好和 write() 方法相反,即从文件中读取 count 个字节的数据。该方法的语法格式如下:
istream & read(char* buffer, int count);
其中,buffer 用于指定读取后存放字节的起始位置,count 指定读取字节的个数。同样,该方法也会返回一个调用该方法的对象的引用。
和 write() 方法类似,read() 方法从文件读指针指向的位置开始读取若干字节。所谓文件读指针,可以理解为是 ifstream 或 fstream 对象内部维护的一个变量。文件刚打开时,文件读指针指向文件的开头(如果以 ios::app 方式打开,则指向文件末尾),用 read() 方法读取 n 个字节,读指针指向的位置就向后移动 n 个字节。因此,打开一个文件后连续调用 read() 方法,就能将整个文件的内容读取出来。
通过执行 write() 方法的示例程序,我们将 3 个学生的信息存储到了 students.dat 文件中,下面程序演示了如何使用 read() 方法将它们读取出来:
#include <iostream> #include <fstream> using namespace std; class CStudent { public: char szName[20]; int age; }; int main() { CStudent s; ifstream inFile("students.dat",ios::in|ios::binary); //二进制读方式打开 if(!inFile) { cout << "error" <<endl; return 0; } while(inFile.read((char *)&s, sizeof(s))) { //一直读到文件结束 cout << s.szName << " " << s.age << endl; } inFile.close(); return 0; }
注意,程序中第 18 行直接将 read() 方法作为 while 循环的判断条件,这意味着,read() 方法会一直读取到文件的末尾,将所有字节全部读取完毕,while 循环才会终止。
另外,在使用 read() 方法的同时,如果想知道一共成功读取了多少个字节(读到文件尾时,未必能读取 count 个字节),可以在 read() 方法执行后立即调用文件流对象的 gcount() 成员方法,其返回值就是最近一次 read() 方法成功读取的字节数。
C++ get()和put()读写文件详解
在某些特殊的场景中,我们可能需要逐个读取文件中存储的字符,或者逐个将字符存储到文件中。这种情况下,就可以调用 get() 和 put() 成员方法实现。
C++ ostream::put()成员方法
通过执行 cout.put() 方法向屏幕输出单个字符。我们知道,fstream 和 ofstream 类继承自 ostream 类,因此 fstream 和 ofstream 类对象都可以调用 put() 方法。当 fstream 和 ofstream 文件流对象调用 put() 方法时,该方法的功能就变成了向指定文件中写入单个字符。put() 方法的语法格式如下:
ostream& put (char c);
其中,c 用于指定要写入文件的字符。该方法会返回一个调用该方法的对象的引用形式。例如,obj.put() 方法会返回 obj 这个对象的引用。(个人:put()方法应该是以文本的方式写)
#include <iostream> #include <fstream> using namespace std; int main() { char c; //以二进制形式打开文件 ofstream outFile("out.txt", ios::out | ios::binary); if (!outFile) { cout << "error" << endl; return 0; } while (cin >> c) { //将字符 c 写入 out.txt 文件 outFile.put(c); } outFile.close(); return 0; }
C++ istream::get()成员方法和 put() 成员方法的功能相对的是 get() 方法,其定义在 istream 类中,借助 cin.get() 可以读取用户输入的字符。在此基础上,fstream 和 ifstream 类继承自 istream 类,因此 fstream 和 ifstream 类的对象也能调用 get() 方法。当 fstream 和 ifstream 文件流对象调用 get() 方法时,其功能就变成了从指定文件中读取单个字符(还可以读取指定长度的字符串)。值得一提的是,get() 方法的语法格式有很多(点击这里),这里仅介绍最常用的 2 种:
int get(); istream& get (char& c);
- 其中,第一种语法格式的返回值就是读取到的字符,只不过返回的是它的 ASCII 码,如果碰到输入的末尾,则返回值为 EOF。
- 第二种语法格式需要传递一个字符变量,get() 方法会自行将读取到的字符赋值给这个变量。
本节前面在讲解 put() 方法时,生成了一个 out.txt 文件,下面的样例演示了如何通过 get() 方法逐个读取 out.txt 文件中的字符:
#include <iostream> #include <fstream> using namespace std; int main() { char c; //以二进制形式打开文件 ifstream inFile("out.txt", ios::out | 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() 方法一样,操作系统在接收到 get() 方法的请求后,哪怕只读取一个字符,也会一次性从文件中将很多数据(通常至少是 512 个字节,因为硬盘的一个扇区是 512 B)读到一块内存空间中(可称为文件流输入缓冲区),这样当读取下一个字符时,就不需要再访问硬盘中的文件,直接从该缓冲区中读取即可。
C++ getline():从文件中读取一行字符串
使用getline() 方法从 cin 输入流缓冲区中读取一行字符串。在此基础上,getline() 方法还适用于读取指定文件中的一行数据,我们知道,getline() 方法定义在 istream 类中,而 fstream 和 ifstream 类继承自 istream 类,因此 fstream 和 ifstream 的类对象可以调用 getline() 成员方法。当文件流对象调用 getline() 方法时,该方法的功能就变成了从指定文件中读取一行字符串。该方法有以下 2 种语法格式:
istream& getline (char* s, streamsize n ); istream& getline (char* s, streamsize n, char delim );
- 其中,第一种语法格式用于从文件输入流缓冲区中读取 n-1 个字符到 s,或遇到 \n 为止(哪个条件先满足就按哪个执行),该方法会自动在 s中读入数据的结尾添加 '\0'。
- 第二种语法格式和第一种的区别在于,第一个版本是读到 \n 为止,第二个版本是读到 delim 字符为止。\n 或 delim 都不会被读入 s,但会被从文件输入流缓冲区中取走。
- 以上 2 种格式中,getline() 方法都会返回一个当前所作用对象的引用。比如,obj.getline() 会返回 obj 的引用。
- 注意,如果文件输入流中 \n 或 delim 之前的字符个数达到或超过n,就会导致读取失败。
如果想读取文件中的多行数据,可以这样做:
#include <iostream> #include <fstream> using namespace std; int main() { char c[40]; ifstream inFile("in.txt", ios::in | ios::binary); if (!inFile) { cout << "error" << endl; return 0; } //连续以行为单位,读取 in.txt 文件中的数据 while (inFile.getline(c, 40)) { cout << c << endl; } inFile.close(); return 0; }
C++移动和获取文件读写指针(seekp、seekg、tellg、tellp)
在读写文件时,有时希望直接跳到文件中的某处开始读写,这就需要先将文件的读写指针指向该处,然后再进行读写。
- 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字节,为 0 则不移动。
- ios::end:让文件读指针(或写指针)指向从文件结尾往前的 |offset|(offset 的绝对值)字节处。在此情况下,offset 只能是 0 或者负数。
此外,我们还可以得到当前读写指针的具体位置:
- ifstream 类和 fstream 类还有 tellg 成员函数,能够返回当前文件读指针的位置;
- ofstream 类和 fstream 类还有 tellp 成员函数,能够返回文件当前写指针的位置。
这两个成员函数的原型如下:
int tellg(); int tellp();
要获取文件长度,可以用 seekg 函数将文件读指针定位到文件尾部,再用 tellg 函数获取文件读指针的位置,此位置即为文件长度。
#include <iostream> #include <fstream> #include <cstring> using namespace std; 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; } ioFile.seekg(0,ios::end); //定位读指针到文件尾部,以便用以后tellg 获取文件长度 int cnt = ioFile.tellg() / sizeof(CStudent); cout<<"the file size is:"<<ioFile.tellg()<<"字节"<<endl; cout<<"the file has student number:"<<cnt<<endl; return 0; }