ملخص
كل ما يحسبه البرنامج يختفي حين ينتهي، ما لم يتركه مكتوبًا في مكان يبقى بعده. يعرض هذا الدرس ذلك المكان، أي الملف النصي، والفكرة التي تتيح استعماله دون تعلّم شيء جديد جذريًا: الملف يُقرأ ويُكتب بوصفه مجرى، بالعمليات نفسها التي نستعملها مع لوحة المفاتيح والشاشة. ندرس std::ifstream وstd::ofstream، والتحقق من أن الملف قد فُتح قبل استعماله، ومجلد العمل الذي يحدد أين يُبحث عن الملف؛ وحلقة القراءة الصحيحة، while (std::getline(in, line))، في مقابل الحلقة المبنية على eof()، التي تعالج معطًى زائدًا، والسبب الدقيق لذلك المعطى؛ والكتابة بـstd::println، ووضع الإلحاق، والإغلاق عند الخروج من الكتلة. ويختم الدرس بتنسيقات السطر: فصل الحقول بالفواصل، والتحقق من صحتها بـstd::from_chars، والبتّ في المواصفة فيما يُصنع بالأسطر سيئة التشكيل.
أهداف التعلم
- فتح ملف للقراءة أو للكتابة والتحقق من أنه فُتح قبل استعماله، مع التمييز بين الملف غير الموجود والملف الذي يُبحث عنه في مجلد آخر.
- قراءة ملف سطرًا سطرًا بـ
while (std::getline(in, line))وتفسير سبب معالجةwhile (!in.eof())معطًى زائدًا، بتتبّع حالة المجرى. - كتابة النتائج في ملف بـ
std::println(out, …)، مع الاختيار بين استبدال محتواه والإلحاق في آخره. - معالجة ملف من القيم المفصولة بفواصل في متجه من السجلات، مع التحقق من صحة كل سطر والإبلاغ عن الأسطر غير الصالحة بأرقامها.
الملف مجرى
الديمومة
تعيش متغيرات البرنامج في الذاكرة، وذاكرة البرنامج تُحرَّر حين ينتهي: يبدأ التشغيل الذي يليه دون أن يتذكر شيئًا. ولكي يبقى معطًى بعد البرنامج الذي أنتجه، يجب أن يستقر في وسط دائم، والوسط العام الاستعمال هو الملف: متتالية من البايتات لها اسم، محفوظة في القرص، يحتفظ بها نظام التشغيل من تشغيل إلى آخر. والملف النصي هو الملف الذي تكون بايتاته محارف، بترميز UTF-8 كما رأينا في الدرس النص ليس ASCII، منظمةً في أسطر يفصل بينها المحرف '\n'. إنه أكثر التنسيقات تواضعًا وأطولها عمرًا: يفتحه أي محرر، وتقرؤه أي لغة، ويستطيع الإنسان مراجعته بنظرة.
غير أن C++ لا تعامل الملف معاملة شيء مختلف عن لوحة المفاتيح. فمنذ الدرس المدخلات والمخرجات والأخطاء، وstd::cin مجرى إدخال: مصدر محارف تُستهلك بالترتيب، مع حالة تسجّل ما إذا فشلت العملية الأخيرة. والملف المفتوح للقراءة مجرى إدخال آخر، من النوع std::ifstream (من input file stream)؛ والملف المفتوح للكتابة مجرى إخراج، من النوع std::ofstream. وكلاهما مصرَّح به في الترويسة <fstream>. ويترتب على ذلك أن كل ما تعلمناه عن std::cin في الدرس قراءة المدخلات بإحكام، أي العامل >> وstd::getline وحالة الخطأ، يصحّ دون تغيير في الملف.
الفتح والتحقق
يُفتح الملف عند إنشاء المجرى، واسم الملف بين قوسين معقوصين: std::ifstream in{"grades.txt"};. وقد يفشل الفتح (الملف غير موجود، أو لا إذن بقراءته، أو الاسم مكتوب خطأً)، والفشل لا يوقف البرنامج ولا يُظهر أي رسالة: إنه يترك المجرى في حالة خطأ. والمجرى الذي في حالة خطأ، إذا استُعمل شرطًا، تكون قيمته false؛ ولذلك فالتحقق هو if (!in)، ويجب إجراؤه قبل أول قراءة. يحاول البرنامج الآتي فتح ملف غير موجود.
#include <fstream>#include <iostream>#include <print>int main(){ std::ifstream in{"grades.txt"}; if (!in) { std::println(std::cerr, "تعذّر فتح grades.txt"); return 1; } int first{}; in >> first; std::println("الدرجة الأولى: {}", first);}تعذّر فتح grades.txtانتهى البرنامج بالرمز 1.
ثلاثة تفاصيل تستحق الانتباه. الأول هو std::cerr (من <iostream>)، أي مجرى الأخطاء: يكتب على الشاشة كما يكتب الإخراج القياسي، لكنه قناة مختلفة، فلا تختلط رسائل الخطأ بالنتائج حين يُعاد توجيه الإخراج إلى ملف؛ وتقبل std::println وسيطًا أول هو المجرى الذي يجب أن تكتب فيه. والثاني هو return 1;: رمز إنهاء غير الصفر، كما رأينا في الدرس تشريح البرنامج، ينبّه من شغّل البرنامج إلى أن شيئًا ما لم يسر على ما يرام. والثالث هو ما كان سيحدث لولا التحقق: in >> first على مجرى في حالة خطأ لا يفعل شيئًا، وكان البرنامج سيكتب «الدرجة الأولى: 0» بكل هدوء. والنتيجة الخاطئة التي تبدو صحيحة أسوأ من رسالة الخطأ؛ ومن هنا قاعدة هذا الدرس: كل ملف يُتحقَّق منه عند فتحه.
المسارات النسبية ومجلد العمل
الاسم "grades.txt" لا يقول في أي مجلد يوجد الملف. ومثل هذا الاسم مسار نسبي، ويُحَلّ انطلاقًا من مجلد العمل: المجلد الذي يُشغَّل منه البرنامج، وهو ليس بالضرورة مجلد الملف التنفيذي ولا مجلد الشيفرة المصدرية. البرنامج الآتي، المحفوظ في المجلد project إلى جانب ملف data.txt، يفتح ذلك الملف ويكتب سطره الأول؛ وقد شُغّل مرتين، أولًا من project ثم من المجلد الأعلى.
$ cd project$ g++ -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -g -fsanitize=address,undefined -fno-sanitize-recover=undefined main.cpp -o main$ ./mainالسطر الأول: مرحبًا من الملف$ cd ..$ ./project/mainتعذّر فتح data.txtالملف التنفيذي هو نفسه، والملف ما زال في مكانه؛ والشيء الوحيد الذي تغيّر هو مجلد العمل، ومعه المكان الذي يُبحث فيه عن data.txt. وهذا أكثر أسباب «لا يجد الملف» شيوعًا. والمحرر الذي يشغّل البرنامج بزر يختار مجلد العمل من تلقاء نفسه (في Visual Studio Code يكون عادةً المجلد المفتوح في المحرر، وإن كان ذلك يتوقف على إعداد المهمة)، ولذلك يحسن التشغيل من الطرفية، داخل مجلد البيانات، إلى أن نعرف ما يفعله المحرر. والإغراء المعاكس، أي كتابة مسار مطلق في الشيفرة مثل "/home/ana/curso/data.txt" أو "C:\\Users\\Ana\\data.txt"، يحل المشكلة في جهاز واحد ويخلقها في جميع الأجهزة الأخرى: يتوقف البرنامج عن العمل بمجرد أن يشغّله شخص آخر أو يُنقل المجلد. والمستحسن هو المسار النسبي مع مجلد عمل معروف، أو، وهو أفضل، اسم الملف معطًى يقدّمه المستخدم.
جارٍ تحميل المحتوى…