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)
处理方式 字节被分组并转换为 intdoublestring 等特定类型 字节被视为原始字节序列 (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 功能在以下头文件中提供:<iostream>(其中包含了 <ios><istream><ostream>和 <streambuf>)、<fstream>(用于文件 I/O)以及 <sstream>(用于字符串 I/O)。此外,<iomanip> 头文件提供了操纵器,如 setw()setprecision()setfill() 和 setbase(),用于格式化。

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

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

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

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

设计理念:分层与复用

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

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

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

2.模板类

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

template <class charT, class traits = char_traits<charT> >
class basic_istream;

template <class charT, class traits = char_traits<charT> >
class basic_ostream;

其中:

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

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

操作 说明
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 语句来为这些类命名:

typedef basic_ios<char>           ios;
typedef basic_ios<wchar_t>        wios;
typedef basic_istream<char>       istream;
typedef basic_istream<wchar_t>    wistream;
typedef basic_ostream<char>       ostream;
typedef basic_ostream<wchar_t>    wostream;
typedef basic_iostream<char>      iostream;
typedef basic_iostream<wchar_t>   wiostream;
typedef basic_streambuf<char>     streambuf;
typedef basic_streambuf<wchar_t>  wstreambuf;

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

原始模板实例化 类型别名 说明
basic_istream<char> istream 普通字符输入流(最常用)
basic_istream<wchar_t> wistream 宽字符输入流
basic_ostream<char> ostream 普通字符输出流
basic_ostream<wchar_t> wostream 宽字符输出流
basic_iostream<char> iostream 双向流(即可读又可写)
basic_streambuf<char> streambuf 流缓冲区基类

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

(2)char 类型的特化类

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

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

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

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

三、Buffered IO

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

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

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

1.核心组件:streambuf

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

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

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

四、<iostream>简介

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

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

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

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

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

1. 关键区别:cerr vs clog

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

特性 cerr clog
缓冲 无缓冲 有缓冲
刷新时机 每次输出立即刷新 缓冲区满或显式刷新
用途 紧急错误信息 普通日志/调试信息
性能 较慢(每次系统调用) 较快(批量输出)
#include <iostream>
#include <thread>
#include <chrono>

int main() {
    std::cerr << "错误:立即显示" << std::endl;
    std::clog << "日志:可能稍后才显示" << std::endl;
    // clog 的输出可能会留在缓冲区中,直到程序结束或缓冲区满
}

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

类型 字符类型 典型使用场景
窄字符版本(cincout 等) char 英文、ASCII 文本
宽字符版本(wcinwcout 等) wchar_t 国际化文本、中文等 Unicode
#include <iostream>
#include <locale>

int main() {
    // 设置全局 locale 为中文环境
    std::locale::global(std::locale("zh_CN.UTF-8"));
    
    // 宽字符输出
    wchar_t chinese[] = L"你好,世界!";
    std::wcout << chinese << std::endl;
    
    // 窄字符输出(英文)
    std::cout << "Hello, World!" << std::endl;
    
    return 0;
}

3. 流的可重定向性

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

#include <iostream>
#include <fstream>

int main() {
    std::ofstream logFile("log.txt");
    
    // 将 clog 重定向到文件
    std::clog.rdbuf(logFile.rdbuf());
    
    std::clog << "这条日志会写入文件,而不是控制台" << std::endl;
    
    // cout 仍然输出到控制台
    std::cout << "这条输出到控制台" << std::endl;
    
    return 0;
}

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

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

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

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

std::cout << "Hello" << std::flush;
std::cerr << "World";

五、流插入与提取 运算符

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

cout << value;   // 将 value 发送到 cout(输出)
cin >> variable; // 从 cin 读取数据到 variable(输入)

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

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

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

cout << value1 << value2 << .... ;  // 连续输出多个值
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.级联操作的原理

cout << a << b << c;

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

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

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

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

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

#include <iostream>
#include <string>

class Person {
private:
    std::string name;
    int age;
    
public:
    // 构造函数
    Person(const std::string& n = "", int a = 0) : name(n), age(a) {}
    
    // 友元声明:重载输出运算符
    friend std::ostream& operator<<(std::ostream& os, const Person& p);
    
    // 友元声明:重载输入运算符
    friend std::istream& operator>>(std::istream& is, Person& p);
};

// 输出运算符实现
std::ostream& operator<<(std::ostream& os, const Person& p) {
    os << "Person{name: " << p.name << ", age: " << p.age << "}";
    return os;  // 返回流引用以支持级联
}

// 输入运算符实现
std::istream& operator>>(std::istream& is, Person& p) {
    std::cout << "Enter name: ";
    is >> p.name;
    std::cout << "Enter age: ";
    is >> p.age;
    return is;  // 返回流引用以支持级联
}

int main() {
    Person p1("Alice", 25);
    std::cout << p1 << std::endl;  // 输出: Person{name: Alice, age: 25}
    
    Person p2;
    std::cin >> p2;   // 输入: Alice 25
    std::cout << p2 << std::endl;
    
    return 0;
}

关键要点

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

5.endl 的配合使用

cout << "Hello" << endl;  // endl 插入换行符并刷新缓冲区
cout << "World\n";        // \n 只插入换行符,不刷新缓冲区

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

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

int a, b, c;
cin >> a >> b >> c;
// 等价于
// (cin >> a) >> b) >> c
// 用户输入: 10 20 30

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

六、ostream 类

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

  • 格式化输出函数(通过重载的流插入运算符 <<)将数值(如 intdouble)从其内部表示形式(例如 16/32 位 int、64 位 double)转换为表示该数值文本形式的字符流。
  • 非格式化输出函数(如 put()write())按原样输出字节,不进行格式转换。
类型 函数 数据转换 典型用途
格式化输出 << 二进制 → 文本 打印用户可读的文本、数字
非格式化输出 put()write() 无转换 二进制文件写入、原始字节传输
#include <iostream>

int main() {
    int num = 65;
    
    // 格式化输出:将 65 转换为字符 '6','5'
    std::cout << num << std::endl;      // 输出: 65
    
    // 非格式化输出:直接输出字节值
    std::cout.put(65);                  // 输出: A (ASCII 65)
    std::cout.write((char*)&num, sizeof(num)); // 输出二进制表示(可能不是可读字符)
    
    return 0;
}

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

ostream 类为每个 C++ 基本类型重载了流插入运算符 <<charunsigned charsigned charshortunsigned shortintunsigned intlongunsigned longlong long(C++11)、unsigned long long(C++11)、floatdouble 和 long double。它将数值从内部表示形式转换为文本形式。

// 整数类型
ostream& operator<<(int val);
ostream& operator<<(long val);
ostream& operator<<(long long val);
ostream& operator<<(unsigned int val);
// ... 等等

// 浮点类型
ostream& operator<<(float val);
ostream& operator<<(double val);
ostream& operator<<(long double val);

// 字符和字符串
ostream& operator<<(char ch);
ostream& operator<<(const char* str);

// 指针
ostream& operator<<(void* ptr);

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

cout << 123 << 1.13 << endl;

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

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

例如:

char str1[] = "apple";
const char* str2 = "orange";

cout << str1 << endl;                    // 使用 char*,打印 C 字符串
cout << str2 << endl;                    // 使用 char*,打印 C 字符串
cout << (void*)str1 << endl;             // 使用 void*,打印地址(C 风格转换)
cout << static_cast<void*>(str2) << endl; // 使用 void*,打印地址(C++ 风格转换)

2.刷新输出缓冲区

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

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

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

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

// ostream 类的成员函数 - std::ostream::flush
ostream& flush();

// 示例
cout << "hello";
cout.flush();

// 操纵器 - std::flush
ostream& flush(ostream& os);

// 示例
cout << "hello" << flush;

(3)endl 操纵器

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

// 操纵器 - std::endl
ostream& endl(ostream& os);

(3)cin 输入前自动刷新

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

#include <iostream>
#include <thread>
#include <chrono>

int main() {
    std::cout << "Please enter a number: ";  // 没有 endl,也没有 \n
    // 此时输出可能还在缓冲区中,尚未显示
    
    std::this_thread::sleep_for(std::chrono::seconds(2));  // 假设一些计算
    
    int num;
    std::cin >> num;  // ⬅️ 在这里,输出缓冲区被自动刷新,提示信息才显示出来
    
    return 0;
}

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

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

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

#include <iostream>

int main() {
    char str[] = "Hello";
    
    std::cout << str << std::endl;           // 输出: Hello(字符串内容)
    std::cout << static_cast<void*>(str) << std::endl;  // 输出: 0x7ffd...(内存地址)
    
    // 不加转换时,编译器选择 const char* 重载
    // 加转换后,编译器选择 void* 重载
}

七、istream 类

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

  • 格式化输入:通过重载的提取运算符 >>,将文本形式(字符流)转换为内部表示形式(如 16/32 位 int、64 位 double)。
  • 非格式化输入:如 get()getline()read() 等函数,按原样读取字符,不进行转换。
类型 函数 数据转换 典型用途
格式化输入 >> 文本 → 二进制 读取用户输入的数字、单词
非格式化输入 get()getline()read() 无转换 读取含空格的字符串、二进制数据
#include <iostream>
#include <string>

int main() {
    int num;
    char line[100];
    
    // 格式化输入:自动将 "123" 转换为整数 123
    std::cin >> num;
    
    // 非格式化输入:原样读取字符,包括空格
    std::cin.getline(line, 100);
    
    return 0;
}

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

istream 类为每个 C++ 基本类型重载了提取运算符 >>charunsigned charsigned charshortunsigned shortintunsigned intlongunsigned longlong long(C++11)、unsigned long long(C++11)、floatdouble 和 long double。它通过将输入的文本转换为相应类型的内部表示来执行格式化。

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

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

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

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

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

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

推荐做法

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


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

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

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

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

int age;
std::string name;

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

场景二:跳过无效输入

int num;
std::cout << "Enter a number: ";
while (!(std::cin >> num)) {
    std::cin.clear();                       // 清除错误状态
    std::cin.ignore(10000, '\n');           // 丢弃错误的输入
    std::cout << "Invalid, try again: ";
}

场景三:忽略整行

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

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

#include <iostream>
#include <string>

int main() {
    int age;
    std::string name;
    
    std::cout << "Age: ";
    std::cin >> age;           // 用户输入: 25[Enter]
                               // 缓冲区: "25\n"
    
    std::cout << "Name: ";
    std::getline(std::cin, name);  // ⚠️ 读取到的是空字符串!
                                   // 因为 getline 读取了残留的 '\n'
    
    // 解决方法:在 getline 前调用 ignore
    // std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    
    return 0;
}

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

本段介绍 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) 读取固定数量 不适用 ❌ 否 二进制数据读取

示例:

#include <iostream>
#include <cstring>

int main() {
    char buffer[100];
    
    // get() 保留分隔符
    std::cin.get(buffer, 100, '\n');
    std::cout << "get(): " << buffer << std::endl;
    
    // 需要手动处理残留的分隔符
    std::cin.get();  // 消耗掉 '\n'
    
    // getline() 丢弃分隔符
    std::cin.getline(buffer, 100);
    std::cout << "getline(): " << buffer << std::endl;
    
    return 0;
}

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

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

#include <iostream>

int main() {
    // put() - 单个字符
    std::cout.put('H').put('i').put('\n');
    
    // write() - 固定数量
    char data[] = "Hello\0World";  // 包含空字符
    std::cout.write(data, 11);      // 输出 "HelloWorld"(空字符也会输出)
    
    // 二进制数据输出
    int num = 0x12345678;
    std::cout.write(reinterpret_cast<char*>(&num), sizeof(num));
    
    return 0;
}

3. gcount() 的重要作用

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

#include <iostream>
#include <fstream>

int main() {
    std::ifstream file("data.bin", std::ios::binary);
    char buffer[256];
    
    file.read(buffer, sizeof(buffer));
    std::streamsize bytesRead = file.gcount();  // 实际读取的字节数
    
    std::cout << "尝试读取: " << sizeof(buffer) << " 字节" << std::endl;
    std::cout << "实际读取: " << bytesRead << " 字节" << std::endl;
    
    return 0;
}

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

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

#include <iostream>
#include <cctype>

int main() {
    char ch;
    
    std::cout << "输入一个数字或字母: ";
    ch = std::cin.peek();  // 偷看下一个字符,但不提取
    
    if (std::isdigit(ch)) {
        std::cin >> ch;  // 确认是数字,正式读取
        std::cout << "你输入了数字: " << ch << std::endl;
    } else {
        std::cin.get(ch);  // 读取并处理字母
        std::cout << "你输入了字母: " << ch << std::endl;
    }
    
    // putback 示例:读取后放回
    char c = std::cin.get();
    std::cout << "读取到: " << c << std::endl;
    std::cin.putback(c);  // 放回去
    std::cout << "放回去了,可以重新读取" << std::endl;
    
    return 0;
}

5. 函数关系图

非格式化输入函数
├── 单字符输入
│   ├── get()      → 返回 int(可检测 EOF)
│   └── get(char&) → 返回 istream&
├── 字符串输入
│   ├── get(buf, n, delim)      → 保留分隔符
│   └── getline(buf, n, delim)  → 丢弃分隔符
├── 二进制输入
│   └── read(buf, n) → 固定数量,无转换
└── 辅助函数
    ├── gcount()  → 查询上次读取数量
    ├── peek()    → 前瞻
    └── 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::failbitios::failbit 访问。

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

成员函数 说明
good() 如果设置了 goodbit(即无错误),返回 true
eof() 如果设置了 eofbit,返回 true
fail() 如果设置了 failbit 或 badbit,返回 true
bad() 如果设置了 badbit,返回 true
clear() 清除 eofbitfailbit 和 badbit
#include <iostream>
#include <fstream>

int main() {
    std::ifstream file("data.txt");
    int num;
    
    file >> num;
    
    // 检查流状态
    if (file.good()) {
        std::cout << "读取成功: " << num << std::endl;
    }
    
    if (file.eof()) {
        std::cout << "到达文件末尾" << std::endl;
    }
    
    if (file.fail()) {
        std::cout << "读取失败(格式错误或数据不足)" << std::endl;
    }
    
    if (file.bad()) {
        std::cout << "严重错误(流损坏)" << std::endl;
    }
    
    return 0;
}

1. 四种状态标志的含义

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

2.状态标志的关系

good() == true  ⇔  goodbit 被设置(无任何错误)
good() == false ⇔  eofbit 或 failbit 或 badbit 被设置

fail() == true  ⇔  failbit 或 badbit 被设置(不包含 eofbit)
bad()  == true  ⇔  badbit 被设置
eof()  == true  ⇔  eofbit 被设置

3.状态转换图

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

#include <iostream>
#include <limits>

int main() {
    int age;
    
    std::cout << "请输入年龄: ";
    while (!(std::cin >> age)) {
        // 输入失败:failbit 被设置
        
        if (std::cin.eof()) {
            std::cout << "输入流已关闭" << std::endl;
            return 1;
        }
        
        std::cout << "无效输入,请输入数字: ";
        
        // 清除 failbit
        std::cin.clear();
        
        // 丢弃错误的输入行
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    }
    
    std::cout << "年龄是: " << age << std::endl;
    return 0;
}

4. clear() 的多种用法

#include <iostream>

int main() {
    int num;
    
    std::cin >> num;
    
    // 清除所有错误标志
    std::cin.clear();
    
    // 清除特定标志(保留其他)
    std::cin.clear(std::cin.rdstate() & ~std::ios::failbit);
    
    // 设置特定状态
    std::cin.clear(std::ios::eofbit);
    
    return 0;
}

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

#include <iostream>
#include <fstream>

int main() {
    std::ifstream file("numbers.txt");
    int num;
    
    // ❌ 错误的循环条件
    while (!file.eof()) {      // 陷阱:eofbit 在尝试读取后才会设置
        file >> num;
        std::cout << num << std::endl;  // 可能多输出一次
    }
    
    // ✅ 正确的循环条件
    while (file >> num) {      // 读取成功才进入循环
        std::cout << num << std::endl;
    }
    
    // 等价写法
    while (true) {
        file >> num;
        if (file.fail()) break;  // 读取失败(包括 eof)时退出
        std::cout << num << std::endl;
    }
    
    return 0;
}

6. rdstate() 获取原始状态

#include <iostream>

int main() {
    int num;
    std::cin >> num;
    
    std::ios::iostate state = std::cin.rdstate();
    
    if (state & std::ios::eofbit) {
        std::cout << "EOF" << std::endl;
    }
    if (state & std::ios::failbit) {
        std::cout << "FAIL" << std::endl;
    }
    if (state & std::ios::badbit) {
        std::cout << "BAD" << std::endl;
    }
    
    return 0;
}

十、格式化输入/输出

C++ 提供了一组操纵器来执行输入和输出格式化:(更详细的介绍可参考C++ 《iomanip》库全方位详解

  • <iomanip> 头文件setw()setprecision()setbase()setfill()
  • <iostream> 头文件fixed/scientificleft/right/internalboolalpha/noboolalpha

1. 操纵器分类汇总

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

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

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

// setw() 是非粘性的
cout << setw(10) << 123 << 456 << endl;  // 输出: "       123456"
// 只有 123 使用了宽度 10,456 使用默认宽度

// 其他操纵器是粘性的
cout << hex << 123 << " " << 456 << endl;  // 输出: "7b 1c8"
cout << dec << endl;  // 需要手动恢复

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

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

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

int main() {
    cout << showpos;
    cout << setfill('0');
    
    cout << internal << setw(8) << 123 << endl;   // +0000123
    cout << internal << setw(8) << -456 << endl;  // -0000456
    // 符号左对齐,数字右对齐
    
    return 0;
}

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

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

int main() {
    double pi = 3.14159265358979;
    
    // 默认模式:总有效数字 4 位
    cout << setprecision(4) << pi << endl;  // 3.142
    
    // fixed 模式:小数点后 4 位
    cout << fixed << setprecision(4) << pi << endl;  // 3.1416
    
    // scientific 模式:小数点后 4 位
    cout << scientific << setprecision(4) << pi << endl;  // 3.1416e+00
    
    return 0;
}

5. 进制与 showbase 的组合效果

进制 noshowbase(默认) showbase
dec 123 123
hex 7b 0x7b
oct 173 0173
cout << showbase;
cout << dec << 123 << endl;   // 123
cout << hex << 123 << endl;   // 0x7b
cout << oct << 123 << endl;   // 0173

6. 保存和恢复格式状态

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

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

int main() {
    // 保存当前格式状态
    ios::fmtflags old_flags = cout.flags();
    int old_precision = cout.precision();
    char old_fill = cout.fill();
    
    // 临时更改格式
    cout << hex << showbase << setw(10) << setfill('0') << 255 << endl;
    
    // 恢复格式状态
    cout.flags(old_flags);
    cout.precision(old_precision);
    cout.fill(old_fill);
    
    cout << 255 << endl;  // 恢复正常输出
    
    return 0;
}
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐