文章目录
- C++的IO流
-
- C语言的输入输出与缓冲区
- 流是什么
- [C++ IO流](#C++ IO流)
-
- [C++ 标准IO流](#C++ 标准IO流)
-
- [cin 缓冲与输入规则](#cin 缓冲与输入规则)
- [循环输入与 operator bool](#循环输入与 operator bool)
- 自定义类型重载流插入和流提取
- C++文件IO流
- stringstream
-
- [stringstream 基本概念](#stringstream 基本概念)
- stringstream的使用场景
C++的IO流
C语言的输入输出与缓冲区
C语言中最常见的输入输出方式是 scanf() 与 printf()。scanf() 从标准输入设备(通常是键盘)读取数据,并按格式说明符把值存入变量;printf() 把指定文字或格式化数据输出到标准输出设备(通常是屏幕)。C语言输入输出并不是直接和硬件交互,而是借助输入缓冲区、输出缓冲区完成数据中转。缓冲区的存在可以屏蔽底层低级 I/O 的实现差异,也能支持"按行读取"等更符合人类习惯的行为。

流是什么
"流"可以理解为数据从一处流向另一处的抽象过程。C++ 中的流是对"有序、连续、具有方向性的数据"的抽象描述,数据单位可以是 bit、byte、packet 等。输入流表示数据从外部设备流向内存,输出流表示数据从内存流向外部设备。C++ 通过流类库把这种输入输出过程封装成对象,使程序员可以用统一方式操作键盘、屏幕、文件和字符串。
流的核心特性:是"有序连续"和"具有方向性"。有序连续意味着数据按照一定顺序到达或写出,不能随意跳跃;方向性意味着输入流和输出流职责不同,输入流用于提取数据,输出流用于插入数据。
cpp
#include <iostream>
int main()
{
int x = 0;
std::cout << "请输入一个整数:";
std::cin >> x; // 数据从键盘流向内存
std::cout << "x = " << x << std::endl; // 数据从内存流向屏幕
return 0;
}
C++ IO流
C++ 系统实现了一个庞大的 IO 类库,其中 ios 是基类,其他流类大多直接或间接派生自 ios。标准输入输出流、文件流、字符串流都建立在这套继承体系之上。

C++ 标准IO流
C++ 标准库提供了四个全局标准流对象:cin、cout、cerr、clog。cin 是 istream 类型对象,用于标准输入;cout、cerr、clog 是 ostream 类型对象,用于标准输出、错误输出和日志输出。它们的基本插入/提取方式相似,但应用场景和缓冲策略不同:cerr 通常用于立即显示错误信息,clog 通常用于带缓冲的日志输出。使用这些对象需要包含 <iostream> 并引入 std 命名空间。
cpp
int main()
{
int x = 10;
std::cout << "标准输出:x = " << x << std::endl;
std::cerr << "标准错误:用于立即提示错误" << std::endl;
std::clog << "日志输出:用于记录日志" << std::endl;
std::cin >> x;
std::cout << "重新输入后 x = " << x << std::endl;
return 0;
}
注意:
cout、cerr、clog都是ostream对象,但cerr通常不带缓冲或立即刷新,适合错误提示;clog带缓冲,适合日志。不要简单认为三者完全等价。- 标准流对象是全局对象,使用时必须确保包含正确头文件并正确使用命名空间。
endl不仅输出换行,还会刷新缓冲区;频繁使用可能影响性能,普通换行可用\n。
cin 缓冲与输入规则
cin 是缓冲流:输入先进入输入缓冲区,再由 cin 提取;一次输入过多,剩余数据留在缓冲区供后续读取。回车前可修改,回车后通常无法撤回。提取类型必须匹配,否则会置流状态位,程序可继续但后续提取可能失败。空格和回车可作分隔符,多数据可一行或分行输入;但用 >> 读字符/字符串时不能读空格,也不能直接读回车。
cpp
int main()
{
int id = 0;
std::string line;
std::cout << "请输入编号:";
std::cin >> id;
// 清除输入缓冲区中残留的换行符,否则 getline 可能直接读到空行
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "请输入一整行内容:";
std::getline(std::cin, line);
std::cout << "id=" << id << ", line=" << line << std::endl;
return 0;
}
循环输入与 operator bool
在线编程题中经常需要循环输入。while (cin >> a) 能成立的原因是:operator>> 返回 istream&,而 istream 提供了 explicit operator bool() const,当流读取成功时转换为 true,当读取失败或到达文件末尾时转换为 false。因此 while (cin >> a) 既完成了输入,又完成了循环条件判断。类似地,可以写 while (cin >> a >> b >> c) 连续读取多个元素。
cpp
int main()
{
int x = 0;
// 单个元素循环输入,Windows 下 Ctrl+Z 结束,Linux 下 Ctrl+D 结束
while (std::cin >> x) {
std::cout << "x * 2 = " << x * 2 << std::endl;
}
std::cout << "输入结束" << std::endl;
return 0;
}
自定义类型重载流插入和流提取
C++ 标准库已经为所有内置类型重载了<<和 >>,所以 cin、cout 可以直接输入输出 int、double 等类型。对于自定义类型,如果要支持标准输入输出,需要自己重载流插入运算符 << 和流提取运算符 >>。通常把它们写成全局函数,并声明为类的友元,以便访问私有成员;返回值必须是流引用,才能支持链式操作和 while (cin >> d) 这类写法。
cpp
class Date {
friend std::ostream& operator<<(std::ostream& out, const Date& d);
friend std::istream& operator>>(std::istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
explicit operator bool() const {
// 示例规则:年份为 0 时认为无效,可用于结束循环
return _year != 0;
}
private:
int _year;
int _month;
int _day;
};
std::istream& operator>>(std::istream& in, Date& d) {
in >> d._year >> d._month >> d._day;
return in;
}
std::ostream& operator<<(std::ostream& out, const Date& d) {
out << d._year << "-" << d._month << "-" << d._day;
return out;
}
int main() {
Date d(2022, 4, 10);
std::cout << d << std::endl;
while (std::cin >> d) { // 这里调用的是istream库
if (!d) { // 这里调用 Date::operator bool()
break;
}
std::cout << d << std::endl;
}
return 0;
}
C++文件IO流
C++ 根据文件内容的数据格式,通常分为二进制文件和文本文件。文件流操作的一般步骤是:定义文件流对象、打开磁盘文件建立联系、使用提取/插入运算符或成员函数读写、关闭文件。ifstream 用于只输入,ofstream 用于只输出,fstream 用于既输入又输出。
文件流对象与打开模式
文件流对象把磁盘文件抽象成流,使我们可以像操作cin、cout 一样操作文件。打开文件时可以指定模式,例如 ios::in 表示读取,ios::out 表示写出,ios::binary 表示二进制模式,ios::trunc 表示截断文件,ios::app 表示追加。ifstream 默认包含输入模式,ofstream 默认包含输出模式并通常会截断已有文件。工程代码中必须检查文件是否成功打开。
cpp
#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ofstream ofs("demo.txt", std::ios::out | std::ios::trunc);
if (!ofs) {
std::cerr << "打开输出文件失败\n";
return 1;
}
ofs << "hello\n" << "world\n";
ofs.close();
std::ifstream ifs("demo.txt");
if (!ifs) {
std::cerr << "打开输入文件失败\n";
return 1;
}
std::string line;
while (std::getline(ifs, line)) {
std::cout << line << std::endl;
}
return 0;
}
注意:
- 使用文件流后应检查
is_open()或直接判断流对象,否则文件不存在、权限不足时后续读写会失败。 ofstream默认可能截断文件,如果希望追加内容,应使用std::ios::app。- 文件使用完毕后应
close(),虽然析构时通常也会关闭,但显式关闭可以更早释放资源并便于检查错误。
二进制文件读写
二进制文件按内存字节形式读写,常用 write() / read()。适合保存结构体、数组等"按内存布局"的数据,效率高,但可移植性差:不同编译器/平台在结构体填充、大小端、类型长度上可能不同。用二进制保存自定义类型时,该类型应尽量简单可拷贝,不能包含指针、虚函数等依赖进程地址的内容。
cpp
#include <stdexcept>
struct ServerInfo {
char _address[32];
int _port;
Date _date;
};
class ConfigManagerBin {
public:
explicit ConfigManagerBin(const char* filename) : _filename(filename) {}
void writeBin(const ServerInfo& info) {
std::ofstream ofs(_filename, std::ios::out | std::ios::binary | std::ios::trunc);
if (!ofs) {
throw std::runtime_error("打开二进制文件失败");
}
ofs.write(reinterpret_cast<const char*>(&info), sizeof(info));
if (!ofs) {
throw std::runtime_error("二进制写入失败");
}
}
void readBin(ServerInfo& info) {
std::ifstream ifs(_filename, std::ios::in | std::ios::binary);
if (!ifs) {
throw std::runtime_error("打开二进制文件失败");
}
ifs.read(reinterpret_cast<char*>(&info), sizeof(info));
if (!ifs) {
throw std::runtime_error("二进制读取失败");
}
}
private:
std::string _filename;
};
int main() {
ServerInfo winfo = {"192.0.0.1", 80, Date(2022, 4, 10)};
ConfigManagerBin cf("test.bin");
cf.writeBin(winfo);
ServerInfo rbinfo;
cf.readBin(rbinfo);
std::cout << rbinfo._address << " "
<< rbinfo._port << " "
<< rbinfo._date << std::endl;
return 0;
}
注意:直接 write((char*)&info, sizeof(info)) 会把结构体填充字节也写入,不同编译器下sizeof(ServerInfo) 可能不同,导致文件格式不兼容。
文本文件读写
文本文件读写用流插入/提取运算符(<< / >>),把数据格式化成可读文本。优点是直观、易调试、跨平台处理换行较方便;缺点是要解析、效率通常低于二进制、字段中不能随意含分隔符。自定义类型的文本读写同样靠重载 << / >>。若字符串字段可能含空格,不能简单用 >> 按空白分隔读取,应设计明确的分隔格式或改用 JSON/XML。
cpp
void writeText(const char* filename, const ServerInfo& info) {
std::ofstream ofs(filename, std::ios::out | std::ios::trunc);
if (!ofs) {
throw std::runtime_error("打开文本文件失败");
}
ofs << info._address << " "
<< info._port << " "
<< info._date << "\n";
}
void readText(const char* filename, ServerInfo& info) {
std::ifstream ifs(filename);
if (!ifs) {
throw std::runtime_error("打开文本文件失败");
}
ifs >> info._address >> info._port >> info._date;
if (!ifs) {
throw std::runtime_error("文本读取失败");
}
}
int main() {
ServerInfo winfo = {"192.0.0.1", 80, Date(2022, 4, 10)};
writeText("test.txt", winfo);
ServerInfo rtinfo;
readText("test.txt", rtinfo);
std::cout << rtinfo._address << " "
<< rtinfo._port << " "
<< rtinfo._date << std::endl;
return 0;
}
注意:
- 文本读写依赖字段分隔符,若字符串字段包含空格,
ifs >> info._address只能读到第一个单词,后续字段会错位。 - 读取后应检查流状态,文件格式不符合预期时不能继续使用未完整读取的对象。
- 文本模式在不同平台上的换行符处理可能不同;对格式要求严格的场景应明确使用二进制模式或统一换行约定。
stringstream
在C语言中,如果想要将一个整形变量的数据转化为字符串格式,如何去做?
- 使用
itoa()函数 - 使用
sprintf()函数
但是两个函数在转化时,都得需要先给出保存结果的空间,那空间要给多大呢,就不太好界定,而且转化格式不匹配时,可能还会得到错误的结果甚至程序崩溃。
stringstream 是 C++ 字符串流类,定义在 <sstream> 中。该头文件下主要有 istringstream、ostringstream 和 stringstream,分别用于字符串输入、字符串输出和双向输入输出。stringstream 底层维护一个 string 对象来保存结果,可以避免 C 语言 itoa()、sprintf() 需要预先分配字符数组、格式化不匹配可能导致崩溃等问题。它常用于数值与字符串互转、字符串拼接、结构数据序列化和反序列化。
stringstream 基本概念
stringstream 把字符串当流用,可像 cin/cout 一样使用 << 和 >>。它底层管理一个 string,str() 取出底层字符串,str("") 清空。相比字符数组,无需预估缓冲区大小,且会推导参数类型,减少格式化错误。多次转换时要注意流状态和底层字符串内容两个层面。
cpp
#include <sstream>
int main() {
std::stringstream ss;
ss << "abc" << 123;
std::cout << ss.str() << std::endl;
return 0;
}
stringstream的使用场景
- 数值类型与字符串互转
C 语言中,整数转字符串可用itoa()或sprintf(),但都需预先分配空间,大小难界定;格式不匹配时可能出错甚至崩溃。stringstream用string管理结果,无需预估字符数组大小,类型转换也更安全。使用时先写入数值,再提取到字符串,或直接用str()取底层字符串。多次转换时须同时处理"流状态"和"底层字符串内容"。
cpp
int main() {
int a = 12345678;
std::string sa;
std::stringstream s;
s << a;
s >> sa;
std::cout << "第一次转换: " << sa << std::endl;
// 清空底层字符串内容
s.str("");
// 清除上一次提取后可能产生的 eof/fail 状态
s.clear();
double d = 12.34;
s << d;
s >> sa;
std::cout << "第二次转换: " << sa << std::endl;
std::string svalue = s.str();
std::cout << "底层字符串: " << svalue << std::endl;
return 0;
}
注意:复用 stringstream 时 clear() 和 str("") 都要调用:只 clear() 不清内容会累积(如 "1234567812.34");只 str("") 不清状态,残留的 eofbit/failbit 会导致下次转换失败。提取到末尾可能置失败/EOF 状态,复用前必须重置。通常先 str("") 再 clear(),顺序可反,但两者都不可少。
- 字符串拼接
stringstream可以像输出流一样把多个字符串、数值拼接成一个字符串。与直接用 + 拼接相比,它尤其适合混合多种类型的数据,例如把字符串、整数、浮点数组合成一条日志或配置文本。拼接完成后通过str()取出结果。若需要复用同一个流,应先清空底层字符串。
cpp
int main() {
std::stringstream sstream;
sstream << "first" << " " << "string,";
sstream << " second string";
std::cout << "strResult is: " << sstream.str() << std::endl;
sstream.str("");
sstream << "third string";
std::cout << "After clear, strResult is: " << sstream.str() << std::endl;
return 0;
}
- 结构数据序列化与反序列化
- 序列化是把内存中的结构数据转换为可存储或可传输的字符串;
- 反序列化是把字符串解析回结构数据。
ostringstream适合把结构体字段按约定格式写入字符串,istringstream适合按相同格式提取字段。
cpp
struct ChatInfo {
std::string _name;
int _id;
Date _date;
std::string _msg;
};
int main() {
ChatInfo winfo = {"张三", 135246, Date(2022, 4, 10), "晚上一起看电影吧"};
std::ostringstream oss;
oss << winfo._name << " "
<< winfo._id << " "
<< winfo._date << " "
<< winfo._msg;
std::string str = oss.str();
std::cout << "序列化结果: " << str << std::endl;
ChatInfo rInfo;
std::istringstream iss(str);
iss >> rInfo._name >> rInfo._id >> rInfo._date >> rInfo._msg;
if (!iss) {
std::cerr << "反序列化失败" << std::endl;
return 1;
}
std::cout << "姓名: " << rInfo._name << "(" << rInfo._id << ")\n";
std::cout << "时间: " << rInfo._date << "\n";
std::cout << "消息: " << rInfo._msg << std::endl;
return 0;
}
注意:用空格分隔字段时,消息内容不能包含空格,否则 iss >> rInfo._msg 只能读到第一个单词,后续解析会错位。
完