C++IO流
C++的IO流库是一个用于输入输出的强大、可扩展但有时也有些令人困惑的框架。我们将从基础概念一直讲到高级用法。
1. IO流库的核心概念与层次结构
原理与作用
C++的IO流库提供了类型安全、可扩展的输入输出机制。相比于C语言的printf
和scanf
,它的核心优势在于:
- 类型安全:编译器能检查类型是否匹配,不会出现
%d
对应double
的运行时错误。 - 可扩展性:你可以通过重载
<<
和>>
操作符,让你自定义的类也能像内置类型一样进行IO操作。 - 状态管理:流对象维护一个状态,可以检测IO操作是否成功(如文件结束、格式错误等)。
层次结构
C++ IO流库的核心是一个复杂的继承体系,理解它是掌握流库的关键。
ios_base
|
ios
/ \
/ \
/ \
/ \
basic_istream<> basic_ostream<>
\ /
\ /
\ /
\ /
basic_iostream<>
|
|
----------------------------
| | |
basic_ifstream<> basic_ofstream<> basic_fstream<>
(文件输入) (文件输出) (文件输入输出)
| | |
basic_istringstream<> basic_ostringstream<> basic_stringstream<>
(字符串输入) (字符串输出) (字符串输入输出)
ios_base
:定义了所有流类的基本属性,与模板参数无关(如格式标志、浮点数精度等)。basic_ios<>
:模板类,管理流的状态(如goodbit
,eofbit
,failbit
,badbit
)和流缓冲区(streambuf
)。basic_istream<>
,basic_ostream<>
,basic_iostream<>
:分别定义了输入、输出和输入输出的基本接口。我们常用的cin
(istream
)和cout
(ostream
)就是它们的特化别名。- 文件流 (
basic_ifstream<>
,basic_ofstream<>
,basic_fstream<>
):用于文件操作,继承自相应的通用流类。 - 字符串流 (
basic_istringstream<>
,basic_ostringstream<>
,basic_stringstream<>
):用于在内存中读写字符串,极其有用,继承自相应的通用流类。
常用的类型别名(特化char
类型):
typedef basic_ios<char> ios;
typedef basic_istream<char> istream;
typedef basic_ostream<char> ostream;
typedef basic_iostream<char> iostream;
typedef basic_ifstream<char> ifstream;
typedef basic_ofstream<char> ofstream;
typedef basic_fstream<char> fstream;
typedef basic_istringstream<char> istringstream;
typedef basic_ostringstream<char> ostringstream;
typedef basic_stringstream<char> stringstream;
2. 标准IO对象 (cin, cout, cerr, clog)
C++预定义了四个标准流对象,它们在<iostream>
头文件中声明。
对象 | 类型 | 缓冲情况 | 用途 |
---|---|---|---|
cin |
istream |
带缓冲 | 标准输入(通常对应键盘) |
cout |
ostream |
带缓冲 | 标准输出(通常对应终端/控制台) |
cerr |
ostream |
无缓冲 | 标准错误输出,用于显示错误信息,立即刷新 |
clog |
ostream |
带缓冲 | 标准错误输出,用于记录日志 |
代码示例与区别:
#include <iostream>
using namespace std;
int main() {
int value;
cout << "Please enter a number: "; // 输出到标准输出(缓冲)
cin >> value; // 从标准输入读取
if (cin.fail()) {
cerr << "Error: Invalid input!\n"; // 立即无缓冲输出错误信息
} else {
cout << "You entered: " << value << endl;
clog << "User successfully entered a number: " << value << '\n'; // 记录日志(缓冲)
}
// 演示缓冲区别:cout和clog可能不会立即显示,但cerr会。
cout << "This is cout (buffered).";
clog << "This is clog (buffered).";
cerr << "This is cerr (unbuffered).";
// 程序结束时,cout和clog的缓冲区才会被刷新并显示
return 0;
}
3. 格式化输入输出
流对象有一系列成员函数和操纵符 (Manipulators) 来控制输出的格式(如精度、宽度、进制等)。操纵符在<ios>
, <iomanip>
, <iostream>
中定义。
常用操纵符示例:
#include <iostream>
#include <iomanip> // 用于带参数的操纵符
int main() {
double pi = 3.141592653589793;
int num = 42;
// 1. 设置宽度(只对下一次IO有效)
std::cout << std::setw(10) << num << "|\n"; // 输出: " 42|"
// 2. 设置填充字符
std::cout << std::setfill('*') << std::setw(10) << num << "\n"; // 输出: "********42"
// 3. 设置浮点数精度和格式
std::cout << std::fixed << std::setprecision(2) << pi << "\n"; // 输出: "3.14"
std::cout << std::scientific << pi << "\n"; // 输出: "3.14e+00"
// 4. 设置进制
std::cout << std::hex << num << "\n"; // 十六进制: "2a"
std::cout << std::oct << num << "\n"; // 八进制: "52"
std::cout << std::dec << num << "\n"; // 改回十进制: "42"
// 5. 设置对齐
std::cout << std::left << std::setw(10) << num << "\n"; // 左对齐: "42 "
std::cout << std::right << std::setw(10) << num << "\n"; // 右对齐: " 42"
// 6. 布尔值输出格式
std::cout << std::boolalpha << true << " " << false << "\n"; // 输出: "true false"
std::cout << std::noboolalpha << true << "\n"; // 输出: "1"
return 0;
}
成员函数方式实现格式化:
#include <iostream>
int main() {
double pi = 3.14159;
// cout.precision() 等同于 setprecision
std::streamsize old_precision = std::cout.precision(4);
std::cout << pi << '\n'; // 输出 3.142
std::cout.precision(old_precision); // 恢复原有精度
// cout.width() 等同于 setw
std::cout.width(10);
std::cout << 123 << '\n'; // 输出 " 123"
return 0;
}
4. 文件输入输出 (File I/O)
使用ifstream
(读)、ofstream
(写)、fstream
(读写)类进行文件操作。核心步骤:打开 -> 操作 -> 关闭。
文件打开模式 (Open Modes):
模式标志 | 作用 |
---|---|
std::ios::in |
为读而打开 |
std::ios::out |
为写而打开(默认会截断文件) |
std::ios::app |
追加模式,所有写入都追加到文件末尾 |
std::ios::ate |
打开后定位到文件末尾 |
std::ios::trunc |
截断文件(如果文件存在) |
std::ios::binary |
二进制模式(非常重要!避免文本模式的转换) |
代码示例:
#include <iostream>
#include <fstream>
#include <string>
int main() {
// --- 写入文件 ---
std::ofstream outfile("example.txt"); // 隐式使用 ios::out | ios::trunc
// 显式指定模式:std::ofstream outfile("example.txt", std::ios::out | std::ios::app);
if (outfile.is_open()) { // 总是检查是否成功打开!
outfile << "This is a line.\n";
outfile << "This is another line.\n";
outfile.close(); // 可以显式关闭,但析构时也会自动关闭
} else {
std::cerr << "Unable to open file for writing!";
}
// --- 读取文件 ---
std::ifstream infile("example.txt"); // 隐式使用 ios::in
std::string line;
if (infile) { // 更简洁的检查方式:ifstream 重载了 bool 操作符
std::cout << "Reading file contents:\n";
while (std::getline(infile, line)) { // 逐行读取,推荐方式
std::cout << line << '\n';
}
infile.close();
}
// --- 读写文件 ---
std::fstream iofile("data.txt", std::ios::in | std::ios::out);
if (!iofile) {
// 如果文件不存在,就创建它
iofile.open("data.txt", std::ios::in | std::ios::out | std::ios::trunc);
}
if (iofile) {
iofile << "Some data";
// 移动文件指针到开始位置以便读取
iofile.seekg(0, std::ios::beg);
iofile >> line;
std::cout << "Read: " << line << std::endl;
iofile.close();
}
return 0;
}
5. 字符串流 (String Streams)
字符串流(<sstream>
)允许你像操作流一样操作字符串。这是极其强大和常用的工具,主要用于:
- 字符串格式化:将各种类型的数据组合成一个复杂的字符串。
- 字符串解析:从一个字符串中提取各种类型的数据。
- 类型转换:在字符串和其他数据类型之间进行转换。
代码示例:
#include <iostream>
#include <sstream>
#include <string>
int main() {
// --- 1. 字符串格式化 (替代 sprintf) ---
std::ostringstream oss;
std::string name = "Alice";
int age = 25;
double score = 87.5;
oss << "Name: " << name << ", Age: " << age << ", Score: " << score;
std::string info_str = oss.str(); // 获取格式化后的字符串
std::cout << info_str << std::endl; // 输出: Name: Alice, Age: 25, Score: 87.5
// --- 2. 字符串解析 (替代 sscanf) ---
std::string data = "123 3.14 hello";
std::istringstream iss(data);
int n;
double d;
std::string s;
iss >> n >> d >> s; // 从字符串流中提取数据
std::cout << "Extracted: " << n << ", " << d << ", " << s << std::endl;
// --- 3. 类型转换 ---
std::string num_str = "42";
int value;
std::istringstream converter(num_str);
if (converter >> value) { // 使用流的状态检查转换是否成功
std::cout << "Converted to int: " << value * 2 << std::endl;
} else {
std::cerr << "Conversion failed!";
}
return 0;
}
6. 流状态与错误处理
每个流对象都维护一个状态,由以下状态标志位组成:
goodbit
:一切正常,值为0。eofbit
:已到达文件末尾(End-of-File)。failbit
:上次IO操作失败(如类型不匹配),但流本身未损坏。badbit
:流已损坏,发生了严重的错误(如读写操作本身失败)。
相关成员函数:
good()
:如果goodbit
被设置(即没有错误)则返回true
。eof()
:如果eofbit
被设置则返回true
。fail()
:如果failbit
或badbit
被设置则返回true
。bad()
:如果badbit
被设置则返回true
。clear()
:重置流状态。rdstate()
:返回当前的状态位。
代码示例:
#include <iostream>
#include <limits>
int main() {
int number;
std::cout << "Enter an integer: ";
std::cin >> number;
// 检查输入是否成功
if (std::cin.fail()) {
std::cerr << "Error: Invalid input (not an integer).\n";
std::cin.clear(); // 重置错误状态,否则后续所有IO都会失败
// 清空输入缓冲区中的错误数据
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
} else {
std::cout << "You entered: " << number << '\n';
}
// 演示eof
std::cout << "Enter some text (Ctrl+D/Ctrl+Z to send EOF): ";
char c;
while (std::cin.get(c)) { // 循环读取字符
std::cout << c;
}
if (std::cin.eof()) {
std::cout << "\nEOF encountered.\n";
}
return 0;
}
7. 重载输入/输出操作符
这是C++ IO流库可扩展性的体现。你可以让你自定义的类支持<<
和>>
操作。
规则:
- 重载
operator<<
为非成员函数(通常是友元)。 - 重载
operator>>
为非成员函数(通常是友元)。 - 参数和返回类型都是流的引用。
代码示例:
#include <iostream>
class Person {
public:
Person() = default;
Person(const std::string& n, int a) : name(n), age(a) {}
// 声明为友元,以便访问私有成员
friend std::ostream& operator<<(std::ostream& os, const Person& p);
friend std::istream& operator>>(std::istream& is, Person& p);
private:
std::string name;
int age = 0;
};
// 重载输出操作符 <<
std::ostream& operator<<(std::ostream& os, const Person& p) {
os << "Name: " << p.name << ", Age: " << p.age;
return os; // 必须返回流引用以支持链式调用
}
// 重载输入操作符 >>
std::istream& operator>>(std::istream& is, Person& p) {
std::cout << "Enter name and age: ";
is >> p.name >> p.age;
// 注意:这里应该进行错误检查,这里为了简洁省略了
return is;
}
int main() {
Person alice("Alice", 30);
Person bob;
std::cout << alice << std::endl; // 输出: Name: Alice, Age: 30
std::cin >> bob; // 输入: Bob 25
std::cout << bob; // 输出: Name: Bob, Age: 25
return 0;
}
8. 二进制文件操作与序列化
文本模式会进行转换(如\n
-> \r\n
),而二进制模式直接读写内存字节,不做任何转换。用于处理非文本数据(如图片、视频、自定义数据结构)。
核心函数:
read(const char_type* s, std::streamsize count)
:从流中读取二进制数据。write(const char_type* s, std::streamsize count)
:向流中写入二进制数据。
代码示例(简单序列化/反序列化):
#include <iostream>
#include <fstream>
struct Data {
int id;
double value;
char name[20];
};
int main() {
Data data_to_write = {1, 3.14, "Binary Example"};
// --- 二进制写入 ---
std::ofstream outfile("data.bin", std::ios::binary);
if (outfile) {
outfile.write(reinterpret_cast<const char*>(&data_to_write), sizeof(Data));
outfile.close();
}
// --- 二进制读取 ---
Data data_to_read;
std::ifstream infile("data.bin", std::ios::binary);
if (infile) {
infile.read(reinterpret_cast<char*>(&data_to_read), sizeof(Data));
infile.close();
std::cout << "Read Data: ID=" << data_to_read.id
<< ", Value=" << data_to_read.value
<< ", Name=" << data_to_read.name << std::endl;
}
return 0;
}
警告:这种简单的二进制序列化不可移植(不同平台字节序、数据类型大小可能不同),也不安全(指针数据无效)。生产环境应使用专门的序列化库(如 Protocol Buffers, FlatBuffers)。
总结与最佳实践
- 理解层次结构:知道
istream
,ifstream
,istringstream
之间的关系。 - 总是检查流状态:在文件操作和输入后,使用
if (stream)
或stream.fail()
进行检查。 - 优先使用字符串流:
sstream
是进行字符串格式化和解析的利器,比C风格的sprintf
和sscanf
更安全。 - 谨慎使用二进制IO:明白其局限性和风险。
- 利用操纵符:掌握
<iomanip>
中的工具来控制格式。 - 重载
<<
和>>
:让你自定义的类无缝集成到C++的IO框架中。 - 区分
cerr
和clog
:错误信息用无缓冲的cerr
,日志记录用缓冲的clog
。 - 注意缓冲:
endl
会刷新缓冲区,影响性能。在需要频繁输出的场景,使用\n
可能更好。