انتقل إلى المحتوى

I/O: Streams, Formatting & std::print

Master C++ I/O from classic iostream to modern std::format and std::print, including stream states, file I/O, and common buffer pitfalls.

The Evolution of C++ I/O

C++ I/O has evolved significantly across the language's history. The classic iostream library (std::cin,std::cout) has been the standard since C++98, but it has well-known ergonomic and performance issues. C++20 introduced std::format, a type-safe formatting library inspired by Python'sstr.format(). C++23 added std::print, which combines formatting and output in a single call.

Understanding all three approaches is important: iostream is everywhere in existing code,std::formatis the modern way to format strings, andstd::printis the future of C++ output.

Classic 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 — لا توجد أخطاء. الدفق جاهز لعمليات الإدخال/الإخراج.
- 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() التالي كسلسلة فارغة
  • إصلاح: الاتصال بـ cin.ignore(numeric_limits::max(), '\n') بين >> وgetline()
  • البديل: استخدم 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 تنسيقًا آمنًا من النوع يشبه 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++ الحديث ثلاث طرق للإدخال/الإخراج. إليك متى تستخدم كل منها:

استخدم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) تنسيق سلسلة آمنًا يشبه لغة Python
  • يجمع std::print/println (C++23) بين التنسيق والإخراج - وهي الطريقة المفضلة للتعليمات البرمجية الجديدة
  • تعد معالجات iostream ذات حالة (مثبتة) - يتجنب التنسيق std::format هذه المشكلة تمامًا

اختبار — اختبر معرفتك

(10 نقطة خبرة)

1. ماذا يحدث إذا اتصلت بـ`std::getline(std::cin, str)`مباشرة بعد`std::cin >> number`؟

2. ماذا ينتج`std::format("{:#06x}", 255)`؟

3. لماذا يتم إغلاق تدفقات الملفات (ifstream/ofstream) تلقائيًا عندما تخرج عن النطاق؟