تستخدم لغة C++ مجاري التدفق لإدارة دخل وخرج الملفات، إذ تستخدم:
-
std::istream
لقراءة النص. -
std::ostream
لكتابة النص. -
std::streambuf
لقراءة أو كتابة المحارف. -
يستخدم الدَّخل المُنسّق (Formatted input) العاملَ
operator>>
. -
يستخدم الخرج المنسّق العامل
operator<<
.
تستخدم المجاري std::locale
مثلًا للحصول على تفاصيل التنسيق والترجمة بين الترميزات الخارجية والترميز الداخلي.
الكتابة في الملفات
هناك عدة طرق للكتابة في ملف، لعل أسهلها هو استخدام مجرى خرج الملفّات ofstream
مع عامل الإدارج في المجرى (<<
)، انظر:
std::ofstream os("foo.txt"); if(os.is_open()){ os << "Hello World!"; }
تستطيع استخدام الدالة التابعة لمجرى خرج الملفّات write()
بدلًا من <<
، انظر المثال التالي حيث سنكتب ثلاثة محارف من data
:
std::ofstream os("foo.txt"); if (os.is_open()) { char data[] = "Foo"; os.write(data, 3); }
يجب أن تتحقق دائمًا بعد الكتابة في المجرى من راية حالة الخطأ badbit
، إذ أنها تشير إلى نجاح العملية أو فشلها، وتستطيع ذلك باستدعاء دالة تابع مجرى خَرْج الملف bad()
، انظر:
os << "Hello Badbit!"; // قد تفشل هذه العملية لسبب ما if (os.bad()) // فشل في الكتابة
فتح ملف
تُفتح الملفات بنفس الطّريقة في جميع مجاري الملفات الثلاث (ifstream
، ofstream
، وfstream
). يمكنك فتح الملف مباشرة في المُنشئ (constructor)، انظر المثال التالي حيث نفتح الملف foo.txt للقراءة فقط، ثم للكتابة فقط، ثم للقراءة والكتابة:
std::ifstream ifs("foo.txt"); std::ofstream ofs("foo.txt"); std::fstream iofs("foo.txt");
أو يمكنك استخدام دالة تابع مجرى الملف open()
لتنفيذ نفس الشيء:
std::ifstream ifs; ifs.open("bar.txt"); // للقراءة فقط "bar.txt" فتح الملف std::ofstream ofs; ofs.open("bar.txt"); // للكتابة فقط "bar.txt" فتح الملف std::fstream iofs; iofs.open("bar.txt"); // للقراءة والكتابة "bar.txt" فتح الملف
يجب عليك التحقّق دائمًا ممّا إذا كان الملف قد فُتِح بنجاح (حتى أثناء الكتابة)، قد تفشل عملية الفتح لعدة أسباب، انظر بعضها فيما يلي:
- عدم وجود الملف
- صلاحيات الوصول (access rights) غير صالحة.
- الملف قيد الاستخدام في الوقت الراهن.
- وقوع خطأ في القرص.
- فصل القرص من الحاسوب.
- …
يمكن إجراء عملية التحقّق على النحو التالي:
// 'foo.txt' محاولة قراءة std::ifstream ifs("fooo.txt"); // خطأ في الكتابة، لا يمكن فتح الملف // التحقق مما إذا كان الملف قد فُتِح بنجاح if (!ifs.is_open()) { // لم يُفتح الملف بعد، اتخاذ الإجراءات المناسبة throw CustomException(ifs, "الملف لم يُفتح"); }
إن كان المسار يحتوي على شرطة مائلة عكسية \
كما هو الحال في نظام Windows، فيجب أن تهربها بشكل سليم. انظر المثال التالي لفتح الملف foo.txt في ويندوز:
std::ifstream ifs("c:\\\\folder\\\\foo.txt"); // تهريب الشرطة المائلة العكسية
الإصدار ≥ C++ 11
أو يمكنك استخدام القيم مصنفة النوع الخام (raw literal)، انظر:
std::ifstream ifs(R"(c:\\folder\\foo.txt)"); // استخدام قيم مصنفة النوع خام
أو استخدم شرطة مائلة /
بدلاً من ذلك:
std::ifstream ifs("c:/folder/foo.txt");
الإصدار ≥ C++ 11
إن أردت أن تفتح ملفًّا يحتوي على محارف من غير ASCII في مسارٍ في Windows، فيمكنك حاليًا استخدام المَحرَف العام غير القياسي (non-standard wide character) في المسار، انظر المثال التالي حيث نضع كلمة "مثال" بالبلغارية في مسار الملف:
std::ifstream ifs(LR"(пример\\foo.txt)"); // استخدام محرف عام مع سلسلة نصية خام
القراءة من ملف
هناك عدة طرق لقراءة البيانات من ملف، فإن كنت تعرف تنسيق (format) البيانات فيمكنك استخدام عامل الاستخراج من المجرى - stream extraction operator - (>>
). دعنا نفرض أنّ لديك ملفّا يُسمّى foo.txt يحتوي على البيانات التالية:
John Doe 25 4 6 1987 Jane Doe 15 5 24 1976
فعندها يمكنك استخدام الشّيفرة أدناه لقراءة تلك البيانات من الملف، لاحظ أننا سنستخرج الاسم الأول firstname
واسم العائلة lastname
والعمر age
وشهر الولادة bmonth
ويوم الولادة bday
وعام الولادة byear
. أيضًا، انتبه إلى أن >>
تعيد القيمة false
إن بلغت نهاية الملف أو لم تتوافق بيانات الدخل مع نوع المتغير، فلا يمكن استخراج نص foo
إلى متغير int
مثلًا.
// تعريف المتغيرات std::ifstream is("foo.txt"); std::string firstname, lastname; int age, bmonth, bday, byear; while (is >> firstname >> lastname >> age >> bmonth >> bday >> byear) // معالجة البيانات المقروءة
يستخرج العامل >>
كل المحارف ويتوقف إذا وجد حرفًا لا يمكنه تخزينه أو محرفًا خاصًّا (special character):
-
بالنسبة للسًلاسل النصية، يتوقًف العامل عند المسافة الفارغة ()، أو عند السًطر الجديد (
\n
). - بالنًسبة للأعداد، يتوقف العامل عند المحارف غير الرقمية.
هذا يعني أنّ النُّسخ التالية من ملف foo.txt
ستُقرأ بنجاح من قبل الشّيفرة السابقة:
John Doe 25 4 6 1987 Jane Doe 15 5 24 1976
يعيدُ عاملُ >>
المجرى المُمرّرَ إليه، لهذا يمكن سَلْسَلةُ هذا العامل من أجل قراءة البيانات على التوالي. كذلك من الممكن استخدام المجرى كتعبير بولياني (كما هو مُوضّح في حلقة while
في الشّيفرة السابقة)، ذلك أن أصناف المجرى بها عامل تحويل للنوع bool
.
سيعيد العامل bool()
القيمة true
طالما أنّ المجرى لا يحتوي على أخطاء، أما إن تغيّرت حالة المجرى إلى حالة خطأ (على سبيل المثال، إذا لم يكن من الممكن استخراج مزيد من البيانات)، فسوف يعيد العاملُ bool()
القيمةfalse
، وعليه فإن حلقة while
في الشّيفرة السابقة ستُنهى بعد قراءة كامل الملف.
إذا أردت قراءة كامل الملف كسلسلة نصّية، فيمكنك استخدام الشّيفرة التالية:
// 'foo.txt' فتح std::ifstream is("foo.txt"); std::string whole_file; // الذهاب إلى نهاية الملف is.seekg(0, std::ios::end); // تخصيص ذاكرة للملف whole_file.reserve(is.tellg()); // الذهاب إلى بداية الملف is.seekg(0, std::ios::beg); // 'whole_file' تعيين محتوى // إلى جميع محارف الملف whole_file.assign(std::istreambuf_iterator<char>(is), std::istreambuf_iterator<char>());
توفر هذه الشيفرة مساحة للسّلسلة النصية string
من أجل الاقتصاد في الذاكرة. أما إن أردت قراءة الملف سطرًا سطرًا، فيمكنك استخدام الدالة getline()
، انظر المثال التالي حيث تعيد هذه الدالة القيمة false
إن لم يكن ثمة أسطر أخرى متبقية:
std::ifstream is("foo.txt"); for (std::string str; std::getline(is, str);) { // معالجة السّطر المقروء }
إذا أردت قراءة عدد محدّد من المحارف فاستخدم دالة تابع المجرى read()
:
std::ifstream is("foo.txt"); char str[4]; // قراءة أربعة حروف من الملف is.read(str, 4);
يجب أن تتحقق دائمًا بعد تنفيذ أمر القراءة من راية حالة الخطأ failbit
إذ أنها تشير إلى نجاح العملية أو فشلها، وتستطيع ذلك باستدعاء دالة تابع مجرى خَرْج الملف fail()
، انظر:
is.read(str, 4); // قد تفشل هذه العملية لسبب ما if (is.fail()) // فشل في القراءة!
أوضاع الفتح
يمكنك تحديد وضع الفتح عند إنشاء مجرى ملف، ووضع الفتح ما هو إلا إعداد للتّحكم في كيفيّة فتح المجرى للملف، تستطيع العثور على جميع الأوضاع في مجال اسم std::ios
. يمكن تمرير وضع الفتح كمعامل ثاني إلى منشئ مجرى الملف أو إلى تابعه open()
:
std::ofstream os("foo.txt", std::ios::out | std::ios::trunc); std::ifstream is; is.open("foo.txt", std::ios::in | std::ios::binary);
تجدر الإشارة إلى أنّه يجب عليك تعيين معامِل الوضع الافتراضي ios::in
أو ios::out
إذا أردت تعيين قيم الرايات الأخرى، لأنها لا تٌعيّن ضمنيًا من قبل أعضاء مجرى الدّخل (iostream) رغم أنّ لديها قيمة افتراضية صحيحة.
يُستخدم وضع الفتح الافتراضي أدناه إذا لم تحدِّد وضعًا للفتح:
- ifstream - دخل
- ofstream - خرج
- fstream - دخل وخرج
أوضاع الفتح التي تستطيع تحديدها هي:
الوضع | المعنى | الغرض | الوصف |
---|---|---|---|
app | ألحِق (append) | إخراج | إضافة البيانات إلى نهاية الملف |
binary | ثنائي/بِتِّي (Binary) | إخراج/إدخال | الإدخال والإخراج يحدث بالبتات |
in | دخل (input) | إدخال | فتح الملف للقراءة |
out | خرج (output) | إخراج | فتح الملف للكتابة |
trunc | بتر (truncate) | إخراج/إدخال | حذف محتوى الملف عند الفتح |
ate | عند النهاية (at end) | إدخال | الذهاب إلى نهاية الملف عند الفتح |
ملاحظة: في حال تعيين الوضع البتّي binary
، فستُقرأ وتُكتب البيانات تمامًا كما هي؛ أما إن لم تُحدَّد فسيكون ممكنًا ترجمة محرف السّطر الجديد '\n'
إلى نهاية السّطر المناسبة لنظام التشغيل المستخدم. على سبيل المثال، في نظام Windows، تسلسل نهاية السطر هو CRLF ("\r\n"
).
- الكتابة:
"\n" => "\r\n"
- القراءة:
"\r\n" => "\n"
قراءة ملف ASCII إلى مكتبة std::string
انظر المثال التالي، سيكون محتوى file.txt
موجودًا في buffer.str()
:
std::ifstream f("file.txt"); if (f) { std::stringstream buffer; buffer << f.rdbuf(); f.close(); }
يعيد التابع rdbuf()
مؤشرًا إلى streambuf
، والذي يمكن إضافته إلى المخزن المؤقّت buffer
عبر دالة التابع stringstream::operator<<
. هناك احتمال آخر (من اقتراح سكوت مايرز):
std::ifstream f("file.txt"); if (f) { std::string str((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>()); // `str` عمليّات على }
هذه الطّريقة لا تحتاج شيفرة كبيرة وتسمح بقراءة الملفّ مباشرة إلى أي حاوية مكتبة قياسيّة وليست قاصرة على السّلاسل النصية فقط، لكن قد تكون بطيئة إن كان الملف كبيرُا.
ملاحظة: الأقواس الإضافية حول الوسيط الأول في مُنشئ السّلسلة النصية ضرورية لمنع حدوث مشكلة في التحليل (parsing).
وأخيرًا وليس آخرًا:
std::ifstream f("file.txt"); if (f) { f.seekg(0, std::ios::end); const auto size = f.tellg(); std::string str(size, ' '); f.seekg(0); f.read( & str[0], size); f.close(); // `str` عمليات على }
وهو أسرع خيار من بين الخيارات الثلاثة المقترحة.
الكتابة في الملفات باستخدام إعدادات محليّة غير قياسية
استخدم std::locale
و std::basic_ios::imbue()
إن كنت بحاجة إلى الكتابة في ملف باستخدام إعدادات محليّة غير الإعدادات الافتراضية، واسترشد بالتوجيهات الآتية:
- يجب عليك دائمًا تطبيق إعدادات محليّة على المجرى قبل فتح الملف.
- بمجرد أن تطعِّم المجرى بالإعدادات الجديدة، لا تغيّر الإعدادات المحليّة.
أسباب تلك التوجيهات: تحويل مجرى ملف إلى إعدادات محليّة ستكون له تداعيات غير متوقّعه إذا لم تكن الإعدادات الحاليّة مستقلّة، أو إن لم تكن تشير إلى بداية الملف.
مجاري UTF-8 وغيرها ليست مستقلة، كما أنّ مجاري الملفّات ذات الإعدادات المحليّة UTF-8 قد تحاول قراءة المِحرف BOM من الملف عند فتحه؛ لذلك فعليك أن تنتبه، فقراءة الملف بعد الفتح قد لا تكون من بدايته دائمًا.
#include <iostream> #include <fstream> #include <locale> int main() { std::cout << "User-preferred locale setting is " << std::locale("").name().c_str() << std::endl; // اكتب عدد عشري باستخدام الإعدادات المحليّة للمستخدم std::ofstream ofs1; ofs1.imbue(std::locale("")); ofs1.open("file1.txt"); ofs1 << 78123.456 << std::endl; // استخدم إعدادات محليّة محددة، الأسماء تختلف من نظام لآخر std::ofstream ofs2; ofs2.imbue(std::locale("en_US.UTF-8")); ofs2.open("file2.txt"); ofs2 << 78123.456 << std::endl; // "C" التحوّل إلى الإعدادات المحليّة لـ std::ofstream ofs3; ofs3.imbue(std::locale::classic()); ofs3.open("file3.txt"); ofs3 << 78123.456 << std::endl; }
التبديل إلى الإعدادات المحليّة التقليدية للغة "C" مفيد في حال كان البرنامج يستخدم لغة افتراضيّة مختلفة وكنت تريد توحيد معيار القراءة والكتابة. وإن استخدمنا الإعدادات المحليّة المفضّلة في "C"، فإنّ المثال أعلاه سيُكتب هكذا:
78,123.456 78,123.456 78123.456
على سبيل المثال، إذا كانت الإعدادات المحليّة المفضّلة هي اللغة الألمانية ومن ثم فإنها تستخدم تنسيقا مختلفًا للأرقام، فإنّ المثال سيُكتب هكذا:
78 123,456 78,123.456 78123.456
(لاحظ الفاصلة العشرية في السطر الأول).
تجنب التحقق من نهاية الملف داخل شرط الحلقة
لا تعيد eof
القيمة true
إلا بعد قراءة نهاية الملف، وهي لا تشير إلى أنّ القراءة التالية ستكون نهاية المجرى. انظر:
while (!f.eof()) { // هذا يبدو جيدًا f >> buffer; // صحيحة eof هنا تصبح /* `buffer` استخدم */ }
يمكن أن تكتب أيضًا:
while (!f.eof()) { f >> buffer >> std::ws; if (f.fail()) break; /* `buffer` استخدم */ }
لكنّ الشّيفرة:
while (f >> buffer) { /* `buffer` استخدم */ }
أبسط وأصحّ.
مراجع أخرى:
-
std::ws
: تتجاهل المسافة البيضاء في بداية مجرى الدخل. -
std::basic_ios::fail
: تعيدtrue
إذا حدث خطأ في المجرى المرتبط بها.
تفريغ المجرى (Flushing a stream)
تقوم مجاري الملفات -إضافة إلى العديد من أنواع المجاري الأخرى- بالتخزين المؤقت (buffering) افتراضيًا، وذلك يعني أن الكتابة في المجرى قد لا تؤدي إلى تغير الملف المقابل فورًا، بل يجب أن تفرِّغ المجرى إن أردت ذلك، إما باستدعاء التابع flush()
أو عبر معالج المجرى std::flush
، انظر:
std::ofstream os("foo.txt"); os << "Hello World!" << std::flush; char data[3] = "Foo"; os.write(data, 3); os.flush();
يجمع مُعالج المجرى std::endl
بين كتابة سطر جديد وتفريغ المجرى، انظر الشيفرة التالية حيث ينفِّذ كلا السطران نفس الشيء:
os << "Hello World!\n" << std::flush; os << "Hello world!" << std::endl;
يحسّن التخزين المؤقّت (Buffering) أداء عمليات الكتابة في المجرى، لهذا يُفضّل أن تتجنّب التطبيقاتُ كثيرةُ الكتابةِ استخدام التفريع ما لم يكن لازمًا. لكن على العكس من ذلك، إذا لم يكن البرنامج يكثر من عمليات الدّخل والخرج (I/O)، فمن الأفضل الإكثار من التفريغ لتجنب تكدّس البيانات في كائن المجرى.
قراءة ملف في حاوية
في المثال التالي، سنستخدم std::string
و operator>>
لقراءة عناصر من ملف.
std::ifstream file("file3.txt"); std::vector < std::string > v; std::string s; while (file >> s) // الاستمرار في القراءة حتى النهاية { v.push_back(s); }
في المثال أعلاه، كرّرنا -من iterate- على الملف بقراءة "عنصر" واحد في كل مرّة باستخدام العامل <<
. يمكن تحقيق الشّيء نفسه باستخدام std::istream_iterator
، والذي هو مكرّر دخْلٍ يقرأ "عنصرًا" واحدًا في كل مرة من المجرى. كذلك يمكن إنشاء معظم الحاويات باستخدام مُكرِّرين كما يلي:
std::ifstream file("file3.txt"); std::vector<std::string> v(std::istream_iterator<std::string>{file}, std::istream_iterator<std::string>{});
يمكننا توسيع هذه الشّيفرة لقراءة أيّ نوع من الكائنات من خلال تمرير الكائن الذي نريد قراءته كمعامل قالب (template parameter) إلى std::istream_iterator
. وهكذا، يمكننا توسيع الشّيفرة السّابقة لقراءة الأسطر (بدلاً من الكلمات). انظر المثال التالي، لاحظ أنه لعدم وجود نوع مضمّن يقرأ الأسطر باستخدام <<
فإننا سنبني صنفًا مساعدًا لفعل ذلك، وسيقوم بعملية التحويل إلى سلسلة نصية (string) عند استخدامه في سياق نصي:
struct Line { // تخزين البيانات هنا std::string data; // تحويل الكائن إلى سلسلة نصية operator std::string const & () const { return data; } // قراءة سطر من المجرى friend std::istream & operator >> (std::istream & stream, Line & line) { return std::getline(stream, line.data); } }; std::ifstream file("file3.txt"); // قراءة أسطر الملف إلى حاوية std::vector<std::string> v(std::istream_iterator<Line>{file}, std::istream_iterator<Line>{});
نسخ الملفات
std::ifstream src("source_filename", std::ios::binary); std::ofstream dst("dest_filename", std::ios::binary); dst << src.rdbuf();
الإصدار ≥ C++ 17
الطريقة القياسيّة لنسخ ملف في C++ 17 هي تضمين التّرويسة copy_file
، انظر:
std::fileystem::copy_file("source_filename", "dest_filename");
طُوِّرت المكتبة filesystem في البداية كوحدة boost (boost.filesystem
)، تم دُمِجت بعد ذلك في التوصيف ISO C++ بدءًا من C++ 17.
إغلاق ملف
نادرًا ما يكون عليك إغلاق الملفات في C++، لأنّ المجرى سوف يغلق الملف المرتبط به تلقائيًا في المفكك (destructor) الخاص به. ومع ذلك يُفضّل أن تحُدّّ من عمر كائنات المجرى، حتى لا يبقى مِقبض (handle) الملفّ مفتوحًا لفترة أطول من اللازم.
على سبيل المثال، يمكنك فعل ذلك عبر وضع جميع العمليّات المُنفّذة على الملف في نطاق خاص ({}
)، لاحظ أن ofstream
ستكون خارج النطاق بنهاية هذه الشيفرة:
std::string const prepared_data = prepare_data(); { // فتح ملف للكتابة std::ofstream output("foo.txt"); // كتابة البيانات output << prepared_data; } // سيتولى المدمر إغلاق الملف
لا يكون استدعاء close()
بشكل صريح ضروريًّا إلا إن أردت إعادة استخدام نفس كائن fstream
لاحقًا لكنّك لم ترغب في إبقائه مفتوحًا إلى ذلك الحين، انظر المثال التالي الذي نجريه على ملف foo.txt
:
// افتح الملف لأول مرة std::ofstream output("foo.txt"); // جهِّز بعض البيانات لتكتبها في الملف std::string const prepared_data = prepare_data(); // اكتب البيانات إلى الملف output << prepared_data; // أغلق الملف output.close(); // قد يستغرق تحضير البيانات وقتًا طويلًا // لذا لن نفتح مجرى خرج الملف حتى نكون جاهزين للكتابة فيه std::string const more_prepared_data = prepare_complex_data(); // افتح الملف مرة أخرى بمجرد أن تكون جاهزًا للكتابة فيه output.open("foo.txt"); // اكتب البيانات في الملف output << more_prepared_data; // أغلق الملف مرة أخرى output.close();
قراءة بُنية struct
من ملف نصّي منسق
الإصدار ≥ C++ 11 انظر المثال التالي، سنحدد حملًا زائدًا للعامل <<operator
على أنه دالة friend
تمنح امتياز الوصول إلى البيانات الخاصة للأعضاء.
struct info_type { std::string name; int age; float height; friend std::istream & operator >> (std::istream & is, info_type & info) { // تجاوز المسافة البيضاء is >> std::ws; std::getline(is, info.name); is >> info.age; is >> info.height; return is; } }; void func4() { auto file = std::ifstream("file4.txt"); std::vector < info_type > v; for (info_type info; file >> info;) // اقرأ حتى النهاية { // لن نصل إلى هنا إلا في حال نجاح عملية القراءة v.push_back(info); } for (auto const & info: v) { std::cout << " name: " << info.name << '\n'; std::cout << " age: " << info.age << " years" << '\n'; std::cout << "height: " << info.height << "lbs" << '\n'; std::cout << '\n'; } }
ملف file4.txt
لنفرض أن البيانات التالية موجودة في ملفّ file4.txt
:
Wogger Wabbit 2 6.2 Bilbo Baggins 111 81.3 Mary Poppins 29 154.8
إذًا يكون الخرج ما يلي:
name: Wogger Wabbit age: 2 years height: 6.2lbs name: Bilbo Baggins age: 111 years height: 81.3lbs name: Mary Poppins age: 29 years height: 154.8lbs
هذا الدرس جزء من سلسلة دروس عن C++.
ترجمة -بتصرّف- للفصل Chapter 12: File I/O من كتاب C++ Notes for Professionals
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.