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:
#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';
}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 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_limitsgiữ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:
#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';
}
}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:
#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
}Nhấn Chạy để biên dịch và chạy ví dụ này.
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
- 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?