上次学习string的时候看到了输入输出流,对这方面的知识有些遗忘,做个笔记。

C++ 的 “流(Stream)” 是对数据输入 / 输出(I/O) 的抽象封装,核心思想是把数据的传输(比如从键盘读、向屏幕写、读写文件)看作 “水流”—— 数据像水一样在 “源” 和 “目标” 之间流动,而流对象就是操控这个 “水流” 的工具。iostream 是 C++ 标准库中处理基本输入输出流的核心头文件,也是理解所有流操作的基础。

以下从「流的核心概念、iostream 体系、基础用法、流的状态、进阶流操作、常见坑点」六个维度,彻底讲清楚 C++ 流的所有关键知识点。

一、先理解:为什么需要 “流”?

在没有流的 C 语言中,I/O 是通过 printf/scanf/fopen 等函数实现的,存在两个核心问题:

  1. 类型不安全printf("%d", "hello") 这种类型不匹配的错误,编译时无法发现,运行时才崩溃;
  2. 接口不统一:读写屏幕、文件、网络的接口完全不同(printf vs fprintf vs 自定义网络写函数),学习和使用成本高。

C++ 的 “流” 解决了这些问题:

  • 类型安全:流的操作符(<</>>)会自动匹配数据类型,编译时就能检测错误;
  • 接口统一:无论读写屏幕、文件还是字符串,都用几乎相同的 <</>> 操作符;
  • 可扩展:支持自定义类型的流操作(比如给自定义类重载 <</>>)。

二、流的核心概念

1. 流的本质

流是字节序列的抽象,分为两种核心方向:

  • 输入流(Input Stream):数据从 “外部设备”(键盘、文件、网络)流向 “程序内存”(比如 cin 是从键盘读数据到程序);
  • 输出流(Output Stream):数据从 “程序内存” 流向 “外部设备”(比如 cout 是从程序写数据到屏幕)。

2. 流的分类(按处理对象)

流类型 处理对象 核心头文件 核心对象
标准 I/O 流 屏幕、键盘、标准错误 <iostream> cin/cout/cerr/clog
文件 I/O 流 磁盘文件 <fstream> ifstream/ofstream/fstream
字符串 I/O 流 内存中的字符串(替代 char []) <sstream> istringstream/ostringstream/stringstream

3. iostream 是什么?

iostream 是 C++ 标准库的头文件名称,也是 “输入输出流(Input/Output Stream)” 的缩写,它定义了:

  • 抽象基类:ios(所有流的基类,管理流状态、格式控制);
  • 核心流类:istream(输入流基类)、ostream(输出流基类)、iostream(继承 istream+ostream,支持双向流);
  • 全局流对象:cin(标准输入)、cout(标准输出)、cerr(标准错误)、clog(标准日志)。

三、标准 I/O 流(iostream 核心)

<iostream> 定义的 4 个全局流对象是日常开发中最常用的,对应操作系统的 “标准设备”:

1. 核心流对象详解

流对象 全称 对应设备 特性 典型用法
cin console input 键盘 输入流,默认阻塞,可重定向 读用户输入
cout console output 屏幕 输出流,带缓冲区,可重定向 打印普通信息
cerr console error 屏幕 错误输出流,无缓冲区,立即输出 打印错误信息
clog console log 屏幕 日志输出流,带缓冲区,优先级低于 cerr 打印调试 / 日志信息
关键区别:cout vs cerr vs clog
  • 缓冲区cout/clog 是 “行缓冲”(遇到 endl 或缓冲区满时刷新),cerr 是 “无缓冲”(写了就立即输出);
  • 用途
    • cout:正常业务输出(比如 “计算结果:100”);
    • cerr:紧急错误(比如 “文件打开失败!”),确保错误信息优先显示;
    • clog:非紧急日志(比如 “程序启动时间:2026-01-26”)。

2. 基础操作:<<和>> 运算符

<<(插入符)和 >>(提取符)是流的核心操作符,支持几乎所有内置类型,且类型安全

(1)cout 输出(<< 插入符)
#include <iostream>
using namespace std; // 否则需写 std::cout

int main() {
    int a = 10;
    double b = 3.14;
    string s = "hello";
    
    // 基础输出:自动匹配类型
    cout << "整数:" << a << endl;       // 输出:整数:10(endl 换行+刷新缓冲区)
    cout << "浮点数:" << b << " ";      // 输出:浮点数:3.14 (无换行)
    cout << "字符串:" << s << '\n';     // 输出:字符串:hello(仅换行,不刷新缓冲区)
    
    // 格式控制(后续讲)
    cout << hex << a << endl; // 输出 10 的十六进制:a
    return 0;
}
(2)cin 输入(>> 提取符)
int main() {
    int a;
    string s;
    
    // 读取整数:遇到空格/换行停止
    cout << "输入一个整数:";
    cin >> a; // 用户输入 10 → a=10
    
    // 读取字符串:遇到空格/换行停止(无法读带空格的字符串)
    cout << "输入一个字符串:";
    cin >> s; // 用户输入 "hello world" → s="hello"(只取到第一个空格前)
    
    cout << "你输入的:" << a << " " << s << endl;
    return 0;
}

3. 读取带空格的字符串:getline

cin >> s 无法读取带空格的字符串,需用 getline

#include <iostream>
#include <string> // getline 依赖<string>
using namespace std;

int main() {
    string line;
    cout << "输入一行文字(可含空格):";
    getline(cin, line); // 读取整行,直到换行符(换行符被丢弃)
    cout << "你输入的:" << line << endl; // 输入 "hello world" → 输出完整字符串
    return 0;
}
坑点:cin >> 后接 getline 会 “吞掉换行符”
int main() {
    int a;
    string line;
    
    cin >> a; // 用户输入 10 并按回车 → 输入缓冲区:"10\n"
    // cin >> a 读取 10,缓冲区剩余 "\n"
    getline(cin, line); // 直接读取剩余的 "\n",line 为空
    
    // 解决方案:忽略缓冲区的换行符
    cin >> a;
    cin.ignore(); // 忽略一个字符(换行符)
    getline(cin, line); // 正常读取
    return 0;
}

4. 常用流方法(补充 <</>> 之外的操作)

流对象 方法 功能 示例
cin get() 读取单个字符(包括空格 / 换行) char c = cin.get();
cin ignore(n) 忽略前 n 个字符(默认 1) cin.ignore(10);
cin clear() 清除流的错误状态(比如输入类型不匹配) cin.clear();
cin sync() 刷新输入缓冲区 cin.sync();
cout put(c) 输出单个字符 cout.put('a');
cout flush() 手动刷新输出缓冲区(立即显示) cout.flush();
示例:cin 输入类型错误的处理
int main() {
    int a;
    cout << "输入整数:";
    cin >> a; // 用户输入 "abc" → 输入失败,cin 进入错误状态
    
    if (cin.fail()) { // 检查流是否失败
        cout << "输入类型错误!" << endl;
        cin.clear(); // 清除错误状态
        cin.ignore(numeric_limits<streamsize>::max(), '\n'); // 忽略缓冲区所有字符直到换行
    }
    
    // 重新输入
    cin >> a; // 此时可正常读取
    return 0;
}

需包含 <limits> 头文件使用 numeric_limits

四、流的状态(核心:理解流的 “健康度”)

所有流对象都继承自 ios 类,ios 维护了流的状态标志,决定流是否能正常读写。

1. 流的状态标志(4 种核心)

标志 含义 检测方法
goodbit 流状态正常(无错误) cin.good()
eofbit 到达流末尾(比如读文件到结尾、cin 按 Ctrl+D/Z) cin.eof()
failbit 操作失败(比如输入类型不匹配、打开文件失败) cin.fail()
badbit 严重错误(比如流对应的设备损坏、内存不足) cin.bad()

2. 状态检测与重置

  • 检测:直接用 cin/cout 作为布尔值(等价于 !fail()):
    int a;
    while (cin >> a) { // 只要输入正常,循环继续;输入错误/EOF 退出
        cout << "你输入了:" << a << endl;
    }
    
  • 重置clear() 清除错误标志,ignore() 清空缓冲区:
    cin.clear(); // 清除所有错误标志,恢复 goodbit
    cin.ignore(); // 清空缓冲区残留数据
    

五、进阶流操作

1. 格式控制(输出格式化)

cout 支持通过格式标志操纵符(manipulator) 控制输出格式(比如进制、小数位数、对齐)。

(1)常用操纵符(需包含 <iomanip> 头文件)
操纵符 功能 示例
endl 换行 + 刷新缓冲区 cout << endl;
hex/oct/dec 输出十六进制 / 八进制 / 十进制 cout << hex << 10;
fixed 固定小数位数输出 cout << fixed << 3.1415;
scientific 科学计数法输出 cout << scientific << 3.14;
setprecision(n) 设置小数精度(n 位) cout << setprecision(2);
setw(n) 设置输出宽度(不足补空格) cout << setw(5) << 10;
left/right 左对齐 / 右对齐(配合 setw) cout << left << setw(5) << 10;
示例:格式控制
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
    double pi = 3.1415926;
    
    // 固定小数位(2 位)
    cout << fixed << setprecision(2) << pi << endl; // 3.14
    
    // 科学计数法
    cout << scientific << pi << endl; // 3.141593e+00
    
    // 宽度 + 对齐
    cout << setw(8) << right << 100 << endl; // "     100"(右对齐,宽度8)
    cout << setw(8) << left << 100 << endl;  // "100     "(左对齐,宽度8)
    
    // 进制转换
    cout << hex << 255 << endl; // ff(十六进制)
    cout << dec << 255 << endl; // 255(恢复十进制)
    return 0;
}

2. 字符串流(sstream):内存中的流

<sstream> 定义了 “字符串流”,把内存中的字符串当作流来处理,核心用途:

  • 字符串和数值的转换(比 atoi/to_string 更灵活);
  • 拼接 / 解析复杂字符串。
(1)ostringstream:写字符串(输出流)
#include <sstream>
#include <string>
using namespace std;

int main() {
    ostringstream oss;
    // 像 cout 一样写数据到内存字符串
    oss << "姓名:" << "张三" << ",年龄:" << 18 << ",分数:" << 90.5;
    
    // 转为 string
    string s = oss.str();
    cout << s << endl; // 输出:姓名:张三,年龄:18,分数:90.5
    return 0;
}
(2)istringstream:读字符串(输入流)
int main() {
    string s = "10 3.14 hello";
    istringstream iss(s);
    
    int a;
    double b;
    string str;
    // 像 cin 一样从字符串中读取数据
    iss >> a >> b >> str;
    
    cout << a << " " << b << " " << str << endl; // 10 3.14 hello
    return 0;
}
经典场景:拆分带分隔符的字符串
// 拆分 "a,b,c,d" 为 ["a","b","c","d"]
#include <sstream>
#include <vector>
#include <string>
using namespace std;

vector<string> split(const string& s, char delim) {
    vector<string> res;
    istringstream iss(s);
    string token;
    // 按分隔符读取
    while (getline(iss, token, delim)) {
        res.push_back(token);
    }
    return res;
}

int main() {
    string s = "a,b,c,d";
    vector<string> v = split(s, ',');
    for (auto& str : v) {
        cout << str << " "; // a b c d
    }
    return 0;
}

3. 文件流(fstream):文件的流操作

<fstream> 是对文件的流封装,用法和 cin/cout 几乎一致,核心对象:

  • ifstream:文件输入流(读文件);
  • ofstream:文件输出流(写文件);
  • fstream:文件双向流(读写)。
示例:读 / 写文件
#include <fstream>
#include <string>
using namespace std;

int main() {
    // 1. 写文件
    ofstream ofs("test.txt"); // 打开文件(不存在则创建,存在则覆盖)
    if (!ofs) { // 检查文件是否打开成功
        cerr << "文件打开失败!" << endl;
        return 1;
    }
    ofs << "hello file" << endl; // 写数据
    ofs.close(); // 手动关闭(也可让 ofs 析构时自动关闭)
    
    // 2. 读文件
    ifstream ifs("test.txt");
    if (!ifs) {
        cerr << "文件打开失败!" << endl;
        return 1;
    }
    string line;
    while (getline(ifs, line)) { // 逐行读取
        cout << line << endl; // 输出:hello file
    }
    ifs.close();
    return 0;
}

六、自定义类型的流操作(重载 <</>>)

C++ 支持给自定义类重载 <<(输出)和 >>(输入)运算符,让自定义类型也能像内置类型一样用流操作。

示例:给 Student 类重载流操作符

#include <iostream>
#include <string>
using namespace std;

class Student {
public:
    string name;
    int age;
    double score;
    
    // 构造函数
    Student(string n = "", int a = 0, double s = 0) : name(n), age(a), score(s) {}
};

// 重载 <<:输出 Student 对象(必须是全局函数)
ostream& operator<<(ostream& os, const Student& s) {
    os << "[" << s.name << ", " << s.age << ", " << s.score << "]";
    return os; // 返回流对象,支持链式调用
}

// 重载 >>:输入 Student 对象
istream& operator>>(istream& is, Student& s) {
    is >> s.name >> s.age >> s.score; // 按 "姓名 年龄 分数" 格式输入
    return is;
}

int main() {
    Student s("张三", 18, 90.5);
    cout << s << endl; // 输出:[张三, 18, 90.5]
    
    Student s2;
    cout << "输入学生信息(姓名 年龄 分数):";
    cin >> s2; // 用户输入 "李四 20 88.0"
    cout << s2 << endl; // 输出:[李四, 20, 88]
    return 0;
}

七、常见坑点与避坑指南

1. 流的缓冲区导致输出顺序异常

cout << "hello"; // 缓冲区未刷新,暂不显示
cerr << "world"; // cerr 无缓冲,立即显示
// 实际输出:worldhello(而非 hello world)
// 解决方案:手动刷新 cout
cout << "hello" << flush;
cerr << "world";

2. cin >> 跳过空白字符

cin >> 会自动跳过空格、换行、制表符等空白字符,若要读取这些字符,需用 cin.get()

char c1, c2;
cin >> c1; // 输入 "a b" → c1='a'
cin.get(c2); // c2=' '(读取空格)

3. 字符串流的 str () 是拷贝,而非引用

ostringstream oss;
oss << "hello";
string s = oss.str(); // 拷贝 oss 内部的字符串
oss << " world";
cout << s << endl; // 输出 hello(s 是旧值,不会变)

4. 文件流未关闭导致数据丢失

虽然文件流析构时会自动关闭,但如果程序异常退出,析构函数可能不执行,导致数据未写入磁盘:

ofstream ofs("test.txt");
ofs << "hello";
// ofs.close(); // 建议手动关闭,确保数据写入
exit(1); // 异常退出,数据可能丢失

八、核心总结(必记知识点)

  1. 流的本质:数据传输的抽象,分为输入流(读)和输出流(写),接口统一且类型安全;
  2. iostream 核心
    • cin/cout:标准输入输出,>>/<< 是核心操作符;
    • getline:读取带空格的整行字符串;
    • 流状态:good()/eof()/fail() 检测流的健康度,clear() 重置状态;
  3. 进阶流
    • sstream:内存字符串的读写,适合字符串 / 数值转换、字符串拆分;
    • fstream:文件的读写,用法和标准流一致;
  4. 自定义流:重载 <</>> 让自定义类型支持流操作;
  5. 避坑重点
    • cin >> 后接 getlineignore 换行符;
    • cerr 无缓冲,输出顺序可能和 cout 不一致;
    • 流错误需 clear() + ignore() 重置。

理解 C++ 流的核心是 “抽象”—— 无论数据是来自键盘、文件还是内存字符串,都用相同的方式(<</>>)处理,这也是 C++ 相比 C 语言 I/O 更优雅、更安全的根本原因。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐