[C.C++] C++ 标准IO流详解

362 0
Honkers 2026-4-22 20:36:19 来自手机 | 显示全部楼层 |阅读模式

C语言并未将输入/输出功能内置于语言本身。换言之,它没有像read或write这样的关键字。相反,它将输入输出功能交由外部库函数处理(例如stdio库中的printf和scanf)。ANSI C标准将这些输入输出函数称为标准输入输出包(stdio.h)。C++延续了这一做法,并在iostream和fstream等库中对输入输出功能进行了标准化和规范化。

C++IO特性

  • C++的输入输出是类型安全的。
  • C++的输入输出操作基于字节流,与设备无关。

一、流概述

C/C++的输入输出基于流,即程序中进出的字节序列(如同管道中的水和油流动)。在输入操作中,数据字节从输入源(如键盘、文件、网络或其他程序)流向程序;在输出操作中,数据字节从程序流向输出目标(如控制台、文件、网络或其他程序)。流作为程序与实际I/O设备之间的中介,使得程序员无需直接操作设备,从而实现设备无关的输入输出操作。

C++ 同时提供了格式化 I/O 和非格式化 I/O 函数。在格式化或高级 I/O 中,字节被分组并转换为特定类型,如 int、double、string 或用户自定义类型。在非格式化或低级 I/O 中,字节被视为原始字节,不进行任何转换。格式化 I/O 操作通过重载流插入运算符(<<)和流提取运算符(>>)来支持,这提供了一致的公共 I/O 接口。非格式化I/O使用类似于C语言的API接口(read(),write()等)实现。

特性格式化 I/O (High-Level)非格式化 I/O (Low-Level)
处理方式字节被分组并转换为 int, double, string 等特定类型字节被视为原始字节序列 (raw bytes),不做转换
主要操作符/函数<< (插入符) 和 >> (提取符)read(), write(), get(), getline()等
典型场景标准控制台输入输出、读写配置文件二进制文件读写、网络数据包处理
优点类型安全、直观便捷、代码简洁效率高、精确控制、无数据转换开销

为了执行输入和输出,C++ 程序需要执行以下步骤:

  1. 构造一个流对象
  2. 将该流对象关联到一个实际的 I/O 设备(例如键盘、控制台、文件、网络、另一个程序)
  3. 通过流对象公共接口中定义的函数,以设备无关的方式对流执行输入/输出操作。其中一些函数负责在外部格式和内部格式之间转换数据(格式化 I/O),而另一些则不进行转换(非格式化或二进制 I/O)
  4. 断开流与实际 I/O 设备的关联(例如关闭文件)
  5. 释放流对象

二、C++IO类库

1.头文件

C++ 的 I/O 功能在以下头文件中提供:(其中包含了 和 )、(用于文件 I/O)以及 (用于字符串 I/O)。此外, 头文件提供了操纵器,如 setw()、setprecision()、setfill() 和 setbase(),用于格式化。

这些 C++ 头文件确实是 I/O 流库的核心组成部分。它们共同构建了一个强大、统一且可扩展的输入输出系统。

简单来说, 和  分别负责控制台、文件和字符串三大 I/O 场景,而  则专门用于精细化控制数据的显示格式

为了更直观地理解它们的分工,可以参考下面的表格:

头文件核心组件主要用途典型操作
std::cin, std::cout, std::cerr, std::clog标准输入输出(控制台、终端)std::cin >> var;
std::cout << "Hello";
std::ifstream, std::ofstream, std::fstream文件输入输出(读写磁盘文件)std::ifstream in("data.txt");
in >> value;
std::istringstream, std::ostringstream, std::stringstream字符串流(内存中的格式化转换)std::ostringstream oss;
oss << "Age: " << 25;
std::setw(), std::setprecision(), std::setfill(), std::setbase()格式化输出(控制宽度、精度、填充字符、进制)std::cout << std::setw(10) << std::setfill('*') << 42;

设计理念:分层与复用

值得一提的是, 本身并不是一个庞大的单体,它内部包含了  和 ,这种设计体现了职责分离的思想:

  •  定义了所有流共有的状态和类型。
  •  和  定义了输入和输出的核心逻辑。
  •  则作为底层接口,负责与实际的字符来源或目的地(如键盘、文件)打交道。

这种分层结构让开发者能够灵活地组合和扩展,比如通过继承 std::streambuf 就能创建支持自定义设备(如网络流)的 I/O 流。

2.模板类

核心思想是:避免为每种字符类型重复编写代码,而是用模板生成。为了支持各种字符集(C++98/03 中的 char 和 wchar_t;以及 C++11 引入的 char16_t、char32_t),流类被设计为模板类,可以使用实际的字符类型进行实例化。大多数模板类接受两个类型参数。例如:

  1. template <class charT, class traits = char_traits<charT> >
  2. class basic_istream;
  3. template <class charT, class traits = char_traits<charT> >
  4. class basic_ostream;
复制代码

其中:

  • charT 是字符类型,例如 char 或 wchar_t;
  • traits 是另一个模板类 char_traits 的实例,定义了字符操作的属性,例如字符集的排序顺序(排列序列)。

char_traits 是一个模板类,它封装了与字符类型相关的基本操作,例如:

操作说明
eq(c1, c2)判断两个字符是否相等
lt(c1, c2)判断字符顺序(用于排序)
length(s)获取字符串长度
copy(dst, src, n)复制字符序列
find(s, n, c)在字符串中查找字符
eof()返回文件结束标记

通过 traits 参数,basic_istream 不需要关心字符比较、复制等细节,只要调用 traits::eq() 即可。不同的字符类型可以拥有不同的 char_traits 特化

(1)模板实例化与 typedef

如前所述,basic_xxx 模板类可以使用字符类型(如 char 和 wchar_t)进行实例化。C++ 进一步提供了 typedef 语句来为这些类命名:

  1. typedef basic_ios<char> ios;
  2. typedef basic_ios<wchar_t> wios;
  3. typedef basic_istream<char> istream;
  4. typedef basic_istream<wchar_t> wistream;
  5. typedef basic_ostream<char> ostream;
  6. typedef basic_ostream<wchar_t> wostream;
  7. typedef basic_iostream<char> iostream;
  8. typedef basic_iostream<wchar_t> wiostream;
  9. typedef basic_streambuf<char> streambuf;
  10. typedef basic_streambuf<wchar_t> wstreambuf;
复制代码

直接写 basic_istream 太繁琐了,所以标准库提供了常见的类型别名:

原始模板实例化类型别名说明
basic_istreamistream普通字符输入流(最常用)
basic_istreamwistream宽字符输入流
basic_ostreamostream普通字符输出流
basic_ostreamwostream宽字符输出流
basic_iostreamiostream双向流(即可读又可写)
basic_streambufstreambuf流缓冲区基类

你平时写的 cin、cout 实际上就是 istream 和 ostream 类型的对象。

(2)char 类型的特化类

我们将重点关注 char 类型的特化类:

  • ios_base 和 ios:用于维护公共流属性的超类,如格式标志、字段宽度、精度和区域设置。超类 ios_base(非模板类)维护与模板参数无关的数据;而子类 ios(basic_ios 的实例化)维护与模板参数相关的数据。
  • istream(basic_istream)和 ostream(basic_ostream:提供输入和输出的公共接口。
  • iostream(basic_iostream:istream 和 ostream 的子类,支持双向输入输出操作。请注意,istream 和 ostream 是单向流;而 iostream 是双向流。basic_iostream 模板和 iostream 类在  头文件中声明,而不是在  头文件中。
  • ifstream、ofstream 和 fstream:用于文件输入、输出和双向输入输出。
  • istringstream、ostringstream 和 stringstream:用于字符串缓冲区的输入、输出和双向输入输出。
  • streambuf、filebuf 和 stringbuf:为流、文件流和字符串流提供内存缓冲区,并提供访问和管理缓冲区的公共接口。

类名职责
ios_base非模板基类,维护与字符类型无关的状态(如格式标志、字段宽度、精度、区域设置)
iosbasic_ios 的别名,维护与 char 相关的状态
istream / ostream单方向输入/输出的公共接口(最常用)
iostream双向流(继承自 istream 和 ostream),支持读写
ifstream / ofstream / fstream文件流(输入、输出、双向)
istringstream / ostringstream / stringstream字符串流(在内存中的 string 上做 I/O)
streambuf / filebuf / stringbuf缓冲区管理类,提供底层的内存或文件操作接口

一个容易被忽略的点:basic_iostream 和 iostream 虽然名字看起来像是 iostream 头文件中的内容,但它们实际上是在  头文件中声明的(因为 iostream 同时依赖输入和输出,而输入的核心定义在  中)。

三、Buffered IO

Buffered I/O(缓冲I/O) 是 C++ 标准库中 I/O 流系统的核心设计理念。它的基本思想是:在内存中维护一个缓冲区,批量地进行数据读写,而不是每次操作都直接访问底层设备(如磁盘、控制台、网络)

直接操作底层 I/O 设备是非常昂贵的:

操作类型代价示例
直接 I/O很高每次 read()/write() 都触发系统调用
缓冲 I/O较低累积一定数据后,只触发少数系统调用

1.核心组件:streambuf

C++ 中缓冲机制的核心类是 std::streambuf。它负责:

  • 管理缓冲区:维护一组指针(读/写缓冲区的位置)
  • 填充缓冲区:从实际设备读取数据到缓冲区
  • 刷新缓冲区:将缓冲区数据写入实际设备
  1. // streambuf 维护的关键指针(示意图)
  2. class streambuf {
  3. char* pbase(); // 缓冲区起始位置
  4. char* pptr(); // 写指针当前位置
  5. char* epptr(); // 写缓冲区末尾
  6. char* eback(); // 读缓冲区起始
  7. char* gptr(); // 读指针当前位置
  8. char* egptr(); // 读缓冲区末尾
  9. };
复制代码

2.三种常用的 streambuf 派生类

派生类用途缓冲区位置
std::filebuf文件 I/O 缓冲内存中的文件缓冲区
std::stringbuf字符串 I/O 缓冲std::string 对象
std::basic_streambuf自定义设备缓冲由用户定义(如网络流)

3.缓冲区刷新时机

触发条件说明
缓冲区满自动刷新
显式调用 flush()std::cout.flush();
输出 std::endl插入换行符 + 刷新
对象析构流对象销毁时自动刷新
同步设置 std::unitbuf每次输出后立即刷新
输入操作请求从输入流读取时可能刷新输出缓冲区

4.三种缓冲模式

C++ 流支持三种缓冲策略(通过 std::ios_base::sync_with_stdio 和相关设置):

模式说明典型流
全缓冲缓冲区满才刷新文件流(ofstream)
行缓冲遇到换行符刷新终端输出(cout 通常为行缓冲)
无缓冲立即刷新错误流(cerr)

四、简介

 头文件还包含了以下头文件: 和 。因此,你的程序只需要包含  头文件即可进行 I/O 操作。

一个常见的误解:很多人以为  是 C++ I/O 的全部。实际上,真正的核心类(如 basic_istream)定义在  中,而  只是将这些组件组合在一起,并声明了全局对象。

 头文件声明了以下标准流对象:

  • cin(istream 类的对象,即 basic_istream 的特化)、wcin(wistream 类的对象,即 basic_istream 的特化):对应标准输入流,默认关联到键盘。
  • cout(ostream 类的对象)、wcout(wostream 类的对象):对应标准输出流,默认关联到显示控制台。
  • cerr(ostream 类的对象)、wcerr(wostream 类的对象):对应标准错误流,默认关联到显示控制台。
  • clog(ostream 类的对象)、wclog(wostream 类的对象):对应标准日志流,默认关联到显示控制台。
对象名宽字符版本类型默认设备典型用途
cinwcinistream键盘程序输入
coutwcoutostream控制台常规输出
cerrwcerrostream控制台立即输出错误信息(无缓冲)
clogwclogostream控制台记录日志信息(有缓冲)

理解这四个标准流对象,是掌握 C++ I/O 的起点。它们的设计体现了 C++ 的几个核心思想:类型安全(通过运算符重载)、设备无关性(可重定向)、以及国际化支持(窄/宽字符双版本)。

1. 关键区别:cerr vs clog

这是很多开发者容易混淆的地方:

特性cerrclog
缓冲无缓冲有缓冲
刷新时机每次输出立即刷新缓冲区满或显式刷新
用途紧急错误信息普通日志/调试信息
性能较慢(每次系统调用)较快(批量输出)
  1. #include <iostream>
  2. #include <thread>
  3. #include <chrono>
  4. int main() {
  5. std::cerr << "错误:立即显示" << std::endl;
  6. std::clog << "日志:可能稍后才显示" << std::endl;
  7. // clog 的输出可能会留在缓冲区中,直到程序结束或缓冲区满
  8. }
复制代码

2. 字符集支持:窄字符 vs 宽字符

类型字符类型典型使用场景
窄字符版本(cin、cout 等)char英文、ASCII 文本
宽字符版本(wcin、wcout 等)wchar_t国际化文本、中文等 Unicode
  1. #include <iostream>
  2. #include <locale>
  3. int main() {
  4. // 设置全局 locale 为中文环境
  5. std::locale::global(std::locale("zh_CN.UTF-8"));
  6. // 宽字符输出
  7. wchar_t chinese[] = L"你好,世界!";
  8. std::wcout << chinese << std::endl;
  9. // 窄字符输出(英文)
  10. std::cout << "Hello, World!" << std::endl;
  11. return 0;
  12. }
复制代码

3. 流的可重定向性

虽然 cout、cerr、clog 默认都输出到控制台,但你可以将它们重定向到不同的目标:

  1. #include <iostream>
  2. #include <fstream>
  3. int main() {
  4. std::ofstream logFile("log.txt");
  5. // 将 clog 重定向到文件
  6. std::clog.rdbuf(logFile.rdbuf());
  7. std::clog << "这条日志会写入文件,而不是控制台" << std::endl;
  8. // cout 仍然输出到控制台
  9. std::cout << "这条输出到控制台" << std::endl;
  10. return 0;
  11. }
复制代码

4. 一个容易被忽略的细节:cerr 与 cout 的同步

cerr 和 cout 默认是关联的(tie在一起)。这意味着:

  1. std::cout << "Hello";
  2. std::cerr << "World";
  3. // 即使没有换行,cerr 的输出可能会在 cout 之前出现
  4. // 因为 cerr 的无缓冲特性可能导致输出顺序交错
复制代码

如果想保证顺序,可以显式刷新:

  1. std::cout << "Hello" << std::flush;
  2. std::cerr << "World";
复制代码

五、流插入与提取 运算符

格式化输出通过流插入运算符 << 和流提取运算符 >> 在流上执行。例如:

  1. cout << value; // 将 value 发送到 cout(输出)
  2. cin >> variable; // 从 cin 读取数据到 variable(输入)
复制代码

请注意,cin/cout 必须是左操作数,数据流动方向与箭头方向一致。

<< 和 >> 运算符被重载以处理基本类型(如 int、double)和类类型(如 string)。你也可以为自定义类型重载这些运算符。

cin << 和 cout >> 返回对 cin 和 cout 的引用,因此支持级联操作。例如:

  1. cout << value1 << value2 << .... ; // 连续输出多个值
  2. cin >> variable1 >> variable2 .... ; // 连续输入多个变量
复制代码

 这是C++ I/O 中最核心、最常用的两个运算符:<< 和 >>。它们的设计体现了 C++ 运算符重载的优雅之处。

1. 方向记忆技巧

运算符名称数据流向记忆方法
<<流插入数据 → 流箭头指向流对象(如 cout)
>>流提取流 → 变量箭头指向变量

直观理解

  • cout << x:把 x 插入到输出流中 → 数据流出程序
  • cin >> x:从输入流中提取数据到 x → 数据流入程序

2.为什么箭头方向是这样的

<< 和 >> 原本是 C++ 的左移右移位运算符。Bjarne Stroustrup(C++ 之父)选择重载它们作为 I/O 运算符,是因为:

  • cout << x 看起来像把 x 向左推向 cout → 直观
  • 运算符优先级和结合性也适合级联操作

3.级联操作的原理

  1. cout << a << b << c;
复制代码

上述代码实际上被解析为:

  1. ((cout << a) << b) << c;
复制代码
  • cout << a 返回 cout 的引用
  • 然后 (cout) << b 继续操作
  • 以此类推

这就是为什么可以无限级联的原因。

4.为自定义类型重载 << 和 >>

你可以让自己的类也支持流 I/O:

  1. #include <iostream>
  2. #include <string>
  3. class Person {
  4. private:
  5. std::string name;
  6. int age;
  7. public:
  8. // 构造函数
  9. Person(const std::string& n = "", int a = 0) : name(n), age(a) {}
  10. // 友元声明:重载输出运算符
  11. friend std::ostream& operator<<(std::ostream& os, const Person& p);
  12. // 友元声明:重载输入运算符
  13. friend std::istream& operator>>(std::istream& is, Person& p);
  14. };
  15. // 输出运算符实现
  16. std::ostream& operator<<(std::ostream& os, const Person& p) {
  17. os << "Person{name: " << p.name << ", age: " << p.age << "}";
  18. return os; // 返回流引用以支持级联
  19. }
  20. // 输入运算符实现
  21. std::istream& operator>>(std::istream& is, Person& p) {
  22. std::cout << "Enter name: ";
  23. is >> p.name;
  24. std::cout << "Enter age: ";
  25. is >> p.age;
  26. return is; // 返回流引用以支持级联
  27. }
  28. int main() {
  29. Person p1("Alice", 25);
  30. std::cout << p1 << std::endl; // 输出: Person{name: Alice, age: 25}
  31. Person p2;
  32. std::cin >> p2; // 输入: Alice 25
  33. std::cout << p2 << std::endl;
  34. return 0;
  35. }
复制代码

关键要点

  • 必须返回流对象的引用(std::ostream& 或 std::istream&)
  • 通常声明为友元函数(非成员函数),因为左操作数是流对象,不是你的类对象
  • 输入运算符需要修改对象,所以参数是非 const 引用

5.endl 的配合使用

  1. cout << "Hello" << endl; // endl 插入换行符并刷新缓冲区
  2. cout << "World\n"; // \n 只插入换行符,不刷新缓冲区
复制代码

endl 在调试时很有用(确保输出立即显示),但会降低性能。生产代码中,优先使用 \n。

7. 输入运算符的链式输入

  1. int a, b, c;
  2. cin >> a >> b >> c;
  3. // 等价于
  4. // (cin >> a) >> b) >> c
  5. // 用户输入: 10 20 30
复制代码

输入时,空格、Tab、换行符都作为分隔符。

六、ostream 类

ostream 类是 basic_ostream 的 typedef。它包含两组输出函数:格式化输出非格式化输出

  • 格式化输出函数(通过重载的流插入运算符 <<)将数值(如 int、double)从其内部表示形式(例如 16/32 位 int、64 位 double)转换为表示该数值文本形式的字符流。
  • 非格式化输出函数(如 put()、write())按原样输出字节,不进行格式转换。
类型函数数据转换典型用途
格式化输出<<二进制 → 文本打印用户可读的文本、数字
非格式化输出put()、write()无转换二进制文件写入、原始字节传输
  1. #include <iostream>
  2. int main() {
  3. int num = 65;
  4. // 格式化输出:将 65 转换为字符 '6','5'
  5. std::cout << num << std::endl; // 输出: 65
  6. // 非格式化输出:直接输出字节值
  7. std::cout.put(65); // 输出: A (ASCII 65)
  8. std::cout.write((char*)&num, sizeof(num)); // 输出二进制表示(可能不是可读字符)
  9. return 0;
  10. }
复制代码

1.重载的流插入运算符 << 进行格式化输出

ostream 类为每个 C++ 基本类型重载了流插入运算符 <<:char、unsigned char、signed char、short、unsigned short、int、unsigned int、long、unsigned long、long long(C++11)、unsigned long long(C++11)、float、double 和 long double。它将数值从内部表示形式转换为文本形式。

  1. // 整数类型
  2. ostream& operator<<(int val);
  3. ostream& operator<<(long val);
  4. ostream& operator<<(long long val);
  5. ostream& operator<<(unsigned int val);
  6. // ... 等等
  7. // 浮点类型
  8. ostream& operator<<(float val);
  9. ostream& operator<<(double val);
  10. ostream& operator<<(long double val);
  11. // 字符和字符串
  12. ostream& operator<<(char ch);
  13. ostream& operator<<(const char* str);
  14. // 指针
  15. ostream& operator<<(void* ptr);
复制代码

<< 运算符返回对调用它的 ostream 对象的引用。因此,你可以级联 << 操作,例如:

  1. cout << 123 << 1.13 << endl;
复制代码

<< 运算符还为以下指针类型重载:

  • const char*、const signed char*、const unsigned char*:用于输出 C 字符串和字面量。它使用空终止字符来判断字符数组的结尾。
  • void*:可用于输出地址。

例如:

  1. char str1[] = "apple";
  2. const char* str2 = "orange";
  3. cout << str1 << endl; // 使用 char*,打印 C 字符串
  4. cout << str2 << endl; // 使用 char*,打印 C 字符串
  5. cout << (void*)str1 << endl; // 使用 void*,打印地址(C 风格转换)
  6. cout << static_cast<void*>(str2) << endl; // 使用 void*,打印地址(C++ 风格转换)
复制代码

2.刷新输出缓冲区

你可以通过以下方式刷新输出缓冲区:

方式代码是否插入换行是否刷新缓冲区
输出 \ncout << "hello\n";❌(可能不刷新)
endlcout << "hello" << endl;
flushcout << "hello" << flush;
cout.flush()cout.flush();

性能提示:频繁使用 endl 会降低性能,因为每次都会强制刷新缓冲区。在普通文本输出中,优先使用 \n。

(1)flush 成员函数或操纵器

  1. // ostream 类的成员函数 - std::ostream::flush
  2. ostream& flush();
  3. // 示例
  4. cout << "hello";
  5. cout.flush();
  6. // 操纵器 - std::flush
  7. ostream& flush(ostream& os);
  8. // 示例
  9. cout << "hello" << flush;
复制代码

(3)endl 操纵器

endl 插入换行符并刷新缓冲区。输出换行符 '\n' 可能不会刷新输出缓冲区,但 endl 会。

  1. // 操纵器 - std::endl
  2. ostream& endl(ostream& os);
复制代码

(3)cin 输入前自动刷新

当输入操作等待时,输出缓冲区会被自动刷新。例如:

  1. #include <iostream>
  2. #include <thread>
  3. #include <chrono>
  4. int main() {
  5. std::cout << "Please enter a number: "; // 没有 endl,也没有 \n
  6. // 此时输出可能还在缓冲区中,尚未显示
  7. std::this_thread::sleep_for(std::chrono::seconds(2)); // 假设一些计算
  8. int num;
  9. std::cin >> num; // ⬅️ 在这里,输出缓冲区被自动刷新,提示信息才显示出来
  10. return 0;
  11. }
复制代码

原理:cin 与 cout 通过 tie() 关联在一起。当 cin 等待输入时,会自动调用 cout.flush()。

3.字符串输出与地址输出的区别

这是一个容易混淆的地方:

  1. #include <iostream>
  2. int main() {
  3. char str[] = "Hello";
  4. std::cout << str << std::endl; // 输出: Hello(字符串内容)
  5. std::cout << static_cast<void*>(str) << std::endl; // 输出: 0x7ffd...(内存地址)
  6. // 不加转换时,编译器选择 const char* 重载
  7. // 加转换后,编译器选择 void* 重载
  8. }
复制代码

七、istream 类

与 ostream 类类似,istream 类是 basic_istream 的 typedef。它也支持格式化输入非格式化输入

  • 格式化输入:通过重载的提取运算符 >>,将文本形式(字符流)转换为内部表示形式(如 16/32 位 int、64 位 double)。
  • 非格式化输入:如 get()、getline()、read() 等函数,按原样读取字符,不进行转换。
类型函数数据转换典型用途
格式化输入>>文本 → 二进制读取用户输入的数字、单词
非格式化输入get()、getline()、read()无转换读取含空格的字符串、二进制数据
  1. #include <iostream>
  2. #include <string>
  3. int main() {
  4. int num;
  5. char line[100];
  6. // 格式化输入:自动将 "123" 转换为整数 123
  7. std::cin >> num;
  8. // 非格式化输入:原样读取字符,包括空格
  9. std::cin.getline(line, 100);
  10. return 0;
  11. }
复制代码

1.重载的流提取运算符 >> 进行格式化输入

istream 类为每个 C++ 基本类型重载了提取运算符 >>:char、unsigned char、signed char、short、unsigned short、int、unsigned int、long、unsigned long、long long(C++11)、unsigned long long(C++11)、float、double 和 long double。它通过将输入的文本转换为相应类型的内部表示来执行格式化。

>> 运算符有几个重要特性:

特性说明示例
跳过前导空白自动跳过空格、Tab、换行符输入 " 123" → 读取 123
空格分隔遇到空白字符停止读取输入 "hello world" → 只读 "hello"
类型转换自动将文本转换为目标类型输入 "3.14" → double 变量得到 3.14
错误处理类型不匹配时设置失败状态输入 "abc" 给 int → failbit 被设置
  1. istream& operator>> (type&); // type 为 int、double 等
复制代码

>> 运算符返回对调用它的 istream 对象的引用。因此,你可以级联 >> 操作,例如:

  1. cin >> number1 >> number2 >> ...;
复制代码
  1. >>运算符还为以下指针类型重载:
复制代码
  • char*、signed char*、unsigned char*:用于输入 C 字符串。它使用空白字符作为分隔符,并为 C 字符串添加空终止字符。

使用 >> 读取 C 字符串(char*)是不安全的:

  1. char buffer[10];
  2. std::cin >> buffer; // ⚠️ 如果输入超过 9 个字符,会发生缓冲区溢出!
复制代码

推荐做法

  • 使用 std::string 代替 C 字符串
  • 或使用 setw() 限制宽度:std::cin >> std::setw(10) >> buffer;


2.刷新输入缓冲区 - ignore()

你可以使用 ignore() 来丢弃输入缓冲区中的字符:

  1. istream& ignore (int n = 1, int delim = EOF);
  2. // 读取并丢弃最多 n 个字符,或直到遇到 delim 字符(以先到者为准)
复制代码

场景一:清除残留的换行符

  1. int age;
  2. std::string name;
  3. std::cin >> age; // 输入年龄后按回车,换行符留在缓冲区
  4. std::cin.ignore(); // 清除缓冲区中的换行符
  5. std::getline(std::cin, name); // 现在可以正确读取姓名(可能包含空格)
复制代码

场景二:跳过无效输入

  1. int num;
  2. std::cout << "Enter a number: ";
  3. while (!(std::cin >> num)) {
  4. std::cin.clear(); // 清除错误状态
  5. std::cin.ignore(10000, '\n'); // 丢弃错误的输入
  6. std::cout << "Invalid, try again: ";
  7. }
复制代码

场景三:忽略整行

  1. // 跳过当前行的剩余内容
  2. std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
复制代码

3.cin >> 与 getline() 混用的常见陷阱

  1. #include <iostream>
  2. #include <string>
  3. int main() {
  4. int age;
  5. std::string name;
  6. std::cout << "Age: ";
  7. std::cin >> age; // 用户输入: 25[Enter]
  8. // 缓冲区: "25\n"
  9. std::cout << "Name: ";
  10. std::getline(std::cin, name); // ⚠️ 读取到的是空字符串!
  11. // 因为 getline 读取了残留的 '\n'
  12. // 解决方法:在 getline 前调用 ignore
  13. // std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
  14. return 0;
  15. }
复制代码

八、 非格式化输入/输出函数

本段介绍 C++ 中非格式化 I/O 的核心函数。与格式化 I/O(<< 和 >>)不同,这些函数处理的是原始字节,不进行任何转换。

1.输入函数对比:get() vs getline() vs read()

函数分隔符处理是否保留分隔符是否添加 \0适用场景
get(char*, n, delim)遇到 delim 停止✅ 保留在流中✅ 是需要查看分隔符的场景
getline(char*, n, delim)遇到 delim 停止❌ 丢弃✅ 是常规行读取(推荐)
read(char*, n)读取固定数量不适用❌ 否二进制数据读取

示例:

  1. #include <iostream>
  2. #include <cstring>
  3. int main() {
  4. char buffer[100];
  5. // get() 保留分隔符
  6. std::cin.get(buffer, 100, '\n');
  7. std::cout << "get(): " << buffer << std::endl;
  8. // 需要手动处理残留的分隔符
  9. std::cin.get(); // 消耗掉 '\n'
  10. // getline() 丢弃分隔符
  11. std::cin.getline(buffer, 100);
  12. std::cout << "getline(): " << buffer << std::endl;
  13. return 0;
  14. }
复制代码

2. put() 与 write() 的区别

函数输出内容停止条件典型用途
put(char)单个字符输出一个字符逐字符输出
write(buf, n)n 个字符输出指定数量二进制输出、固定长度块

  1. #include <iostream>
  2. int main() {
  3. // put() - 单个字符
  4. std::cout.put('H').put('i').put('\n');
  5. // write() - 固定数量
  6. char data[] = "Hello\0World"; // 包含空字符
  7. std::cout.write(data, 11); // 输出 "HelloWorld"(空字符也会输出)
  8. // 二进制数据输出
  9. int num = 0x12345678;
  10. std::cout.write(reinterpret_cast<char*>(&num), sizeof(num));
  11. return 0;
  12. }
复制代码

3. gcount() 的重要作用

gcount() 通常用于检查实际读取了多少数据:

  1. #include <iostream>
  2. #include <fstream>
  3. int main() {
  4. std::ifstream file("data.bin", std::ios::binary);
  5. char buffer[256];
  6. file.read(buffer, sizeof(buffer));
  7. std::streamsize bytesRead = file.gcount(); // 实际读取的字节数
  8. std::cout << "尝试读取: " << sizeof(buffer) << " 字节" << std::endl;
  9. std::cout << "实际读取: " << bytesRead << " 字节" << std::endl;
  10. return 0;
  11. }
复制代码

4. peek() 和 putback() 的典型用法

这两个函数用于前瞻回退,在解析复杂格式时非常有用:

  1. #include <iostream>
  2. #include <cctype>
  3. int main() {
  4. char ch;
  5. std::cout << "输入一个数字或字母: ";
  6. ch = std::cin.peek(); // 偷看下一个字符,但不提取
  7. if (std::isdigit(ch)) {
  8. std::cin >> ch; // 确认是数字,正式读取
  9. std::cout << "你输入了数字: " << ch << std::endl;
  10. } else {
  11. std::cin.get(ch); // 读取并处理字母
  12. std::cout << "你输入了字母: " << ch << std::endl;
  13. }
  14. // putback 示例:读取后放回
  15. char c = std::cin.get();
  16. std::cout << "读取到: " << c << std::endl;
  17. std::cin.putback(c); // 放回去
  18. std::cout << "放回去了,可以重新读取" << std::endl;
  19. return 0;
  20. }
复制代码

5. 函数关系图

  1. 非格式化输入函数
  2. ├── 单字符输入
  3. │ ├── get() → 返回 int(可检测 EOF)
  4. │ └── get(char&) → 返回 istream&
  5. ├── 字符串输入
  6. │ ├── get(buf, n, delim) → 保留分隔符
  7. │ └── getline(buf, n, delim) → 丢弃分隔符
  8. ├── 二进制输入
  9. │ └── read(buf, n) → 固定数量,无转换
  10. └── 辅助函数
  11. ├── gcount() → 查询上次读取数量
  12. ├── peek() → 前瞻
  13. └── putback() → 回退
复制代码

6. 常见陷阱与注意事项

陷阱说明解决方案
getline() 与 >> 混用>> 留下换行符,导致 getline() 读空行使用 cin.ignore() 清除
read() 没有 \0 终止直接输出可能越界手动添加终止符或记录长度
gcount() 的返回值只对最后一次非格式化输入有效在需要的结果后立即调用
get() 返回 int 而不是 char为了能返回 EOF(通常是 -1)用 int 变量接收,检查 EOF

九、流的状态

流超类 ios_base 维护一个数据成员来描述流的状态,这是一个类型为 iostate 的位掩码。标志包括:

  • eofbit:当输入操作到达文件末尾时设置。
  • failbit:上一次输入操作未能读取预期的字符,或输出操作未能写入预期的字符时设置。例如,getline() 在未遇到分隔符的情况下读取了 n 个字符。
  • badbit:由于 I/O 操作失败(如文件读/写错误)或流缓冲区出现问题而导致的严重错误。
  • goodbit:上述错误都不存在,值为 0。

这些标志在 ios_base 中声明为公共静态成员。可以通过 ios_base::failbit 直接访问,也可以通过子类如 cin::failbit、ios::failbit 访问。

然而,更方便的方式是使用 ios 类的以下公共成员函数:

成员函数说明
good()如果设置了 goodbit(即无错误),返回 true
eof()如果设置了 eofbit,返回 true
fail()如果设置了 failbit 或 badbit,返回 true
bad()如果设置了 badbit,返回 true
clear()清除 eofbit、failbit 和 badbit
  1. #include <iostream>
  2. #include <fstream>
  3. int main() {
  4. std::ifstream file("data.txt");
  5. int num;
  6. file >> num;
  7. // 检查流状态
  8. if (file.good()) {
  9. std::cout << "读取成功: " << num << std::endl;
  10. }
  11. if (file.eof()) {
  12. std::cout << "到达文件末尾" << std::endl;
  13. }
  14. if (file.fail()) {
  15. std::cout << "读取失败(格式错误或数据不足)" << std::endl;
  16. }
  17. if (file.bad()) {
  18. std::cout << "严重错误(流损坏)" << std::endl;
  19. }
  20. return 0;
  21. }
复制代码

1. 四种状态标志的含义

标志含义典型触发场景是否可恢复
goodbit一切正常无任何错误-
eofbit到达文件末尾读取到 EOF通常需要重新打开流
failbit逻辑错误输入 "abc" 到 int 变量;getline 读到最大长度而未遇分隔符✅ 可清除后重试
badbit严重错误文件损坏、磁盘错误、流缓冲区不可用通常无法恢复

2.状态标志的关系

  1. good() == true ⇔ goodbit 被设置(无任何错误)
  2. good() == false ⇔ eofbit 或 failbit 或 badbit 被设置
  3. fail() == true ⇔ failbit 或 badbit 被设置(不包含 eofbit)
  4. bad() == true ⇔ badbit 被设置
  5. eof() == true ⇔ eofbit 被设置
复制代码

3.状态转换图

这是最经典的用法——在输入失败时恢复:

  1. #include <iostream>
  2. #include <limits>
  3. int main() {
  4. int age;
  5. std::cout << "请输入年龄: ";
  6. while (!(std::cin >> age)) {
  7. // 输入失败:failbit 被设置
  8. if (std::cin.eof()) {
  9. std::cout << "输入流已关闭" << std::endl;
  10. return 1;
  11. }
  12. std::cout << "无效输入,请输入数字: ";
  13. // 清除 failbit
  14. std::cin.clear();
  15. // 丢弃错误的输入行
  16. std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
  17. }
  18. std::cout << "年龄是: " << age << std::endl;
  19. return 0;
  20. }
复制代码

4. clear() 的多种用法

  1. #include <iostream>
  2. int main() {
  3. int num;
  4. std::cin >> num;
  5. // 清除所有错误标志
  6. std::cin.clear();
  7. // 清除特定标志(保留其他)
  8. std::cin.clear(std::cin.rdstate() & ~std::ios::failbit);
  9. // 设置特定状态
  10. std::cin.clear(std::ios::eofbit);
  11. return 0;
  12. }
复制代码

5. 常见陷阱:eof() 与 fail() 的区别

  1. #include <iostream>
  2. #include <fstream>
  3. int main() {
  4. std::ifstream file("numbers.txt");
  5. int num;
  6. // ❌ 错误的循环条件
  7. while (!file.eof()) { // 陷阱:eofbit 在尝试读取后才会设置
  8. file >> num;
  9. std::cout << num << std::endl; // 可能多输出一次
  10. }
  11. // ✅ 正确的循环条件
  12. while (file >> num) { // 读取成功才进入循环
  13. std::cout << num << std::endl;
  14. }
  15. // 等价写法
  16. while (true) {
  17. file >> num;
  18. if (file.fail()) break; // 读取失败(包括 eof)时退出
  19. std::cout << num << std::endl;
  20. }
  21. return 0;
  22. }
复制代码

6. rdstate() 获取原始状态

  1. #include <iostream>
  2. int main() {
  3. int num;
  4. std::cin >> num;
  5. std::ios::iostate state = std::cin.rdstate();
  6. if (state & std::ios::eofbit) {
  7. std::cout << "EOF" << std::endl;
  8. }
  9. if (state & std::ios::failbit) {
  10. std::cout << "FAIL" << std::endl;
  11. }
  12. if (state & std::ios::badbit) {
  13. std::cout << "BAD" << std::endl;
  14. }
  15. return 0;
  16. }
复制代码

十、格式化输入/输出

C++ 提供了一组操纵器来执行输入和输出格式化:

  •  头文件:setw()、setprecision()、setbase()、setfill()
  •  头文件:fixed/scientific、left/right/internal、boolalpha/noboolalpha等

1. 操纵器分类汇总

类别操纵器头文件粘性说明
字段控制setw(n)设置字段宽度
setfill(c)设置填充字符
left/right/internal设置对齐方式
浮点数setprecision(n)设置精度
fixed/scientific设置格式模式
showpoint/noshowpoint是否显示尾随零
整数进制dec/hex/oct设置进制
setbase(n)设置进制
showbase/noshowbase显示进制前缀
uppercase/nouppercase大写输出
正号showpos/noshowpos显示正号
布尔值boolalpha/noboolalphatrue/false 或 0/1

2. 粘性(Sticky)vs 非粘性(Non-sticky)

这是使用操纵器时最容易出错的地方:

  1. // setw() 是非粘性的
  2. cout << setw(10) << 123 << 456 << endl; // 输出: " 123456"
  3. // 只有 123 使用了宽度 10,456 使用默认宽度
  4. // 其他操纵器是粘性的
  5. cout << hex << 123 << " " << 456 << endl; // 输出: "7b 1c8"
  6. cout << dec << endl; // 需要手动恢复
复制代码

3. internal 对齐方式的特殊用途

internal 对齐在显示带符号的数字时非常有用:

  1. #include <iostream>
  2. #include <iomanip>
  3. using namespace std;
  4. int main() {
  5. cout << showpos;
  6. cout << setfill('0');
  7. cout << internal << setw(8) << 123 << endl; // +0000123
  8. cout << internal << setw(8) << -456 << endl; // -0000456
  9. // 符号左对齐,数字右对齐
  10. return 0;
  11. }
复制代码

4. 浮点数格式的精度陷阱

  1. #include <iostream>
  2. #include <iomanip>
  3. using namespace std;
  4. int main() {
  5. double pi = 3.14159265358979;
  6. // 默认模式:总有效数字 4 位
  7. cout << setprecision(4) << pi << endl; // 3.142
  8. // fixed 模式:小数点后 4 位
  9. cout << fixed << setprecision(4) << pi << endl; // 3.1416
  10. // scientific 模式:小数点后 4 位
  11. cout << scientific << setprecision(4) << pi << endl; // 3.1416e+00
  12. return 0;
  13. }
复制代码

5. 进制与 showbase 的组合效果

进制noshowbase(默认)showbase
dec123123
hex7b0x7b
oct1730173
  1. cout << showbase;
  2. cout << dec << 123 << endl; // 123
  3. cout << hex << 123 << endl; // 0x7b
  4. cout << oct << 123 << endl; // 0173
复制代码

6. 保存和恢复格式状态

当需要临时改变格式时,最好保存当前状态:

  1. #include <iostream>
  2. #include <iomanip>
  3. using namespace std;
  4. int main() {
  5. // 保存当前格式状态
  6. ios::fmtflags old_flags = cout.flags();
  7. int old_precision = cout.precision();
  8. char old_fill = cout.fill();
  9. // 临时更改格式
  10. cout << hex << showbase << setw(10) << setfill('0') << 255 << endl;
  11. // 恢复格式状态
  12. cout.flags(old_flags);
  13. cout.precision(old_precision);
  14. cout.fill(old_fill);
  15. cout << 255 << endl; // 恢复正常输出
  16. return 0;
  17. }
复制代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2026 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行