跳至内容

I/O:流、格式化与 std::print

掌握 C++ I/O ── 从经典的 iostream 到现代的 std::format 和 std::print,包括流状态、文件 I/O 和常见的缓冲区陷阱。

C++ I/O 的演进

C++ I/O 在语言历史中发生了重大演变。经典的 iostream 库(std::cin、std::cout)自 C++98 以来一直是标准,但它有众所周知的人体工程学和性能问题。C++20 引入了 std::format,一个受 Python 的 str.format() 启发的类型安全格式化库。C++23 添加了 std::print,将格式化和输出合并在一次调用中。

理解这三种方式都很重要:iostream 在现有代码中随处可见,std::format 是现代格式化字符串的方式,std::print 是 C++ 输出的未来。

经典 I/O:cout、cin、cerr

iostream 库提供了用于标准输入、输出和错误输出的流对象:

io_basics.cppc++23
#include <iostream>
#include <string>
#include <limits>    // For numeric_limits

int main() {
    // Output with cout
    std::cout << "Hello, " << "World!" << '\n';      // Chained insertion
    std::cout << "The answer is: " << 42 << '\n';    // Automatic type formatting

    // Error output — cerr is unbuffered (immediate), clog is buffered
    std::cerr << "Error: something went wrong\n";

    // Input with cin
    std::cout << "Enter your name: ";
    std::string name;
    std::getline(std::cin, name);         // Read entire line (including spaces)

    std::cout << "Enter your age: ";
    int age{};
    std::cin >> age;                       // Read a single value (stops at whitespace)

    // Check if input succeeded
    if (std::cin.fail()) {
        std::cerr << "Invalid input!\n";
        std::cin.clear();                  // Reset error flags
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');  // Discard bad input
    }

    // The getline-after-cin trap:
    // After cin >> age, a '\n' remains in the buffer.
    // A subsequent getline() would read an empty string!
    // Fix: call cin.ignore() before getline():
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

    std::string city;
    std::cout << "Enter your city: ";
    std::getline(std::cin, city);          // Now works correctly

    std::cout << name << " (" << age << ") from " << city << '\n';
}
输出

点击“运行”即可编译并运行这个示例。

流状态标志

每个流维护一组状态标志来指示其当前状况:

- goodbit ── 无错误。流已准备好进行 I/O 操作。
- eofbit ── 已到达文件/输入的末尾。当流尝试读取超过末尾时设置。
- failbit ── 发生逻辑错误(例如尝试读取整数但遇到文本)。调用 clear() 后流仍可使用。
- badbit ── 发生不可恢复的 I/O 错误(例如磁盘故障)。流通常不可用。

你可以通过 stream.good()、stream.eof()、stream.fail()、stream.bad() 检查这些状态,或在布尔上下文中使用流:if (std::cin) 等同于 if (!std::cin.fail())。

一个关键模式:总是在输入操作之后而不是之前检查流状态。常见的惯用写法 while (std::cin >> value) 在一步中完成读取和检查 ── 当读取失败时停止。

常见陷阱

C++ 中最常见的 I/O 错误涉及混合使用 >>(格式化提取)和 std::getline()。>> 运算符读取一个值但将尾随的换行符留在缓冲区中。随后的 std::getline() 会将那个剩余的换行符读取为空字符串,看起来像跳过了输入。

修复方法是在 >> 和 std::getline() 之间调用 std::cin.ignore(std::numeric_limits::max(), '\n') 以丢弃剩余的换行符。

另一种方法是对所有输入使用 std::getline() 并使用 std::istringstream 或 std::stoi/std::stod 自行解析字符串。这完全避免了这个问题并提供了更好的错误处理。

  • 在 cin >> x 之后,缓冲区中仍有换行符 ── 下一个 getline() 会将其读取为空字符串
  • 修复:在 >> 和 getline() 之间调用 cin.ignore(numeric_limits::max(), '\n')
  • 替代方案:对所有输入使用 getline() 并用 stoi/stod 或 istringstream 解析字符串
  • 始终在输入后检查流状态:if (cin >> x) 而不是盲目读取

文件 I/O

文件 I/O 使用与控制台 I/O 相同的流接口,std::ifstream 用于读取,std::ofstream 用于写入:

file_io.cppc++23
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

int main() {
    // Writing to a file
    {
        std::ofstream out("scores.txt");  // Opens for writing (creates or truncates)
        if (!out) {
            std::cerr << "Failed to open file for writing\n";
            return 1;
        }
        out << "Alice 95\n";
        out << "Bob 87\n";
        out << "Charlie 92\n";
    }  // File is automatically closed when 'out' goes out of scope (RAII!)

    // Reading from a file
    std::ifstream in("scores.txt");
    if (!in) {
        std::cerr << "Failed to open file for reading\n";
        return 1;
    }

    std::string name;
    int score{};
    std::vector<std::pair<std::string, int>> results;

    while (in >> name >> score) {           // Read until failure (EOF or bad data)
        results.emplace_back(name, score);
    }

    for (const auto& [n, s] : results) {
        std::cout << n << ": " << s << '\n';
    }

    // Appending to a file
    std::ofstream append("scores.txt", std::ios::app);  // Open in append mode
    append << "Diana 98\n";

    // Reading entire file line by line
    std::ifstream in2("scores.txt");
    std::string line;
    while (std::getline(in2, line)) {
        std::cout << "Line: " << line << '\n';
    }
}
输出

点击“运行”即可编译并运行这个示例。

现代格式化:std::format 与 std::print

C++20 的 std::format 和 C++23 的 std::print 提供了类型安全的、类 Python 的格式化,比 iostream 操纵器更安全也更易读:

format_print.cppc++23
#include <format>   // C++20
#include <print>    // C++23
#include <string>
#include <vector>
#include <numbers>  // C++20: mathematical constants

int main() {
    // std::format (C++20) — returns a formatted std::string
    std::string msg = std::format("Hello, {}!", "World");
    // msg == "Hello, World!"

    // Positional arguments
    std::string pos = std::format("{1} comes before {0}", "second", "first");
    // pos == "first comes before second"

    // Formatting specifiers
    std::string nums = std::format(
        "int: {:>10d}\n"    // Right-aligned, width 10, decimal
        "hex: {:#06x}\n"    // '0x' prefix, width 6, zero-padded hex
        "float: {:.3f}\n"   // 3 decimal places, fixed notation
        "sci: {:.2e}\n"     // 2 decimal places, scientific notation
        "pi: {:.10f}",      // 10 decimal places
        42, 255, 3.14159, 12345.6789, std::numbers::pi
    );

    // std::print (C++23) — format + output in one step
    std::print("Name: {}, Age: {}\n", "Alice", 30);
    std::print("Pi is approximately {:.6f}\n", std::numbers::pi);

    // std::println (C++23) — like print but adds a newline
    std::println("Score: {:>5}", 95);       // "Score:    95"
    std::println("Score: {:>5}", 100);      // "Score:   100"

    // Formatting containers — print each element
    std::vector<int> values = {10, 20, 30, 40, 50};
    for (std::size_t i = 0; i < values.size(); ++i) {
        std::println("[{:2}] = {:>4}", i, values[i]);
    }

    // Why prefer std::format/print over iostream?
    // 1. Type-safe (unlike printf)
    // 2. Readable format strings (unlike iostream manipulators)
    // 3. No state leakage (iostream manipulators are sticky!)
    // 4. Better performance in many cases
}
输出

点击“运行”即可编译并运行这个示例。

最佳实践

现代 C++ 提供了三种 I/O 方式。以下是何时使用每种:

使用 std::print/std::println(C++23)在编译器支持的情况下用于新代码。它是最清晰的语法,类型安全,并避免了 iostream 的有状态操纵器问题。

使用 std::format(C++20)当你需要格式化字符串(而非直接输出)时,或当编译器支持 C++20 但不支持 C++23 时。

使用 iostream 当与期望流对象的代码交互时,当你需要流抽象时(例如从 cin 读取、文件 I/O),或在遗留代码库中工作时。

避免使用 C 的 printf/scanf ── 它们不是类型安全的(传递错误的格式说明符是未定义行为),也不能与 std::string 等 C++ 类型配合使用。

  • 优先使用 std::print/println(C++23)编写新的输出代码 ── 最清晰最安全
  • 当需要格式化字符串而非直接输出时使用 std::format(C++20)
  • iostream 仍然需要用于 cin、文件 I/O 和基于流的抽象
  • 避免使用 printf/scanf ── 它们不是类型安全的且不能直接处理 std::string
关键要点
  • 始终在输入操作之后检查流状态 ── 使用 'if (cin >> x)' 或 'while (getline(in, line))'
  • 混合使用 cin >> 和 getline() 会导致缓冲区中换行符的错误 ── 在它们之间调用 cin.ignore()
  • 文件流使用 RAII ── 当它们离开作用域时自动关闭
  • std::format(C++20)提供类型安全的、类 Python 的字符串格式化
  • std::print/println(C++23)结合了格式化和输出 ── 新代码的首选方式
  • iostream 操纵器是有状态的(粘滞的)── std::format 完全避免了这个问题

测验 — 检验你的知识

(10 经验值)

1. 如果在 `std::cin >> number` 之后立即调用 `std::getline(std::cin, str)` 会发生什么?

2. `std::format("{:#06x}", 255)` 产生什么结果?

3. 为什么文件流(ifstream/ofstream)在离开作用域时会自动关闭?