C: إدخال/إخراج الملفات
الملف كخزنة — يجب فتحها قبل الاستخدام (fopen)، واتباع قواعد تخزين واسترجاع المحتويات (أوضاع القراءة/الكتابة)، وقفلها دائمًا عند الانتهاء (fclose)، وإلا قد تضيع بياناتك.
1. المفاهيم الأساسية للملفات
تتعامل C مع جميع الأجهزة الخارجية بوصفها "تدفقات". هناك نوعان من تدفقات الملفات:
- تدفق نصي: يتألف من أحمر مرئية، قد تُترجم أحرف السطر الجديد، مناسب للقراءة البشرية
- تدفق ثنائي: تسلسل خام من البايتات، يُقرأ ويُكتب كما هو، مناسب لمعالجة البرامج
يدير نظام التشغيل الملفات المفتوحة عبر بنية FILE، ونتعامل معها عبر مؤشر FILE *.
(1) مؤشر الملف
FILE *fp;
هذا المؤشر هو "المفتاح" لجميع عمليات الملف اللاحقة. بدونه لا يمكن فعل شيء.
2. فتح وإغلاق الملفات
(1) fopen
FILE *fopen(const char *filename, const char *mode);
| الوضع | المعنى | إذا لم يوجد الملف | إذا وُجد الملف |
|---|---|---|---|
"r" |
قراءة فقط | خطأ | القراءة من البداية |
"w" |
كتابة فقط | إنشاء | اقتطاع المحتوى |
"a" |
إلحاق | إنشاء | الإلحاق في النهاية |
"r+" |
قراءة وكتابة | خطأ | العمل من البداية |
"w+" |
قراءة وكتابة | إنشاء | اقتطاع المحتوى |
"a+" |
قراءة وإلحاق | إنشاء | الإلحاق في النهاية |
"rb" |
قراءة ثنائية فقط | خطأ | القراءة من البداية |
"wb" |
كتابة ثنائية فقط | إنشاء | اقتطاع المحتوى |
"ab" |
إلحاق ثنائي | إنشاء | الإلحاق في النهاية |
"w" يقتطع الملفات الموجودة! هذا الخطأ الأكثر شيوعًا بين المبتدئين — استخدام "w" عندما كان القصد الإلحاق، مما يؤدي إلى فقدان جميع البيانات.
(2) fclose
int fclose(FILE *fp);
إغلاق الملف يفرغ المخزن المؤقت ويحرر الموارد. يُرجع 0 عند النجاح، وEOF عند الفشل.
#include <stdio.h>
int main(void) {
FILE *fp = fopen("data.txt", "w");
if (fp == NULL) {
printf("Failed to open file\n");
return 1;
}
fprintf(fp, "Hello, File!\n");
fclose(fp);
return 0;
}
fopen يجب أن يقابله fclose. نسيان إغلاق الملف يسبب تسربًا في الذاكرة وفقدان البيانات.
3. إدخال/إخراج الملفات النصية
(1) إدخال/إخراج الأحرف: fgetc / fputc
int fgetc(FILE *fp);
int fputc(int ch, FILE *fp);
يقرأ fgetc حرفًا واحدًا، ويُرجع EOF عند نهاية الملف. يكتب fputc حرفًا واحدًا.
(2) إدخال/إخراج السلاسل: fgets / fputs
char *fgets(char *str, int n, FILE *fp);
int fputs(const char *str, FILE *fp);
يقرأ fgets على الأكثر n-1 حرفًا، ويتوقف عند سطر جديد أو نهاية الملف، ويضيف '\0' تلقائيًا.
#include <stdio.h>
int main(void) {
FILE *fp = fopen("poem.txt", "w");
if (fp == NULL) {
return 1;
}
fputs("Riverside at sunset\n", fp);
fputs("River flows to the sea\n", fp);
fclose(fp);
fp = fopen("poem.txt", "r");
if (fp == NULL) {
return 1;
}
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line);
}
fclose(fp);
return 0;
}
Riverside at sunset
River flows to the sea
4. إدخال/إخراج الملفات الثنائية
(1) fread / fwrite
size_t fread(void *ptr, size_t size, size_t count, FILE *fp);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *fp);
المعاملات: ptr هو مخزن البيانات المؤقت، وsize هو حجم كل عنصر بالبايت، وcount هو عدد العناصر. القيمة المُرجعة هي العدد الفعلي للعناصر المقروءة أو المكتوبة.
#include <stdio.h>
typedef struct {
int id;
char name[20];
float score;
} Student;
int main(void) {
Student stu = {1, "Alice", 92.5f};
FILE *fp = fopen("student.dat", "wb");
if (fp == NULL) {
return 1;
}
fwrite(&stu, sizeof(Student), 1, fp);
fclose(fp);
Student read_stu;
fp = fopen("student.dat", "rb");
if (fp == NULL) {
return 1;
}
fread(&read_stu, sizeof(Student), 1, fp);
fclose(fp);
printf("ID: %d, Name: %s, Score: %.1f\n",
read_stu.id, read_stu.name, read_stu.score);
return 0;
}
ID: 1, Name: Alice, Score: 92.5
5. الإدخال/الإخراج المنسق
(1) fprintf / fscanf
int fprintf(FILE *fp, const char *format, ...);
int fscanf(FILE *fp, const char *format, ...);
استخدامهما مطابق لـ printf/scanf، مع إضافة معامل مؤشر الملف فقط.
#include <stdio.h>
int main(void) {
FILE *fp = fopen("scores.txt", "w");
if (fp == NULL) {
return 1;
}
fprintf(fp, "%s %d %.1f\n", "Bob", 2, 88.0);
fprintf(fp, "%s %d %.1f\n", "Carol", 3, 95.5);
fclose(fp);
fp = fopen("scores.txt", "r");
if (fp == NULL) {
return 1;
}
char name[20];
int id;
float score;
while (fscanf(fp, "%s %d %f", name, &id, &score) == 3) {
printf("Name: %s, ID: %d, Score: %.1f\n", name, id, score);
}
fclose(fp);
return 0;
}
Name: Bob, ID: 2, Score: 88.0
Name: Carol, ID: 3, Score: 95.5
6. مؤشر موضع الملف
لكل ملف مفتوح "مؤشر موضع" يشير إلى موقع القراءة/الكتابة الحالي. يشبه الإشارة المرجعية أثناء القراءة — أينما وصلت، تحدد الإشارة ذلك الموضع.
(1) fseek
int fseek(FILE *fp, long offset, int origin);
| origin | المعنى |
|---|---|
SEEK_SET |
بداية الملف |
SEEK_CUR |
الموضع الحالي |
SEEK_END |
نهاية الملف |
(2) ftell / rewind
long ftell(FILE *fp);
void rewind(FILE *fp);
يُرجع ftell الموضع الحالي (إزاحة البايت من بداية الملف). rewind يكافئ fseek(fp, 0, SEEK_SET).
▶ مثال
استخدام fseek وftell لحساب حجم الملف:
#include <stdio.h>
int main(void) {
FILE *fp = fopen("data.txt", "rb");
if (fp == NULL) {
printf("Failed to open file\n");
return 1;
}
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
rewind(fp);
printf("File size: %ld bytes\n", size);
fclose(fp);
return 0;
}
▶ مثال
الوصول العشوائي إلى سجل محدد في ملف ثنائي:
#include <stdio.h>
typedef struct {
int id;
char name[20];
float score;
} Student;
int main(void) {
Student students[3] = {
{1, "Alice", 92.5f},
{2, "Bob", 88.0f},
{3, "Carol", 95.5f}
};
FILE *fp = fopen("students.dat", "wb");
if (fp == NULL) {
return 1;
}
fwrite(students, sizeof(Student), 3, fp);
fclose(fp);
fp = fopen("students.dat", "rb");
if (fp == NULL) {
return 1;
}
int target = 2;
fseek(fp, (target - 1) * sizeof(Student), SEEK_SET);
Student s;
fread(&s, sizeof(Student), 1, fp);
fclose(fp);
printf("Record %d: ID=%d, Name=%s, Score=%.1f\n",
target, s.id, s.name, s.score);
return 0;
}
Record 2: ID=2, Name=Bob, Score=88.0
7. دوال الملفات الأخرى
(1) feof
int feof(FILE *fp);
يتحقق مما إذا تم الوصول إلى نهاية الملف. يُرجع قيمة غير صفرية عند EOF. ملاحظة: يصبح feof صحيحًا فقط بعد فشل عملية قراءة، وليس كفحص مسبق.
(2) fflush
int fflush(FILE *fp);
يفرض تفريغ المخزن المؤقت، وكتابة البيانات المخزنة إلى الملف. استدعاء fflush على تدفق إدخال يؤدي إلى سلوك غير معرّف.
(3) remove / rename
int remove(const char *filename);
int rename(const char *old, const char *new);
حذف وإعادة تسمية الملفات. يُرجع 0 عند النجاح.
8. النمط العام لعمليات الملفات
معظم عمليات الملفات تتبع هذا النمط:
- فتح الملف بـ
fopenوالتحقق من القيمة المُرجعة - قراءة أو كتابة البيانات في حلقة
- إغلاق الملف بـ
fclose
#include <stdio.h>
int main(void) {
FILE *fp = fopen("input.txt", "r");
if (fp == NULL) {
perror("fopen");
return 1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
fclose(fp);
return 0;
}
fopen قاعدة مطلقة! قد لا يكون الملف موجودًا، أو قد تفتقر إلى الصلاحيات، أو قد يكون القرص ممتلئًا — استخدام مؤشر فارغ دون تحقق سيؤدي إلى انهيار فوري.
❓ أسئلة شائعة
\n و\r\n أثناء القراءة/الكتابة، بينما لا يقوم الوضع الثنائي بأي ترجمة. في لينكس، لا فرق بينهما.📖 ملخص
- افتح الملفات بـ
fopen، وتحقق دائمًا مما إذا كانت القيمة المُرجعةNULL - استخدم
fgets/fputs، وfprintf/fscanfللوضع النصي، وfread/fwriteللوضع الثنائي - الوضع
"w"يقتطع الملف، والوضع"a"يُلحق في النهاية — لا تخلط بينهما fseek/ftell/rewindتتحكم في مؤشر موضع الملف للوصول العشوائي- كل
fopenيجب أن يقابلهfclose— اجعلها عادة
📝 تمارين
- اكتب برنامجًا يحسب عدد الأسطر والكلمات والأحرف في ملف نصي
- نفّذ برنامج نسخ ملفات بسيط يقرأ ملف مصدر بالوضع الثنائي ويكتب إلى ملف وجهة
- اكتب برنامجًا يحفظ مصفوفة من البنى في ملف ثنائي، ثم يقرأها ويعرضها