一.C++文件操作基础
C++ 的文件操作主要基于流(stream)机制,通过标准库中的 <fstream> 头文件来实现。相比于 C 语言的文件操作,C++ 的流式操作更加面向对象,也更安全易用。
1.什么是流
所谓流,是对数据传输过程的一种高度抽象,流就像一根数据管道,数据就像管道中的水流一样,具有方向性、有序且连续地从一端流向另一端。
从底层来看,流本质上是一个类,假如说,我们进行cout << 'A'操作,那么ostream 类的 << 运算符被调用,然后将A扔给其内部关联的 streambuf 对象的成员函数(sputc('A')),streambuf主要处理缓冲区相关,sputc 会先判断缓冲区满是否已经满了,没满则直接写入缓冲区,满了,则把整个缓冲区积攒的数据,通过一次性的系统调用全部刷入外部设备,在将A存放进去。
根据数据流动的方向,流主要分为两类:
输入流 :数据从外部设备(如键盘、文件)流向程序内存。核心操作符是提取运算符 >>。
输出流:数据从程序内存流向外部设备(如屏幕、文件)。核心操作符是插入运算符 <<。
在C++中,根据数据来源和去向的不同,标准库将流分成了三大类:
标准的I/O流:也就是我们经常使用的cin/cout。
文件流:ifstream/ofstream,就是C++文件操作的基础。
字符串流:istringstream / ostringstream,与文件流十分相似。
常用的文件打开方式:
ios::in:只读模式。文件不存在则打开失败。
ios::out:只写模式。文件不存在则创建;文件已存在则默认清空原有内容。
ios::app:追加模式。写入的内容会添加到文件末尾,不会清空原内容。
ios::ate:打开文件后,文件指针直接定位到文件末尾。
ios::trunc:打开文件时,如果文件存在,则清空其内容(ios::out 默认包含此模式)。
ios::binary:以二进制模式打开(默认是文本模式),不进行换行符等特殊字符的转换。
2.文件操作相关的流
都定义在<fstream>头文件中。
ifstream:输入流,默认使用ios::in打开方式
**ofstream:**输出流,默认使用ios::out打开方式
fstream:文件流,无默认模式,同时支持读和写操作
二.普通文件操作的具体流程
1.写文件
cpp
//创建输出流对象并打开文件(如果文件不存在会自动创建)
ofstream outFile("test.txt", ios::out);//这里的打开方式也可以不加,使用默认的
///检查文件是否成功打开
if (!outFile.is_open())
{
throw "文件打开失败";
}
//写文件
outFile << "ABCD" << endl;
//关闭文件
outFile.close();
当然这里我们也可以使用ofstream,但是这样的话不能省略打开方式。
2.读文件
cpp
//创建输入流对象并打开文件
ifstream inFile("test.txt", ios::in);//这里的打开方式也可以不加,使用默认的
///检查文件是否成功打开
if (!inFile.is_open())
{
throw "文件打开失败";
}
//读文件(按行读取)
string line;
while (getline(inFile, line)) {
cout << line << endl;
}
//关闭文件
inFile.close();
类似的,我们也可以使用ofstream,但是这样的话不能省略打开方式。
关于这里的读取方式,其实存在许多种,除了使用(getline)按行读取,我们还可以使用>>操作符读取,但是>>操作符无法读取空格(空格会被当作分隔符):
cpp
string word;
while (inFile >> word) {
cout << word << endl;
}
我们还可以使用get,按字符读取,包括空格和换行符,但这样相较于getline,函数调用次数过多,效率较低:
cpp
char c;
while (inFile.get(c)) {
cout << c;
}
三.二进制文件的文件操作
对于复杂数据结构的文件读写操作,我们一般使用二进制的文件读写方式,相较于普通的文件读写方式,使用二进制,可以提高读写的速度,减少文件的大小,确保数据的绝对精确(主要对于浮点数,由于进制转换的先天缺陷)。
二进制的读写必须使用write与read函数,write(数据的地址, 数据的字节大小),read(存放数据的地址, 数据的字节大小)
例如,我们想要将一个Student对象的信息写进文件中:
cpp
struct Student
{
char _name[20];
int _age;
};
int main()
{
Student s1 {"zhangsan",18};
//指定多种打开方式时,可以使用"|"
ofstream outFile("Student.dat", ios::out | ios::binary);
if (!outFile.is_open())
{
throw "文件打开失败";
}
//使用 reinterpret_cast<const char*> 将结构体地址转为字节指针
outFile.write(reinterpret_cast<const char*>(&s1), sizeof(Student));
outFile.close();
Student s2;//准备一个空的结构体用来接收数据
ifstream inFie("Student.dat", ios::in | ios::binary);
//使用 reinterpret_cast<char*> 将结构体地址转为字节指针
inFie.read(reinterpret_cast<char*>(&s2), sizeof(Student));
//检查实际读取的字节数是否符合预期(防止文件损坏或读取不完整)
if (inFie.gcount() == sizeof(Student))
{
cout << "name:" << s2._name << endl
<< "age:" << s2._age<<endl;
}
else
{
cerr << "读取数据不完整" << endl;
}
inFie.close();
}
实际上对于二进制读写类,我们一般要求这个类其需要是POD类型(普通的旧式数据),POD类型要求这个类没有构造,析构,拷贝构造,赋值引用,不能有虚函数或虚基类,所有非静态数据成员必须有相同的访问控制权限(即与C语言的结构体相同)。
注意:对于包含动态数据的复杂类型(vector,string等),绝对不能直接写入文件,解决办法是先写入字符串的长度(size_t),再写入字符串的实际内容(data())。