كل ما يحسبه برنامج يختفي حين يخرج، إلّا إن كتبه. وتعيش واجهة الملفات في C داخل <stdio.h> - الترويسة نفسها التي فيها printf - وهي متشابهة عمدًا: الملف تيّار من البايتات، وللدوال التي تعمل على الشاشة توائم تأخذ وسيط تيّار.
ونوع المقبض هو FILE *. ولا تنظر أبدًا داخل FILE؛ فهو نوع معتم (انظر typedef) ولا تحمل منه سوى مؤشّر.
الخطوات الثلاث
تتبع كل عملية على ملف الشكل نفسه: افتح، استخدم، أغلق.
وفحص NULL ليس وسوسة اختيارية. فـ fopen تفشل كلما كان الملف غير موجود (في وضع القراءة)، أو كان الدليل غير قابل للكتابة، أو كان المسار خاطئًا، أو نفدت مقابض الملفات من العملية - وكل واحدة من هذه تُعيد NULL. واستخدام مقبض NULL هو خطأ تجزئة.
وإرسال الخطأ إلى stderr بدل stdout هو العُرف: فهو غير مخزَّن مؤقّتًا، ويمكن إعادة توجيهه بمعزل عن الخرج الحقيقي.
أوضاع fopen
الوسيط الثاني سلسلة وضع قصيرة. والخطأ فيه أشدّ الأخطاء تدميرًا في هذا المجال كله، لأن "w" تُفرِغ ملفًا موجودًا بصمت.
| الوضع | القراءة | الكتابة | إن كان الملف موجودًا | إن لم يكن |
|---|---|---|---|---|
"r" | نعم | لا | يُفتح عند البداية | يفشل، ويُعيد NULL |
"w" | لا | نعم | يُقتطع إلى فارغ | يُنشأ |
"a" | لا | نعم | يكتب في النهاية فقط | يُنشأ |
"r+" | نعم | نعم | يُفتح عند البداية | يفشل، ويُعيد NULL |
"w+" | نعم | نعم | يُقتطع إلى فارغ | يُنشأ |
"a+" | نعم | نعم | يقرأ من أي مكان، ويكتب في النهاية | يُنشأ |
أضف b إلى أي منها ("rb" و"wb" و"ab+") للوضع الثنائي - والمزيد عنه في النهاية.
وقاعدتان تمنعان فقدان بيانات حقيقيًا:
- استخدم
"r"حين تقصد القراءة. فـ"r+"على اسم ملف فيه خطأ مطبعي تفشل بأمان؛ و"w+"تُنشئ ملفًا فارغًا فلا تخسر سوى الوقت. أما"w"على الاسم الصحيح حين كنت تقصد"r"فتدمّر البيانات. - استخدم
"a"للسجلّات. فكلfprintfتهبط في النهاية بصرف النظر عن موضع التيّار، وهو بالضبط ما يريده السجلّ.
الكتابة والقراءة في برنامج واحد
المحرّر أدناه يجري رحلة ذهاب وإياب كاملة - ينشئ ملفًا، ويكتب فيه سجلّات، ويغلقه، ويعيد فتحه للقراءة، ويطبع ما يجده.
fprintf وfscanf هما printf وscanf مع تيّار كوسيط أول؛ وكل ما يتعلّق بـ محدّدات التنسيق فيهما متطابق، بما في ذلك العرض %31s الذي يمنع name من الفيضان.
وشرط الحلقة هو == 2، أي عدد العناصر التي يطلبها التنسيق. والفحص مقابل EOF بدلًا من ذلك علّة كلاسيكية: فسطر مشوّه يجعل fscanf تُعيد 0 لا EOF، فتدور الحلقة إلى الأبد على إدخال لا تستطيع استهلاكه.
القراءة سطرًا سطرًا بـ fgets
fscanf مريحة للبيانات صارمة التنسيق. أما لملفات النصّ - الإعدادات والسجلّات وCSV وأي شيء كتبه إنسان - فاقرأ أسطرًا كاملة. وهذه الحلقة هي النموذجية:
ما يجعل fgets الخيار الافتراضي الصحيح:
- تأخذ حجم المخزن، فلا يمكن أن تفيض. مرّر
sizeof lineفيبقى الاستدعاء صحيحًا إن غيّرت حجم المصفوفة. - تُعيد
NULLعند نهاية الملف أو عند الخطأ، وهو شرط حلقة نظيف. - تحتفظ بالسطر الجديد حين يتّسع السطر في المخزن. وذلك مفيد - فغياب
'\n'ممّا حصلت عليه يعني أن السطر كان أطول من مخزنك وأن الباقي ما زال ينتظر. وstrcspn(line, "\n")تجد فهرس السطر الجديد (أو طول السلسلة إن لم يوجد)، فإسناد'\0'هناك يقصّه في الحالتين.
وللتمييز بين نهاية ملف حقيقية وخطأ، اسأل بعد الحلقة:
if (ferror(in)) {
fprintf(stderr, "read error\n");
} else if (feof(in)) {
/* نهاية طبيعية */
}
ولا تكتب while (!feof(fp)) كشرط حلقة. فـ feof لا تصبح محقّقة إلّا بعد أن تكون قراءة قد فشلت سلفًا، فتعالج تلك الحلقة محتويات المخزن الأخيرة مرة إضافية. افحص القيمة المُعادة من دالة القراءة بدلًا من ذلك - كما تفعل الحلقتان أعلاه.
محرفًا محرفًا: fgetc وfputc
للعمل على مستوى البايت - عدّ المحارف، تحويل ملف، النسخ - تعالج fgetc وfputc محرفًا واحدًا لكل استدعاء.
وثمّة تفصيلة مهمّة: c معرّفة بنوع int لا char. فـ fgetc تُعيد int كي تستطيع إعادة كل قيمة بايت ممكنة و الحارس المميّز EOF (وهو -1). وتخزينها في char يجعل المقارنة مع EOF إما خاطئة دائمًا أو صحيحة زورًا للبايت 0xFF، تبعًا لكون char موقّعًا على منصّتك. وهذا من أقدم فخاخ C.
فحص الأخطاء على نحو سليم
القراءة في بيئة إنتاج تبدو هكذا:
يحمل errno رمزًا يصف آخر فشل، وتحوّله strerror إلى جملة؛ وتطبع perror رسالتك مع تلك الجملة في استدعاء واحد. وكلتاهما تحتاجان <errno.h> و<string.h> على الترتيب.
ويمكن أن تفشل fclose أيضًا - فهي تُفرِغ البيانات المخزَّنة مؤقّتًا، وقد لا تتّسع الكتابة على القرص - لذا افحصها في أي أمر مهمّ:
if (fclose(fp) != 0) {
fprintf(stderr, "failed to flush and close\n");
}
الوضع الثنائي في فقرة واحدة
قد يترجم الوضع النصّي نهايات الأسطر (فعلى ويندوز تصير \n هي \r\n عند الكتابة والعكس عند القراءة) وقد يعامل بايتات معيّنة معاملة خاصة. وللبيانات التي ليست نصًا - صورة، أو بنية مفرَّغة حرفيًا، أو كتلة مضغوطة - افتح بـ b واستخدم fread/fwrite، فهما ينقلان بايتات خامًا:
تكتب fwrite(ptr, size, count, fp) عدد count عنصرًا بحجم size بايت وتُعيد كم استطاعت؛ وfread تقابلها. وانتبه إلى أن ملفًا مكتوبًا بهذه الطريقة مرتبط بالجهاز الذي كتبه - فحشو البنى وحجم الأعداد الصحيحة وترتيب البايتات كلها تتسرّب إلى البايتات - فهو مناسب لذاكرة مؤقّتة أو ملف خردة وخاطئ لصيغة يجب أن تقرأها برامج أخرى.
أخطاء شائعة
- عدم فحص
fopenبحثًا عنNULL. فالانهيار التالي يُلقى باللوم فيه على القراءة، لا على الفتح. - الفتح بـ
"w"حين كنت تقصد"r". فيُفرَّغ الملف قبل أن تلاحظ. - نسيان
fclose. فيضيع الخرج المخزَّن مؤقّتًا، ويسرّب كل ملف غير مغلق مقبضًا. while (!feof(fp)). تعالج السطر الأخير مرتين. افحص استدعاء القراءة بدلًا من ذلك.char c = fgetc(fp). تكسر المقارنة معEOF. استخدمint.fscanf("%s", buf)بلا عرض. هو فيضان المخزن نفسه الذي في scanf على لوحة المفاتيح.- المسارات النسبية.
fopen("data.txt", "r")تبحث في دليل العمل، لا بجوار الملف التنفيذي. وإن "اختفى" ملف، فذلك هو السبب عادةً.
الأسئلة الشائعة
كيف تفتح ملفًا في لغة C؟
FILE *fp = fopen("data.txt", "r"); يفتح ملفًا للقراءة ويُعيد مقبض FILE *، أو NULL إن فشل. افحص دائمًا NULL قبل استخدام المقبض، واستدعِ fclose(fp) حين تنتهي.
ما أوضاع fopen في لغة C؟
"r" للقراءة (يجب أن يوجد الملف)، و"w" للكتابة (يُنشئ، أو يقتطع ملفًا موجودًا إلى فارغ)، و"a" للإضافة (يُنشئ، ويكتب في النهاية). وإضافة + تجعل كلًّا منها للقراءة والكتابة: "r+" و"w+" و"a+". وإضافة b ("rb" و"wb") تفتح بالوضع الثنائي.
كيف تقرأ ملفًا سطرًا سطرًا في لغة C؟
استخدم fgets في حلقة while: while (fgets(line, sizeof line, fp) != NULL) { ... }. فهي تتوقّف عند كل سطر جديد أو عند امتلاء المخزن، وتُعيد NULL عند نهاية الملف، ولا يمكن أن تفيض لأنك تمرّر حجم المخزن.
لماذا يكون ملفي فارغًا بعد الكتابة إليه في لغة C؟
في الغالب لأنك نسيت fclose. فالخرج مخزَّن مؤقّتًا، وقد تكون البيانات ما زالت في الذاكرة حين ينتهي البرنامج على نحو غير طبيعي. وfclose تُفرِغ وتُغلق؛ وfflush(fp) تُفرِغ دون إغلاق. والسبب الآخر هو الفتح بـ "w" مرة ثانية، فيقتطع الملف الذي كتبته للتوّ.