C++ I/O 流
输入输出(I/O)是程序与外部世界交互的基本方式。C++ 提供了一套类型安全、可扩展的 I/O 流库,用于处理控制台输入输出、文件操作和字符串流。
I/O 流概述
流的概念
"流"(Stream)是对数据传输的抽象。可以把流想象成数据的管道,数据像水流一样从源端流向目的端:
输入流:数据源 → 程序(如:键盘输入、文件读取)
输出流:程序 → 数据目的地(如:屏幕输出、文件写入)
C++ I/O 库的核心优势:
- 类型安全:编译器检查类型匹配,避免
printf/scanf的类型错误 - 可扩展:可以为自定义类型重载
<<和>>运算符 - 统一接口:控制台、文件、字符串使用相同的方式操作
标准流对象
C++ 预定义了四个标准流对象:
#include <iostream>
int main() {
// std::cin - 标准输入流(键盘)
int age;
std::cin >> age;
// std::cout - 标准输出流(屏幕)
std::cout << "年龄: " << age << std::endl;
// std::cerr - 标准错误流(无缓冲)
std::cerr << "错误信息" << std::endl;
// std::clog - 标准日志流(有缓冲)
std::clog << "日志信息" << std::endl;
return 0;
}
标准流的特点:
| 流对象 | 用途 | 缓冲 | 说明 |
|---|---|---|---|
cin | 标准输入 | 有 | 默认连接键盘 |
cout | 标准输出 | 有 | 默认连接屏幕 |
cerr | 标准错误 | 无 | 立即输出,用于错误信息 |
clog | 标准日志 | 有 | 缓冲输出,用于普通日志 |
基本输入输出
输出流
std::cout 配合 << 运算符实现输出:
#include <iostream>
int main() {
// 基本输出
std::cout << "Hello, World!";
// 链式输出
std::cout << "值1: " << 42 << ", 值2: " << 3.14 << std::endl;
// 不同类型
int i = 100;
double d = 3.14159;
char c = 'A';
const char* s = "字符串";
std::cout << "整数: " << i << std::endl;
std::cout << "浮点: " << d << std::endl;
std::cout << "字符: " << c << std::endl;
std::cout << "字符串: " << s << std::endl;
// bool 输出
bool flag = true;
std::cout << "布尔值: " << flag << std::endl; // 输出 1
std::cout << std::boolalpha; // 启用布尔字面值
std::cout << "布尔值: " << flag << std::endl; // 输出 true
return 0;
}
输入流
std::cin 配合 >> 运算符实现输入:
#include <iostream>
#include <string>
int main() {
int age;
std::string name;
double salary;
// 基本输入
std::cout << "请输入年龄: ";
std::cin >> age;
std::cout << "请输入姓名: ";
std::cin >> name; // 遇到空格停止
std::cout << "请输入薪资: ";
std::cin >> salary;
// 输出结果
std::cout << "姓名: " << name
<< ", 年龄: " << age
<< ", 薪资: " << salary << std::endl;
return 0;
}
读取整行
>> 运算符在遇到空白字符时停止,要读取整行需要使用 std::getline:
#include <iostream>
#include <string>
int main() {
std::string name;
std::string address;
// std::cin >> name 会留下换行符在缓冲区
std::cout << "请输入姓名: ";
std::cin >> name;
// 清除缓冲区中的换行符
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
// 读取整行(包括空格)
std::cout << "请输入地址: ";
std::getline(std::cin, address);
std::cout << "姓名: " << name << std::endl;
std::cout << "地址: " << address << std::endl;
return 0;
}
// getline 使用示例
void readMultipleLines() {
std::string line;
std::cout << "请输入多行内容(空行结束):" << std::endl;
while (std::getline(std::cin, line)) {
if (line.empty()) break; // 空行结束
std::cout << "读取到: " << line << std::endl;
}
}
输入状态检查
#include <iostream>
int main() {
int value;
std::cout << "请输入一个整数: ";
std::cin >> value;
// 检查输入状态
if (std::cin.fail()) {
std::cout << "输入格式错误!" << std::endl;
std::cin.clear(); // 清除错误状态
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
} else {
std::cout << "输入的值: " << value << std::endl;
}
// 更好的方式:循环读取直到成功
while (true) {
std::cout << "请输入一个整数: ";
if (std::cin >> value) {
break; // 输入成功
}
std::cout << "格式错误,请重新输入!" << std::endl;
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
return 0;
}
流格式化
格式标志
C++ 提供了多种格式化输出的方式:
#include <iostream>
#include <iomanip> // 需要此头文件
int main() {
double pi = 3.14159265358979;
int num = 255;
// 数值格式
std::cout << "默认: " << pi << std::endl; // 3.14159
std::cout << "固定小数: " << std::fixed << pi << std::endl; // 3.141593
std::cout << "科学计数: " << std::scientific << pi << std::endl; // 3.141593e+00
// 精度控制
std::cout << "精度3: " << std::setprecision(3) << pi << std::endl; // 3.142
// 宽度控制
std::cout << "宽度10: " << std::setw(10) << num << std::endl; // " 255"
std::cout << "左对齐: " << std::left << std::setw(10) << num << std::endl; // "255 "
std::cout << "右对齐: " << std::right << std::setw(10) << num << std::endl; // " 255"
// 填充字符
std::cout << "填充0: " << std::setfill('0') << std::setw(5) << num << std::endl; // "00255"
// 进制
std::cout << "十进制: " << std::dec << num << std::endl; // 255
std::cout << "十六进制: " << std::hex << num << std::endl; // ff
std::cout << "八进制: " << std::oct << num << std::endl; // 377
std::cout << "十六进制(大写): " << std::uppercase << std::hex << num << std::endl; // FF
// 显示正号
std::cout << "显示正号: " << std::showpos << 42 << std::endl; // +42
// 布尔值格式
std::cout << std::boolalpha << true << std::endl; // true
std::cout << std::noboolalpha << true << std::endl; // 1
return 0;
}
操纵符汇总
常用的 I/O 操纵符:
#include <iomanip>
// 数值格式
std::dec // 十进制
std::hex // 十六进制
std::oct // 八进制
std::fixed // 固定小数点格式
std::scientific // 科学计数法
std::hexfloat // 十六进制浮点(C++11)
std::defaultfloat // 默认浮点格式(C++11)
// 精度和宽度
std::setprecision(n) // 设置精度
std::setw(n) // 设置宽度(仅对下一个输出有效)
// 对齐和填充
std::left // 左对齐
std::right // 右对齐
std::internal // 内部对齐(符号左对齐,数值右对齐)
std::setfill(c) // 设置填充字符
// 正负号
std::showpos // 显示正号
std::noshowpos // 不显示正号
// 布尔值
std::boolalpha // 输出 true/false
std::noboolalpha // 输出 1/0
// 大小写
std::uppercase // 十六进制大写
std::nouppercase // 十六进制小写
// 空白字符
std::skipws // 输入时跳过空白
std::noskipws // 输入时不跳过空白
// 换行
std::endl // 输出换行并刷新缓冲区
std::ends // 输出空字符 '\0'
std::flush // 刷新缓冲区
格式化表格示例
#include <iostream>
#include <iomanip>
#include <string>
void printTable() {
// 表头
std::cout << std::left
<< std::setw(15) << "姓名"
<< std::setw(10) << "年龄"
<< std::setw(12) << "薪资"
<< std::setw(10) << "部门"
<< std::endl;
// 分隔线
std::cout << std::setfill('-')
<< std::setw(47) << ""
<< std::setfill(' ')
<< std::endl;
// 数据行
std::cout << std::left
<< std::setw(15) << "张三"
<< std::setw(10) << 28
<< std::right << std::setw(10) << std::fixed << std::setprecision(2) << 8500.50 << " "
<< std::left << std::setw(10) << "研发"
<< std::endl;
std::cout << std::left
<< std::setw(15) << "李四"
<< std::setw(10) << 32
<< std::right << std::setw(10) << 12000.00 << " "
<< std::left << std::setw(10) << "管理"
<< std::endl;
}
文件流
文件流类
C++ 提供三个文件流类:
#include <fstream>
// std::ifstream - 输入文件流(读取)
// std::ofstream - 输出文件流(写入)
// std::fstream - 输入输出文件流(读写)
文件打开模式
// 文件打开模式
std::ios::in // 打开用于读取
std::ios::out // 打开用于写入
std::ios::app // 追加模式
std::ios::ate // 打开后定位到文件末尾
std::ios::trunc // 打开时清空文件
std::ios::binary // 二进制模式
// 组合模式
std::ofstream file("data.txt", std::ios::out | std::ios::app); // 写入+追加
写入文件
#include <fstream>
#include <iostream>
void writeToFile() {
// 方式1:构造时打开
std::ofstream outFile("output.txt");
if (!outFile.is_open()) {
std::cerr << "无法打开文件" << std::endl;
return;
}
// 写入数据
outFile << "第一行" << std::endl;
outFile << "第二行" << std::endl;
outFile << "数字: " << 42 << std::endl;
// 关闭文件(析构时自动关闭,但显式关闭更清晰)
outFile.close();
// 方式2:追加模式
std::ofstream appendFile("output.txt", std::ios::app);
appendFile << "追加的内容" << std::endl;
appendFile.close();
// 方式3:二进制写入
std::ofstream binFile("data.bin", std::ios::binary);
int numbers[] = {1, 2, 3, 4, 5};
binFile.write(reinterpret_cast<char*>(numbers), sizeof(numbers));
binFile.close();
}
// 文件写入完整示例
void saveData(const std::string& filename) {
std::ofstream file(filename);
if (!file) {
throw std::runtime_error("无法打开文件: " + filename);
}
// 写入表头
file << "ID,Name,Score" << std::endl;
// 写入数据
for (int i = 0; i < 10; i++) {
file << i << ",Student" << i << "," << (i * 10 + 50) << std::endl;
}
// file 析构时自动关闭
}
读取文件
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
// 方式1:逐行读取
void readLineByLine(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
std::cerr << "无法打开文件" << std::endl;
return;
}
std::string line;
while (std::getline(file, line)) {
std::cout << line << std::endl;
}
file.close();
}
// 方式2:逐词读取
void readWordByWord(const std::string& filename) {
std::ifstream file(filename);
std::string word;
while (file >> word) { // 自动跳过空白字符
std::cout << word << std::endl;
}
}
// 方式3:逐字符读取
void readCharByChar(const std::string& filename) {
std::ifstream file(filename);
char ch;
while (file.get(ch)) { // 包括空白字符
std::cout << ch;
}
}
// 方式4:读取到字符串末尾
std::string readAll(const std::string& filename) {
std::ifstream file(filename);
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
return content;
}
// 方式5:二进制读取
void readBinary(const std::string& filename) {
std::ifstream file(filename, std::ios::binary);
// 获取文件大小
file.seekg(0, std::ios::end);
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
// 读取到 vector
std::vector<char> buffer(size);
if (file.read(buffer.data(), size)) {
std::cout << "读取 " << size << " 字节" << std::endl;
}
}
// 方式6:读取结构化数据
struct Person {
std::string name;
int age;
double score;
};
std::vector<Person> readCSV(const std::string& filename) {
std::vector<Person> people;
std::ifstream file(filename);
std::string line;
// 跳过表头
std::getline(file, line);
while (std::getline(file, line)) {
Person p;
size_t pos = 0;
// 解析 CSV 行
pos = line.find(',');
p.name = line.substr(0, pos);
line.erase(0, pos + 1);
pos = line.find(',');
p.age = std::stoi(line.substr(0, pos));
line.erase(0, pos + 1);
p.score = std::stod(line);
people.push_back(p);
}
return people;
}
文件定位
#include <fstream>
void filePositionDemo() {
std::fstream file("data.bin", std::ios::in | std::ios::out | std::ios::binary);
// 获取当前位置
std::streampos pos = file.tellg(); // 输入位置
std::streampos outPos = file.tellp(); // 输出位置
// 定位到指定位置
file.seekg(10); // 定位到第 10 字节
file.seekg(10, std::ios::beg); // 从开头偏移 10 字节
file.seekg(-10, std::ios::end); // 从末尾向前偏移 10 字节
file.seekg(0, std::ios::cur); // 保持当前位置
// 输出定位
file.seekp(0, std::ios::end); // 定位到末尾准备追加
// 获取文件大小
file.seekg(0, std::ios::end);
std::streamsize fileSize = file.tellg();
file.seekg(0, std::ios::beg); // 回到开头
}
字符串流
字符串流用于在内存中进行格式化输入输出,常用于数据转换和字符串处理。
std::stringstream
#include <sstream>
#include <string>
#include <iostream>
int main() {
// 字符串输出流:构建字符串
std::ostringstream oss;
oss << "姓名: " << "张三" << ", 年龄: " << 25;
std::string result = oss.str(); // 获取结果字符串
std::cout << result << std::endl;
// 字符串输入流:解析字符串
std::istringstream iss("100 3.14 hello");
int i;
double d;
std::string s;
iss >> i >> d >> s;
std::cout << "整数: " << i << ", 浮点: " << d << ", 字符串: " << s << std::endl;
// 双向字符串流
std::stringstream ss;
ss << 42; // 写入
std::string str;
ss >> str; // 读取为字符串
std::cout << "转换结果: " << str << std::endl;
return 0;
}
类型转换
#include <sstream>
#include <string>
// 任意类型转字符串
template<typename T>
std::string toString(const T& value) {
std::ostringstream oss;
oss << value;
return oss.str();
}
// 字符串转任意类型
template<typename T>
T fromString(const std::string& str) {
std::istringstream iss(str);
T value;
iss >> value;
return value;
}
// 使用示例
void conversionDemo() {
// 数字转字符串
std::string s1 = toString(42); // "42"
std::string s2 = toString(3.14159); // "3.14159"
// 字符串转数字
int n = fromString<int>("123"); // 123
double d = fromString<double>("3.14"); // 3.14
// C++17 更简洁的方式
std::string str = std::to_string(42); // 整数转字符串
int num = std::stoi("42"); // 字符串转整数
double val = std::stod("3.14"); // 字符串转浮点
}
字符串格式化
#include <sstream>
#include <iomanip>
// 格式化时间
std::string formatTime(int hour, int minute, int second) {
std::ostringstream oss;
oss << std::setfill('0')
<< std::setw(2) << hour << ":"
<< std::setw(2) << minute << ":"
<< std::setw(2) << second;
return oss.str();
}
// 格式化货币
std::string formatCurrency(double amount) {
std::ostringstream oss;
oss << std::fixed << std::setprecision(2) << "¥" << amount;
return oss.str();
}
// 构建复杂字符串
std::string buildMessage(const std::string& name, int count, double total) {
std::ostringstream oss;
oss << "用户 " << name << " 购买了 " << count << " 件商品,"
<< "总计 " << std::fixed << std::setprecision(2) << total << " 元";
return oss.str();
}
自定义类型的 I/O
通过重载 << 和 >> 运算符,可以为自定义类型提供 I/O 支持:
#include <iostream>
#include <string>
// 复数类
class Complex {
public:
double real;
double imag;
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 重载输出运算符
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << c.real;
if (c.imag >= 0) os << "+";
os << c.imag << "i";
return os;
}
// 重载输入运算符
friend std::istream& operator>>(std::istream& is, Complex& c) {
char ch; // 用于读取分隔符
is >> c.real >> ch >> c.imag >> ch; // 格式: a+bi
return is;
}
};
// 日期类
class Date {
int year, month, day;
public:
Date(int y = 0, int m = 0, int d = 0) : year(y), month(m), day(d) {}
friend std::ostream& operator<<(std::ostream& os, const Date& d) {
os << d.year << "-" << d.month << "-" << d.day;
return os;
}
friend std::istream& operator>>(std::istream& is, Date& d) {
char sep1, sep2;
is >> d.year >> sep1 >> d.month >> sep2 >> d.day;
// 可以添加验证逻辑
return is;
}
};
int main() {
Complex c1(3, 4);
std::cout << c1 << std::endl; // 输出: 3+4i
Complex c2;
std::cout << "请输入复数 (格式: a+bi): ";
std::cin >> c2;
std::cout << "你输入的是: " << c2 << std::endl;
return 0;
}
流状态
状态标志
每个流都有状态标志,用于指示流的状态:
#include <iostream>
void streamStateDemo() {
std::cin.clear(); // 清除所有错误状态
// 状态检查函数
if (std::cin.good()) {
// 流状态正常
}
if (std::cin.eof()) {
// 到达文件末尾
}
if (std::cin.fail()) {
// I/O 操作失败(可恢复)
}
if (std::cin.bad()) {
// 流发生严重错误(不可恢复)
}
// 状态标志
// std::ios::goodbit - 无错误
// std::ios::eofbit - 到达末尾
// std::ios::failbit - 操作失败
// std::ios::badbit - 严重错误
// 设置/获取状态
std::iostream::iostate state = std::cin.rdstate();
std::cin.setstate(std::ios::failbit); // 设置失败状态
std::cin.clear(); // 清除所有状态
std::cin.clear(std::ios::goodbit); // 设置为正常状态
}
异常处理
可以配置流在特定状态下抛出异常:
#include <fstream>
#include <iostream>
void streamExceptionDemo() {
std::ifstream file("nonexistent.txt");
// 设置在 badbit 或 failbit 时抛出异常
file.exceptions(std::ifstream::badbit | std::ifstream::failbit);
try {
std::string line;
while (std::getline(file, line)) {
std::cout << line << std::endl;
}
} catch (const std::ifstream::failure& e) {
std::cerr << "文件操作异常: " << e.what() << std::endl;
}
// 关闭异常
file.exceptions(std::ifstream::goodbit);
}
缓冲区
刷新缓冲区
#include <iostream>
void bufferDemo() {
// std::endl - 输出换行并刷新
std::cout << "带刷新的换行" << std::endl;
// std::flush - 仅刷新缓冲区
std::cout << "手动刷新" << std::flush;
// std::unitbuf - 每次输出后自动刷新
std::cout << std::unitbuf; // 开启
std::cout << "每次输出后刷新";
std::cout << std::nounitbuf; // 关闭
// 绑定流:tie
std::cin.tie(&std::cout); // 默认:cin 操作前刷新 cout
// 这确保了提示信息在等待输入前显示
}
自定义缓冲区
#include <streambuf>
#include <iostream>
// 简单的输出缓冲区
class LogBuffer : public std::streambuf {
protected:
std::streamsize xsputn(const char* s, std::streamsize n) override {
// 添加时间戳前缀
std::cout << "[LOG] ";
std::cout.write(s, n);
return n;
}
int overflow(int c) override {
if (c != EOF) {
std::cout << "[LOG] " << static_cast<char>(c);
}
return c;
}
};
void customBufferDemo() {
LogBuffer buffer;
std::ostream logStream(&buffer);
logStream << "这是一条日志" << std::endl;
logStream << "另一条日志" << std::endl;
}
实用示例
文件复制
#include <fstream>
bool copyFile(const std::string& src, const std::string& dst) {
std::ifstream in(src, std::ios::binary);
std::ofstream out(dst, std::ios::binary);
if (!in || !out) {
return false;
}
out << in.rdbuf(); // 高效复制整个文件
return true;
}
配置文件解析
#include <fstream>
#include <string>
#include <map>
#include <sstream>
std::map<std::string, std::string> parseConfig(const std::string& filename) {
std::map<std::string, std::string> config;
std::ifstream file(filename);
std::string line;
while (std::getline(file, line)) {
// 跳过空行和注释
if (line.empty() || line[0] == '#') continue;
// 解析 key=value
size_t pos = line.find('=');
if (pos != std::string::npos) {
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + 1);
// 去除空白
key.erase(0, key.find_first_not_of(" \t"));
key.erase(key.find_last_not_of(" \t") + 1);
value.erase(0, value.find_first_not_of(" \t"));
value.erase(value.find_last_not_of(" \t") + 1);
config[key] = value;
}
}
return config;
}
日志系统
#include <fstream>
#include <ctime>
#include <iomanip>
class Logger {
std::ofstream logFile;
public:
Logger(const std::string& filename) : logFile(filename, std::ios::app) {}
void log(const std::string& level, const std::string& message) {
auto now = std::time(nullptr);
auto tm = *std::localtime(&now);
logFile << std::put_time(&tm, "%Y-%m-%d %H:%M:%S")
<< " [" << level << "] " << message << std::endl;
}
void info(const std::string& msg) { log("INFO", msg); }
void warn(const std::string& msg) { log("WARN", msg); }
void error(const std::string& msg) { log("ERROR", msg); }
};
小结
本章学习了:
- I/O 流概述:流的概念和标准流对象
- 基本输入输出:cout、cin 的使用
- 格式化输出:操纵符和格式标志
- 文件流:文件的读写操作
- 字符串流:stringstream 的使用
- 自定义 I/O:重载
<<和>>运算符 - 流状态:状态标志和异常处理
- 缓冲区:刷新和自定义缓冲区
下一步
接下来让我们学习 C++ 右值引用与移动语义,了解现代 C++ 的性能优化技术。