توثيق لغة البرمجة C

دليل شامل للغة البرمجة C من المبتدئين وحتى المستوى المتقدم مع شرح تفصيلي للأوامر والمفاهيم

1. مقدمة عن لغة C

لغة C هي لغة برمجة عامة الغرض طوّرها دينيس ريتشي في مختبرات بيل بين عامي 1969-1973 لاستخدامها في نظام التشغيل يونكس. تعتبر من أكثر لغات البرمجة استخداماً وتأثيراً في تاريخ الحوسبة، حيث تشكل الأساس للغات الأخرى مثل C++ وC# وJava وPython.

خصائص لغة C:

  • لغة منخفضة المستوى نسبياً مع ميزات لغات المستوى العالي
  • كفاءة عالية في الأداء وإمكانية الوصول المباشر إلى ذاكرة النظام
  • قابلية النقل عبر الأنظمة المختلفة
  • بنية بسيطة نسبياً مع عدد قليل من الكلمات المحجوزة
  • تفاعل مباشر مع نظام التشغيل

تستخدم لغة C على نطاق واسع في تطوير أنظمة التشغيل، المترجمات، قواعد البيانات، برامج التحكم بالأجهزة، وتطبيقات الأداء العالي. تعلم لغة C يوفر أساساً متيناً لفهم كيفية عمل الحواسيب والبرمجيات بشكل عام.

المعيار القياسي للغة C:

تطورت لغة C عبر الزمن من خلال عدة معايير قياسية:

  • K&R C - الإصدار الأصلي المعرف في كتاب كيرنيغان وريتشي
  • ANSI C (C89/C90) - أول معيار رسمي للغة
  • C99 - أضاف العديد من الميزات الجديدة مثل التعليقات بأسلوب // والمتغيرات داخل الدوال
  • C11 - تحسينات في الأمان والتزامن
  • C17/C18 - آخر المعايير مع تصحيحات وتحسينات

2. إعداد بيئة العمل

قبل البدء بكتابة برامج بلغة C، تحتاج إلى إعداد بيئة العمل المناسبة. يتطلب ذلك تثبيت مترجم لغة C (compiler) ومحرر نصوص أو بيئة تطوير متكاملة (IDE).

المترجمات (Compilers)

لنظام ويندوز:

  • MinGW (Minimalist GNU for Windows)
  • Microsoft Visual C++ Compiler
  • Cygwin

لنظام لينكس:

  • GCC (GNU Compiler Collection)
  • Clang

لنظام ماك:

  • Xcode Command Line Tools (يشمل Clang)
  • GCC (عبر Homebrew)

بيئات التطوير (IDEs)

  • Visual Studio Code: محرر خفيف مع إضافات للغة C. مناسب لجميع الأنظمة.
  • Code::Blocks: بيئة مفتوحة المصدر سهلة الاستخدام.
  • Visual Studio: للمشاريع الكبيرة على نظام ويندوز.
  • CLion: بيئة متكاملة احترافية من JetBrains.
  • Dev-C++: بيئة بسيطة وسهلة للمبتدئين.

كتابة أول برنامج بلغة C

hello_world.c
#include <stdio.h>

int main() {
    // هذا تعليق - أول برنامج في لغة C
    printf("مرحبا بالعالم!\n");  // طباعة على الشاشة
    
    return 0;  // إرجاع قيمة 0 للدلالة على نجاح البرنامج
}

شرح كل سطر:

  1. #include <stdio.h> - استدعاء مكتبة الإدخال والإخراج القياسية للتعامل مع العمليات الأساسية.
  2. int main() - تعريف الدالة الرئيسية في البرنامج التي يبدأ منها التنفيذ. int تشير إلى أن الدالة ترجع قيمة صحيحة.
  3. // هذا تعليق - تعليق لا يؤثر على تنفيذ البرنامج.
  4. printf("مرحبا بالعالم!\n"); - طباعة النص على الشاشة. الرمز \n يمثل سطر جديد.
  5. return 0; - إرجاع قيمة 0 لنظام التشغيل للإشارة إلى نجاح البرنامج.

خطوات تجميع وتشغيل البرنامج:

$ gcc hello_world.c -o hello_world

$ ./hello_world

مرحبا بالعالم!

3. أساسيات لغة C

3.1 أنواع البيانات

لغة C توفر عدة أنواع أساسية من البيانات التي يمكن استخدامها لتخزين القيم المختلفة. فهم هذه الأنواع مهم لكتابة برامج فعالة واستخدام الذاكرة بشكل أمثل.

نوع البيانات الحجم النطاق الوصف
char 1 بايت -128 إلى 127 يستخدم لتخزين الأحرف أو الأعداد الصغيرة
unsigned char 1 بايت 0 إلى 255 نفس النوع السابق لكن بدون إشارة
int 4 بايت (عادة) -2,147,483,648 إلى 2,147,483,647 للأعداد الصحيحة
unsigned int 4 بايت (عادة) 0 إلى 4,294,967,295 أعداد صحيحة بدون إشارة
short 2 بايت -32,768 إلى 32,767 أعداد صحيحة أصغر
long 4 أو 8 بايت -2,147,483,648 إلى 2,147,483,647 أو أكثر أعداد صحيحة كبيرة
float 4 بايت ±3.4e±38 (تقريباً) أعداد عشرية بدقة 6 أرقام
double 8 بايت ±1.7e±308 (تقريباً) أعداد عشرية بدقة 15 رقم
void - - يستخدم لتمثيل عدم وجود قيمة

ملاحظات مهمة:

  • حجم أنواع البيانات قد يختلف حسب النظام والمترجم.
  • يمكن استخدام sizeof() لمعرفة حجم نوع البيانات بالبايت.
  • عند استخدام unsigned تزداد قيمة الحد الأعلى لكن لا يمكن تخزين قيم سالبة.

مثال على أنواع البيانات:

#include <stdio.h>

int main() {
    // تعريف متغيرات من أنواع مختلفة
    char character = 'A';
    int integer = 123;
    float decimal = 3.14;
    double precise = 3.141592653589793;
    
    // طباعة قيم المتغيرات
    printf("الحرف: %c\n", character);
    printf("العدد الصحيح: %d\n", integer);
    printf("العدد العشري: %f\n", decimal);
    printf("العدد العشري الدقيق: %lf\n", precise);
    
    // طباعة حجم كل نوع
    printf("حجم char: %lu بايت\n", sizeof(character));
    printf("حجم int: %lu بايت\n", sizeof(integer));
    printf("حجم float: %lu بايت\n", sizeof(decimal));
    printf("حجم double: %lu بايت\n", sizeof(precise));
    
    return 0;
}

3.2 المتغيرات

المتغيرات هي أسماء معطاة لمواقع في ذاكرة الحاسوب لتخزين القيم التي يمكن تغييرها أثناء تنفيذ البرنامج.

قواعد تسمية المتغيرات:

  • يمكن أن تتكون من الحروف (a-z, A-Z)، الأرقام (0-9)، والشرطة السفلية (_).
  • يجب أن تبدأ بحرف أو شرطة سفلية، وليس برقم.
  • الأسماء حساسة لحالة الأحرف (case-sensitive)، فالمتغير name يختلف عن Name.
  • لا يمكن استخدام الكلمات المحجوزة في اللغة كأسماء متغيرات (مثل int, float, return).

تعريف وتهيئة المتغيرات:

// تعريف متغير بدون تهيئة
int number;

// تعريف متغير مع تهيئة
int count = 10;

// تعريف عدة متغيرات من نفس النوع
float x, y, z;

// تعريف وتهيئة عدة متغيرات
char a = 'A', b = 'B', c = 'C';

الثوابت (Constants):

الثوابت هي قيم لا يمكن تغييرها بعد تعريفها. هناك عدة طرق لتعريف الثوابت في C:

// باستخدام #define (بدون إشارة مساواة أو فاصلة منقوطة)
#define PI 3.14159

// باستخدام const (مع إشارة مساواة وفاصلة منقوطة)
const float GRAVITY = 9.8;

int main() {
    printf("قيمة باي: %f\n", PI);
    printf("الجاذبية: %f\n", GRAVITY);
    
    // الكود التالي سيسبب خطأ في الترجمة
    // GRAVITY = 10.5; // لا يمكن تغيير قيمة الثابت
    
    return 0;
}

مدى حياة المتغيرات (Variable Scope):

يحدد مدى حياة المتغير أين يمكن استخدامه في البرنامج:

  • المتغيرات المحلية (Local Variables): معرفة داخل دالة أو كتلة، ويمكن استخدامها فقط داخل تلك الدالة أو الكتلة.
  • المتغيرات العامة (Global Variables): معرفة خارج جميع الدوال، ويمكن استخدامها في أي مكان في البرنامج.
  • المتغيرات الثابتة (Static Variables): تحتفظ بقيمتها بين استدعاءات الدوال المختلفة.

3.3 العوامل الحسابية والمنطقية

تستخدم العوامل (Operators) لإجراء عمليات على المتغيرات والقيم. تقدم لغة C مجموعة واسعة من العوامل.

العوامل الحسابية:

العامل الوصف مثال
+ الجمع a + b
- الطرح a - b
* الضرب a * b
/ القسمة a / b
% باقي القسمة a % b
++ زيادة بمقدار 1 a++ أو ++a
-- نقصان بمقدار 1 a-- أو --a

عوامل المقارنة:

العامل الوصف مثال
== يساوي a == b
!= لا يساوي a != b
> أكبر من a > b
< أصغر من a < b
>= أكبر من أو يساوي a >= b
<= أصغر من أو يساوي a <= b

العوامل المنطقية:

العامل الوصف مثال
&& و المنطقية (AND) a > 0 && b > 0
|| أو المنطقية (OR) a > 0 || b > 0
! النفي (NOT) !a

عوامل التعيين:

العامل الوصف مثال ما يعادله
= تعيين قيمة a = b a = b
+= تعيين بعد الجمع a += b a = a + b
-= تعيين بعد الطرح a -= b a = a - b
*= تعيين بعد الضرب a *= b a = a * b
/= تعيين بعد القسمة a /= b a = a / b
%= تعيين بعد باقي القسمة a %= b a = a % b

مثال على استخدام العوامل:

#include <stdio.h>

int main() {
    int a = 10, b = 3;
    
    // العوامل الحسابية
    printf("a + b = %d\n", a + b);
    printf("a - b = %d\n", a - b);
    printf("a * b = %d\n", a * b);
    printf("a / b = %d\n", a / b);  // النتيجة: 3 (قسمة صحيحة)
    printf("a %% b = %d\n", a % b);  // النتيجة: 1 (باقي القسمة)
    
    // عوامل المقارنة
    printf("a == b: %d\n", a == b);  // النتيجة: 0 (false)
    printf("a != b: %d\n", a != b);  // النتيجة: 1 (true)
    printf("a > b: %d\n", a > b);    // النتيجة: 1 (true)
    
    // العوامل المنطقية
    int x = 5, y = 0;
    printf("x > 0 && y > 0: %d\n", x > 0 && y > 0);  // النتيجة: 0 (false)
    printf("x > 0 || y > 0: %d\n", x > 0 || y > 0);  // النتيجة: 1 (true)
    printf("!y: %d\n", !y);  // النتيجة: 1 (true لأن y = 0)
    
    // عوامل التعيين
    int c = 20;
    c += 5;  // c = c + 5
    printf("c بعد c += 5: %d\n", c);  // النتيجة: 25
    
    return 0;
}

3.4 الإدخال والإخراج

عمليات الإدخال والإخراج أساسية في أي لغة برمجة. في C، تتم هذه العمليات من خلال دوال مكتبة stdio.h.

دوال الإخراج (Output Functions):

1. دالة printf():

هي الدالة الأساسية للطباعة على الشاشة وتسمح بتنسيق البيانات المختلفة.

printf("محدد التنسيق", البيانات);

محددات التنسيق الشائعة:

المحدد النوع مثال
%d, %i عدد صحيح (int) printf("العدد: %d", 10);
%f عدد عشري (float) printf("العدد: %f", 3.14);
%c حرف (char) printf("الحرف: %c", 'A');
%s سلسلة نصية (string) printf("النص: %s", "مرحبا");
%% علامة النسبة المئوية (%) printf("النسبة: 10%%");
2. دالة puts():

تستخدم لطباعة سلسلة نصية متبوعة بسطر جديد.

puts("مرحبا بالعالم");  // تطبع "مرحبا بالعالم" مع إضافة سطر جديد
3. دالة putchar():

تستخدم لطباعة حرف واحد.

putchar('A');  // تطبع الحرف 'A'

دوال الإدخال (Input Functions):

1. دالة scanf():

هي الدالة الأساسية لقراءة مدخلات المستخدم وتخزينها في متغيرات.

scanf("محدد التنسيق", &المتغير);

⚠️ يجب استخدام علامة & قبل اسم المتغير في scanf() (باستثناء المصفوفات) للإشارة إلى عنوان المتغير في الذاكرة.

#include <stdio.h>

int main() {
    int age;
    float height;
    char name[50];  // مصفوفة حروف لتخزين الاسم
    
    // قراءة عدد صحيح
    printf("أدخل عمرك: ");
    scanf("%d", &age);
    
    // قراءة عدد عشري
    printf("أدخل طولك بالمتر: ");
    scanf("%f", &height);
    
    // مسح المدخل السابق (حل مشكلة المؤقت)
    getchar();
    
    // قراءة سلسلة نصية
    printf("أدخل اسمك: ");
    scanf("%s", name);  // لاحظ أننا لا نستخدم & مع المصفوفات
    
    // عرض البيانات المدخلة
    printf("\nمرحبا %s، عمرك %d سنة وطولك %.2f متر.\n", name, age, height);
    
    return 0;
}
2. دالة getchar():

تستخدم لقراءة حرف واحد من المدخلات.

char ch;
ch = getchar();  // قراءة حرف واحد
3. دالة gets():

تستخدم لقراءة سطر كامل من النص.

⚠️ دالة gets() تعتبر غير آمنة لأنها لا تتحقق من حجم المصفوفة، مما قد يؤدي إلى تجاوز الحدود. يفضل استخدام fgets() بدلاً منها.

char str[100];
printf("أدخل نصاً: ");
fgets(str, 100, stdin);  // قراءة سطر بأمان (باستخدام fgets بدلا من gets)
printf("النص المدخل: %s", str);

4. بنى التحكم

بنى التحكم تسمح للبرنامج باتخاذ قرارات وتنفيذ عمليات متكررة بناءً على شروط معينة. تنقسم بنى التحكم إلى ثلاث فئات رئيسية: بنى الشرط، بنى التكرار (الحلقات)، وبنى التحويل.

4.1 جمل الشرط

1. جملة if البسيطة:

تنفذ كتلة من الكود فقط إذا كان الشرط صحيحاً.

if (شرط) {
    // الكود الذي سيتم تنفيذه إذا كان الشرط صحيحاً
}
#include <stdio.h>

int main() {
    int num = 10;
    
    if (num > 0) {
        printf("العدد موجب.\n");
    }
    
    return 0;
}

2. جملة if-else:

تنفذ كتلة من الكود إذا كان الشرط صحيحاً، وكتلة أخرى إذا كان خاطئاً.

if (شرط) {
    // الكود الذي سيتم تنفيذه إذا كان الشرط صحيحاً
} else {
    // الكود الذي سيتم تنفيذه إذا كان الشرط خاطئاً
}
#include <stdio.h>

int main() {
    int num = -5;
    
    if (num >= 0) {
        printf("العدد موجب أو صفر.\n");
    } else {
        printf("العدد سالب.\n");
    }
    
    return 0;
}

3. جملة if-else if-else:

تسمح باختبار شروط متعددة بالتتابع.

if (شرط1) {
    // الكود الذي سيتم تنفيذه إذا كان الشرط1 صحيحاً
} else if (شرط2) {
    // الكود الذي سيتم تنفيذه إذا كان الشرط1 خاطئاً والشرط2 صحيحاً
} else {
    // الكود الذي سيتم تنفيذه إذا كانت جميع الشروط السابقة خاطئة
}
#include <stdio.h>

int main() {
    int score = 85;
    
    if (score >= 90) {
        printf("تقدير: ممتاز\n");
    } else if (score >= 80) {
        printf("تقدير: جيد جداً\n");
    } else if (score >= 70) {
        printf("تقدير: جيد\n");
    } else if (score >= 60) {
        printf("تقدير: مقبول\n");
    } else {
        printf("تقدير: راسب\n");
    }
    
    return 0;
}

4. العبارة الشرطية (Ternary Operator):

اختصار لجملة if-else البسيطة.

النتيجة = (شرط) ? قيمة_إذا_صحيح : قيمة_إذا_خاطئ;
#include <stdio.h>

int main() {
    int a = 10, b = 20;
    int max;
    
    // استخدام العبارة الشرطية
    max = (a > b) ? a : b;
    
    printf("الرقم الأكبر هو: %d\n", max);
    
    return 0;
}

4.2 الحلقات التكرارية

1. حلقة for:

تستخدم عندما نعرف مسبقاً عدد مرات التكرار.

for (التهيئة; الشرط; التحديث) {
    // الكود الذي سيتم تكراره
}
#include <stdio.h>

int main() {
    int i;
    
    // طباعة الأرقام من 1 إلى 5
    for (i = 1; i <= 5; i++) {
        printf("%d ", i);
    }
    printf("\n");
    
    return 0;
}

2. حلقة while:

تستخدم عندما لا نعرف مسبقاً عدد مرات التكرار، وتستمر طالما الشرط صحيح.

while (شرط) {
    // الكود الذي سيتم تكراره
}
#include <stdio.h>

int main() {
    int num = 1;
    
    // طباعة الأرقام من 1 إلى 5
    while (num <= 5) {
        printf("%d ", num);
        num++;
    }
    printf("\n");
    
    return 0;
}

3. حلقة do-while:

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

do {
    // الكود الذي سيتم تكراره
} while (شرط);
#include <stdio.h>

int main() {
    int num = 1;
    
    // طباعة الأرقام من 1 إلى 5
    do {
        printf("%d ", num);
        num++;
    } while (num <= 5);
    printf("\n");
    
    return 0;
}

4. عبارات التحكم في الحلقات:

break:

تُستخدم للخروج من الحلقة.

#include <stdio.h>

int main() {
    int i;
    
    for (i = 1; i <= 10; i++) {
        if (i == 6) {
            break;  // الخروج من الحلقة عند i = 6
        }
        printf("%d ", i);
    }
    printf("\n");  // النتيجة: 1 2 3 4 5
    
    return 0;
}
continue:

تُستخدم لتخطي التكرار الحالي والانتقال إلى التكرار التالي.

#include <stdio.h>

int main() {
    int i;
    
    for (i = 1; i <= 10; i++) {
        if (i % 2 == 0) {
            continue;  // تخطي الأرقام الزوجية
        }
        printf("%d ", i);
    }
    printf("\n");  // النتيجة: 1 3 5 7 9
    
    return 0;
}

4.3 جمل التبديل

تُستخدم جملة switch كبديل لسلسلة طويلة من جمل if-else if عندما نريد مقارنة قيمة متغير مع قيم ثابتة متعددة.

switch (التعبير) {
    case قيمة1:
        // الكود الذي سيتم تنفيذه إذا كان التعبير = قيمة1
        break;
    case قيمة2:
        // الكود الذي سيتم تنفيذه إذا كان التعبير = قيمة2
        break;
    // يمكن إضافة المزيد من الحالات
    default:
        // الكود الذي سيتم تنفيذه إذا لم تتطابق أي من الحالات السابقة
}
#include <stdio.h>

int main() {
    char grade = 'B';
    
    switch (grade) {
        case 'A':
            printf("ممتاز!\n");
            break;
        case 'B':
            printf("جيد جداً!\n");
            break;
        case 'C':
            printf("جيد!\n");
            break;
        case 'D':
            printf("مقبول!\n");
            break;
        case 'F':
            printf("راسب!\n");
            break;
        default:
            printf("تقدير غير صالح!\n");
    }
    
    return 0;
}

⚠️ لاحظ أهمية استخدام عبارة break في نهاية كل حالة. بدونها، سيستمر التنفيذ في الحالات التالية حتى يصل إلى عبارة break أو نهاية جملة switch.

5. الدوال

الدوال هي مجموعة من التعليمات البرمجية التي تؤدي مهمة محددة. تساعد الدوال على تقسيم البرنامج إلى أجزاء منطقية وإعادة استخدام الكود.

5.1 تعريف الدوال

الشكل العام لتعريف الدالة في C:

نوع_الإرجاع اسم_الدالة(قائمة_المعاملات) {
    // جسم الدالة
    return قيمة_الإرجاع;  // إذا كان نوع الإرجاع ليس void
}
  • نوع_الإرجاع: نوع البيانات للقيمة التي سترجعها الدالة (int, float, void, إلخ).
  • اسم_الدالة: معرف فريد للدالة.
  • قائمة_المعاملات: المتغيرات التي تستقبلها الدالة (يمكن أن تكون فارغة).
  • جسم الدالة: مجموعة التعليمات التي تنفذها الدالة.
  • قيمة_الإرجاع: القيمة التي ترجعها الدالة، ويجب أن تكون من نفس النوع المحدد في نوع_الإرجاع.

نماذج الدوال (Function Prototypes):

يمكن الإعلان عن دالة قبل تعريفها باستخدام نموذج الدالة، مما يسمح باستدعاء الدالة قبل تعريفها في الكود.

// نموذج الدالة
نوع_الإرجاع اسم_الدالة(قائمة_أنواع_المعاملات);
#include <stdio.h>

// نماذج الدوال
int add(int, int);
void printResult(int);

int main() {
    int result = add(5, 3);
    printResult(result);
    
    return 0;
}

// تعريف الدوال
int add(int a, int b) {
    return a + b;
}

void printResult(int result) {
    printf("النتيجة: %d\n", result);
}

5.2 المعاملات والمخرجات

1. تمرير المعاملات بالقيمة (Pass by Value):

في لغة C، يتم تمرير المعاملات بالقيمة افتراضياً، مما يعني أن الدالة تعمل على نسخة من المتغير وليس المتغير الأصلي.

#include <stdio.h>

void modify(int num) {
    num = num * 2;  // تعديل النسخة المحلية من num
    printf("داخل الدالة: %d\n", num);
}

int main() {
    int x = 10;
    
    printf("قبل استدعاء الدالة: %d\n", x);
    modify(x);
    printf("بعد استدعاء الدالة: %d\n", x);  // قيمة x لم تتغير
    
    return 0;
}

2. تمرير المعاملات بالمرجع (Pass by Reference):

يمكن تمرير عنوان المتغير (باستخدام المؤشرات) للسماح للدالة بتعديل المتغير الأصلي.

#include <stdio.h>

void modifyRef(int *num) {
    *num = *num * 2;  // تعديل القيمة الأصلية عبر المؤشر
    printf("داخل الدالة: %d\n", *num);
}

int main() {
    int x = 10;
    
    printf("قبل استدعاء الدالة: %d\n", x);
    modifyRef(&x);  // تمرير عنوان x
    printf("بعد استدعاء الدالة: %d\n", x);  // الآن x = 20
    
    return 0;
}

3. المعاملات الافتراضية:

لغة C لا تدعم المعاملات الافتراضية مباشرة كما في اللغات الأخرى، لكن يمكن محاكاة هذا السلوك باستخدام تقنيات مختلفة.

#include <stdio.h>

// تعريف قيمة افتراضية باستخدام ماكرو
#define DEFAULT_MULTIPLIER 2

int multiply(int num, int multiplier) {
    return num * multiplier;
}

// دالة أخرى تستدعي الأولى مع قيمة افتراضية
int multiplyDefault(int num) {
    return multiply(num, DEFAULT_MULTIPLIER);
}

int main() {
    printf("5 * 3 = %d\n", multiply(5, 3));
    printf("5 * DEFAULT = %d\n", multiplyDefault(5));
    
    return 0;
}

4. تمرير المصفوفات كمعاملات:

عند تمرير مصفوفة إلى دالة، يتم تمرير المؤشر إلى العنصر الأول في المصفوفة.

#include <stdio.h>

// طريقة 1: مصفوفة بدون تحديد الحجم
void printArray(int arr[], int size) {
    int i;
    for (i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

// طريقة 2: باستخدام المؤشر
void printArrayPtr(int *arr, int size) {
    int i;
    for (i = 0; i < size; i++) {
        printf("%d ", *(arr + i));  // يمكن أيضاً استخدام arr[i]
    }
    printf("\n");
}

int main() {
    int numbers[] = {10, 20, 30, 40, 50};
    int size = sizeof(numbers) / sizeof(numbers[0]);
    
    printArray(numbers, size);
    printArrayPtr(numbers, size);
    
    return 0;
}

5.3 الدوال التكرارية

الدالة التكرارية (Recursive Function) هي دالة تستدعي نفسها. هذا النمط مفيد لحل المشكلات التي يمكن تقسيمها إلى مشكلات فرعية مشابهة.

#include <stdio.h>

// دالة تكرارية لحساب العدد التراكمي (Factorial)
int factorial(int n) {
    // الحالة القاعدية (Base Case)
    if (n == 0 || n == 1) {
        return 1;
    }
    // الحالة التكرارية (Recursive Case)
    else {
        return n * factorial(n - 1);
    }
}

int main() {
    int num = 5;
    printf("%d! = %d\n", num, factorial(num));  // 5! = 120
    
    return 0;
}

مثال آخر: دالة تكرارية لحساب سلسلة فيبوناتشي.

#include <stdio.h>

// دالة تكرارية لحساب فيبوناتشي
int fibonacci(int n) {
    if (n <= 1) {
        return n;
    } else {
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

int main() {
    int i;
    printf("سلسلة فيبوناتشي للعشر أرقام الأولى: ");
    for (i = 0; i < 10; i++) {
        printf("%d ", fibonacci(i));
    }
    printf("\n");  // النتيجة: 0 1 1 2 3 5 8 13 21 34
    
    return 0;
}

⚠️ يجب الحذر عند استخدام الدوال التكرارية، لأنها قد تؤدي إلى استنزاف ذاكرة المكدس (Stack Overflow) إذا كان عمق التكرار كبيراً جداً. تأكد دائماً من وجود حالة قاعدية تنهي التكرار.

6. المصفوفات

المصفوفة هي مجموعة من العناصر من نفس النوع مخزنة في مواقع متتالية في الذاكرة. تسمح المصفوفات بتخزين ومعالجة مجموعات من البيانات تحت اسم متغير واحد.

6.1 المصفوفات أحادية البعد

تعريف وتهيئة المصفوفات:

// تعريف مصفوفة بدون تهيئة
int numbers[5];  // مصفوفة من 5 أعداد صحيحة

// تعريف وتهيئة مصفوفة
int scores[5] = {85, 92, 78, 90, 88};

// تعريف وتهيئة مصفوفة بدون تحديد الحجم (يحدد الحجم تلقائياً)
char vowels[] = {'a', 'e', 'i', 'o', 'u'};

// تهيئة جزئية (العناصر المتبقية تُهيأ بالقيمة 0)
float values[5] = {1.5, 2.5};

الوصول إلى عناصر المصفوفة:

#include <stdio.h>

int main() {
    int numbers[5] = {10, 20, 30, 40, 50};
    
    // الوصول إلى عنصر واحد
    printf("العنصر الثالث: %d\n", numbers[2]);  // الفهارس تبدأ من 0
    
    // تعديل قيمة عنصر
    numbers[3] = 45;
    printf("العنصر الرابع بعد التعديل: %d\n", numbers[3]);
    
    // طباعة جميع العناصر باستخدام حلقة
    int i;
    printf("جميع العناصر: ");
    for (i = 0; i < 5; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");
    
    return 0;
}

حجم المصفوفة:

#include <stdio.h>

int main() {
    int numbers[] = {10, 20, 30, 40, 50, 60};
    
    // حساب عدد العناصر في المصفوفة
    int size = sizeof(numbers) / sizeof(numbers[0]);
    
    printf("عدد العناصر في المصفوفة: %d\n", size);
    
    return 0;
}

6.2 المصفوفات متعددة الأبعاد

المصفوفات متعددة الأبعاد هي مصفوفات تحتوي على مصفوفات أخرى. الأكثر شيوعاً هي المصفوفات ثنائية الأبعاد التي يمكن تصورها كجداول.

تعريف وتهيئة المصفوفات ثنائية الأبعاد:

// تعريف مصفوفة ثنائية الأبعاد
int matrix[3][4];  // مصفوفة 3×4

// تعريف وتهيئة مصفوفة ثنائية الأبعاد
int grid[2][3] = {
    {1, 2, 3},  // الصف الأول
    {4, 5, 6}   // الصف الثاني
};

// طريقة أخرى للتهيئة
int another[2][3] = {1, 2, 3, 4, 5, 6};

الوصول إلى عناصر المصفوفة ثنائية الأبعاد:

#include <stdio.h>

int main() {
    int matrix[3][4] = {
        {10, 20, 30, 40},
        {50, 60, 70, 80},
        {90, 100, 110, 120}
    };
    
    // الوصول إلى عنصر واحد
    printf("العنصر في الصف 1، العمود 2: %d\n", matrix[1][2]);  // 70
    
    // تعديل قيمة عنصر
    matrix[2][3] = 125;
    printf("العنصر بعد التعديل: %d\n", matrix[2][3]);
    
    // طباعة جميع عناصر المصفوفة باستخدام حلقات متداخلة
    int i, j;
    printf("المصفوفة كاملة:\n");
    for (i = 0; i < 3; i++) {
        for (j = 0; j < 4; j++) {
            printf("%4d", matrix[i][j]);
        }
        printf("\n");
    }
    
    return 0;
}

المصفوفات ثلاثية الأبعاد وأكثر:

// مصفوفة ثلاثية الأبعاد (2×3×4)
int cube[2][3][4];

// الوصول إلى عنصر
cube[1][2][3] = 100;

// تهيئة مصفوفة ثلاثية الأبعاد
int cube[2][2][2] = {
    {{1, 2}, {3, 4}},    // الطبقة الأولى
    {{5, 6}, {7, 8}}     // الطبقة الثانية
};

6.3 السلاسل النصية

في لغة C، السلاسل النصية هي مصفوفات من نوع char تنتهي بمحرف نهاية النص '\0' (NULL terminator).

تعريف وتهيئة السلاسل النصية:

// طريقة 1: باستخدام مصفوفة حروف
char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};

// طريقة 2: باستخدام النص المحاط بعلامات اقتباس
char str2[] = "Hello";  // المترجم يضيف '\0' تلقائياً

// طريقة 3: تحديد الحجم (يجب أن يكون كافياً للنص + محرف النهاية)
char str3[10] = "Hello";  // المساحة المتبقية تُملأ بـ '\0'

// مؤشر إلى سلسلة نصية ثابتة
char *str4 = "Hello";

⚠️ عند استخدام char *str = "Hello"، فإن السلسلة النصية تكون ثابتة (read-only)، ولا ينبغي محاولة تعديلها. لسلاسل نصية قابلة للتعديل، استخدم مصفوفات الحروف.

دوال التعامل مع السلاسل النصية:

توفر مكتبة string.h العديد من الدوال للتعامل مع السلاسل النصية:

#include <stdio.h>
#include <string.h>

int main() {
    char str1[20] = "Hello";
    char str2[] = "World";
    char str3[20];
    int len;
    
    // حساب طول السلسلة
    len = strlen(str1);
    printf("طول السلسلة str1: %d\n", len);
    
    // نسخ سلسلة
    strcpy(str3, str1);
    printf("str3 بعد النسخ: %s\n", str3);
    
    // دمج سلسلتين
    strcat(str1, str2);
    printf("str1 بعد الدمج: %s\n", str1);
    
    // مقارنة سلسلتين
    if (strcmp(str1, str3) == 0) {
        printf("السلسلتان متطابقتان\n");
    } else {
        printf("السلسلتان غير متطابقتين\n");
    }
    
    // البحث عن حرف في سلسلة
    char *ptr = strchr(str1, 'o');
    if (ptr) {
        printf("تم العثور على 'o' في الموضع: %ld\n", ptr - str1);
    }
    
    return 0;
}

بعض الدوال الشائعة الأخرى للتعامل مع السلاسل النصية:

  • strncpy(): نسخ عدد محدد من الأحرف.
  • strncat(): دمج عدد محدد من الأحرف.
  • strstr(): البحث عن سلسلة فرعية.
  • strtok(): تقسيم السلسلة إلى أجزاء.

7. المؤشرات

المؤشرات هي من أهم وأقوى المفاهيم في لغة C. المؤشر هو متغير يخزن عنوان متغير آخر في الذاكرة. يمكن استخدام المؤشرات للوصول إلى المتغيرات والمصفوفات بطريقة ديناميكية وفعالة.

7.1 أساسيات المؤشرات

تعريف وتهيئة المؤشرات:

// تعريف مؤشر (بدون تهيئة)
int *ptr;

// تعريف متغير ومؤشر يشير إليه
int num = 10;
int *ptr = #  // & هو عامل "عنوان" يرجع عنوان المتغير

// يمكن أيضاً تعريف المؤشر أولاً ثم إسناد العنوان لاحقاً
int *ptr;
ptr = #

الوصول إلى قيمة المتغير من خلال المؤشر:

#include <stdio.h>

int main() {
    int num = 10;
    int *ptr = #
    
    printf("قيمة num: %d\n", num);
    printf("عنوان num: %p\n", &num);
    printf("قيمة ptr (أي عنوان num): %p\n", ptr);
    printf("القيمة التي يشير إليها ptr: %d\n", *ptr);  // * هو عامل "محتوى العنوان"
    
    // تغيير قيمة المتغير من خلال المؤشر
    *ptr = 20;
    printf("قيمة num بعد التعديل: %d\n", num);
    
    return 0;
}

المؤشر الفارغ (NULL Pointer):

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = NULL;  // تهيئة مؤشر بالقيمة NULL
    
    printf("قيمة المؤشر الفارغ: %p\n", ptr);
    
    // تحقق دائماً من أن المؤشر ليس فارغاً قبل استخدامه
    if (ptr != NULL) {
        printf("القيمة التي يشير إليها ptr: %d\n", *ptr);
    } else {
        printf("المؤشر فارغ ولا يشير إلى أي عنوان صالح.\n");
    }
    
    return 0;
}

⚠️ محاولة الوصول إلى محتوى مؤشر فارغ (NULL) ستؤدي إلى خطأ في التنفيذ (segmentation fault). لذا من الممارسات الجيدة دائماً التحقق من أن المؤشر ليس NULL قبل محاولة الوصول إلى محتواه.

7.2 المؤشرات والمصفوفات

هناك علاقة وثيقة بين المؤشرات والمصفوفات في لغة C. اسم المصفوفة هو في الواقع مؤشر إلى أول عنصر فيها.

#include <stdio.h>

int main() {
    int numbers[5] = {10, 20, 30, 40, 50};
    int *ptr = numbers;  // لا تحتاج لعامل &
    
    printf("عنوان المصفوفة (numbers): %p\n", numbers);
    printf("عنوان أول عنصر (&numbers[0]): %p\n", &numbers[0]);
    printf("قيمة المؤشر (ptr): %p\n\n", ptr);
    
    // الوصول إلى عناصر المصفوفة باستخدام المؤشر
    printf("العناصر باستخدام الفهارس المصفوفة: %d %d %d\n", numbers[0], numbers[1], numbers[2]);
    printf("العناصر باستخدام الفهارس المؤشر: %d %d %d\n", ptr[0], ptr[1], ptr[2]);
    printf("العناصر باستخدام عمليات المؤشر: %d %d %d\n\n", *ptr, *(ptr + 1), *(ptr + 2));
    
    // تغيير قيم المصفوفة باستخدام المؤشر
    *ptr = 15;         // تغيير العنصر الأول
    *(ptr + 2) = 35;   // تغيير العنصر الثالث
    
    printf("المصفوفة بعد التعديل: ");
    int i;
    for (i = 0; i < 5; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");
    
    return 0;
}

حساب المؤشرات (Pointer Arithmetic):

#include <stdio.h>

int main() {
    int numbers[5] = {10, 20, 30, 40, 50};
    int *ptr = numbers;
    
    printf("ptr    يشير إلى العنصر: %d\n", *ptr);
    printf("ptr + 1 يشير إلى العنصر: %d\n", *(ptr + 1));
    printf("ptr + 2 يشير إلى العنصر: %d\n", *(ptr + 2));
    
    // تحريك المؤشر للأمام
    ptr++;
    printf("بعد ptr++، ptr يشير إلى العنصر: %d\n", *ptr);
    
    ptr += 2;
    printf("بعد ptr += 2، ptr يشير إلى العنصر: %d\n", *ptr);
    
    // تحريك المؤشر للخلف
    ptr--;
    printf("بعد ptr--، ptr يشير إلى العنصر: %d\n", *ptr);
    
    return 0;
}

ℹ️ عند إجراء عمليات حسابية على المؤشرات، يتم تعديل العنوان بناءً على حجم نوع البيانات. فمثلاً، إذا كان المؤشر من نوع int وافترضنا أن حجم int هو 4 بايت، فإن ptr + 1 سيزيد العنوان بمقدار 4 بايت، وليس بايت واحد.

7.3 إدارة الذاكرة الديناميكية

تتيح لغة C للمبرمج إدارة الذاكرة بشكل ديناميكي من خلال مجموعة من الدوال الموجودة في مكتبة stdlib.h.

الدوال الرئيسية لإدارة الذاكرة:

  • malloc(): تخصيص كتلة ذاكرة بحجم محدد.
  • calloc(): تخصيص مجموعة من العناصر وتهيئتها بالقيمة صفر.
  • realloc(): تغيير حجم كتلة ذاكرة تم تخصيصها سابقاً.
  • free(): تحرير كتلة ذاكرة تم تخصيصها ديناميكياً.

استخدام malloc() و free():

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    int n, i;
    
    printf("أدخل عدد العناصر: ");
    scanf("%d", &n);
    
    // تخصيص ذاكرة ديناميكياً
    ptr = (int*) malloc(n * sizeof(int));
    
    // التحقق من نجاح تخصيص الذاكرة
    if (ptr == NULL) {
        printf("خطأ: فشل في تخصيص الذاكرة.\n");
        return 1;
    }
    
    // إدخال العناصر
    printf("أدخل %d عدد:\n", n);
    for (i = 0; i < n; i++) {
        scanf("%d", &ptr[i]);
    }
    
    // عرض العناصر
    printf("العناصر المدخلة: ");
    for (i = 0; i < n; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");
    
    // تحرير الذاكرة عند الانتهاء
    free(ptr);
    
    return 0;
}

استخدام calloc() و realloc():

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    int n, i, new_size;
    
    printf("أدخل عدد العناصر: ");
    scanf("%d", &n);
    
    // تخصيص ذاكرة وتهيئتها بالقيمة صفر
    ptr = (int*) calloc(n, sizeof(int));
    
    if (ptr == NULL) {
        printf("خطأ: فشل في تخصيص الذاكرة.\n");
        return 1;
    }
    
    // عرض العناصر (جميعها صفر بسبب calloc)
    printf("العناصر بعد calloc: ");
    for (i = 0; i < n; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");
    
    // إدخال عناصر جديدة
    printf("أدخل %d عدد:\n", n);
    for (i = 0; i < n; i++) {
        scanf("%d", &ptr[i]);
    }
    
    // تغيير حجم المصفوفة
    printf("أدخل الحجم الجديد: ");
    scanf("%d", &new_size);
    
    ptr = (int*) realloc(ptr, new_size * sizeof(int));
    
    if (ptr == NULL) {
        printf("خطأ: فشل في إعادة تخصيص الذاكرة.\n");
        return 1;
    }
    
    // إذا كان الحجم الجديد أكبر، أدخل العناصر الإضافية
    if (new_size > n) {
        printf("أدخل %d عدد إضافي:\n", new_size - n);
        for (i = n; i < new_size; i++) {
            scanf("%d", &ptr[i]);
        }
    }
    
    // عرض العناصر بعد التعديل
    printf("العناصر بعد realloc: ");
    for (i = 0; i < new_size; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");
    
    // تحرير الذاكرة
    free(ptr);
    
    return 0;
}

⚠️ الأخطاء الشائعة في إدارة الذاكرة:

  • تسرب الذاكرة (Memory Leak): عدم تحرير الذاكرة باستخدام free() بعد الانتهاء منها.
  • تكرار التحرير (Double Free): محاولة تحرير نفس كتلة الذاكرة أكثر من مرة.
  • استخدام الذاكرة بعد تحريرها (Use After Free): محاولة الوصول إلى الذاكرة بعد تحريرها.
  • تجاوز الحدود (Buffer Overflow): الكتابة أو القراءة خارج حدود الذاكرة المخصصة.

8. الهياكل والاتحادات

الهياكل (Structures) والاتحادات (Unions) هي أنواع بيانات مركبة في لغة C تسمح بتجميع عناصر من أنواع مختلفة تحت اسم واحد.

8.1 الهياكل (Structures)

الهيكل هو مجموعة من عناصر من أنواع مختلفة يمكن الوصول إليها باستخدام اسم واحد. يمكن تعريف هيكل كنوع بيانات جديد في البرنامج.

تعريف الهياكل:

// تعريف هيكل
struct اسم_الهيكل {
    نوع_البيانات1 اسم_العنصر1;
    نوع_البيانات2 اسم_العنصر2;
    // ...
    نوع_البياناتN اسم_العنصرN;
};

إنشاء متغيرات من نوع الهيكل:

#include <stdio.h>
#include <string.h>

// تعريف هيكل للطالب
struct Student {
    char name[50];
    int roll_number;
    float marks;
};

int main() {
    // إنشاء متغير من نوع Student
    struct Student s1;
    
    // تعيين قيم للعناصر
    strcpy(s1.name, "أحمد محمد");
    s1.roll_number = 101;
    s1.marks = 85.5;
    
    // عرض بيانات الطالب
    printf("معلومات الطالب:\n");
    printf("الاسم: %s\n", s1.name);
    printf("الرقم: %d\n", s1.roll_number);
    printf("الدرجات: %.1f\n", s1.marks);
    
    return 0;
}

تهيئة الهياكل:

// طريقة 1: تهيئة عند التعريف
struct Student s1 = {"أحمد محمد", 101, 85.5};

// طريقة 2: تهيئة عنصر بعنصر
struct Student s2;
strcpy(s2.name, "محمد علي");
s2.roll_number = 102;
s2.marks = 90.0;

// طريقة 3: تهيئة باستخدام designated initializers (C99)
struct Student s3 = {
    .marks = 75.0,
    .name = "سارة أحمد",
    .roll_number = 103
};

المؤشرات للهياكل:

#include <stdio.h>
#include <string.h>

struct Student {
    char name[50];
    int roll_number;
    float marks;
};

int main() {
    struct Student s1;
    struct Student *ptr = &s1;
    
    // تعيين قيم باستخدام المؤشر
    strcpy(ptr->name, "أحمد محمد");  // استخدام عامل -> للوصول للعناصر
    ptr->roll_number = 101;
    ptr->marks = 85.5;
    
    // عرض بيانات الطالب باستخدام المؤشر
    printf("معلومات الطالب:\n");
    printf("الاسم: %s\n", ptr->name);
    printf("الرقم: %d\n", ptr->roll_number);
    printf("الدرجات: %.1f\n", ptr->marks);
    
    return 0;
}

مصفوفات الهياكل:

#include <stdio.h>
#include <string.h>

struct Student {
    char name[50];
    int roll_number;
    float marks;
};

int main() {
    // مصفوفة من 3 طلاب
    struct Student students[3];
    int i;
    
    // إدخال بيانات الطلاب
    for (i = 0; i < 3; i++) {
        printf("أدخل بيانات الطالب %d:\n", i+1);
        
        printf("الاسم: ");
        scanf("%s", students[i].name);
        
        printf("الرقم: ");
        scanf("%d", &students[i].roll_number);
        
        printf("الدرجات: ");
        scanf("%f", &students[i].marks);
    }
    
    // عرض بيانات جميع الطلاب
    printf("\nقائمة الطلاب:\n");
    for (i = 0; i < 3; i++) {
        printf("الطالب %d: %s، الرقم: %d، الدرجات: %.1f\n", 
               i+1, students[i].name, students[i].roll_number, students[i].marks);
    }
    
    return 0;
}

8.2 الاتحادات (Unions)

الاتحاد يشبه الهيكل، لكن جميع أعضائه يشتركون في نفس مساحة الذاكرة. حجم الاتحاد يساوي حجم أكبر عضو فيه.

#include <stdio.h>

// تعريف اتحاد
union Data {
    int i;
    float f;
    char str[20];
};

int main() {
    union Data data;
    
    // عند تعيين قيمة لعضو في الاتحاد، تتغير قيمة الأعضاء الأخرى
    data.i = 10;
    printf("data.i: %d\n", data.i);
    printf("data.f: %f\n", data.f);  // قيمة غير متوقعة
    
    data.f = 220.5;
    printf("data.i: %d\n", data.i);  // قيمة غير متوقعة
    printf("data.f: %f\n", data.f);
    
    // حجم الاتحاد = حجم أكبر عضو
    printf("حجم الاتحاد: %zu بايت\n", sizeof(data));
    
    return 0;
}

ℹ️ تُستخدم الاتحادات لتوفير الذاكرة عندما نحتاج إلى تخزين قيم مختلفة الأنواع، لكن ليس في نفس الوقت. مثلاً، في تطبيق رسائل النظام، قد تحتوي الرسالة على أنواع مختلفة من البيانات حسب نوع الرسالة.

8.3 الحقول البتية (Bit Fields)

الحقول البتية هي ميزة خاصة في هياكل لغة C تسمح بتحديد عدد البتات المطلوب لكل عضو، مما يوفر الذاكرة.

#include <stdio.h>

struct PackedData {
    unsigned int flag1 : 1;  // حقل من بت واحد
    unsigned int flag2 : 1;
    unsigned int flag3 : 1;
    unsigned int type : 4;   // حقل من 4 بت
    unsigned int value : 8;  // حقل من 8 بت
};

int main() {
    struct PackedData data;
    
    data.flag1 = 1;
    data.flag2 = 0;
    data.flag3 = 1;
    data.type = 7;     // قيمة بين 0 و 15 (2^4 - 1)
    data.value = 127;  // قيمة بين 0 و 255 (2^8 - 1)
    
    printf("الأعلام: %u %u %u\n", data.flag1, data.flag2, data.flag3);
    printf("النوع: %u\n", data.type);
    printf("القيمة: %u\n", data.value);
    
    printf("حجم الهيكل: %zu بايت\n", sizeof(data));
    
    return 0;
}

9. التعامل مع الملفات

توفر لغة C مجموعة من الدوال للتعامل مع الملفات من خلال مكتبة stdio.h. تسمح هذه الدوال بإنشاء وقراءة وكتابة وتعديل الملفات.

9.1 فتح وإغلاق الملفات

فتح ملف:

FILE *fopen(const char *filename, const char *mode);

أوضاع فتح الملف:

الوضع الوصف
"r" فتح للقراءة فقط. يجب أن يكون الملف موجوداً.
"w" فتح للكتابة فقط. إذا كان الملف موجوداً، يتم محو محتواه. إذا لم يكن موجوداً، يتم إنشاؤه.
"a" فتح للإضافة. إذا كان الملف موجوداً، تتم الكتابة في نهايته. إذا لم يكن موجوداً، يتم إنشاؤه.
"r+" فتح للقراءة والكتابة. يجب أن يكون الملف موجوداً.
"w+" فتح للقراءة والكتابة. إذا كان الملف موجوداً، يتم محو محتواه. إذا لم يكن موجوداً، يتم إنشاؤه.
"a+" فتح للقراءة والإضافة. إذا كان الملف موجوداً، تتم القراءة من البداية والكتابة في النهاية. إذا لم يكن موجوداً، يتم إنشاؤه.

إغلاق ملف:

int fclose(FILE *stream);
#include <stdio.h>

int main() {
    FILE *file;
    
    // فتح ملف للكتابة
    file = fopen("test.txt", "w");
    
    // التحقق من نجاح فتح الملف
    if (file == NULL) {
        printf("خطأ في فتح الملف!\n");
        return 1;
    }
    
    printf("تم فتح الملف بنجاح.\n");
    
    // إغلاق الملف
    fclose(file);
    printf("تم إغلاق الملف.\n");
    
    return 0;
}

9.2 الكتابة إلى ملفات

توجد عدة دوال للكتابة إلى الملفات:

1. fprintf():

تعمل مثل printf() لكنها تكتب إلى ملف بدل الشاشة.

#include <stdio.h>

int main() {
    FILE *file;
    int num = 123;
    float pi = 3.14;
    char name[] = "أحمد";
    
    file = fopen("data.txt", "w");
    if (file == NULL) {
        printf("خطأ في فتح الملف!\n");
        return 1;
    }
    
    // كتابة نص منسق إلى الملف
    fprintf(file, "الرقم: %d\n", num);
    fprintf(file, "باي: %.2f\n", pi);
    fprintf(file, "الاسم: %s\n", name);
    
    fclose(file);
    printf("تمت الكتابة إلى الملف بنجاح.\n");
    
    return 0;
}

2. fputs():

لكتابة سلسلة نصية إلى ملف.

int fputs(const char *str, FILE *stream);

3. fputc():

لكتابة حرف واحد إلى ملف.

int fputc(int char, FILE *stream);

4. fwrite():

لكتابة كتل من البيانات إلى ملف.

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
#include <stdio.h>

struct Student {
    char name[50];
    int roll;
    float marks;
};

int main() {
    FILE *file;
    struct Student student = {"أحمد محمد", 101, 85.5};
    
    file = fopen("student.dat", "wb");  // فتح ملف ثنائي
    if (file == NULL) {
        printf("خطأ في فتح الملف!\n");
        return 1;
    }
    
    // كتابة هيكل الطالب إلى الملف
    fwrite(&student, sizeof(struct Student), 1, file);
    
    fclose(file);
    printf("تمت كتابة بيانات الطالب إلى الملف الثنائي.\n");
    
    return 0;
}

9.3 القراءة من ملفات

توجد عدة دوال للقراءة من الملفات:

1. fscanf():

تعمل مثل scanf() لكنها تقرأ من ملف بدل لوحة المفاتيح.

#include <stdio.h>

int main() {
    FILE *file;
    int num;
    float pi;
    char name[50];
    
    file = fopen("data.txt", "r");
    if (file == NULL) {
        printf("خطأ في فتح الملف!\n");
        return 1;
    }
    
    // قراءة البيانات من الملف
    fscanf(file, "الرقم: %d\n", &num);
    fscanf(file, "باي: %f\n", &pi);
    fscanf(file, "الاسم: %s\n", name);
    
    fclose(file);
    
    // عرض البيانات المقروءة
    printf("الرقم: %d\n", num);
    printf("باي: %.2f\n", pi);
    printf("الاسم: %s\n", name);
    
    return 0;
}

2. fgets():

لقراءة سطر من الملف.

char *fgets(char *str, int n, FILE *stream);

3. fgetc():

لقراءة حرف واحد من الملف.

int fgetc(FILE *stream);

4. fread():

لقراءة كتل من البيانات من ملف.

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
#include <stdio.h>

struct Student {
    char name[50];
    int roll;
    float marks;
};

int main() {
    FILE *file;
    struct Student student;
    
    file = fopen("student.dat", "rb");  // فتح الملف الثنائي للقراءة
    if (file == NULL) {
        printf("خطأ في فتح الملف!\n");
        return 1;
    }
    
    // قراءة بيانات الطالب من الملف
    fread(&student, sizeof(struct Student), 1, file);
    
    fclose(file);
    
    // عرض بيانات الطالب
    printf("الاسم: %s\n", student.name);
    printf("الرقم: %d\n", student.roll);
    printf("الدرجات: %.1f\n", student.marks);
    
    return 0;
}

9.4 التنقل في الملفات

يمكن التنقل إلى موضع معين في الملف باستخدام دوال مثل fseek() و rewind() و ftell().

#include <stdio.h>

int main() {
    FILE *file;
    char buffer[100];
    long position;
    
    file = fopen("example.txt", "w+");  // فتح للقراءة والكتابة
    if (file == NULL) {
        printf("خطأ في فتح الملف!\n");
        return 1;
    }
    
    // كتابة بعض البيانات
    fputs("السطر الأول\n", file);
    fputs("السطر الثاني\n", file);
    fputs("السطر الثالث\n", file);
    
    // الحصول على الموضع الحالي
    position = ftell(file);
    printf("الموضع الحالي: %ld بايت\n", position);
    
    // العودة إلى بداية الملف
    rewind(file);
    printf("العودة إلى بداية الملف.\n");
    
    // قراءة السطر الأول
    fgets(buffer, 100, file);
    printf("قراءة: %s", buffer);
    
    // الانتقال إلى موضع معين (بداية السطر الثالث)
    fseek(file, 24, SEEK_SET);  // القيمة 24 قد تختلف حسب طول السطور
    printf("الانتقال إلى موضع معين.\n");
    
    // قراءة من الموضع الجديد
    fgets(buffer, 100, file);
    printf("قراءة: %s", buffer);
    
    fclose(file);
    
    return 0;
}

10. موجهات المعالج القبلي

المعالج القبلي (Preprocessor) هو برنامج يعالج الكود المصدري قبل ترجمته. يتم تنفيذ موجهات المعالج القبلي (التي تبدأ بعلامة #) في مرحلة ما قبل الترجمة.

10.1 استدعاء الملفات (#include)

يستخدم للتضمين محتويات ملف في الكود المصدري:

// تضمين ملف من المكتبة القياسية
#include <stdio.h>

// تضمين ملف محلي
#include "myheader.h"

10.2 تعريف الثوابت الرمزية (#define)

يستخدم لتعريف الثوابت الرمزية والماكروهات:

// تعريف ثابت رمزي
#define PI 3.14159
#define MAX_SIZE 100

// تعريف ماكرو بدون معاملات
#define NEWLINE printf("\n");

// تعريف ماكرو بمعاملات
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main() {
    printf("قيمة PI: %f\n", PI);
    printf("مربع 5: %d\n", SQUARE(5));
    printf("القيمة العظمى من 10 و 20: %d\n", MAX(10, 20));
    NEWLINE;
    
    return 0;
}

⚠️ يجب الحذر عند تعريف الماكروهات وإحاطة المعاملات بأقواس لتجنب أخطاء الأولوية في العمليات الحسابية.

10.3 التحكم الشرطي في التضمين

يسمح بتضمين أو استبعاد أجزاء من الكود بناءً على شروط معينة:

#include <stdio.h>

// تعريف ثابت للتحكم في الإصدار
#define DEBUG 1

int main() {
    int x = 10;
    
    // تضمين كود للتصحيح فقط
    #if DEBUG
        printf("وضع التصحيح: x = %d\n", x);
    #endif
    
    // مثال آخر
    #ifdef DEBUG
        printf("DEBUG معرّف.\n");
    #else
        printf("DEBUG غير معرّف.\n");
    #endif
    
    // التحقق من عدم تعريف ثابت
    #ifndef MAX_SIZE
        #define MAX_SIZE 1000
    #endif
    
    printf("MAX_SIZE = %d\n", MAX_SIZE);
    
    return 0;
}

10.4 الماكروهات المدمجة

توفر لغة C بعض الماكروهات المدمجة مثل:

#include <stdio.h>

int main() {
    printf("اسم الملف الحالي: %s\n", __FILE__);
    printf("رقم السطر الحالي: %d\n", __LINE__);
    printf("وقت الترجمة: %s\n", __TIME__);
    printf("تاريخ الترجمة: %s\n", __DATE__);
    printf("إصدار المعيار: %ld\n", __STDC_VERSION__);
    
    return 0;
}

11. مفاهيم متقدمة

نستعرض في هذا القسم بعض المفاهيم المتقدمة في لغة C والتي تساعد المبرمجين على كتابة برامج أكثر فعالية وقوة.

11.1 التعدادات (Enumerations)

التعدادات هي نوع بيانات مخصص يتكون من مجموعة من الثوابت المسماة (المعدودات). تساعد على جعل الكود أكثر وضوحاً وقابلية للقراءة.

#include <stdio.h>

// تعريف تعداد للأيام
enum Day {
    SUNDAY,    // قيمة 0 (افتراضياً)
    MONDAY,    // قيمة 1
    TUESDAY,   // قيمة 2
    WEDNESDAY, // قيمة 3
    THURSDAY,  // قيمة 4
    FRIDAY,    // قيمة 5
    SATURDAY   // قيمة 6
};

// تعداد مع قيم محددة
enum Month {
    JAN = 1, FEB, MAR, APR, MAY, JUN,
    JUL, AUG, SEP, OCT, NOV, DEC
};

int main() {
    enum Day today = WEDNESDAY;
    enum Month month = JUL;
    
    printf("اليوم: %d\n", today);
    printf("الشهر: %d\n", month);
    
    // استخدام التعدادات في العبارات الشرطية
    if (today == FRIDAY || today == SATURDAY) {
        printf("عطلة نهاية الأسبوع!\n");
    } else {
        printf("يوم عمل.\n");
    }
    
    return 0;
}

11.2 تعريف الأنواع (Typedefs)

تُستخدم typedef لإنشاء أسماء مستعارة لأنواع البيانات، مما يجعل الكود أكثر وضوحاً وأسهل في الصيانة.

#include <stdio.h>
#include <string.h>

// استخدام typedef مع الأنواع الأساسية
typedef unsigned int uint;
typedef char * string;

// استخدام typedef مع الهياكل
typedef struct {
    char name[50];
    int age;
    float salary;
} Employee;

// استخدام typedef مع المؤشرات للدوال
typedef int (*MathFunc)(int, int);

// دوال للاختبار
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int main() {
    // استخدام الأنواع المعرفة بـ typedef
    uint num = 10;
    string name = "أحمد";
    
    printf("العدد: %u\n", num);
    printf("الاسم: %s\n", name);
    
    // استخدام typedef مع الهياكل
    Employee emp1;
    strcpy(emp1.name, "محمد");
    emp1.age = 30;
    emp1.salary = 5000.0;
    
    printf("الموظف: %s، العمر: %d، الراتب: %.2f\n", emp1.name, emp1.age, emp1.salary);
    
    // استخدام typedef مع مؤشرات الدوال
    MathFunc operation = add;
    printf("الجمع: %d\n", operation(5, 3));
    
    operation = subtract;
    printf("الطرح: %d\n", operation(5, 3));
    
    return 0;
}

11.3 التعامل مع البتات

توفر لغة C مجموعة من العوامل للتعامل المباشر مع البتات. هذه العمليات مفيدة في برمجة الأنظمة المضمنة وعندما نحتاج إلى كفاءة في استخدام الذاكرة.

العوامل البتية:

العامل الاسم الوصف
& AND البتية تعطي 1 إذا كان البت في كلا العددين 1
| OR البتية تعطي 1 إذا كان البت في أحد العددين أو كليهما 1
^ XOR البتية تعطي 1 إذا كان البت في أحد العددين 1 وليس في كليهما
~ NOT البتية تعكس قيمة كل بت (1 يصبح 0 و 0 يصبح 1)
<< الإزاحة لليسار تزيح البتات لليسار، وتملأ البتات الجديدة بأصفار
>> الإزاحة لليمين تزيح البتات لليمين، وتملأ البتات الجديدة بأصفار أو بقيمة بت الإشارة
#include <stdio.h>

int main() {
    unsigned int a = 60;  // 00111100 في الثنائي
    unsigned int b = 13;  // 00001101 في الثنائي
    int result;
    
    // عمليات AND، OR، XOR البتية
    result = a & b;  // 00001100 = 12
    printf("a & b = %d\n", result);
    
    result = a | b;  // 00111101 = 61
    printf("a | b = %d\n", result);
    
    result = a ^ b;  // 00110001 = 49
    printf("a ^ b = %d\n", result);
    
    result = ~a;     // 11000011 للبايت الأقل أهمية (قيمة سالبة للـ int)
    printf("~a = %d\n", result);
    
    // عمليات الإزاحة
    result = a << 2; // 11110000 = 240
    printf("a << 2 = %d\n", result);
    
    result = a >> 2; // 00001111 = 15
    printf("a >> 2 = %d\n", result);
    
    return 0;
}

عمليات بتية شائعة:

#include <stdio.h>

// تعريف الأعلام البتية
#define FLAG_A (1 << 0)  // 00000001
#define FLAG_B (1 << 1)  // 00000010
#define FLAG_C (1 << 2)  // 00000100
#define FLAG_D (1 << 3)  // 00001000

int main() {
    unsigned char flags = 0;  // 00000000
    
    // ضبط العلم (تعيين بت إلى 1)
    flags |= FLAG_A;  // 00000001
    flags |= FLAG_C;  // 00000101
    
    printf("الأعلام بعد الضبط: %d\n", flags);
    
    // فحص العلم
    if (flags & FLAG_A) {
        printf("العلم A مضبوط.\n");
    }
    
    if (flags & FLAG_B) {
        printf("العلم B مضبوط.\n");
    } else {
        printf("العلم B غير مضبوط.\n");
    }
    
    // مسح العلم (تعيين بت إلى 0)
    flags &= ~FLAG_A;  // 00000100
    
    printf("الأعلام بعد المسح: %d\n", flags);
    
    // تبديل حالة العلم
    flags ^= FLAG_C;  // 00000000
    flags ^= FLAG_D;  // 00001000
    
    printf("الأعلام بعد التبديل: %d\n", flags);
    
    return 0;
}

12. أفضل الممارسات

اتباع أفضل الممارسات في البرمجة يساعد على كتابة كود أكثر أماناً وقابلية للصيانة والتطوير. فيما يلي بعض الممارسات الموصى بها عند البرمجة بلغة C:

إدارة الذاكرة

  • دائماً قم بتحرير الذاكرة التي تم تخصيصها ديناميكياً باستخدام free() بعد الانتهاء منها.
  • تحقق دائماً من نجاح تخصيص الذاكرة قبل استخدامها.
  • تجنب تسرب الذاكرة عن طريق التأكد من تحرير جميع الموارد المخصصة.
  • استخدم أدوات مثل Valgrind للكشف عن مشاكل الذاكرة.

أسلوب الكود

  • استخدم أسماء متغيرات ودوال ذات معنى وواضحة.
  • أضف تعليقات لشرح الأجزاء المعقدة أو غير الواضحة من الكود.
  • حافظ على تنسيق متسق في جميع أنحاء الكود.
  • اكتب دوالاً قصيرة تؤدي مهمة واحدة فقط.

الأمان

  • تحقق دائماً من حدود المصفوفات لتجنب تجاوزها.
  • استخدم fgets() بدلاً من gets() للقراءة من المدخلات.
  • استخدم نسخاً آمنة من دوال السلاسل النصية مثل strncpy() بدلاً من strcpy().
  • تأكد من التحقق من صحة جميع المدخلات الخارجية.

القابلية للصيانة

  • قسّم الكود إلى ملفات منطقية متماسكة.
  • استخدم ملفات رأسية (.h) لتعريف الواجهات العامة.
  • استخدم #define و const للقيم الثابتة بدلاً من القيم الحرفية في الكود.
  • حافظ على مستوى منخفض من الاعتماديات بين الوحدات البرمجية.

نصائح عامة

  • استخدم أدوات تحليل الكود الثابت لاكتشاف الأخطاء المحتملة قبل تشغيل البرنامج.
  • قم دائماً بالتعامل مع أخطاء الإدخال/الإخراج والتحقق من قيم الإرجاع من الدوال.
  • اختبر الحالات الحدية والقيم الخاصة (مثل الصفر، القيم القصوى، إلخ).
  • أعد استخدام الكود عندما يكون ذلك ممكناً عن طريق إنشاء مكتبات أو وحدات مشتركة.
  • تذكر أن الكود القابل للقراءة غالباً ما يكون أكثر أهمية من الكود المبتكر.

13. الأخطاء الشائعة

التعرف على الأخطاء الشائعة في لغة C يساعد المبرمجين على تجنبها وتسريع عملية تصحيح الأخطاء.

أخطاء إدارة الذاكرة

1. تسرب الذاكرة (Memory Leak)

يحدث عندما لا يتم تحرير الذاكرة المخصصة ديناميكياً.

// مثال على تسرب الذاكرة
void memoryLeak() {
    int *ptr = (int*) malloc(sizeof(int));
    *ptr = 10;
    // نسيان استدعاء free(ptr)
    return;  // تسرب ذاكرة - لا يمكن الوصول إلى ptr بعد الخروج من الدالة
}

2. استخدام الذاكرة بعد تحريرها (Use After Free)

محاولة الوصول إلى ذاكرة تم تحريرها بالفعل.

int *ptr = (int*) malloc(sizeof(int));
*ptr = 10;
free(ptr);  // تحرير الذاكرة
*ptr = 20;  // خطأ: استخدام الذاكرة بعد تحريرها

3. تجاوز حدود المصفوفة (Buffer Overflow)

الكتابة خارج حدود المصفوفة المخصصة.

int arr[5];
for (int i = 0; i <= 5; i++) {  // خطأ: التكرار يشمل i=5 (خارج حدود المصفوفة)
    arr[i] = i;  // تجاوز حدود المصفوفة عند i=5
}

أخطاء التركيب والترجمة

1. نسيان الفاصلة المنقوطة

من أكثر الأخطاء شيوعاً هو نسيان وضع الفاصلة المنقوطة في نهاية العبارات.

int x = 10  // خطأ: نسيان الفاصلة المنقوطة
printf("قيمة x: %d\n", x);

2. استخدام = بدلاً من == في العبارات الشرطية

استخدام عامل التعيين (=) بدلاً من عامل المقارنة (==) في العبارات الشرطية.

int x = 5;
if (x = 10) {  // خطأ: هذا يقوم بتعيين x = 10 ثم تقييم الشرط كـ true
    printf("x تساوي 10\n");
}

// الصحيح:
if (x == 10) {
    printf("x تساوي 10\n");
}

3. عدم تعريف المتغيرات قبل استخدامها

استخدام متغير قبل تعريفه.

sum = a + b;  // خطأ: استخدام المتغير sum قبل تعريفه
int sum;

// الصحيح:
int sum;
sum = a + b;

أخطاء التعامل مع المؤشرات

1. عدم تهيئة المؤشرات

استخدام مؤشر غير مهيأ يمكن أن يؤدي إلى سلوك غير متوقع أو انهيار البرنامج.

int *ptr;  // مؤشر غير مهيأ
*ptr = 10;  // خطأ: محاولة الكتابة إلى عنوان غير معروف

// الصحيح:
int *ptr = (int*) malloc(sizeof(int));
if (ptr != NULL) {
    *ptr = 10;
}

2. تحديث مؤشر إلى متغير محلي

إرجاع مؤشر إلى متغير محلي سيؤدي إلى مشاكل لأن المتغير المحلي يتم تدميره عند الخروج من الدالة.

int* getLocalPtr() {
    int localVar = 10;
    return &localVar;  // خطأ: إرجاع مؤشر إلى متغير محلي سيختفي
}

// الصحيح: استخدام الذاكرة الديناميكية
int* getSafePtr() {
    int *ptr = (int*) malloc(sizeof(int));
    *ptr = 10;
    return ptr;  // يجب على المستدعي تحرير هذه الذاكرة
}

3. الخلط بين المؤشرات والمصفوفات

عدم فهم الفرق بين اسم المصفوفة والمؤشر.

char str[] = "Hello";
char *ptr = "World";

str = ptr;  // خطأ: لا يمكن تعيين قيمة لاسم المصفوفة

// الصحيح:
strcpy(str, ptr);  // نسخ محتوى المؤشر إلى المصفوفة

أخطاء منطقية

1. أخطاء في شروط الحلقات

شروط الحلقات غير الصحيحة قد تؤدي إلى حلقات لا نهائية أو عدم تنفيذ الحلقة.

// حلقة لا نهائية
int i = 0;
while (i < 10) {
    printf("%d ", i);
    // نسيان زيادة قيمة i
}

// عدم تنفيذ الحلقة
for (i = 10; i < 0; i++) {  // خطأ: شرط غير صحيح (i < 0 دائماً خاطئ عندما i = 10)
    printf("%d ", i);
}

2. القسمة على صفر

محاولة القسمة على صفر تسبب خطأ في وقت التشغيل.

int a = 10, b = 0;
int result = a / b;  // خطأ: قسمة على صفر

// الصحيح: التحقق قبل القسمة
if (b != 0) {
    result = a / b;
} else {
    printf("خطأ: محاولة القسمة على صفر\n");
}

3. الخلط بين && و ||

استخدام العامل المنطقي الخاطئ في العبارات الشرطية.

int age = 25;

// قد تكون مقصودة كـ "إذا كان العمر أقل من 18 أو أكبر من 60"
if (age < 18 && age > 60) {  // خطأ منطقي: لا يمكن للعمر أن يكون أقل من 18 وأكبر من 60 في نفس الوقت
    printf("سعر تذكرة مخفض\n");
}

// الصحيح:
if (age < 18 || age > 60) {
    printf("سعر تذكرة مخفض\n");
}

أخطاء الإدخال/الإخراج

1. عدم التحقق من نجاح فتح الملف

استخدام مؤشر الملف دون التحقق من نجاح عملية الفتح.

FILE *file = fopen("data.txt", "r");
fprintf(file, "بيانات للكتابة");  // خطأ: لم يتم التحقق مما إذا كان file == NULL

// الصحيح:
FILE *file = fopen("data.txt", "r");
if (file == NULL) {
    perror("خطأ في فتح الملف");
} else {
    fprintf(file, "بيانات للكتابة");
    fclose(file);
}

2. عدم تحديد محددات التنسيق الصحيحة

استخدام محددات تنسيق غير متوافقة مع نوع البيانات.

int num = 10;
float pi = 3.14;

printf("العدد: %f\n", num);  // خطأ: استخدام %f مع متغير من نوع int
printf("باي: %d\n", pi);     // خطأ: استخدام %d مع متغير من نوع float

// الصحيح:
printf("العدد: %d\n", num);
printf("باي: %f\n", pi);

3. نسيان إغلاق الملفات

نسيان إغلاق الملفات بعد الانتهاء من استخدامها.

void writeToFile() {
    FILE *file = fopen("data.txt", "w");
    if (file != NULL) {
        fprintf(file, "بيانات للكتابة");
        // نسيان استدعاء fclose(file)
    }
    return;  // الملف سيظل مفتوحاً مما قد يؤدي إلى تسرب الموارد
}

نصائح لتجنب الأخطاء وتصحيحها

  • استخدم أدوات لتحليل الكود مثل Valgrind للكشف عن تسرب الذاكرة والأخطاء المتعلقة بالمؤشرات.
  • قم بتمكين جميع تحذيرات المترجم (مثل -Wall -Wextra مع GCC).
  • استخدم أدوات التصحيح (debuggers) مثل GDB للتعقب خطوة بخطوة.
  • راجع الكود بانتظام واطلب من الآخرين مراجعته.
  • اختبر الحالات الحدية والقيم الخاصة.
  • قم بتقسيم المشكلات المعقدة إلى أجزاء أصغر واختبرها بشكل منفصل.
  • استخدم assert() للتحقق من الشروط الأساسية أثناء التطوير.

الخلاصة

لغة C هي لغة برمجة قوية وفعالة تشكل أساساً للعديد من اللغات والأنظمة الحديثة. على الرغم من أنها ليست اللغة الأسهل للتعلم، إلا أن فهم مفاهيمها الأساسية يوفر فهماً عميقاً لكيفية عمل أنظمة الحوسبة.

في هذه الوثيقة، تناولنا الجوانب الأساسية والمتقدمة للغة C، بدءاً من أنواع البيانات والمتغيرات، مروراً ببنى التحكم والدوال، وصولاً إلى المفاهيم المتقدمة مثل المؤشرات وإدارة الذاكرة والتعامل مع الملفات. كما تطرقنا إلى أفضل الممارسات والأخطاء الشائعة وكيفية تجنبها.

استمر في الممارسة وتطبيق ما تعلمته من خلال مشاريع عملية. التعلم المستمر والتجربة هما مفتاح إتقان البرمجة بلغة C.

مصادر إضافية للتعلم

  • كتاب "لغة البرمجة C" للمؤلفين كيرنيغان وريتشي (K&R).
  • كتاب "C Programming: A Modern Approach" للمؤلف K. N. King.
  • موقع GeeksforGeeks لشروحات وتمارين بلغة C.
  • منصة W3Schools لتعلم C بشكل تفاعلي.
  • دورات تعليمية على منصات مثل Coursera وUdemy وedX.
  • المشاركة في مجتمعات المبرمجين ومنتديات المناقشة مثل Stack Overflow.