رفتن به محتوا

ورودی/خروجی: جریان‌ها، قالب‌بندی و std::print

بر ورودی/خروجی ‎C++‎ از iostream کلاسیک تا std::format و std::print مدرن، شامل حالت‌های جریان، ورودی/خروجی فایل و دام‌های رایج بافر مسلط شوید.

تکامل ورودی/خروجی ‎C++‎

ورودی/خروجی ‎C++‎ در طول تاریخ زبان به طور قابل توجهی تکامل یافته است. کتابخانه کلاسیک iostream (std::cin، std::cout) از ‎C++‎98 استاندارد بوده، اما مشکلات ارگونومیک و عملکردی شناخته‌شده‌ای دارد. ‎C++‎20 std::format را معرفی کرد، کتابخانه قالب‌بندی ایمن از نظر نوع الهام‌گرفته از str.format() پایتون. ‎C++‎23 std::print را اضافه کرد که قالب‌بندی و خروجی را در یک فراخوانی واحد ترکیب می‌کند.

درک هر سه رویکرد مهم است: iostream در همه جای کد موجود هست، std::format روش مدرن قالب‌بندی رشته‌ها است، و std::print آینده خروجی ‎C++‎ است.

ورودی/خروجی کلاسیک: 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 — بدون خطا. جریان برای عملیات ورودی/خروجی آماده است.
- eofbit — انتهای فایل/ورودی رسیده. وقتی جریان تلاش می‌کند فراتر از انتها بخواند تنظیم می‌شود.
- failbit — خطای منطقی رخ داده (مثلاً تلاش برای خواندن عدد صحیح اما مواجهه با متن). جریان پس از فراخوانی clear() همچنان قابل استفاده است.
- badbit — خطای غیرقابل بازیابی ورودی/خروجی رخ داده (مثلاً خرابی دیسک). جریان معمولاً غیرقابل استفاده است.

این‌ها را با stream.good()، stream.eof()، stream.fail()، stream.bad() یا با استفاده از جریان در زمینه بولین بررسی کنید: if (std::cin) معادل if (!std::cin.fail()) است.

الگوی حیاتی: همیشه حالت جریان را پس از عملیات ورودی بررسی کنید، نه قبل. الگوی رایج while (std::cin >> value) خواندن و بررسی را در یک مرحله انجام می‌دهد — وقتی خواندن شکست بخورد متوقف می‌شود.

دام

رایج‌ترین باگ ورودی/خروجی در ‎C++‎ شامل ترکیب >> (استخراج قالب‌بندی‌شده) با std::getline() است. عملگر >> مقداری می‌خواند اما خط جدید انتهایی را در بافر باقی می‌گذارد. سپس std::getline() بعدی آن خط جدید باقیمانده را به عنوان رشته خالی می‌خواند و به نظر می‌رسد ورودی را رد کرده است.

راه‌حل فراخوانی std::cin.ignore(std::numeric_limits::max(), '\n') بین >> و std::getline() برای دور ریختن خط جدید باقیمانده است.

رویکرد دیگر استفاده از 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) به جای خواندن کورکورانه

ورودی/خروجی فایل

ورودی/خروجی فایل از همان رابط جریان ورودی/خروجی کنسول استفاده می‌کند، با 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

std::format در ‎C++‎20 و std::print در ‎C++‎23 قالب‌بندی ایمن از نظر نوع و شبیه پایتون فراهم می‌کنند که هم ایمن‌تر و هم خواناتر از دستکاری‌کننده‌های 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++‎ مدرن سه رویکرد ورودی/خروجی به شما می‌دهد. هر کدام را چه زمانی استفاده کنید:

از std::print/std::println (‎C++‎23) استفاده کنید برای کد جدید وقتی کامپایلرتان آن را پشتیبانی می‌کند. تمیزترین نحو، ایمن از نظر نوع، و مشکلات دستکاری‌کننده‌های حالت‌دار iostream را ندارد.

از std::format (‎C++‎20) استفاده کنید وقتی نیاز به رشته قالب‌بندی‌شده دارید (نه خروجی مستقیم)، یا وقتی کامپایلرتان ‎C++‎20 را پشتیبانی می‌کند اما ‎C++‎23 را نه.

از iostream استفاده کنید وقتی با کدی تعامل دارید که اشیاء جریان انتظار دارد، وقتی نیاز به انتزاع جریان دارید (مثلاً خواندن از cin، ورودی/خروجی فایل)، یا وقتی با پایگاه‌های کد قدیمی کار می‌کنید.

از printf/scanf زبان C اجتناب کنید — ایمن از نظر نوع نیستند (ارسال مشخص‌کننده قالب اشتباه رفتار تعریف‌نشده است) و با انواع ‎C++‎ مانند std::string کار نمی‌کنند.

  • std::print/println (‎C++‎23) را برای کد خروجی جدید ترجیح دهید — تمیزترین و ایمن‌ترین
  • از std::format (‎C++‎20) استفاده کنید وقتی نیاز به رشته قالب‌بندی‌شده دارید، نه خروجی مستقیم
  • iostream همچنان برای cin، ورودی/خروجی فایل و انتزاعات مبتنی بر جریان مورد نیاز است
  • از printf/scanf اجتناب کنید — ایمن از نظر نوع نیستند و نمی‌توانند std::string را مستقیماً مدیریت کنند
نکات کلیدی
  • همیشه حالت جریان را پس از عملیات ورودی بررسی کنید — از 'if (cin >> x)' یا 'while (getline(in, line))' استفاده کنید
  • ترکیب cin >> و getline() باعث باگ خط‌جدید‌در‌بافر می‌شود — بین آن‌ها cin.ignore() فراخوانی کنید
  • جریان‌های فایل از RAII استفاده می‌کنند — وقتی از دامنه خارج شوند به طور خودکار بسته می‌شوند
  • std::format (‎C++‎20) قالب‌بندی رشته ایمن از نظر نوع و شبیه پایتون فراهم می‌کند
  • 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) وقتی از دامنه خارج می‌شوند به طور خودکار بسته می‌شوند؟