Chuyển đến nội dung

I/O: Luồng, định dạng & std::print

Làm chủ I/O C++ từ iostream cổ điển đến std::format và std::print hiện đại, bao gồm trạng thái luồng, I/O tệp và các lỗi bộ đệm phổ biến.

Sự phát triển của I/O C++

I/O C++ đã phát triển đáng kể trong suốt lịch sử của ngôn ngữ. Thư viện iostream cổ điển (std::cin,std::cout) đã trở thành tiêu chuẩn kể từ C++98, nhưng nó có các vấn đề về hiệu suất và công thái học nổi tiếng. C++20 đã giới thiệu std::format, một thư viện định dạng an toàn kiểu được lấy cảm hứng từstr.format()của Python. C++23 đã thêm std::print, kết hợp định dạng và đầu ra trong một lệnh gọi duy nhất.

Hiểu cả ba cách tiếp cận đều quan trọng: iostream có ở khắp mọi nơi trong mã hiện có,std::formatlà cách hiện đại để định dạng chuỗi vàstd::printlà tương lai của đầu ra C++.

I/O cổ điển: cout, cin, cerr

Thư viện iostream cung cấp các đối tượng luồng cho đầu vào, đầu ra và đầu ra lỗi tiêu chuẩn:

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';
}
Đầu ra

Nhấn Chạy để biên dịch và chạy ví dụ này.

Truyền phát cờ bang

Mỗi luồng duy trì một bộ cờ trạng thái cho biết tình trạng hiện tại của luồng:

- goodbit — Không có lỗi. Luồng đã sẵn sàng cho các hoạt động I/O.
- eofbit — Đã đến phần cuối của tệp/đầu vào. Đặt thời điểm luồng cố gắng đọc qua phần cuối.
- failbit — Đã xảy ra lỗi logic (ví dụ: cố đọc số nguyên nhưng gặp phải văn bản). Luồng vẫn có thể sử dụng được sau khi gọiclear().
- badbit — Đã xảy ra lỗi I/O không thể phục hồi (ví dụ: lỗi ổ đĩa). Luồng này thường không sử dụng được.

Bạn kiểm tra những điều này bằngstream.good(),stream.eof(),stream.fail(),stream.bad()hoặc bằng cách sử dụng luồng trong ngữ cảnh boolean:if (std::cin)tương đương vớiif (!std::cin.fail()).

Mẫu quan trọng: luôn kiểm tra trạng thái luồng sau các thao tác nhập, không phải trước đó. Thành ngữ phổ biếnwhile (std::cin >> value)đọc và kiểm tra trong một bước - nó dừng khi đọc không thành công.

Lỗi thường gặp

Lỗi I/O phổ biến nhất trong C++ liên quan đến việc trộn>>(trích xuất có định dạng) vớistd::getline(). Toán tử>>đọc một giá trị nhưng để lại dòng mới ở cuối bộ đệm.std::getline()tiếp theo sẽ đọc dòng mới còn sót lại đó dưới dạng một chuỗi trống, dường như bỏ qua đầu vào.

Cách khắc phục là gọistd::cin.ignore(std::numeric_limits::max(), '\n')giữa>>vàstd::getline()để loại bỏ dòng mới còn sót lại.

Một cách tiếp cận khác là sử dụngstd::getline()cho tất cả đầu vào và tự phân tích chuỗi bằngstd::istringstreamhoặcstd::stoi/std::stod. Điều này tránh hoàn toàn vấn đề và giúp bạn xử lý lỗi tốt hơn.

  • Sau cin >> x, một dòng mới vẫn còn trong bộ đệm — getline() tiếp theo đọc nó dưới dạng một chuỗi trống
  • Khắc phục: gọi cin.ignore(numeric_limits::max(), '\n') giữa >> và getline()
  • Thay thế: sử dụng getline() cho TẤT CẢ các chuỗi đầu vào và phân tích cú pháp bằng stoi/stod hoặc istringstream
  • Luôn kiểm tra trạng thái luồng sau khi nhập: if (cin >> x) thay vì đọc một cách mù quáng

Tệp vào/ra

I/O tệp sử dụng giao diện luồng giống như I/O của bảng điều khiển, vớistd::ifstreamđể đọc vàstd::ofstreamđể ghi:

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';
    }
}
Đầu ra

Nhấn Chạy để biên dịch và chạy ví dụ này.

Định dạng hiện đại: std::format & std::print

std::formatcủa C++20 vàstd::printcủa C++23 cung cấp định dạng an toàn kiểu, giống Python, vừa an toàn hơn vừa dễ đọc hơn các trình thao tác 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
}
Đầu ra

Nhấn Chạy để biên dịch và chạy ví dụ này.

Thực hành tốt

C++ hiện đại cung cấp cho bạn ba cách tiếp cận I/O. Đây là khi nào nên sử dụng từng cái:

Sử dụngstd::print/std::println(C++23) cho mã mới khi trình biên dịch của bạn hỗ trợ. Đây là cú pháp rõ ràng nhất, an toàn về kiểu và tránh được các vấn đề về thao tác trạng thái của iostream.

Sử dụngstd::format(C++20) khi bạn cần một chuỗi được định dạng (không phải đầu ra trực tiếp) hoặc khi trình biên dịch của bạn hỗ trợ C++20 nhưng không hỗ trợ C++23.

Sử dụng iostream khi tương tác với mã yêu cầu đối tượng luồng, khi bạn cần trừu tượng hóa luồng (ví dụ: đọc từ cin, tệp I/O) hoặc khi làm việc với các cơ sở mã cũ.

Tránhprintf/scanftừ C — chúng không an toàn về loại (chuyển sai định dạng định dạng là hành vi không xác định) và không hoạt động với các loại C++ nhưstd::string.

  • Ưu tiên std::print/println (C++23) cho mã đầu ra mới - sạch nhất và an toàn nhất
  • Sử dụng std::format (C++20) khi bạn cần một chuỗi được định dạng, không phải đầu ra trực tiếp
  • iostream vẫn cần thiết cho cin, tệp I/O và trừu tượng hóa dựa trên luồng
  • Tránh printf/scanf — chúng không an toàn về kiểu và không thể xử lý trực tiếp std::string
Điểm chính
  • Luôn kiểm tra trạng thái luồng sau các thao tác nhập — sử dụng 'if (cin >> x)' hoặc 'while (getline(in, line))'
  • Việc trộn cin >> và getline() gây ra lỗi dòng mới trong bộ đệm — hãy gọi cin.ignore() giữa chúng
  • Luồng tệp sử dụng RAII - chúng tự động đóng khi vượt quá phạm vi
  • std::format (C++20) cung cấp định dạng chuỗi giống như Python, an toàn kiểu
  • std::print/println (C++23) kết hợp định dạng và đầu ra — cách ưa thích cho mã mới
  • trình điều khiển iostream có trạng thái (dính) - std::format tránh hoàn toàn vấn đề này

Câu đố — Kiểm tra kiến thức

(10 XP)

1. Điều gì xảy ra nếu bạn gọi`std::getline(std::cin, str)`ngay sau`std::cin >> number`?

2. `std::format("{:#06x}", 255)`tạo ra cái gì?

3. Tại sao các luồng tệp (ifstream/ofstream) tự động đóng khi chúng vượt quá phạm vi?