C++IO库
C++IO库
- 1. IO继承家族类
- 2. IO流状态
- 3. 管理输出缓冲区
- 4. 标准IO流
- 5. 文件IO流
- 6. string IO流
1. IO继承家族类
- C++语⾔不直接处理输⼊输出,⽽是通过家族定义在标准库中的类型在处理IO。这些类型⽀持从设备中读取数据和向设备中写⼊数据的IO操作,设备可以是⽂件、控制台窗⼝等。
- 到⽬前为⽌,我们已经使⽤过的IO类型和对象都是操纵char数据的,默认情况下这些对象都是关联到用户的控制台窗⼝。但是实际中IO类不仅仅是从控制台窗⼝控制输⼊输出,还⽀持⽂件和string类的IO操作。其次IO类型是⽤模板实现的,还⽀持对wchar_t数据的输⼊输出。
- 通过下图1-1和1-2可以看到C++IO类型设计的是⼀个继承家族,通过继承家族类解决控制台/⽂件/string的IO操作。
- https://legacy.cplusplus.com/reference/
- https://zh.cppreference.com/w/cpp/io
2. IO流状态
- IO操作的过程中,可能会发⽣各种错误,IO流对象中给了四种状态标识错误,可以参考下图2-1和2-2进⾏理解。goodbit表示流没有错误/eofbit表示流到达⽂件结束/failbit表示IO操作失败了/badbit表示流崩溃了出现了系统级错误。
- ⼀个常⻅的IO流错误是cin>>i,i是⼀个int类型的对象,如果我们在控制台输⼊⼀个字符,cin对象的failbit状态位就会被设置,cin就进⼊错误状态,⼀个流⼀旦发⽣错误,后续的IO操作都会失败,我们可以调⽤cin.clear()函数来恢复cin的状态为goodbit。
- badbit表示系统级错误,如不可恢复的读写错误,通常情况下,badbit⼀旦被设置了,流就⽆法再使⽤了。
- failbit表示⼀个逻辑错误,如期望读取⼀个整形,但是却读取到⼀个字符,failbit被设置了,流是可以恢复的,恢复以后可以继续使⽤。
- 如果到达⽂件结束位置eofbit和failbit都会被置位。如果想再次读取当前⽂件,可以恢复⼀下流的状态,同时重置⼀个⽂件指针位置。
- goodbit表示流未发⽣错误。
- 也可以⽤setstate和rdstate两个函数来控制流状态,eofbit/failbit/badbit/goodbit是ios_base基类中定义的静态成员变量,是可以直接使⽤的,并且是他们是可以组合的位运算值,具体使⽤细节可以参考⽂档。
#include <iostream>using namespace std;int main()
{cout << cin.good() << endl;cout << cin.eof() << endl;cout << cin.bad() << endl;cout << cin.fail() << endl << endl;int i = 0;// 输入一个字符或多个字符,cin读取失败,流状态被标记为failbitcin >> i;cout << i << endl;cout << cin.good() << endl;cout << cin.eof() << endl;cout << cin.bad() << endl;cout << cin.fail() << endl << endl;if (cin.fail()){// clear可以恢复流状态为goodbitcin.clear();// 我们还要把缓冲区中的一个或多个字符都读取出来// 读到数字停下来,否则再去cin >> i还是会失败char ch = cin.peek(); // 看下一个要读取的是什么while (!(ch >= '0' && ch <= '9')){ch = cin.get(); //读取字符cout << ch;ch = cin.peek();}cout << endl;}cout << cin.good() << endl;cout << cin.eof() << endl;cout << cin.bad() << endl;cout << cin.fail() << endl << endl;cin >> i;cout << i << endl;return 0;
}
3. 管理输出缓冲区
- 任何输出流都管理着⼀个缓冲区,⽤来保存程序写的数据。如果我们执⾏os << “hello world”;字符串可能⽴即输出,也可能被操作系统保存在缓冲区中,随后再输出。有了缓冲区机制,操作系统就可能将多个输出操作组合成为⼀个单⼀的系统级写操作。因为设备的写操作通常是很耗时的,允许操作系统将多个输出操作组合为单⼀的设备写操作肯可能带来很⼤的性能提升。
- 会触发缓冲区刷新,将数据真正的写到输出设备或⽂件的原因有很多,如:<1>程序正常结束;<2>缓冲区满了;<3>输出了操纵符endl或flush会⽴即刷新缓冲区<4>我们使⽤了操作符unitbuf设置流的内部状态,来清空缓冲区,cerr就设置了unitbuf,所以cerr输出都是⽴即刷新的。<5>⼀个输出流关联到另⼀个流时,当这个流读写时,输出流会⽴即刷新缓冲区。例如默认情况下cerr和cin都被关联到cout,所以读cin或写cerr时,都会导致cout缓冲区会被⽴即刷新。
- tie可以⽀持跟其他流绑定和解绑,具体参考⽂档https://legacy.cplusplus.com/reference/ios/ios/tie/
#include <iostream>
#include <fstream>using namespace std;void func(ostream& os)
{os << "hello world";os << "hello bit";// "hello world" 和 "hello bit"是否输出不确定system("pause");// 遇到endl,"hello world"和"hello bit"一定刷新缓冲区输出了//os << endl;//os << flush;int i;cin >> i;os << "hello cat";// "hello cat"是否输出不确定system("pause");
}int main()
{ofstream ofs("test.txt");//func(cout);// unitbuf设置后,ofs每次写都直接刷新//ofs << unitbuf;// cin绑定到ofs,cin进行读时,会刷新ofs的缓冲区cin.tie(&ofs);func(ofs);return 0;
}
#include <iostream>using namespace std;int main()
{// 在io需求比较高的地方,如部分大量输入的竞赛题中,加上以下几行代码可以提高C++IO效率// 并且建议用'\n'代替endl,因为endl会刷新缓冲区// 关闭同步,C++ 流不再与 C 流同步,大幅提升 I/O 速度(尤其是大量数据时)// 但不能再混用 cin/cout 和 scanf/printf,否则可能导致程序崩溃或输出乱序。ios_base::sync_with_stdio(false);// 关闭同步后,以下程序的输出顺序可能为 b a ccout << "a\n";printf("b\n");cout << "c\n";// 解绑cin和cout关联绑定的其他流cin.tie(nullptr);cout.tie(nullptr);return 0;
}
4. 标准IO流
- C++标准IO流前⾯已经使⽤得⽐较多了,C++标准IO流默认是关联到控制台窗⼝的。cin是istream类型全局对象,cout/cerr/clog是ostream类型的全局对象,内置类型这两个类都直接进⾏了重载实现,所以可以直接使⽤,⾃定义类型就需要我们⾃⼰重载<<和>>运算符。
- ostream和istream是不⽀持拷⻉的,只⽀持移动(外部不能使⽤,因为是保护成员)。
- istream的cin对象⽀持转换为bool值,进⾏条件逻辑判断,⼀旦被设置了badbit或failbit标志位,就返回false,如果是goodbit就返回true。
- ostream和istream还有不少其他接⼝,实践中相对⽤得⽐较少,需要时⼤家查查⽂档。
#include <iostream>
#include <fstream>
#include <string>using namespace std;int main()
{int i = 0, j = 1;// 持续的输入,要结束需要输入Ctrl+Z换行,Ctrl+Z用于告诉程序输入已经完成// 类似于在文件末尾添加一个标记// istream& operator>>(int i),>>运算符重载的返回值是istream对象// istream对象可以调用operator bool转换为bool值// 本质在底层是将cin的eofbit和failbit标志位设置了,cin调用operator bool// 函数语法逻辑上实现转换为bool值// cin的operator bool 前面加了explicit,是不支持隐式类型转换的// cout << (bool)cin;while (cin >> i >> j) // 但是这里却隐式类型转换了,可以理解为编译器对这里做了特殊处理{cout << i << ":" << j << endl;}cout << cin.good() << endl;cout << cin.eof() << endl;cout << cin.bad() << endl;cout << cin.fail() << endl << endl;// 流一旦发生错误就不能再用了,清理重置一下才能使用cin.clear();string s;while (cin >> s){cout << s << endl;}return 0;
}
5. 文件IO流
- ofstream是输出⽂件流,也就是写⽂件的流,ofstream是ostream的派⽣类;ifstream是输⼊⽂件流,也就是读⽂件的流,ifstream是istream的派⽣类;fstream是ifstream和ofstream的派⽣类,既可以读也可以写。
- https://legacy.cplusplus.com/reference/fstream/
- https://zh.cppreference.com/w/cpp/io
- ⽂件流对象可以在构造时打开⽂件,也可以调⽤open函数打开⽂件,打开⽂件的mode有5-1图中的⼏种。in为读打开;out为写打开;binary以⼆进制模式打开;ate打开后⽴即寻位到流结尾;app每次写⼊前寻位到流结尾;trunc在打开时舍弃流的内容;这些值是ios_base中定义的成员变量继承下来的,并且他们也是组合的独⽴⼆进制位值,需要组合时,可以或到⼀起。他们之间的区别,具体参考下⾯的代码演示。
- ⽂件流打开后如果需要可以主动调⽤close函数关闭,也可以不关闭,因为流对象析构函数中会关闭。
- ⽂件流打开⽂件失败或读写失败,也会使⽤IO流状态标记,我们调⽤operator bool或operator!判断即可。
- ifstream⽂件流的读数据主要可以使⽤get/read/>>重载,ofstream⽂件流写数据主要可以使⽤put/write/<<重载,具体主要参考下⾯代码的演示。
- 相⽐C语⾔⽂件读写的接⼝,C++fstream流功能更强⼤⽅便,使⽤<<和>>进⾏⽂件读写很⽅便,尤其是针对⾃定义类型对象的读写。
#include <iostream>
#include <fstream>
#include <string>using namespace std;int main()
{ofstream ofs("test.txt");// 字符和字符串的写ofs.put('x');ofs.write("hello\nworld", 11);// 使用<<进行写ofs << "22222222" << endl;int x = 111;double y = 1.11;ofs << x << endl;ofs << y << endl;ofs.close();// app和ate都是尾部追加,不同的是app不能移动文件指针,永远在文件尾写// ate可以移动文件指针,写到其他位置,但是ate有点问题,建议慎用,下面使用会讲ofs.open("test.txt", ios_base::out | ios_base::app);ofs << "1111111" << endl;// seekp 是移动文件指针用的ofs.seekp(0, ios_base::beg); // 但是由于app的原因,指针还是在文件末尾ofs << x << " " << y << endl;ofs.close();// out是默认就会加的,out写会清空文件,但ate又阻止不了,所以要慎用/*ofs.open("test.txt", ios_base::out | ios_base::ate);*/// 要想用ate的话,可以加上ios_base::in,它不会清空文件ofs.open("test.txt", ios_base::out | ios_base::in | ios_base::ate);ofs << "1111111" << endl;ofs.seekp(0, ios_base::beg); // 这次是写在开头,但是要注意写的时候会覆盖写的位置的字符ofs << x << " " << y << endl;ofs.close();// out 和 out | trunc都会先把数据清掉,再写数据(官方文档明确是这样写的)// https://en.cppreference.com/w/cpp/io/basic_filebuf/open// 那么trunc存在的意义是什么呢?out | trunc更明确地表达了文件中有内容时要清除掉// 对于代码维护者和阅读者来说能清晰地理解这个行为,在一些复杂地文件系统环境或不同的// C++文件流实现库中,out行为不完全等同于截断内容地情况(虽然当前主流实现基本一致)// out | trunc更明确地表达要清楚内容的行为ofs.open("test.txt", ios_base::out);//ofs.open("test.txt", ios_base::out | ios_base::trunc);ofs << "xxxx";ofs.close();return 0;
}
int main()
{// 实现一个图片文件的复制,需要用二进制方式打开读写,第一个参数可以给文件的绝对路径ifstream ifs("C:\\Users\\48784\\Desktop\\Code\\picture\\10-18.png", ios_base::in | ios_base::binary);ofstream ofs("C:\\Users\\48784\\Desktop\\Code\\picture\\10-18-copy.png", ios_base::out | ios_base::binary);int n = 0;while (ifs && ofs){char ch = ifs.get();ofs << ch;++n;}cout << n << endl;cout << ofs.good() << endl;cout << ofs.eof() << endl;cout << ofs.bad() << endl;cout << ofs.fail() << endl << endl;return 0;
}
#include<iostream>
#include<fstream>
#include<string>using namespace std;class Date
{friend ostream& operator << (ostream& out, const Date& d);friend istream& operator >> (istream& in, Date& d);
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};istream& operator >> (istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;
}ostream& operator << (ostream& out, const Date& d)
{out << d._year << " " << d._month << " " << d._day << endl;return out;
}struct ServerInfo
{// 二进制读写时,这里不能用string,否则写到文件中的是string中指向字符数组的指针// 若string对象析构后再再去文件中读取string对象,string中读到是一个野指针。char _address[32];// string _address;int _port;Date _date;
};struct ConfigManager
{
public:ConfigManager(const char* filename):_filename(filename){}// 二进制写// 内存中怎么存,囫囵吞枣,就怎么直接写出去void WriteBin(const ServerInfo& info){ofstream ofs(_filename, ios_base::out | ios_base::binary);ofs.write((const char*)&info, sizeof(info));}// 二进制读// 将文件中的内容直接囫囵吞枣,直接读到内存中void ReadBin(ServerInfo& info){ifstream ifs(_filename, ios_base::in | ios_base::binary);ifs.read((char*)&info, sizeof(info));}void WriteText(const ServerInfo& info){ofstream ofs(_filename);ofs << info._address << " " << info._port << " " << info._date;}void ReadText(ServerInfo& info){ifstream ifs(_filename);ifs >> info._address >> info._port >> info._date;}private:string _filename; //配置文件
};void WriteBin()
{ServerInfo winfo = { "192.0.0.11111111111111111111", 80, {2025,1,10} };// 二进制读写ConfigManager cf_bin("test.bin");cf_bin.WriteBin(winfo);
}void ReadBin()
{// 二进制读写ConfigManager cf_bin("test.bin");ServerInfo rbinfo;cf_bin.ReadBin(rbinfo);cout << rbinfo._address << " " << rbinfo._port << " " << rbinfo._date << endl;
}void WriteText()
{ServerInfo winfo = { "192.0.0.1", 80, {2025,1,10} };// 文本读写ConfigManager cf_text("test.txt");cf_text.WriteText(winfo);
}void ReadText()
{ConfigManager cf_text("test.txt");ServerInfo rtinfo;cf_text.ReadText(rtinfo);cout << rtinfo._address << " " << rtinfo._port << " " << rtinfo._date << endl;
}int main()
{WriteBin();ReadBin();WriteText();ReadText();return 0;
}
6. string IO流
- ostringstream是string的的写⼊流,ostringstream是ostream的派⽣类;istringstream是string的的读出流,istringstream是istream的派⽣类;stringstream是ostringstream和istringstream的派⽣类,既可以读也可以写。这⾥使⽤stringstream会很⽅便。
- stringstream系列底层维护了⼀个string类型的对象⽤来保存结果,使⽤⽅法跟上⾯的⽂件流类似,只是数据读写交互的都是底层的string对象。
- stringstream最常⽤的⽅式还是使⽤<<和>>重载,进⾏数据和string之间的IO转换。
- string流使⽤str函数获取底层的string对象,或者写⼊底层的string对象,具体细节参考下⾯代码理解。
#include <iostream>
#include <sstream>
#include <string>using namespace std;int main()
{int i = 123;Date d = { 2025, 10, 18 };ostringstream oss;oss << i << endl;oss << d << endl;string s = oss.str();cout << s << endl;//stringstream iss(s);//stringstream iss;//iss.str("100 2025 10 17");istringstream iss("100 2025 10 17");int j;Date x;iss >> j >> x;cout << j << endl;cout << x << endl;int a = 1234;int b = 5678;string str;// 将一个整型变量转化为字符串,存储到string类对象中stringstream ss;ss << a << " " << b;ss >> str;cout << str << endl;ss >> str;cout << str << endl;ss >> str;cout << str << endl;cout << ss.fail() << endl;cout << ss.bad() << endl;// 注意多次转换时,必须使用clear将上次转换的状态清空掉// stringstream在转换结尾时(即最后一个转换后),会将其内部状态设置为// badbit和failbit,因此下一次转换必须调用clear()将状态重置为goodbit才可以转换// 但是clear()不会将stringstream底层字符串清空掉,str给一个空串可以清掉底层的字符串ss.clear();ss.str("");double dd = 12.34;ss << dd;ss >> str;cout << str << endl;return 0;
}
#include<iostream>
#include<sstream>
#include<string>using namespace std;class Date
{friend ostream& operator << (ostream& out, const Date& d);friend istream& operator >> (istream& in, Date& d);
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};
istream& operator >> (istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;
}
ostream& operator << (ostream& out, const Date& d)
{out << d._year << " " << d._month << " " << d._day << endl;return out;
}struct ChatInfo
{string _name; // 名字int _id; // idDate _date; // 时间string _msg; // 聊天信息
};int main()
{// 结构信息序列化为字符串ChatInfo winfo = { "张三", 135246, {2025,10,18}, "晚上一起看电影吧" };ostringstream oss;oss << winfo._name << " " << winfo._id << " " << winfo._date << " " << winfo._msg;string str = oss.str();cout << str << endl << endl;// 我们通过网络这个字符串发送给对象,实际开发中,信息相对更复杂,// 一般会选用Json、xml等方式进行更好的支持// 字符串解析成结构信息ChatInfo rInfo;istringstream iss(str);iss >> rInfo._name >> rInfo._id >> rInfo._date >> rInfo._msg;cout << "——————————————————————————————————-" << endl;cout << "姓名:" << rInfo._name << "(" << rInfo._id << ") ";cout << rInfo._date << endl;cout << rInfo._name << ":>" << rInfo._msg << endl;cout << "——————————————————————————————————-" << endl;return 0;
}