C++ Primer 第8章:IO 库
8.1 IO 类
8.1.1 IO 类层次结构
IO 类层次结构:
┌─────────────────────────────────────────────────────┐
│ ios_base │
│ │ │
│ ios │
│ / \ │
│ istream ostream │
│ / \ / \ │
│ ifstream │ ofstream │ │
│ │ │ │
│ istringstream ostringstream │
│ \ / │
│ iostream │
│ / \ │
│ fstream stringstream │
└─────────────────────────────────────────────────────┘
标准流对象:
cin → istream(标准输入)
cout → ostream(标准输出)
cerr → ostream(标准错误,不缓冲)
clog → ostream(标准日志,缓冲)
8.1.2 IO 对象不能拷贝或赋值
// io_no_copy.cpp -- IO对象不能拷贝
#include <iostream>
#include <fstream>
// IO 对象不能拷贝,因此:
// 1. 不能将形参或返回类型设为流类型
// 2. 传递和返回流时必须使用引用
// 3. 读写流会改变其状态,因此引用不能是 const
// ✅ 正确:通过引用传递
void readData(std::istream& is)
{
int val;
while (is >> val)
std::cout << val << " ";
std::cout << std::endl;
}
// ✅ 正确:通过引用返回
std::ostream& printHeader(std::ostream& os)
{
os << "===== 报告 =====" << std::endl;
return os;
}
// ❌ 错误:不能拷贝IO对象
// void badFunc(std::istream is) {} // 错误!
// std::ostream badReturn() {} // 错误!
int main()
{
printHeader(std::cout) << "内容" << std::endl;
return 0;
}
8.1.3 条件状态
// io_state.cpp -- IO条件状态
#include <iostream>
#include <string>
int main()
{
using namespace std;
// IO 流的条件状态标志:
// strm::badbit :流已崩溃(不可恢复)
// strm::failbit :IO操作失败(可恢复)
// strm::eofbit :到达文件末尾
// strm::goodbit :流处于有效状态(值为0)
// ===== 检测流状态 =====
int val;
cout << "输入一个整数:";
cin >> val;
if (cin.good()) cout << "流状态正常" << endl;
if (cin.fail()) cout << "IO操作失败" << endl;
if (cin.bad()) cout << "流已崩溃" << endl;
if (cin.eof()) cout << "到达文件末尾" << endl;
// 最常用的检测方式
if (cin) cout << "流有效" << endl;
else cout << "流无效" << endl;
// ===== 管理条件状态 =====
// rdstate():返回当前状态
auto old_state = cin.rdstate();
// clear():将所有状态位复位(恢复到goodbit)
cin.clear();
cout << "clear后,cin有效:" << boolalpha << (bool)cin << endl;
// setstate():设置特定状态位
cin.setstate(ios::failbit);
cout << "setstate(failbit)后,cin有效:" << (bool)cin << endl;
// 恢复到之前的状态
cin.clear(old_state);
// ===== 实际应用:读取整数直到失败 =====
cout << "\n输入整数(输入非数字结束):" << endl;
int sum = 0, count = 0;
while (cin >> val)
{
sum += val;
count++;
}
// 清除错误状态,继续使用cin
cin.clear();
cin.ignore(1000, '\n'); // 清除缓冲区中的错误输入
if (count > 0)
cout << "共" << count << "个数,平均值:"
<< (double)sum / count << endl;
return 0;
}
8.1.4 管理输出缓冲
// output_buffer.cpp -- 输出缓冲管理
#include <iostream>
int main()
{
using namespace std;
// 输出缓冲区刷新的情况:
// 1. 程序正常结束
// 2. 缓冲区满
// 3. 使用 endl、flush、ends 操纵符
// 4. 使用 unitbuf 设置每次写操作后刷新
// 5. 关联流被读写(cin 和 cout 关联)
// ===== 刷新缓冲区的方式 =====
cout << "Hello" << endl; // 输出 + 换行 + 刷新
cout << "World" << flush; // 输出 + 刷新(不换行)
cout << "!" << ends; // 输出 + 空字符 + 刷新
cout << endl;
// ===== unitbuf:每次写操作后刷新 =====
cout << unitbuf; // 设置每次写后刷新
cout << "每次写后都刷新" << endl;
cout << nounitbuf; // 恢复正常缓冲
// ===== 关联流 =====
// cin 和 cout 默认关联
// 读 cin 之前会刷新 cout 的缓冲区
// 这就是为什么 cout << "提示:" 后不需要 flush 就能显示
// 手动关联流
// cin.tie(&cout); // 默认已关联
// ostream* old = cin.tie(nullptr); // 解除关联
// ⚠️ 如果程序崩溃,缓冲区可能不会刷新
// 调试时使用 cerr(不缓冲)或 endl(刷新)
return 0;
}
8.2 文件输入输出
8.2.1 使用文件流
// file_io.cpp -- 文件输入输出
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <stdexcept>
int main()
{
using namespace std;
// ===== 写入文件 =====
{
ofstream outFile("data.txt"); // 创建并打开文件
// 检查是否成功打开
if (!outFile)
throw runtime_error("无法打开文件 data.txt");
// 写入内容(与 cout 用法相同)
outFile << "张三 85 92 78" << endl;
outFile << "李四 90 88 95" << endl;
outFile << "王五 72 85 80" << endl;
// outFile 离开作用域时自动关闭
}
cout << "写入完成" << endl;
// ===== 读取文件 =====
{
ifstream inFile("data.txt");
if (!inFile)
throw runtime_error("无法打开文件 data.txt");
string name;
int s1, s2, s3;
cout << "\n读取内容:" << endl;
while (inFile >> name >> s1 >> s2 >> s3)
{
double avg = (s1 + s2 + s3) / 3.0;
cout << name << " 平均分:" << avg << endl;
}
}
// ===== 逐行读取 =====
{
ifstream inFile("data.txt");
string line;
int lineNum = 0;
cout << "\n逐行读取:" << endl;
while (getline(inFile, line))
{
cout << ++lineNum << ": " << line << endl;
}
}
return 0;
}
8.2.2 文件模式
// file_modes.cpp -- 文件打开模式
#include <iostream>
#include <fstream>
#include <string>
int main()
{
using namespace std;
/*
文件模式标志:
ios::in 以读方式打开
ios::out 以写方式打开(默认截断)
ios::app 追加:每次写操作前定位到文件末尾
ios::ate 打开文件后立即定位到文件末尾
ios::trunc 截断文件(清空内容)
ios::binary 以二进制方式进行IO
组合规则:
- out 默认隐含 trunc
- 要保留文件内容,必须显式指定 app 或 in
*/
// ===== 各种打开模式 =====
// 1. 只读(默认)
ifstream f1("file.txt");
// 等价于:ifstream f1("file.txt", ios::in);
// 2. 只写(截断)
ofstream f2("file.txt");
// 等价于:ofstream f2("file.txt", ios::out | ios::trunc);
// 3. 追加写入(不截断)
ofstream f3("file.txt", ios::app);
// 等价于:ofstream f3("file.txt", ios::out | ios::app);
// 4. 读写(不截断)
fstream f4("file.txt", ios::in | ios::out);
// 5. 读写(截断)
fstream f5("file.txt", ios::in | ios::out | ios::trunc);
// 6. 二进制读写
fstream f6("file.bin", ios::in | ios::out | ios::binary);
// ===== 实际应用:日志文件 =====
auto writeLog = [](const string& msg) {
ofstream log("app.log", ios::app); // 追加模式
if (log)
log << msg << endl;
};
writeLog("程序启动");
writeLog("执行操作1");
writeLog("执行操作2");
writeLog("程序结束");
// 读取日志
ifstream logFile("app.log");
string line;
cout << "日志内容:" << endl;
while (getline(logFile, line))
cout << " " << line << endl;
return 0;
}
8.2.3 文件流的成员函数
// fstream_members.cpp -- 文件流成员函数
#include <iostream>
#include <fstream>
#include <string>
int main()
{
using namespace std;
// ===== open() 和 close() =====
ifstream inFile; // 未关联任何文件
inFile.open("data.txt"); // 打开文件
if (!inFile.is_open())
{
cerr << "无法打开文件" << endl;
return 1;
}
string line;
while (getline(inFile, line))
cout << line << endl;
inFile.close(); // 关闭文件
// 重新打开另一个文件
inFile.open("other.txt");
if (inFile.is_open())
{
// 处理 other.txt
inFile.close();
}
// ===== 处理多个文件 =====
vector<string> files = {"file1.txt", "file2.txt", "file3.txt"};
for (const auto& filename : files)
{
ifstream f(filename);
if (!f)
{
cerr << "无法打开:" << filename << endl;
continue;
}
cout << "处理文件:" << filename << endl;
string content;
while (getline(f, content))
cout << " " << content << endl;
// f 离开作用域时自动关闭
}
return 0;
}
8.3 string 流
8.3.1 istringstream
// istringstream_demo.cpp -- istringstream详解
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
struct PersonInfo
{
std::string name;
std::vector<std::string> phones;
};
int main()
{
using namespace std;
// ===== 基本用法 =====
string line = "张三 138-1234-5678 010-12345678";
istringstream iss(line);
string name;
iss >> name; // 读取姓名
string phone;
vector<string> phones;
while (iss >> phone) // 读取所有电话号码
phones.push_back(phone);
cout << "姓名:" << name << endl;
cout << "电话:";
for (const auto& p : phones) cout << p << " ";
cout << endl;
// ===== 解析结构化数据 =====
vector<string> lines = {
"张三 138-1234-5678 010-12345678",
"李四 139-8765-4321",
"王五 021-87654321 400-123-4567 010-98765432"
};
vector<PersonInfo> people;
for (const auto& l : lines)
{
PersonInfo info;
istringstream record(l);
record >> info.name;
string p;
while (record >> p)
info.phones.push_back(p);
people.push_back(info);
}
cout << "\n联系人列表:" << endl;
for (const auto& person : people)
{
cout << person.name << ":";
for (const auto& phone : person.phones)
cout << phone << " ";
cout << endl;
}
// ===== 类型转换 =====
string numStr = "42";
int num;
istringstream(numStr) >> num;
cout << "\n字符串转整数:" << num << endl;
string floatStr = "3.14";
double d;
istringstream(floatStr) >> d;
cout << "字符串转浮点:" << d << endl;
return 0;
}
8.3.2 ostringstream
// ostringstream_demo.cpp -- ostringstream详解
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <iomanip>
int main()
{
using namespace std;
// ===== 基本用法:构建字符串 =====
ostringstream oss;
oss << "姓名:" << "张三" << endl;
oss << "年龄:" << 25 << endl;
oss << "成绩:" << fixed << setprecision(1) << 92.5 << endl;
string result = oss.str(); // 获取构建的字符串
cout << result;
// ===== 格式化数字 =====
auto formatNumber = [](double val, int precision = 2) -> string {
ostringstream os;
os << fixed << setprecision(precision) << val;
return os.str();
};
cout << "格式化:" << formatNumber(3.14159, 3) << endl;
cout << "格式化:" << formatNumber(1234567.89) << endl;
// ===== 构建 CSV 行 =====
auto makeCSVRow = [](const vector<string>& fields) -> string {
ostringstream os;
for (size_t i = 0; i < fields.size(); i++)
{
if (i > 0) os << ",";
os << fields[i];
}
return os.str();
};
vector<string> row = {"张三", "25", "北京", "92.5"};
cout << "CSV行:" << makeCSVRow(row) << endl;
// ===== 数值转字符串 =====
auto toString = [](auto val) -> string {
ostringstream os;
os << val;
return os.str();
};
cout << "int转string:" << toString(42) << endl;
cout << "double转string:" << toString(3.14) << endl;
cout << "bool转string:" << toString(true) << endl;
// ===== 延迟输出(先构建,后决定是否输出)=====
vector<string> errors;
vector<int> data = {1, -2, 3, -4, 5};
for (int val : data)
{
if (val < 0)
{
ostringstream err;
err << "发现负数:" << val;
errors.push_back(err.str());
}
}
if (!errors.empty())
{
cout << "\n错误列表:" << endl;
for (const auto& e : errors)
cout << " " << e << endl;
}
return 0;
}
8.3.3 stringstream 综合应用
// stringstream_app.cpp -- stringstream综合应用
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <map>
// 解析 key=value 格式的配置字符串
map<string, string> parseConfig(const string& config)
{
map<string, string> result;
istringstream iss(config);
string token;
while (getline(iss, token, ';')) // 以分号分隔
{
istringstream pair(token);
string key, value;
if (getline(pair, key, '=') && getline(pair, value))
{
// 去除首尾空格
auto trim = [](string& s) {
size_t start = s.find_first_not_of(" \t");
size_t end = s.find_last_not_of(" \t");
s = (start == string::npos) ? "" : s.substr(start, end - start + 1);
};
trim(key);
trim(value);
result[key] = value;
}
}
return result;
}
// 将 vector 转为字符串
template <typename T>
string vectorToString(const vector<T>& v, const string& sep = ", ")
{
ostringstream oss;
for (size_t i = 0; i < v.size(); i++)
{
if (i > 0) oss << sep;
oss << v[i];
}
return oss.str();
}
// 分割字符串
vector<string> split(const string& s, char delim)
{
vector<string> result;
istringstream iss(s);
string token;
while (getline(iss, token, delim))
if (!token.empty())
result.push_back(token);
return result;
}
int main()
{
using namespace std;
// 解析配置
string config = "host=localhost; port=8080; debug=true; timeout=30";
auto cfg = parseConfig(config);
cout << "配置项:" << endl;
for (const auto& [key, val] : cfg)
cout << " " << key << " = " << val << endl;
// vector 转字符串
vector<int> nums = {1, 2, 3, 4, 5};
vector<string> words = {"Hello", "World", "C++"};
cout << "\nvector转字符串:" << endl;
cout << " " << vectorToString(nums) << endl;
cout << " " << vectorToString(words, " | ") << endl;
// 分割字符串
string csv = "张三,25,北京,92.5";
auto fields = split(csv, ',');
cout << "\nCSV解析:" << endl;
for (size_t i = 0; i < fields.size(); i++)
cout << " 字段" << i << ": " << fields[i] << endl;
// 类型安全的字符串转换
auto fromString = [](const string& s, auto& val) -> bool {
istringstream iss(s);
return (bool)(iss >> val);
};
int n;
double d;
bool ok1 = fromString("42", n);
bool ok2 = fromString("3.14", d);
bool ok3 = fromString("abc", n); // 失败
cout << "\n类型转换:" << endl;
cout << " \"42\" → int: " << (ok1 ? to_string(n) : "失败") << endl;
cout << " \"3.14\" → double: " << (ok2 ? to_string(d) : "失败") << endl;
cout << " \"abc\" → int: " << (ok3 ? to_string(n) : "失败") << endl;
return 0;
}
8.4 综合示例:日志系统
// logger.cpp -- 综合示例:日志系统
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <chrono>
#include <iomanip>
#include <stdexcept>
// 日志级别
enum class LogLevel { DEBUG, INFO, WARNING, ERROR, CRITICAL };
class Logger
{
private:
std::string filename_;
LogLevel minLevel_;
bool toConsole_;
std::ofstream logFile_;
// 获取当前时间字符串
static std::string currentTime()
{
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
std::ostringstream oss;
oss << std::put_time(std::localtime(&t), "%Y-%m-%d %H:%M:%S");
return oss.str();
}
// 日志级别转字符串
static std::string levelStr(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO ";
case LogLevel::WARNING: return "WARN ";
case LogLevel::ERROR: return "ERROR";
case LogLevel::CRITICAL: return "CRIT ";
}
return "UNKN ";
}
// 格式化日志消息
std::string format(LogLevel level, const std::string& msg)
{
std::ostringstream oss;
oss << "[" << currentTime() << "] "
<< "[" << levelStr(level) << "] "
<< msg;
return oss.str();
}
public:
Logger(const std::string& filename,
LogLevel minLevel = LogLevel::INFO,
bool toConsole = true)
: filename_(filename), minLevel_(minLevel), toConsole_(toConsole)
{
logFile_.open(filename_, std::ios::app);
if (!logFile_)
throw std::runtime_error("无法打开日志文件:" + filename_);
}
~Logger()
{
if (logFile_.is_open())
logFile_.close();
}
void log(LogLevel level, const std::string& msg)
{
if (level < minLevel_) return;
std::string formatted = format(level, msg);
// 写入文件
logFile_ << formatted << std::endl;
logFile_.flush(); // 立即刷新(确保崩溃时也能保存)
// 输出到控制台
if (toConsole_)
{
if (level >= LogLevel::ERROR)
std::cerr << formatted << std::endl;
else
std::cout << formatted << std::endl;
}
}
// 便捷方法
void debug(const std::string& msg) { log(LogLevel::DEBUG, msg); }
void info(const std::string& msg) { log(LogLevel::INFO, msg); }
void warning(const std::string& msg) { log(LogLevel::WARNING, msg); }
void error(const std::string& msg) { log(LogLevel::ERROR, msg); }
void critical(const std::string& msg) { log(LogLevel::CRITICAL, msg); }
// 读取日志
std::vector<std::string> readLog() const
{
std::vector<std::string> lines;
std::ifstream f(filename_);
std::string line;
while (std::getline(f, line))
lines.push_back(line);
return lines;
}
// 统计各级别日志数量
std::map<std::string, int> statistics() const
{
std::map<std::string, int> stats;
for (const auto& line : readLog())
{
// 从格式化字符串中提取级别
auto start = line.find('[', 22); // 跳过时间戳
auto end = line.find(']', start);
if (start != std::string::npos && end != std::string::npos)
{
std::string level = line.substr(start + 1, end - start - 1);
// 去除空格
while (!level.empty() && level.back() == ' ')
level.pop_back();
stats[level]++;
}
}
return stats;
}
};
int main()
{
using namespace std;
try
{
Logger logger("app.log", LogLevel::DEBUG);
logger.info("程序启动");
logger.debug("调试信息:初始化完成");
logger.info("连接数据库...");
logger.warning("数据库连接超时,正在重试");
logger.info("数据库连接成功");
// 模拟业务操作
for (int i = 1; i <= 3; i++)
{
ostringstream msg;
msg << "处理任务 #" << i;
logger.info(msg.str());
}
logger.error("任务 #2 处理失败:数据格式错误");
logger.info("程序正常结束");
// 读取并显示日志
cout << "\n===== 日志内容 =====" << endl;
for (const auto& line : logger.readLog())
cout << line << endl;
// 统计
cout << "\n===== 日志统计 =====" << endl;
for (const auto& [level, count] : logger.statistics())
cout << level << ": " << count << " 条" << endl;
}
catch (const exception& e)
{
cerr << "错误:" << e.what() << endl;
return 1;
}
return 0;
}
📝 第8章知识点总结
| 知识点 |
核心要点 |
| IO 类层次 |
istream/ostream → ifstream/ofstream → istringstream/ostringstream |
| IO 不能拷贝 |
传递和返回流必须用引用,且不能是 const 引用 |
| 条件状态 |
goodbit/failbit/badbit/eofbit,clear() 复位,rdstate() 查询 |
| 缓冲刷新 |
endl(换行+刷新)、flush(只刷新)、ends(空字符+刷新)、unitbuf |
| 关联流 |
cin 和 cout 默认关联,读 cin 前自动刷新 cout |
| 文件流 |
ifstream/ofstream/fstream,构造时或 open() 打开,离开作用域自动关闭 |
| 文件模式 |
in/out/app/ate/trunc/binary,out 默认隐含 trunc |
| 追加模式 |
ios::app 保留文件内容,每次写操作前定位到末尾 |
| istringstream |
从字符串读取,用于解析结构化文本、类型转换 |
| ostringstream |
向字符串写入,用于格式化字符串、延迟输出 |
| getline 分隔符 |
getline(iss, token, delim) 可指定分隔符,用于解析 CSV 等 |
| is_open() |
检查文件是否成功打开,比直接检查流对象更明确 |
| cerr vs clog |
cerr 不缓冲(立即输出),clog 缓冲,都输出到标准错误 |