أول خطوات في لغة C
درس 2 من 8
2أول خطوات في لغة C
الانتقال من لغات عالية المستوى ومُفسَّرة إلى لغة C المُصرَّفة (Compiled) والقريبة من العتاد، حيث تبدأ فعليًا في فهم "ماذا يحدث فعلاً" عند تشغيل برنامج.
ماذا تشاهد في الرسم: main.c يمرّ على المعالج المسبق ثم المصرّف ثم الرابط، ويخرج برنامجًا تنفيذيًا واحدًا.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
بايثون مثل سيارة أوتوماتيك: تقودها دون أن تفكر في الغيارات. C مثل سيارة يدوية: تتحكم في كل شيء بنفسك، وهي أسرع وأقرب للمحرّك، لكن الخطأ عليك. وقبل أن تقود، يُترجم كل برنامج C إلى لغة الآلة مرة واحدة ثم يعمل مباشرة.
C لغة مُصرَّفة: gcc -Wall -Werror main.c -o main يحوّل المصدر إلى ملف تنفيذي. كل متغير له نوع ثابت (int وchar وfloat)، والبرنامج يبدأ من main، والدوال تُعلن قبل استخدامها، وprintf يطبع بمحددات صيغة مثل %d و%s.
قبل أن تبدأ
🧠منطق برمجي عام
مفاهيم الحلقات والشروط من الجزء الأول كافية للبدء، الجديد هنا هو الصيغة (Syntax) والانضباط الصارم في الأنواع.
أول برنامج قبل أي نظرية
قبل أن تقرأ عن المعالج المسبق والرابط، اكتب برنامجًا يعمل. ما ستراه هنا هو كل ما تحتاجه لفهم بقية الجزء.
١. اطبع سطرًا
كل برنامج C يبدأ من دالة اسمها main. printf تطبع، و\n تعني سطرًا جديدًا، و%d مكان يُوضع فيه رقم. السطر الأول يجلب تعريف printf من المكتبة القياسية للغة C.
#include <stdio.h>
int main(void)
{
printf("Hello from C\n");
printf("%d\n", 2 + 3);
return (0);
}
لتشغيل هذا على جهازك: احفظه في hello.c، ثم gcc -Wall -Werror -o hello hello.c، ثم ./hello. الخطوة الوسطى تسمّى التصريف، وسنفكّكها بعد قليل.
٢. متغيّر برقم
في C تعلن نوع المتغيّر قبل اسمه: int age; تعني عددًا صحيحًا اسمه age. ثم تضع فيه قيمة، وتطبعه بـ%d.
#include <stdio.h>
int main(void)
{
int age;
age = 21;
printf("age: %d\n", age);
printf("next year: %d\n", age + 1);
return (0);
}
لاحظ الفاصلة المنقوطة في نهاية كل جملة، والأقواس المعقوفة حول جسم الدالة. نسيان أيٍّ منهما هو أول خطأ ستراه، ورسالة المصرّف ستذكر رقم السطر.
الفكرة الجوهرية: من الكود المصدري إلى برنامج قابل للتشغيل
شكل 1، على عكس بايثون التي تُفسَّر سطرًا بسطر وقت التشغيل، C تمر بأربع مراحل تحويل كاملة قبل أن تصبح برنامجًا قابلًا للتشغيل مباشرة على المعالج.
1المعالج المسبق (Preprocessor)
يعالج الأسطر التي تبدأ بـ# مثل #include، يستبدلها بمحتوى فعلي قبل التصريف. المهم أن تفهم أن هذه مرحلة استبدال نصي بحت لا تفهم C نفسها بعد: #include <stdio.h> حرفيًا يأخذ محتوى ملف stdio.h كاملًا (تصريحات الدوال القياسية مثل printf) ويلصقه مكان تلك السطر، قبل أن يرى المُصرِّف أي شيء؛ ولهذا بالضبط أي خطأ إملائي في اسم الملف أو عدم وجوده يظهر كخطأ "لم يُعثر على الملف" في هذه المرحلة المبكرة جدًا، قبل حتى أن يبدأ التحقق من صحة الكود نحويًا.
2المُصرِّف (Compiler)
يترجم كود C إلى لغة تجميعية (Assembly) مخصّصة لمعمارية المعالج المستهدف. هذه هي المرحلة التي تفحص فيها C نحو كودك فعليًا وتطابق الأنواع، هنا تظهر أغلب أخطاء التصريف (Compilation Errors) التي تمنعك من المتابعة، لأن المُصرِّف يحتاج يعرف بالضبط حجم كل متغيّر ونوعه ليولّد تعليمات معالج صحيحة؛ هذا عكس تمامًا لغة مُفسَّرة قد تكتشف خطأ نوع فقط عند تنفيذ ذلك السطر تحديدًا وقت التشغيل، لا قبله.
3الرابط (Linker)
يجمع ملفات object المتعددة (كودك + مكتبات قياسية مثل printf) في ملف تنفيذي واحد نهائي. كل ملف object الذي يولّده المُصرِّف يحتوي "فجوات" لأي اسم دالة أو متغيّر استُخدم لكنه مُعرَّف في ملف آخر، مهمة الرابط أن يبحث عن تعريف كل اسم من هذه الفجوات في بقية الملفات أو المكتبات القياسية، ويملأها بالعنوان الصحيح. لهذا السبب بالضبط يظهر خطأ "undefined reference" من الرابط لا من المُصرِّف: الكود صحيح نحويًا تمامًا من منظور المُصرِّف، لكن لم يُعثر على التعريف الفعلي لتلك الدالة في أي مكان عند مرحلة الربط.
4الأنواع الصارمة (Static Typing)
كل متغيّر في C له نوع ثابت مُعلَن (int, char, float...) لا يتغيّر أثناء التنفيذ، هذا يمنح C سرعة وتحكّمًا دقيقًا بالذاكرة، مقابل مرونة أقل من لغات مثل بايثون. السبب الفعلي للسرعة: بمعرفة نوع المتغيّر وقت التصريف، يعرف المُصرِّف بالضبط كم بايت يحجز له ومتى، فيولّد تعليمات معالج مباشرة بلا أي فحص إضافي وقت التشغيل لمعرفة "أي نوع هذا المتغيّر فعلًا الآن"، بينما لغة ديناميكية التنويع تحمل هذا الفحص في كل عملية وصول للمتغيّر طوال وقت التنفيذ، وهذا جزء كبير من كلفتها الإضافية في الأداء.
كيف يعمل، خطوة بخطوة: من الكود إلى التنفيذ
#include <stdio.h>
int main(void)
{
int count;
count = 0;
while (count < 3)
{
printf("مرحبًا! هذه المرة رقم %d\n", count);
count++;
}
return (0);
}
gcc -Wall -Werror -o hello hello.c، يشغّل السلسلة كاملة (معالجة، تصريف، تجميع، ربط) في أمر واحد.#include <stdio.h> يعطي الوصول لدالة printf المعرَّفة في مكتبة قياسية خارجية.int main(void) هي نقطة الدخول الوحيدة التي يبحث عنها الـLinker ليبدأ التنفيذ منها.main (هنا 0) تخبر نظام التشغيل إن نجح البرنامج (0) أو فشل (أي رقم آخر).int count; بدون count = 0;). في C، المتغيّر غير المُهيّأ يحتوي "قيمة قمامة" (Garbage Value) عشوائية من الذاكرة، وليس صفرًا تلقائيًا كما في لغات أخرى.
مثال محلول: حلقات متداخلة لطباعة نمط
طباعة مثلث من النجوم بارتفاع n باستخدام حلقتين متداخلتين، تمرين كلاسيكي يثبّت فهم الحلقات المتداخلة قبل الانتقال لمفاهيم أعقد:
void print_triangle(int n)
{
int i, j;
for (i = 1; i <= n; i++)
{
for (j = 0; j < i; j++)
printf("*");
printf("\n");
}
}
/* لـ n=4 يطبع:
*
**
***
**** */
شكل 2، الحلقة الخارجية تتحكم بعدد الأسطر (i)؛ الحلقة الداخلية تتحكم بعدد النجوم في كل سطر: وعدد تكراراتها يعتمد على قيمة الحلقة الخارجية الحالية.
تعمّق: تشريح برنامج C وأنواعه ودوالّه
هذه المرحلة تضع الأساس الذي تقوم عليه كل مشاريع C: ما هو البرنامج، وكيف تُكتب الدوال وتُعلن، وأين تعيش المتغيرات، وما الأنواع، ولماذا تلتزم بقواعد صارمة للأسلوب من أول يوم.
أولًا: تشريح برنامج C
برنامج C ملف نصي يحوي دوال، وتبدأ كل البرامج من دالة اسمها main هي نقطة الدخول (Entry Point). يستدعيها نظام التشغيل، ويعيد لك قيمتها: الرقم الذي تعيده main هو رمز الخروج، والصفر يعني نجاحًا وغيره يعني خطأً، وهذا ما يقرؤه الشل في المتغير $?. وتحتاج كل دالة تستعملها من مكتبة (مثل printf) إلى أن تضمّن ملف الترويسة الذي يعلن عنها، فتخبر المترجم بشكلها قبل أن يراها مستخدمة.
شكل 3، كل شيء يبدأ من main وينتهي بإعادة رمز الخروج إلى النظام.
ثانيًا: ثلاث طرق للطباعة
| الدالة | ما تطبعه | متى تستخدمها |
|---|---|---|
putchar(c) | حرف واحد | أبسط أداة، وقاعدة أي طباعة |
puts(s) | سلسلة نصية ثم سطر جديد | نص جاهز بلا تنسيق |
printf(fmt, ...) | نص منسّق بمحددات مثل %d | عندما تحتاج دمج قيم |
ثالثًا: الأنواع وأحجامها
تحدد C نوع كل متغير عند إعلانه وهذا النوع يحدد عدد البايتات التي يحجزها ونطاق قيمه. يعطيك العامل sizeof الحجم بالبايت، وهو يُحسب وقت الترجمة. القيم الدقيقة تعتمد على المعالج والمترجم، ولهذا يوجد العلمان -m32 و -m64 اللذان يطلبان من gcc إنتاج برنامج لنظام 32 بت أو 64 بت، فتتغير بهما أحجام بعض الأنواع (وأهمها المؤشر).
| النوع | الحجم النموذجي | الاستخدام |
|---|---|---|
char | 1 بايت | حرف أو عدد صغير |
int | 4 بايت | عدد صحيح |
unsigned int | 4 بايت | عدد صحيح غير سالب بضعف النطاق الموجب |
double | 8 بايت | عدد عشري بدقة عالية |
| مؤشر | 8 بايت في 64 بت | عنوان في الذاكرة |
وللاستعمال: القيمة الصحيحة (غير الصفر) تُعدّ صحيحة في الشروط، والصفر وحده خطأ. فالشرط if (x) يعني if (x != 0). وجدول ASCII يعني أن 'A' + 1 تساوي 'B' لأن الحرف عدد.
رابعًا: الدوال: إعلان وتعريف ونطاق
التعريف (Definition) هو الجسم الكامل للدالة، والإعلان أو النموذج الأولي (Prototype) سطر واحد يخبر المترجم باسم الدالة ونوع قيمتها ووسائطها دون جسمها. يحتاج المترجم الإعلان قبل أول استدعاء ليتحقق من صحته، ولهذا تُجمع النماذج الأولية في ملف ترويسة .h يُضمَّن في الملفات التي تحتاجها. أما النطاق (Scope) فيحدد أين يُرى المتغير: المتغير المحلي يعيش داخل الكتلة { } التي أُعلن فيها وينتهي بانتهائها، وتأخذ كل دالة نسخها الخاصة من وسائطها (تمرير بالقيمة).
شكل 4، النطاقات المتداخلة: كلما تعمّقت صار عمر المتغير أقصر.
خامسًا: لماذا القواعد الصارمة؟
📐أسلوب موحّد للكود
دليل أسلوب تلتزم به وأداة تفحصه آليًا (مثل clang-format): الإزاحة، طول السطر، حجم الدالة، التوثيق. فائدته أن يقرأ أي مبرمج شيفرتك بلا جهد، وهو أيضًا عادة عمل جماعي.
🚨-Wall -Wextra -Werror -pedantic
يحوّل التحذيرات إلى أخطاء كي لا تمر مشكلة صامتة، ويجبرك على الالتزام بالمعيار.
🧱قيود مثل عدم استخدام المكتبة
هدفها أن تبني الأدوات بنفسك فتفهمها. الدالة _putchar المسموحة هي مجرد غلاف يكتب بايتًا واحدًا بنداء write.
📏خمس دوال في الملف كحد أقصى
تعلّمك تقسيم الشيفرة إلى وحدات صغيرة ومتماسكة.
الخلط بين = (إسناد) و == (مقارنة) داخل if. المترجم يقبل if (x = 5) لأنها تعبير صحيح، لكنها تضع 5 في x ثم تعتبر الشرط دائمًا صحيحًا. تلتقطها تحذيرات -Wall.
جرّب بنفسك
ظهر الخطأ «undefined reference» عند البناء. أي مرحلة أنتجته؟
الكود صحيح نحويًا عند المُصرِّف، لكن الرابط لم يجد تعريف الدالة في أي ملف object أو مكتبة.
ماذا يفعل #include <stdio.h> في المعالج المسبق؟
المعالج المسبق مرحلة استبدال نصي بحت؛ لهذا يظهر خطأ «ملف غير موجود» مبكرًا قبل فحص النحو.
ما فائدة الأعلام -Wall -Wextra -Werror -pedantic معًا؟
حتى لا تمر مشكلة صامتة: كل تحذير يوقف البناء حتى تعالجه.
بطاقات مراجعة سريعة
تحقق من فهمك
لماذا تحتاج C لمرحلة تصريف كاملة بينما تعمل بايثون بمجرد تشغيل الملف مباشرة؟
لأن بايثون مُفسَّرة، يُترجَم كل سطر ويُنفَّذ فورًا وقت التشغيل، بينما C مُصرَّفة بالكامل مسبقًا لكود آلة يفهمه المعالج مباشرة، وهذا يمنحها سرعة تنفيذ أعلى بكثير مقابل خطوة تصريف إضافية قبل التشغيل.
في مثال المثلث، لو استبدلت الشرط الداخلي بـ j < n بدل j < i، ماذا يتغيّر في الناتج؟
كل سطر سيطبع نفس عدد النجوم الثابت n بدل عدد متزايد، فينتج مستطيلًا مليئًا بالنجوم بدل مثلث.
لماذا تعيد المترجمات تحذيرًا عند if (x = 5)؟
لأنها إسناد لا مقارنة، فتضع 5 في x ويكون الشرط دائمًا صحيحًا.
ما الذي يغيّره العلم، m32 في البرنامج؟
يبني للمعمارية 32 بت فتتغير أحجام بعض الأنواع كالمؤشر.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.