C: إدخال/إخراج الملفات

الملف كخزنة — يجب فتحها قبل الاستخدام (fopen)، واتباع قواعد تخزين واسترجاع المحتويات (أوضاع القراءة/الكتابة)، وقفلها دائمًا عند الانتهاء (fclose)، وإلا قد تضيع بياناتك.

1. المفاهيم الأساسية للملفات

تتعامل C مع جميع الأجهزة الخارجية بوصفها "تدفقات". هناك نوعان من تدفقات الملفات:

يدير نظام التشغيل الملفات المفتوحة عبر بنية FILE، ونتعامل معها عبر مؤشر FILE *.

(1) مؤشر الملف

TEXT 📖 للعرض فقط
FILE *fp;

هذا المؤشر هو "المفتاح" لجميع عمليات الملف اللاحقة. بدونه لا يمكن فعل شيء.


2. فتح وإغلاق الملفات

(1) fopen

C
FILE *fopen(const char *filename, const char *mode);
الوضع المعنى إذا لم يوجد الملف إذا وُجد الملف
"r" قراءة فقط خطأ القراءة من البداية
"w" كتابة فقط إنشاء اقتطاع المحتوى
"a" إلحاق إنشاء الإلحاق في النهاية
"r+" قراءة وكتابة خطأ العمل من البداية
"w+" قراءة وكتابة إنشاء اقتطاع المحتوى
"a+" قراءة وإلحاق إنشاء الإلحاق في النهاية
"rb" قراءة ثنائية فقط خطأ القراءة من البداية
"wb" كتابة ثنائية فقط إنشاء اقتطاع المحتوى
"ab" إلحاق ثنائي إنشاء الإلحاق في النهاية
💡 نصيحة: الوضع "w" يقتطع الملفات الموجودة! هذا الخطأ الأكثر شيوعًا بين المبتدئين — استخدام "w" عندما كان القصد الإلحاق، مما يؤدي إلى فقدان جميع البيانات.

(2) fclose

TEXT 📖 للعرض فقط
int fclose(FILE *fp);

إغلاق الملف يفرغ المخزن المؤقت ويحرر الموارد. يُرجع 0 عند النجاح، وEOF عند الفشل.

C
#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

TEXT 📖 للعرض فقط
int fgetc(FILE *fp);
int fputc(int ch, FILE *fp);

يقرأ fgetc حرفًا واحدًا، ويُرجع EOF عند نهاية الملف. يكتب fputc حرفًا واحدًا.

(2) إدخال/إخراج السلاسل: fgets / fputs

C
char *fgets(char *str, int n, FILE *fp);
int fputs(const char *str, FILE *fp);

يقرأ fgets على الأكثر n-1 حرفًا، ويتوقف عند سطر جديد أو نهاية الملف، ويضيف '\0' تلقائيًا.

C
#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;
}
TEXT 📖 للعرض فقط
Riverside at sunset
River flows to the sea

4. إدخال/إخراج الملفات الثنائية

(1) fread / fwrite

C
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 هو عدد العناصر. القيمة المُرجعة هي العدد الفعلي للعناصر المقروءة أو المكتوبة.

C
#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;
}
TEXT 📖 للعرض فقط
ID: 1, Name: Alice, Score: 92.5
💡 نصيحة: الوضع الثنائي مثالي لتخزين بيانات البنى — فهو فعّال ويحفظ المحتوى كما هو تمامًا. قد يتعامل الوضع النصي مع أحرف السطر الجديد بشكل مختلف بين أنظمة التشغيل، بينما يحفظ الوضع الثنائي كل شيء كما هو.


5. الإدخال/الإخراج المنسق

(1) fprintf / fscanf

C
int fprintf(FILE *fp, const char *format, ...);
int fscanf(FILE *fp, const char *format, ...);

استخدامهما مطابق لـ printf/scanf، مع إضافة معامل مؤشر الملف فقط.

C
#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;
}
TEXT 📖 للعرض فقط
Name: Bob, ID: 2, Score: 88.0
Name: Carol, ID: 3, Score: 95.5

6. مؤشر موضع الملف

لكل ملف مفتوح "مؤشر موضع" يشير إلى موقع القراءة/الكتابة الحالي. يشبه الإشارة المرجعية أثناء القراءة — أينما وصلت، تحدد الإشارة ذلك الموضع.

(1) fseek

C
int fseek(FILE *fp, long offset, int origin);
origin المعنى
SEEK_SET بداية الملف
SEEK_CUR الموضع الحالي
SEEK_END نهاية الملف

(2) ftell / rewind

TEXT 📖 للعرض فقط
long ftell(FILE *fp);
void rewind(FILE *fp);

يُرجع ftell الموضع الحالي (إزاحة البايت من بداية الملف). rewind يكافئ fseek(fp, 0, SEEK_SET).

▶ مثال

استخدام fseek وftell لحساب حجم الملف:

C
#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;
}
▶ جرّب الكود

▶ مثال

الوصول العشوائي إلى سجل محدد في ملف ثنائي:

C
#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;
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Record 2: ID=2, Name=Bob, Score=88.0
💡 نصيحة: هذا هو مبدأ "الوصول العشوائي" في قواعد البيانات! بحساب الإزاحة، يمكنك القفز مباشرة إلى موضع محدد دون العبور من البداية.


7. دوال الملفات الأخرى

(1) feof

C
int feof(FILE *fp);

يتحقق مما إذا تم الوصول إلى نهاية الملف. يُرجع قيمة غير صفرية عند EOF. ملاحظة: يصبح feof صحيحًا فقط بعد فشل عملية قراءة، وليس كفحص مسبق.

(2) fflush

TEXT 📖 للعرض فقط
int fflush(FILE *fp);

يفرض تفريغ المخزن المؤقت، وكتابة البيانات المخزنة إلى الملف. استدعاء fflush على تدفق إدخال يؤدي إلى سلوك غير معرّف.

(3) remove / rename

C
int remove(const char *filename);
int rename(const char *old, const char *new);

حذف وإعادة تسمية الملفات. يُرجع 0 عند النجاح.


8. النمط العام لعمليات الملفات

معظم عمليات الملفات تتبع هذا النمط:

  1. فتح الملف بـ fopen والتحقق من القيمة المُرجعة
  2. قراءة أو كتابة البيانات في حلقة
  3. إغلاق الملف بـ fclose
C
#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 قاعدة مطلقة! قد لا يكون الملف موجودًا، أو قد تفتقر إلى الصلاحيات، أو قد يكون القرص ممتلئًا — استخدام مؤشر فارغ دون تحقق سيؤدي إلى انهيار فوري.


❓ أسئلة شائعة

س fopen يُرجع NULL لكن الملف موجود بوضوح. ما المشكلة؟
ج على الأرجح مشكلة مسار. المسارات النسبية مبنية على دليل عمل البرنامج، لا دليل الملف المصدر. عند التشغيل في بيئة تطوير، قد يختلف دليل العمل عما تتوقعه.
س ما الفرق بين الوضع النصي والوضع الثنائي؟
ج في ويندوز، يترجم الوضع النصي بين \n و\r\n أثناء القراءة/الكتابة، بينما لا يقوم الوضع الثنائي بأي ترجمة. في لينكس، لا فرق بينهما.
س هل الملفات المكتوبة بـ fwrite متوافقة بين مترجمات مختلفة؟
ج ليس بالضرورة. قد تختلف محاذاة الذاكرة، وترتيب البايتات (endianness)، وأحجام الأنواع الأساسية. الكود عبر المنصات يتطلب تسلسلًا يدويًا.
س لماذا لا يمكن استخدام feof كشرط حلقة؟
ج يصبح feof صحيحًا فقط بعد تجاوز عملية قراءة لنهاية الملف. استخدامه كشرط حلقة يجعل التكرار الأخير يعالج بيانات غير صالحة. استخدم القيمة المُرجعة من fread/fgets بدلاً من ذلك.

📖 ملخص

📝 تمارين

  1. اكتب برنامجًا يحسب عدد الأسطر والكلمات والأحرف في ملف نصي
  2. نفّذ برنامج نسخ ملفات بسيط يقرأ ملف مصدر بالوضع الثنائي ويكتب إلى ملف وجهة
  3. اكتب برنامجًا يحفظ مصفوفة من البنى في ملف ثنائي، ثم يقرأها ويعرضها
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%