17.1 C++输入和输出概述

C++ 程序经常需要与外部世界进行交互:从键盘读取用户输入,将结果显示到屏幕,或者读写文件。C++ 使用流 (Stream) 的概念来处理输入和输出 (I/O) 操作。

17.1.1 流和缓冲区

流 (Stream) 是一个抽象的概念,它表示一个字节序列,这些字节可以从某个源(如键盘、文件、网络连接)流向程序(输入流),或者从程序流向某个目的地(如屏幕、文件、网络连接)(输出流)。将 I/O 操作视为字节流可以提供一个统一的接口来处理各种不同的 I/O 设备。

缓冲区 (Buffer) 是一块内存区域,用于临时存储数据。I/O 操作通常涉及物理设备(如硬盘、屏幕),这些设备的速度往往比 CPU 和内存慢得多。为了提高效率,数据通常不会在程序和设备之间逐个字节地传输,而是先暂存在缓冲区中:

  • 输出: 当程序执行输出操作时(例如使用 cout <<),数据通常先被发送到输出缓冲区。当缓冲区满了、遇到换行符(对于行缓冲的流)、程序结束或显式刷新缓冲区时,缓冲区中的内容才会被一次性地传输到最终的输出设备。
  • 输入: 当程序请求输入时(例如使用 cin >>),系统可能会一次性从输入设备读取比程序请求的更多的数据,并将其存储在输入缓冲区中。程序随后从缓冲区中获取所需的数据。如果缓冲区为空,程序会等待输入设备提供数据。

使用缓冲区的好处:

  • 提高效率: 减少了与慢速 I/O 设备的直接交互次数。
  • 平滑数据流: 允许程序和设备以各自的速度工作。

缓冲区的类型:

  • 完全缓冲 (Fully Buffered): 只有当缓冲区满了,或者显式刷新时,才会进行实际的 I/O 操作(通常用于文件 I/O)。
  • 行缓冲 (Line Buffered): 除了缓冲区满或显式刷新外,遇到换行符 (\n) 时也会进行 I/O 操作(通常用于标准输出 cout,当它连接到终端时)。
  • 无缓冲 (Unbuffered): 数据尽快进行实际的 I/O 操作(通常用于标准错误输出 cerr,以确保错误信息能立即显示)。

17.1.2 流、缓冲区和 iostream 文件

C++ 的 I/O 功能主要定义在 <iostream> 头文件中(以及其他相关头文件如 <fstream>, <sstream>)。<iostream> 定义了一系列用于处理流的类和对象。

核心 I/O 类库:

  • ios_base: 提供了与流无关的基本属性和方法(如格式化标志、流状态)。
  • ios: 基于 ios_base,增加了与具体字符类型相关的错误状态信息。
  • ostream: 基于 ios,提供了输出流的功能(如 operator<<, put(), write())。
  • istream: 基于 ios,提供了输入流的功能(如 operator>>, get(), getline(), read())。
  • iostream: 同时继承自 istreamostream,支持输入和输出操作。

标准流对象: <iostream> 还定义了几个预定义的全局流对象,用于处理标准输入和输出:

  • cin (console input): istream 类的对象,通常连接到标准输入设备(键盘)。
  • cout (console output): ostream 类的对象,通常连接到标准输出设备(屏幕)。它通常是行缓冲的。
  • cerr (console error): ostream 类的对象,通常连接到标准错误设备(屏幕)。它通常是无缓冲的,用于立即显示错误消息。
  • clog (console log): ostream 类的对象,也通常连接到标准错误设备。但它通常是缓冲的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream> // 包含 iostream 以使用 cin, cout, cerr, clog

int main() {
int age;
std::cout << "Enter your age: "; // 使用 cout 输出提示信息到标准输出
std::cin >> age; // 使用 cin 从标准输入读取整数

if (age < 0) {
std::cerr << "Error: Age cannot be negative." << std::endl; // 使用 cerr 输出错误信息到标准错误
std::clog << "Log: Invalid age entered." << std::endl; // 使用 clog 输出日志信息
return 1;
}

std::cout << "You entered: " << age << std::endl; // 输出结果到标准输出

return 0;
}

这些标准流对象使得基本的控制台 I/O 非常方便。

17.1.3 重定向

许多操作系统(如 Linux, macOS, Windows)支持I/O 重定向 (Redirection)**。重定向允许你改变程序的标准输入来源和标准输出/标准错误的目的地,而无需修改程序代码**。

  • 输出重定向 (>): 将程序的标准输出(cout 的内容)发送到指定文件,而不是屏幕。如果文件已存在,通常会覆盖它。

    1
    ./my_program > output.txt

    执行 my_program,其 cout 输出的内容将写入 output.txt 文件。

  • 输出重定向(追加 >>): 将程序的标准输出追加到指定文件的末尾。如果文件不存在,则创建它。

    1
    ./my_program >> output.txt
  • 输入重定向 (<): 使程序从指定文件读取其标准输入(cin 的内容),而不是从键盘。

    1
    ./my_program < input.txt

    my_program 会从 input.txt 文件中读取它期望通过 cin 获得的数据。

  • 错误重定向: 重定向标准错误 (cerr, clog) 的方式因 shell 而异。

    • bash/sh/zsh: 使用 2> (覆盖) 或 2>> (追加)。
      1
      2
      3
      ./my_program 2> error.log          # 重定向 stderr 到 error.log
      ./my_program > output.txt 2> error.log # 重定向 stdout 和 stderr
      ./my_program > combined.log 2>&1 # 将 stderr 重定向到 stdout,然后一起重定向到文件
    • Windows (cmd.exe): 类似地使用 2>2>>

重定向是一个强大的工具,它允许我们将程序的输入和输出与文件连接起来,方便进行测试、日志记录和数据处理,而无需修改程序本身来处理文件 I/O。程序仍然使用 cin, cout, cerr,但操作系统负责将这些标准流连接到文件。

17.2 使用 cout 进行输出

std::cout 是 C++ 标准库 <iostream> 中预定义的一个 ostream 类的全局对象,通常连接到标准输出设备(如屏幕)。它是 C++ 中最常用的输出工具。

17.2.1 重载的 << 运算符

cout 最常见的用法是配合插入运算符 (<<) 使用。这个运算符被重载 (overloaded) 以接受 C++ 中所有的基本数据类型(如 int, float, double, char, 指针类型)以及一些库类型(如 std::string, C 风格字符串 const char*)。

工作原理:

  • cout << value; 表达式的值是 cout 对象本身 (ostream&)。
  • 这使得可以链接 (chain) 多个插入运算符:cout << "Value: " << x << " and " << y;。这会被解释为 ((cout << "Value: ") << x) << " and ") << y;
  • << 运算符会根据 value 的类型,将其转换为适当的字符表示形式,并发送到输出流(通常先到缓冲区)。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <string>

int main() {
int age = 30;
double weight = 65.5;
char initial = 'J';
const char* name = "John Doe";
std::string city = "New York";

std::cout << "--- Basic Output ---" << std::endl; // endl 插入换行符并刷新缓冲区
std::cout << "Name: " << name << std::endl;
std::cout << "Initial: " << initial << std::endl;
std::cout << "Age: " << age << std::endl;
std::cout << "Weight: " << weight << " kg" << std::endl;
std::cout << "City: " << city << std::endl;

// 链接输出
std::cout << "\n--- Chained Output ---" << std::endl;
std::cout << name << " (" << initial << "), age " << age
<< ", lives in " << city << "." << std::endl;

// 输出指针地址 (通常以十六进制显示)
int* ptr_age = &age;
std::cout << "\nAddress of age: " << ptr_age << std::endl;

return 0;
}

对于用户自定义的类类型,如果想让 cout << myObject; 能够工作,需要为该类重载 operator<<(通常作为友元函数),如第 11 章所述。

17.2.2 其他 ostream 方法

除了 << 运算符,ostream 类(cout 是其对象)还提供了其他输出方法:

  • put(char c): 输出单个字符 c

    1
    2
    3
    std::cout.put('H');
    std::cout.put('i');
    std::cout.put('!').put('\n'); // put() 也返回 ostream&,可以链接
  • write(const char* s, streamsize n): 输出从地址 s 开始的内存块中的 n 个字节。它不会在遇到空字符 \0 时停止,而是精确地写入 n 个字节。这对于输出二进制数据或非空字符结尾的字符数组很有用。

    1
    2
    3
    4
    5
    6
    7
    const char* message = "Binary Data";
    // 输出前 6 个字节 "Binary"
    std::cout.write(message, 6) << std::endl;

    char buffer[] = {'A', 'B', '\0', 'C', 'D'};
    // 输出整个 buffer 的 5 个字节,包括空字符
    std::cout.write(buffer, sizeof(buffer)) << std::endl;

17.2.3 刷新输出缓冲区

如前所述,输出通常是缓冲的。有时我们需要确保缓冲区中的内容立即被发送到输出设备,这称为刷新 (Flushing) 缓冲区。

有几种方法可以刷新 cout 的缓冲区:

  1. std::endl (Manipulator): 这是最常用的方法。endl 不仅向流中插入一个换行符 (\n)**,还会显式刷新**输出缓冲区。

    1
    std::cout << "Line 1" << std::endl; // 输出 "Line 1",换行,并刷新
  2. std::flush (Manipulator): 只刷新输出缓冲区,不插入任何字符。

    1
    2
    3
    std::cout << "Processing..." << std::flush; // 立即显示 "Processing...",不换行
    // ...长时间操作...
    std::cout << " Done." << std::endl;
  3. std::ends (Manipulator): 向流中插入一个空字符 (\0)**,然后刷新**缓冲区。这在与其他需要空字符结尾字符串的系统交互时可能有用,但不太常见。

    1
    std::cout << "Null terminated" << std::ends;
  4. 程序正常结束:main 函数返回或调用 exit() 时,通常会自动刷新所有打开的输出流。

  5. 缓冲区满: 当缓冲区满时会自动刷新。

  6. 与输入关联:cin 需要从用户那里读取数据,并且 cout 的缓冲区中有待输出的内容时,通常会先刷新 cout 的缓冲区(以确保提示信息先显示出来)。

endl vs. '\n':

  • cout << endl; 等价于 cout << '\n' << flush;
  • cout << '\n'; 只插入换行符,不保证立即刷新缓冲区(除非流是行缓冲且连接到终端)。

在性能敏感的代码中(例如大量循环输出),频繁使用 endl 可能会因为不必要的刷新而降低效率。在这种情况下,如果不需要每次都立即看到输出,可以只使用 '\n',让缓冲区在更合适的时机(如缓冲区满或程序结束)刷新。

17.2.4 用 cout 进行格式化

默认情况下,cout 会根据数据的类型选择一种“合理”的格式进行输出。但我们可以通过多种方式控制输出的格式,例如字段宽度、小数位数、对齐方式、数字基数等。

格式化可以通过以下方式设置:

  • ios_base 成员函数: ios_base 类(ostream 的基类)提供了一些成员函数来设置格式标志。
  • 操纵符 (Manipulators): 定义在 <iostream><iomanip> 中的特殊函数,可以像插入值一样插入到 cout 语句中来改变流的状态。使用操纵符通常更方便。

1. 修改数值的基数 (dec, hex, oct)

可以使用操纵符 dec, hex, oct 来改变整数输出的基数(默认为十进制 dec)。这些设置是粘性 (sticky) 的,即一旦设置,后续所有整数输出都会采用该基数,直到被重新设置为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

int main() {
int num = 255;
std::cout << "Default (decimal): " << num << std::endl; // 输出 255
std::cout << std::hex; // 设置为十六进制
std::cout << "Hexadecimal: " << num << std::endl; // 输出 ff
std::cout << std::oct; // 设置为八进制
std::cout << "Octal: " << num << std::endl; // 输出 377
std::cout << std::dec; // 恢复为十进制
std::cout << "Decimal again: " << num << std::endl; // 输出 255

// 配合 showbase 和 uppercase
std::cout << std::showbase << std::uppercase; // 显示基数前缀并使用大写字母
std::cout << "Hex with base and uppercase: " << std::hex << num << std::endl; // 输出 0XFF
std::cout << "Octal with base: " << std::oct << num << std::endl; // 输出 0377
std::cout << std::noshowbase << std::nouppercase << std::dec; // 恢复默认

return 0;
}
  • showbase: 显示数值基数的前缀(0x0X 表示十六进制,0 表示八进制)。
  • noshowbase: 不显示基数前缀(默认)。
  • uppercase: 十六进制输出时使用大写字母 A-FX
  • nouppercase: 使用小写字母(默认)。

2. 调整字段宽度 (width(), setw())

可以指定输出下一个值时使用的最小字段宽度。如果值的实际宽度小于指定宽度,则用填充字符(默认为空格)填充。

  • cout.width(int w) (成员函数): 设置下一次输出的最小宽度为 w只对紧随其后的下一次输出有效,之后会自动恢复默认(宽度为 0)。
  • setw(int w) (操纵符, <iomanip>):width() 效果相同,也是只对下一次输出有效。使用 setw 通常更方便。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <iomanip> // for setw

int main() {
double val = 12.3;
std::cout << "*" << val << "*" << std::endl; // 输出 *12.3*

std::cout.width(10); // 设置宽度为 10
std::cout << "*" << val << "*" << std::endl; // 输出 * 12.3* (右对齐)
// 宽度设置失效
std::cout << "*" << val << "*" << std::endl; // 输出 *12.3*

// 使用 setw
std::cout << "*" << std::setw(10) << val << "*" << std::setw(5) << "Hi" << "*" << std::endl;
// 输出 * 12.3* Hi*

return 0;
}

3. 填充字符 (fill())

当字段宽度大于值的实际宽度时,用于填充空白区域的字符可以通过 fill() 成员函数或 setfill() 操纵符设置。这个设置是粘性的。

  • cout.fill(char ch) (成员函数): 设置填充字符为 ch。返回之前的填充字符。
  • setfill(char ch) (操纵符, <iomanip>): 设置填充字符为 ch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <iomanip> // for setw, setfill

int main() {
int num = 42;
std::cout << "Default fill: *" << std::setw(6) << num << "*" << std::endl; // 输出 * 42*

std::cout.fill('*'); // 设置填充字符为 '*'
std::cout << "Star fill: *" << std::setw(6) << num << "*" << std::endl; // 输出 ****42*

// 使用 setfill
std::cout << "Dash fill: *" << std::setfill('-') << std::setw(6) << num << "*" << std::endl; // 输出 ----42*

std::cout.fill(' '); // 恢复默认填充字符
return 0;
}

4. 浮点数精度 (precision(), setprecision())

可以控制浮点数输出的小数位数总有效数字位数,具体取决于浮点数格式(见下文)。

  • cout.precision(int p) (成员函数): 设置精度为 p。返回之前的精度值。默认精度通常是 6。这个设置是粘性的。
  • setprecision(int p) (操纵符, <iomanip>): 设置精度为 p
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <iomanip> // for setprecision
#include <cmath> // for M_PI

int main() {
double pi = M_PI; // 大约 3.14159265...
std::cout << "Default precision: " << pi << std::endl; // 通常输出 6 位有效数字

std::cout.precision(4); // 设置精度为 4
std::cout << "Precision 4: " << pi << std::endl; // 输出 3.142 (默认模式下是有效数字)

// 使用 setprecision
std::cout << "Precision 8: " << std::setprecision(8) << pi << std::endl; // 输出 3.1415927

std::cout.precision(6); // 恢复默认精度
return 0;
}

5. 浮点数格式 (setf(), unsetf(), fixed, scientific)

可以控制浮点数是以定点表示法 (fixed) 还是科学计数法 (scientific) 显示。

  • fixed (操纵符): 使用定点表示法。此时,precision() 控制的是小数点后的位数。
  • scientific (操纵符): 使用科学计数法(例如 1.23e+04)。此时,precision() 控制的是小数点后的位数。
  • 默认格式: 如果既未设置 fixed 也未设置 scientific,则 cout 会自动选择一种格式(定点或科学计数法),以产生更紧凑的表示。此时,precision() 控制的是总的有效数字位数(整数部分+小数部分)。
  • ios_base::floatfield: fixedscientific 都是格式标志位。可以使用 cout.setf(ios_base::fixed, ios_base::floatfield)cout.setf(ios_base::scientific, ios_base::floatfield) 来设置,使用 cout.unsetf(ios_base::floatfield) 来清除设置恢复默认行为。使用操纵符通常更简单。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <iomanip> // for fixed, scientific, setprecision

int main() {
double num1 = 12345.6789;
double num2 = 0.000012345;

std::cout << std::setprecision(5); // 设置精度为 5

std::cout << "Default format:\n";
std::cout << num1 << "\t" << num2 << std::endl; // 输出 12346 1.2345e-05 (精度5表示有效数字)

std::cout << std::fixed; // 设置为定点格式
std::cout << "Fixed format:\n";
std::cout << num1 << "\t" << num2 << std::endl; // 输出 12345.67890 0.00001 (精度5表示小数位数)

std::cout << std::scientific; // 设置为科学计数法格式
std::cout << "Scientific format:\n";
std::cout << num1 << "\t" << num2 << std::endl; // 输出 1.23457e+04 1.23450e-05 (精度5表示小数位数)

// 恢复默认格式 (需要清除 floatfield)
std::cout.unsetf(std::ios_base::floatfield);
std::cout << "Back to default format:\n";
std::cout << num1 << "\t" << num2 << std::endl; // 输出 12346 1.2345e-05

return 0;
}

6. 其他格式标志和操纵符

  • 对齐方式 (left, right, internal):

    • left: 在字段内左对齐输出,填充字符放在右边。

    • right: 在字段内右对齐输出,填充字符放在左边(默认)。

    • internal: 符号(正负号或基数前缀)左对齐,数值右对齐,填充字符放在中间。

    • 可以通过 setf() 设置 ios_base::left, ios_base::right, ios_base::internal 标志,或使用同名操纵符。

      1
      2
      3
      4
      std::cout << "*" << std::setw(10) << std::left << "Hi" << "*" << std::endl;  // 输出 *Hi        *
      std::cout << "*" << std::setw(10) << std::right << -12 << "*" << std::endl; // 输出 * -12*
      std::cout << "*" << std::setw(10) << std::internal << -12 << "*" << std::endl; // 输出 *- 12*
      std::cout << std::right; // 恢复默认右对齐
  • 显示小数点 (showpoint, noshowpoint):

    • showpoint: 强制显示浮点数的小数点和末尾的零(即使小数部分为零)。

    • noshowpoint: 不强制显示(默认)。

      1
      2
      3
      4
      5
      std::cout << std::fixed << std::setprecision(2);
      std::cout << "Default: " << 12.0 << std::endl; // 可能输出 12 (取决于实现)
      std::cout << std::showpoint;
      std::cout << "Showpoint: " << 12.0 << std::endl; // 输出 12.00
      std::cout << std::noshowpoint;
  • 显示正号 (showpos, noshowpos):

    • showpos: 对于非负数,在前面显示 + 号。

    • noshowpos: 不显示正号(默认)。

      1
      2
      3
      4
      std::cout << +10 << " " << -10 << std::endl; // 输出 10 -10
      std::cout << std::showpos;
      std::cout << +10 << " " << -10 << std::endl; // 输出 +10 -10
      std::cout << std::noshowpos;
  • 布尔值格式 (boolalpha, noboolalpha):

    • boolalpha: 将 bool 值输出为字符串 “true” 或 “false”。

    • noboolalpha: 将 bool 值输出为整数 1 或 0(默认)。

      1
      2
      3
      4
      5
      bool flag = true;
      std::cout << "Default bool: " << flag << std::endl; // 输出 1
      std::cout << std::boolalpha;
      std::cout << "Boolalpha: " << flag << std::endl; // 输出 true
      std::cout << std::noboolalpha;

7. 设置格式 (setf())

setf() 是一个更底层的设置格式标志的方法。它有两个版本:

  • fmtflags setf(fmtflags flags): 设置 flags 中指定的位。例如 cout.setf(ios_base::showpos)
  • fmtflags setf(fmtflags flags, fmtflags mask): 先清除 mask 中指定的位,然后设置 flags 中指定的位。这用于设置互斥的标志组,如对齐方式 (adjustfield) 或浮点数格式 (floatfield)。例如 cout.setf(ios_base::left, ios_base::adjustfield)

虽然 setf() 提供了完全的控制,但使用操纵符通常更易读、更方便。

保存和恢复格式状态: 有时你可能想临时改变格式,之后再恢复。可以保存 flags()precision() 的值,之后再用 flags(old_flags)precision(old_precision) 恢复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <iomanip>

int main() {
// 保存当前格式状态
std::ios_base::fmtflags original_flags = std::cout.flags();
std::streamsize original_precision = std::cout.precision();

// 进行一些格式化输出
std::cout << std::fixed << std::setprecision(2) << std::showpos;
std::cout << "Formatted: " << 12.345 << " " << -6.7 << std::endl;

// 恢复原始格式状态
std::cout.flags(original_flags);
std::cout.precision(original_precision);

std::cout << "Restored: " << 12.345 << " " << -6.7 << std::endl;

return 0;
}

通过组合使用这些格式化工具,可以精确地控制 cout 输出数据的外观。

17.3 使用 cin 进行输入

std::cin 是 C++ 标准库 <iostream> 中预定义的一个 istream 类的全局对象,通常连接到标准输入设备(如键盘)。它是 C++ 中获取用户输入的主要方式。

cin 最常见的用法是配合提取运算符 (>>) 使用。这个运算符也被重载以接受各种基本数据类型(int, float, double, char 等)以及 std::string 和 C 风格字符数组。

cin >> 的工作方式:

  1. 跳过空白: >> 运算符默认会跳过输入流中所有前导的空白字符(空格、制表符、换行符)。
  2. 读取和转换: 它会根据目标变量的类型,从缓冲区读取非空白字符,并尝试将这些字符转换为目标类型的值。
  3. 停止读取: 读取会在遇到不适合目标类型的字符(例如,读取 int 时遇到字母)或下一个空白字符时停止。
  4. 存储值: 成功转换的值被存储在目标变量中。
  5. 留下分隔符: 导致读取停止的那个不合适的字符或空白字符会留在输入缓冲区中,等待下一次读取。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <string>

int main() {
int age;
double price;
std::string name;
char initial;

std::cout << "Enter age, price, initial, and name (separated by spaces): ";
// Example input: 30 99.99 J Doe

std::cin >> age >> price >> initial >> name; // 可以链接读取

std::cout << "\n--- You entered ---" << std::endl;
std::cout << "Age: " << age << std::endl; // Output: 30
std::cout << "Price: " << price << std::endl; // Output: 99.99
std::cout << "Initial: " << initial << std::endl; // Output: J
std::cout << "Name: " << name << std::endl; // Output: Doe (只读取到空格前)

// 此时,输入缓冲区中可能还留有 " Doe" 后面的换行符

return 0;
}

17.3.1 cin >> 如何检查输入

cin >> 表达式本身会返回 cin 对象 (istream&)。C++ 允许在需要布尔值的地方(如 ifwhile 条件)使用流对象。当流对象被用作条件时,它会检查流的**状态 (state)**。如果流处于“良好”状态(没有发生错误),条件为 true;如果流发生错误(如读取失败、到达文件末尾),条件为 false

这使得可以编写如下循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

int main() {
int value;
std::cout << "Enter numbers (non-number to quit): ";
// 当 cin >> value 成功读取一个整数时,cin 状态良好,循环继续
// 当输入非数字导致读取失败时,cin 进入 fail 状态,循环终止
while (std::cin >> value) {
std::cout << "You entered: " << value << std::endl;
std::cout << "Enter next number: ";
}
std::cout << "Input terminated or failed." << std::endl;
return 0;
}

17.3.2 流状态

istream 类(以及 ostreamios)内部维护一组状态位 (state flags) 来表示流的当前状态。这些状态位定义在 ios_base 类中,类型为 iostate (一种位掩码类型)。

主要的状态位有:

  • goodbit: 表示流处于正常状态,没有发生错误。值为 0。
  • eofbit: 表示已到达输入流的**末尾 (End Of File)**。当尝试从文件末尾或关闭的输入流读取时设置。
  • failbit: 表示发生可恢复的格式错误。通常是因为输入的数据格式与期望读取的类型不匹配(例如,期望 int 但输入了字母)。设置 failbit 后,后续的 I/O 操作通常会失败,直到流状态被清除。
  • badbit: 表示发生不可恢复的严重错误,可能涉及流本身的损坏或底层 I/O 操作失败(例如,读取磁盘时发生硬件错误)。

一个流可能同时设置了多个状态位(例如,在文件末尾尝试读取失败可能同时设置 eofbitfailbit)。

检查流状态的成员函数:

  • good(): 如果流状态为 goodbit(即所有错误位都未设置),返回 true
  • eof(): 如果设置了 eofbit,返回 true
  • fail(): 如果设置了 failbit badbit,返回 true。这是检查读取操作是否失败的最常用方法。
  • bad(): 如果设置了 badbit,返回 true
  • rdstate(): 返回当前所有状态位的组合(一个 iostate 值)。
  • operator bool(): 重载的布尔转换运算符。如果 fail() 返回 false,则转换为 true;否则转换为 false。这就是为什么可以在 if(cin)while(cin >> value) 中使用流对象的原因。
  • operator!(): 重载的逻辑非运算符。如果 fail() 返回 true,则 !cintrue

清除流状态:

当流进入 failbitbadbit 状态后,后续的 I/O 操作通常会立即失败。为了继续使用该流,必须清除错误状态。

  • clear(iostate state = goodbit): 重置流的状态位。默认情况下(不带参数或参数为 goodbit),它会清除所有错误位 (eofbit, failbit, badbit),使流恢复到 good() 状态。也可以用它来设置特定的状态位。

示例:处理输入错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <limits> // for numeric_limits

int main() {
int number;
std::cout << "Enter an integer: ";

while (!(std::cin >> number)) { // 如果读取失败 (cin 返回 false)
std::cout << "Invalid input. Please enter an integer: ";

// 1. 清除错误状态
std::cin.clear();

// 2. 丢弃缓冲区中的错误输入
// 读取并忽略直到下一个换行符或缓冲区结束的所有字符
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

std::cout << "You entered: " << number << std::endl;

return 0;
}

这个循环会一直要求用户输入,直到成功读取一个整数为止。关键在于读取失败后,必须先 clear() 错误标志,然后 ignore() 掉导致错误的无效输入,否则下一次循环的 cin >> number 还会因为同样的原因失败。

17.3.3 其他 istream 类方法

除了 >> 运算符,istream 类还提供了其他用于输入的成员函数,它们提供了更精细的控制。

1. 单字符输入: get()

get() 有几个版本用于读取单个字符:

  • int get(): 读取下一个字符(即使是空白字符),并将其作为 int 类型返回。如果到达文件末尾,返回 EOF (一个特殊的负整数常量,定义在 <iostream><cstdio> 中)。
  • istream& get(char& ch): 读取下一个字符(即使是空白字符),并将其存储在 ch 中。返回 istream 对象的引用。如果读取失败(如到达文件末尾),会设置 failbit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

int main() {
char ch;
std::cout << "Enter characters (Ctrl+D/Ctrl+Z to end):" << std::endl;

// 使用 get(char&)
while (std::cin.get(ch)) { // 读取成功则循环继续
std::cout.put(ch); // 逐个输出读到的字符
}
std::cout << "\nEnd of input (using get(char&)).\n";

std::cin.clear(); // 清除 eof 状态以便继续演示

std::cout << "\nEnter characters again:" << std::endl;
// 使用 int get()
int char_code;
while ((char_code = std::cin.get()) != EOF) {
std::cout.put(static_cast<char>(char_code));
}
std::cout << "\nEnd of input (using int get()).\n";

return 0;
}

get() 不会跳过空白字符。

2. 字符串输入: get()getline()

istream 提供了用于读取 C 风格字符串(字符数组)的 get()getline() 版本。

  • istream& get(char* s, streamsize n, char delim = '\n'):

    • 从输入流读取字符,存储到字符数组 s 中。
    • 最多读取 n-1 个字符(为末尾的空字符 \0 留出空间)。
    • 遇到分隔符 delim 时停止读取。
    • 分隔符 delim 会被留在输入流中,不会被读取到 s 中。
    • 总是在读取的字符序列末尾添加空字符 \0
    • 如果因为读取了 n-1 个字符而停止,会设置 failbit
  • istream& getline(char* s, streamsize n, char delim = '\n'):

    • get() 类似,最多读取 n-1 个字符。
    • 遇到分隔符 delim 时停止读取。
    • 分隔符 delim 会从输入流中被读取并丢弃,不会存储在 s 中。
    • 总是在读取的字符序列末尾添加空字符 \0
    • 如果因为读取了 n-1 个字符而停止(在遇到分隔符之前),会设置 failbit

主要区别: getline() 会读取并丢弃分隔符,而 get() 会将分隔符留在流中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

int main() {
char line1[50];
char line2[50];

std::cout << "Enter line 1: ";
std::cin.getline(line1, 50); // 读取整行,丢弃换行符

std::cout << "Enter line 2: ";
std::cin.get(line2, 50); // 读取整行,换行符留在缓冲区

std::cout << "Line 1: " << line1 << std::endl;
std::cout << "Line 2: " << line2 << std::endl;

// 检查 get() 之后缓冲区的内容
char next_char;
std::cout << "Character after get(): ";
std::cin.get(next_char); // 读取留在缓冲区的换行符
if (next_char == '\n') {
std::cout << "'\\n'" << std::endl;
} else {
std::cout << "'" << next_char << "'" << std::endl;
}

return 0;
}

注意: 这些是用于 C 风格字符串的版本。对于 std::string 对象,应使用全局的 getline(cin, str) 函数(如 16.1 节所述)。

17.3.4 其他 istream 方法

  • read(char* s, streamsize n): 从流中读取精确 n 个字节,并存储到从 s 开始的内存中。它不会在遇到空字符或分隔符时停止,也不会添加空字符。主要用于读取二进制数据。如果读取的字节数少于 n(例如到达文件末尾),会设置 eofbitfailbit

  • peek(): 返回输入流中的下一个字符的整数值,但不从流中移除该字符。如果到达文件末尾,返回 EOF。可用于在实际读取前查看下一个字符。

  • gcount(): 返回上一次未格式化读取操作(如 get(), getline(), read())实际读取的字符数。对于 >> 运算符无效。

  • putback(char c): 将字符 c 放回输入流中,使其成为下一个被读取的字符。通常只能放回上一个读取的字符。

  • ignore(streamsize n = 1, int delim = EOF): 读取并丢弃输入流中的字符。

    • 最多丢弃 n 个字符。
    • 如果在此之前遇到并读取了分隔符 delim,则停止丢弃。
    • 分隔符 delim 本身也会被丢弃。
    • 常用于清除缓冲区中的无效输入或跳过不需要的部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <limits>

int main() {
char buffer[10];
std::cout << "Enter some text (e.g., abcdefghij): ";
std::cin.read(buffer, 5); // 读取 5 个字节到 buffer
std::cout << "Read " << std::cin.gcount() << " bytes: ";
std::cout.write(buffer, std::cin.gcount()) << std::endl; // 输出读取的内容

char next = std::cin.peek(); // 查看下一个字符
std::cout << "Next char (peek): '" << static_cast<char>(next) << "'" << std::endl;

char actual_next;
std::cin.get(actual_next); // 实际读取下一个字符
std::cout << "Actual next char (get): '" << actual_next << "'" << std::endl;

std::cin.putback(actual_next); // 将字符放回
std::cin.get(actual_next); // 再次读取
std::cout << "Read again after putback: '" << actual_next << "'" << std::endl;

std::cout << "Ignoring rest of the line..." << std::endl;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 忽略到行尾

std::cout << "Enter a final character: ";
char final_char;
std::cin >> final_char; // 现在可以读取新行的字符了
std::cout << "Final char: " << final_char << std::endl;

return 0;
}

通过组合使用 cin >> 和这些 istream 成员函数,可以实现灵活多样的输入处理逻辑,并能有效地处理输入错误。

17.4 文件输入和输出

除了与控制台进行交互(cin, cout),程序经常需要从文件中读取数据或将数据写入文件。C++ 的 iostream 库通过 <fstream> 头文件提供了用于文件 I/O 的类。

<fstream> 定义了三个主要的类:

  1. ifstream (input file stream): 继承自 istream,专门用于从文件读取数据。
  2. ofstream (output file stream): 继承自 ostream,专门用于向文件写入数据。
  3. fstream: 继承自 iostream,可以同时支持文件的读取和写入

这些文件流类的使用方式与 cincout 非常相似,因为它们继承了相同的基类并支持相同的操作符(如 <<, >>) 和方法(如 get(), getline(), write(), read())。

17.4.1 简单的文件 I/O

进行文件 I/O 的基本步骤:

  1. 包含头文件: #include <fstream>
  2. 创建流对象: 创建一个 ifstream (用于读取) 或 ofstream (用于写入) 或 fstream (用于读写) 对象。
  3. 关联文件: 将流对象与一个具体的文件关联起来。这可以在创建对象时通过构造函数完成,也可以之后使用 open() 方法。
  4. 检查是否成功打开: 使用 is_open() 方法或检查流对象的状态来确认文件是否成功打开。
  5. 进行 I/O 操作: 使用 <<, >>, get(), getline(), write(), read() 等方法读写文件,就像使用 cincout 一样。
  6. 关闭文件: 使用 close() 方法关闭文件,断开流对象与文件的关联。重要: 当文件流对象离开作用域时(例如函数结束),其析构函数会自动调用 close(),因此显式调用 close() 通常不是必需的,但有时为了明确或检查关闭状态可以这样做。

示例:写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <fstream> // 包含 fstream 头文件
#include <iostream>
#include <string>

int main() {
// 1. 创建 ofstream 对象并通过构造函数关联文件
// 默认以输出模式 (ios_base::out) 打开,如果文件存在则清空内容
std::ofstream outFile("mydata.txt");

// 2. 检查文件是否成功打开
if (!outFile.is_open()) { // 或者 if (!outFile)
std::cerr << "Error opening file for writing!" << std::endl;
return 1;
}

// 3. 向文件写入数据 (类似 cout)
std::string name = "Alice";
int age = 25;
double score = 95.5;

outFile << "Name: " << name << std::endl;
outFile << "Age: " << age << std::endl;
outFile << "Score: " << score << std::endl;

std::cout << "Data written to mydata.txt" << std::endl;

// 4. 关闭文件 (可选,析构函数会自动调用)
// outFile.close();

return 0; // outFile 在这里离开作用域,析构函数调用 close()
}

示例:读取文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <fstream>
#include <iostream>
#include <string>

int main() {
// 1. 创建 ifstream 对象并关联文件
// 默认以输入模式 (ios_base::in) 打开
std::ifstream inFile("mydata.txt");

// 2. 检查文件是否成功打开
if (!inFile.is_open()) {
std::cerr << "Error opening file for reading!" << std::endl;
return 1;
}

std::cout << "Reading data from mydata.txt:" << std::endl;

// 3. 从文件读取数据 (类似 cin)
std::string line;
// 逐行读取
while (getline(inFile, line)) { // 使用全局 getline 读取整行到 string
std::cout << line << std::endl;
}

// 或者,如果知道格式,可以像 cin 一样读取
/*
std::string label1, name, label2, label3;
int age;
double score;
inFile >> label1 >> name >> label2 >> age >> label3 >> score;
if (inFile) { // 检查读取是否成功
std::cout << "Read Name: " << name << std::endl;
std::cout << "Read Age: " << age << std::endl;
std::cout << "Read Score: " << score << std::endl;
} else {
std::cerr << "Error reading data format." << std::endl;
}
*/

// 4. 关闭文件 (可选)
// inFile.close();

return 0;
}

17.4.2 流状态检查和 is_open()

  • is_open(): 文件流对象提供 is_open() 成员函数,用于检查文件是否成功打开并与流关联。在尝试进行任何 I/O 操作之前,务必调用此函数进行检查。
  • 流状态位: 文件流对象也具有与 cin 相同的状态位 (goodbit, eofbit, failbit, badbit)。
    • 如果 open() 失败,流对象的状态通常会设置为 failbit。因此,if (!outFile)if (!inFile) 也可以用来检查文件是否成功打开。
    • 在读取过程中,如果遇到文件末尾,会设置 eofbit
    • 如果读取的数据格式不匹配(例如,试图将文本读入 int),会设置 failbit
    • 如果发生底层 I/O 错误,会设置 badbit
    • 可以使用 good(), eof(), fail(), bad() 来检查这些状态,并使用 clear() 来清除错误状态(如果可恢复)。

17.4.3 打开多个文件

一个程序可以同时打开多个文件,只需为每个文件创建一个对应的文件流对象即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <fstream>
#include <iostream>
#include <string>

int main() {
std::ifstream sourceFile("input.txt");
std::ofstream destFile("output.txt");

if (!sourceFile.is_open()) {
std::cerr << "Error opening input.txt" << std::endl;
return 1;
}
if (!destFile.is_open()) {
std::cerr << "Error opening output.txt" << std::endl;
return 1; // sourceFile 会自动关闭
}

std::string line;
while (getline(sourceFile, line)) {
// 处理 line ...
destFile << "Copied: " << line << std::endl; // 写入到目标文件
}

std::cout << "File copy finished." << std::endl;

// sourceFile 和 destFile 会在 main 结束时自动关闭
return 0;
}

17.4.4 命令行处理技术

通常,我们希望在运行程序时通过命令行参数来指定要处理的文件名,而不是将文件名硬编码在程序中。main 函数可以接收命令行参数:

1
2
3
int main(int argc, char *argv[]) {
// ...
}
  • argc (argument count): 一个整数,表示传递给程序的命令行参数的数量(包括程序本身的名称)。
  • argv (argument vector): 一个指向 C 风格字符串(char*)的指针数组
    • argv[0] 通常是程序本身的名称。
    • argv[1] 是第一个命令行参数。
    • argv[2] 是第二个命令行参数,依此类推。
    • argv[argc] 是一个空指针 (nullptr)。

示例:使用命令行参数指定输入和输出文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <fstream>
#include <iostream>
#include <string>
#include <vector> // 用于存储参数

int main(int argc, char *argv[]) {
// 检查参数数量是否正确
if (argc != 3) { // 需要程序名 + 输入文件名 + 输出文件名
std::cerr << "Usage: " << argv[0] << " <input_file> <output_file>" << std::endl;
return 1;
}

// argv[1] 是输入文件名, argv[2] 是输出文件名
std::ifstream sourceFile(argv[1]);
std::ofstream destFile(argv[2]);

if (!sourceFile.is_open()) {
std::cerr << "Error opening input file: " << argv[1] << std::endl;
return 1;
}
if (!destFile.is_open()) {
std::cerr << "Error opening output file: " << argv[2] << std::endl;
return 1;
}

// ... (文件处理逻辑,例如复制) ...
char ch;
while (sourceFile.get(ch)) {
destFile.put(ch);
}

std::cout << "Processed " << argv[1] << " to " << argv[2] << std::endl;

return 0;
}

编译后,可以这样运行:./my_program data.in results.out

17.4.5 文件模式

在打开文件时(通过构造函数或 open() 方法),可以指定文件模式 (file mode) 来控制文件的打开方式。文件模式是定义在 ios_base 类中的常量,类型为 openmode (一种位掩码类型)。

常用的文件模式标志:

  • ios_base::in:读取模式打开 (ifstream 默认)。
  • ios_base::out:写入模式打开 (ofstream 默认)。如果文件已存在,清空其内容;如果不存在,则创建。
  • ios_base::app (append):追加模式打开。写入操作将在文件末尾进行。如果文件不存在,则创建。此模式下通常隐含 ios_base::out
  • ios_base::ate (at end): 打开文件并将初始位置定位到文件末尾。但仍可以在文件中的任何位置写入(与 app 不同,app 强制写入到末尾)。
  • ios_base::binary:二进制模式打开文件,而不是文本模式。在二进制模式下,不会对特殊字符(如换行符 \n 或回车符 \r)进行转换。这对于读写非文本文件(如图像、可执行文件)或需要精确字节控制的情况至关重要。
  • ios_base::trunc (truncate): 如果文件已存在,清空其内容。ios_base::out 模式默认包含此行为。

可以使用按位或运算符 (|) 来组合多个模式标志。

open() 方法:

除了构造函数,还可以使用 open() 方法打开文件。

1
2
3
4
5
6
7
8
9
ofstream outFile;
outFile.open("config.cfg", ios_base::out | ios_base::trunc); // 显式指定覆盖写入

ifstream dataFile;
dataFile.open("image.bin", ios_base::in | ios_base::binary); // 以二进制读取模式打开

fstream ioFile;
// 以读写模式打开,如果不存在则创建,不清空内容
ioFile.open("log.dat", ios_base::in | ios_base::out | ios_base::binary);

默认模式:

  • ifstream 默认模式是 ios_base::in
  • ofstream 默认模式是 ios_base::out | ios_base::trunc
  • fstream 没有默认模式,必须显式指定。

17.4.6 随机存取

默认情况下,文件流是按顺序读取或写入的。但有时我们需要直接跳转到文件中的特定位置进行读写,这称为**随机存取 (Random Access)**。

文件流维护着内部的位置指针:

  • 输入指针 (Get Pointer): istream (及 ifstream, fstream) 维护,指示下一次读取操作将从哪里开始。
  • 输出指针 (Put Pointer): ostream (及 ofstream, fstream) 维护,指示下一次写入操作将在哪里进行。

可以使用以下方法来操作这些位置指针:

  • seekg(offset, direction) (seek get): 移动输入指针。

  • seekp(offset, direction) (seek put): 移动输出指针。

    • offset: 一个整数值(类型通常是 streamoff),表示要移动的字节数。可以是正数(向文件末尾移动)或负数(向文件开头移动)。
    • direction: 一个枚举值(类型 seekdir,定义在 ios_base 中),指定 offset 的参考点:
      • ios_base::beg: 从文件开头计算偏移量。
      • ios_base::cur: 从文件当前位置计算偏移量。
      • ios_base::end: 从文件末尾计算偏移量(此时 offset 通常为负数或零)。
  • tellg() (tell get): 返回输入指针的当前位置(类型通常是 streampos,可以转换为整数)。

  • tellp() (tell put): 返回输出指针的当前位置。

注意: 随机存取通常在二进制模式 (ios_base::binary) 下使用更可靠,因为文本模式下的字符转换可能会干扰精确的字节定位。

示例:随机访问文件

假设有一个存储记录的文件 records.dat,每条记录固定大小为 sizeof(Record)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <fstream>
#include <iostream>
#include <vector>

struct Record {
int id;
char name[20];
double value;
};

int main() {
const char* filename = "records.dat";
const size_t recordSize = sizeof(Record);

// --- 写入一些记录 ---
std::ofstream outFile(filename, std::ios::out | std::ios::binary | std::ios::trunc);
if (!outFile) { /* error check */ return 1; }

std::vector<Record> records = {
{1, "Rec A", 10.1},
{2, "Rec B", 20.2},
{3, "Rec C", 30.3},
{4, "Rec D", 40.4}
};
for (const auto& rec : records) {
outFile.write(reinterpret_cast<const char*>(&rec), recordSize);
}
outFile.close();

// --- 随机读取和修改记录 ---
std::fstream ioFile(filename, std::ios::in | std::ios::out | std::ios::binary);
if (!ioFile) { /* error check */ return 1; }

Record tempRec;

// 1. 读取第三条记录 (索引为 2)
long position = 2 * recordSize;
ioFile.seekg(position, std::ios::beg); // 定位到第三条记录的开头
if (ioFile.read(reinterpret_cast<char*>(&tempRec), recordSize)) {
std::cout << "Read record at index 2: ID=" << tempRec.id << ", Name=" << tempRec.name << std::endl;
} else {
std::cerr << "Failed to read record at index 2." << std::endl;
}

// 2. 修改第二条记录 (索引为 1)
if (ioFile.good()) { // 确保流状态良好
position = 1 * recordSize;
ioFile.seekp(position, std::ios::beg); // 定位输出指针到第二条记录开头

// 读取原始记录 (可选,如果需要基于原始值修改)
// ioFile.seekg(position, std::ios::beg);
// ioFile.read(reinterpret_cast<char*>(&tempRec), recordSize);

// 准备新数据并写入
Record updatedRec = {2, "Record B Updated", 25.5};
ioFile.write(reinterpret_cast<const char*>(&updatedRec), recordSize);

if (ioFile.fail()) {
std::cerr << "Failed to write updated record at index 1." << std::endl;
} else {
std::cout << "Updated record at index 1." << std::endl;
}
}

// 3. 获取文件大小
ioFile.seekg(0, std::ios::end); // 定位到文件末尾
long fileSize = ioFile.tellg(); // 获取当前位置 (即文件大小)
std::cout << "File size: " << fileSize << " bytes." << std::endl;
std::cout << "Number of records: " << fileSize / recordSize << std::endl;

ioFile.close();

return 0;
}

这个例子演示了如何使用 seekgseekp 定位到文件中的特定字节位置,并使用 readwrite 进行二进制数据的读写。

17.5 内核格式化

到目前为止,我们讨论的 I/O 操作都是将数据发送到外部设备(如屏幕 cout)或文件(ofstream),或者从外部设备(cin)或文件(ifstream)读取数据。但有时,我们希望在内存中对数据进行格式化,将各种类型的数据(数字、字符等)转换成一个字符串,或者反过来,从一个字符串中按特定格式解析出各种类型的数据。

C++ 标准库通过 <sstream> 头文件提供了字符串流 (String Streams) 类来实现这种内核格式化 (In-memory Formatting)内存中的 I/O

<sstream> 定义了三个主要的类:

  1. ostringstream (output string stream): 继承自 ostream。它允许你像使用 cout 一样,使用插入运算符 (<<) 和其他 ostream 方法将各种类型的数据写入到一个内部的字符串缓冲区中。之后可以获取这个格式化好的字符串。
  2. istringstream (input string stream): 继承自 istream。它允许你将一个已有的 std::string 或 C 风格字符串作为数据源,然后像使用 cin 一样,使用提取运算符 (>>) 和其他 istream 方法从中读取(解析)数据。
  3. stringstream: 继承自 iostream。它结合了 ostringstreamistringstream 的功能,允许对同一个内部字符串缓冲区进行读取和写入操作。

这些字符串流类的行为与文件流或控制台流非常相似,因为它们共享相同的基类和接口。你可以使用同样的格式化操纵符(如 setw, setprecision, fixed, hex 等)来控制字符串流中的格式。

使用 ostringstream 进行格式化输出

当你需要将不同类型的数据组合成一个格式化的字符串时,ostringstream 非常有用。

步骤:

  1. 包含头文件 #include <sstream>
  2. 创建一个 ostringstream 对象。
  3. 像使用 cout 一样,使用 << 将数据插入到流中。可以使用格式化操纵符。
  4. 使用 str() 成员函数获取流内部缓冲区中积累的 std::string 副本。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <sstream> // 包含 sstream 头文件
#include <iostream>
#include <string>
#include <iomanip> // for setprecision, fixed

int main() {
// 1. 创建 ostringstream 对象
std::ostringstream oss;

// 2. 向流中插入数据并格式化
std::string item = "Gadget";
int quantity = 3;
double price = 12.99;
double total = quantity * price;

oss << "Item: " << item << "\n"; // 插入字符串和换行符
oss << "Quantity: " << quantity << "\n";
oss << "Price: $" << std::fixed << std::setprecision(2) << price << "\n"; // 格式化浮点数
oss << "Total: $" << total; // 格式化设置是粘性的

// 3. 获取格式化后的字符串
std::string formatted_string = oss.str();

// 4. 输出或使用该字符串
std::cout << "--- Formatted Output String ---" << std::endl;
std::cout << formatted_string << std::endl;
/* Output:
Item: Gadget
Quantity: 3
Price: $12.99
Total: $38.97
*/

// ostringstream 对象可以重用,但内容会继续累加
// oss << "\nAnother line.";
// std::cout << "\nAfter adding more:\n" << oss.str() << std::endl;

// 如果要清空并重用,可以调用 str("") 或重新构造
oss.str(""); // 清空缓冲区
oss.clear(); // 清除可能存在的流状态 (虽然这里通常不需要)
oss << "New content.";
std::cout << "\nAfter clearing and adding:\n" << oss.str() << std::endl; // Output: New content.

return 0;
}

ostringstream 常用于需要动态构建包含数值和其他数据的日志消息、文件名或显示文本的场景。

使用 istringstream 进行格式化输入

当你有一个字符串,并且想从中按特定格式提取数据(例如,解析配置文件的一行或用户输入的复合数据)时,istringstream 非常有用。

步骤:

  1. 包含头文件 #include <sstream>
  2. 准备好包含待解析数据的 std::string
  3. 使用该字符串创建一个 istringstream 对象。
  4. 像使用 cin 一样,使用 >> 从流中提取数据到变量中。
  5. 可以检查流的状态(如 good(), fail(), eof()) 来判断提取是否成功。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <sstream>
#include <iostream>
#include <string>
#include <vector>

int main() {
std::string data = "Alice 30 95.5"; // 模拟一行数据
std::string config_line = "window_width = 800";
std::string csv_data = "10,20,30,40";

// 1. 解析空格分隔的数据
std::istringstream iss1(data);
std::string name;
int age;
double score;

if (iss1 >> name >> age >> score) { // 尝试提取
std::cout << "Parsed from data: Name=" << name << ", Age=" << age << ", Score=" << score << std::endl;
} else {
std::cerr << "Failed to parse data string." << std::endl;
}

// 2. 解析配置文件行
std::istringstream iss2(config_line);
std::string key, eq;
int value;
if (iss2 >> key >> eq >> value && key == "window_width" && eq == "=") {
std::cout << "Parsed config: Key=" << key << ", Value=" << value << std::endl;
} else {
std::cerr << "Failed to parse config line." << std::endl;
}

// 3. 解析逗号分隔的数据 (需要手动处理分隔符)
std::istringstream iss3(csv_data);
std::vector<int> numbers;
int num;
char comma;
while (iss3 >> num) { // 读取一个数字
numbers.push_back(num);
if (iss3 >> comma && comma == ',') { // 读取并检查逗号
continue; // 如果有逗号,继续循环
} else {
break; // 如果没有逗号或读取失败,结束循环
}
}
// 检查流状态,确保是因为到达末尾而不是其他错误
if (!iss3.eof() && iss3.fail() && !iss3.bad()) {
// 可能最后一个数字后面没有逗号,这是正常的
} else if (!iss3.eof()) {
std::cerr << "Error parsing CSV data." << std::endl;
}

std::cout << "Parsed CSV numbers: ";
for (int n : numbers) {
std::cout << n << " ";
}
std::cout << std::endl;


return 0;
}

istringstream 常用于解析从文件、网络或用户界面接收到的文本数据。

使用 stringstream 进行读写

stringstream 允许你在同一个字符串缓冲区上进行输入和输出操作。你可以先向其中写入数据,然后从中读取数据,或者反过来。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <sstream>
#include <iostream>
#include <string>

int main() {
std::stringstream ss;

// 1. 写入数据到 stringstream
int val1 = 10;
double val2 = 3.14;
ss << "Data: " << val1 << " " << val2;

// 2. 从同一个 stringstream 读取数据
std::string label;
int read_val1;
double read_val2;

// 在读取之前,通常不需要显式重置位置,因为写入指针和读取指针是独立的
// 但如果写入后立即读取,读取指针仍在开头

if (ss >> label >> read_val1 >> read_val2) {
std::cout << "Read from stringstream: Label=" << label
<< ", Val1=" << read_val1 << ", Val2=" << read_val2 << std::endl;
} else {
std::cerr << "Failed to read from stringstream." << std::endl;
}

// 3. 清空并重用,先读取后写入
ss.str("100 Hello"); // 设置新的内容,并重置读写位置
ss.clear(); // 清除 EOF 或其他状态

int num;
std::string word;
ss >> num >> word; // 读取 100 和 "Hello"

// 写入新内容 (会覆盖或追加,取决于实现和指针位置,通常是覆盖)
// 为了安全地追加,最好先获取当前内容,追加后再设置回去,或者使用 ostringstream
std::string current_content = ss.str(); // 获取 "100 Hello"
ss.str(""); // 清空
ss.clear();
ss << current_content << " World " << num * 2; // 写入旧内容、新内容和计算结果

std::cout << "Final stringstream content: " << ss.str() << std::endl;
// Output: 100 Hello World 200

return 0;
}

stringstream 在需要对字符串进行多次格式化转换或解析的场景中很有用,例如在不同格式之间转换数据。

总结: 字符串流提供了一种强大而灵活的方式,可以在内存中完成数据的格式化和解析,其接口与标准的控制台和文件 I/O 保持一致,易于学习和使用。

17.6 总结

本章详细介绍了 C++ 的输入/输出 (I/O) 系统,该系统基于流 (Stream) 的概念,提供了一个统一的接口来处理来自不同源(键盘、文件)和去往不同目的地(屏幕、文件)的数据流。

主要内容回顾:

  1. I/O 概述:

    • 流: 字节序列的抽象,用于连接程序和 I/O 设备。
    • 缓冲区: 临时存储区域,用于提高 I/O 效率。分为完全缓冲、行缓冲、无缓冲。
    • <iostream>: 提供了核心 I/O 类(ios_base, ios, istream, ostream, iostream)和标准流对象 (cin, cout, cerr, clog)。
    • 重定向: 操作系统功能,允许改变标准输入/输出/错误流的来源和目的地,而无需修改代码。
  2. 使用 cout 输出:

    • 主要使用重载的插入运算符 (<<) 输出各种数据类型。
    • 支持链接操作 (cout << a << b;)。
    • 提供 put() 输出单个字符,write() 输出指定字节数的内存块。
    • 缓冲区刷新: 可以通过 endl (换行并刷新)、flush (仅刷新)、ends (插入 \0 并刷新) 或在特定条件下自动刷新。'\n' 只插入换行符。
    • 格式化: 可以通过成员函数 (width(), precision(), fill(), setf()) 或操纵符 (setw(), setprecision(), setfill(), fixed, scientific, hex, oct, dec, left, right, boolalpha 等,需包含 <iomanip>) 控制输出的宽度、精度、填充、对齐、基数、浮点数表示等。
  3. 使用 cin 输入:

    • 主要使用重载的提取运算符 (>>) 读取数据。默认跳过前导空白,读取直到遇到不匹配字符或空白。
    • 流状态: cin 维护状态位 (goodbit, eofbit, failbit, badbit)。可以通过 good(), eof(), fail(), bad() 或流对象本身在布尔上下文中检查状态。failbit 表示格式错误,eofbit 表示到达文件尾。
    • 错误处理: 读取失败后,需使用 clear() 清除错误状态,并使用 ignore() 丢弃缓冲区中的无效输入。
    • 其他方法: get() (读取单个字符,不跳过空白)、getline() (读取一行到 C 风格数组,丢弃分隔符)、read() (读取指定字节数)、peek() (查看下一个字符)、gcount() (获取上次读取的字符数)、putback() (放回字符)、ignore() (丢弃字符)。
  4. 文件 I/O (<fstream>):

    • 使用 ifstream (输入)、ofstream (输出)、fstream (输入/输出) 类。
    • 通过构造函数或 open() 方法关联文件,并指定文件模式 (in, out, app, ate, binary, trunc)。
    • 必须使用 is_open() 或检查流状态来确认文件是否成功打开。
    • 使用与 cin/cout 类似的操作符和方法进行读写。
    • 文件流对象在销毁时自动关闭文件。
    • 可以通过 argcargv 处理命令行参数来指定文件名。
    • 随机存取: 使用 seekg() / tellg() (输入指针) 和 seekp() / tellp() (输出指针) 在文件中定位读写位置,常用于二进制文件。
  5. 内核格式化 (<sstream>):

    • 在内存中进行格式化转换。
    • ostringstream: 将各种数据类型格式化输出到一个内部字符串缓冲区,通过 str() 获取结果字符串。
    • istringstream: 将已有字符串作为输入源,从中按格式解析提取数据。
    • stringstream: 支持对同一个内部字符串缓冲区进行读写操作。
    • 接口与文件流和控制台流一致,支持格式化操纵符。

C++ 的 iostream 库提供了一个类型安全、可扩展且功能丰富的 I/O 框架,适用于控制台、文件以及内存中的格式化操作。

评论