كل ملف C ترجمته يومًا بدأ بسطر مثل #include <stdio.h>، وذلك السطر ليس شيفرة C. بل تعليمة لبرنامج منفصل - المعالج المسبق - يُنفَّذ أولًا، ويعيد كتابة نصّ مصدرك، ويسلّم النتيجة إلى المترجم.
وفهم ذلك الانقسام إلى مرحلتين يفسّر الكثير من سلوك C: لماذا تُلصَق الترويسات بدل أن تُستورَد، ولماذا يُنتج ماكرو سيّئ خطأً على سطر يبدو سليمًا تمامًا، ولماذا يمكن لملف المصدر نفسه أن يُترجَم إلى برامج مختلفة على أنظمة مختلفة.
ما يحدث فعليًا قبل الترجمة
ترجمة ملف .c ليست خطوة واحدة. بل هي أربع تقريبًا:
- المعالجة المسبقة - أطِع كل سطر يبدأ بـ
#، فينتج نصّ مصدر متمدّد كبير واحد (وحدة ترجمة). - الترجمة - حوّل ذلك النص إلى لغة تجميع، ثم إلى ملف كائني.
- التجميع - أنتِج شيفرة آلة.
- الربط - اضمّ الملفات الكائنية مع المكتبات في ملف تنفيذي.
ويعمل المعالج المسبق على النصّ بحتًا. فهو لا يعرف ما المتغيّر ولا ما النوع ولا ما إذا كانت أقواسك المعقوفة متوازنة. بل يرى محارف ورموزًا، ويستبدل بعضها ببعض، ويمضي. وكل ما هو غريب في الماكرو يتبع من تلك الحقيقة الواحدة.
وبحلول قراءة المترجم لهذا البرنامج، لا يوجد GREETING في أي مكان. فقد استبدلها المعالج المسبق سلفًا بالثابت النصّي "Hello from a macro"، تمامًا كما لو كنت كتبته.
عائلة التوجيهات
يبدأ كل توجيه للمعالج المسبق بـ # كأول محرف غير أبيض في سطره. ولا توجد فاصلة منقوطة، وينتهي التوجيه بنهاية السطر ما لم تتابعه بشرطة عكسية.
| التوجيه | ماذا يفعل |
|---|---|
#include | يلصق محتويات ملف آخر |
#define | يعرّف ماكرو (استبدالًا نصّيًا) |
#undef | يزيل تعريف ماكرو |
#ifdef, #ifndef | يُبقي الشيفرة التالية فقط إن كان ماكرو معرَّفًا (أو غير معرَّف) |
#if, #elif, #else, #endif | يُبقي الشيفرة بناءً على تعبير ثابت |
#error | يوقف الترجمة برسالة |
#pragma | تعليمة خاصة بالمترجم، مثل #pragma once |
#line | يغيّر رقم السطر المبلَّغ عنه (نادر) |
وثلاثة من هذه لها صفحات خاصة: يغطّي الماكرو الأمر #define بعمق، وتغطّي الترجمة الشرطية عائلة #if، وتغطّي ملفات الترويسة كيفية استخدام #include لهيكلة برنامج متعدّد الملفات.
#include: الأقواس الزاوية مقابل علامات الاقتباس
يفعل #include شيئًا واحدًا بالضبط: يستبدل بسطره محتويات الملف المسمّى كاملة. وذلك الملف المضمَّن تجري معالجته مسبقًا بدوره، فتتمدّد أسطر #include الخاصة به أيضًا.
#include <stdio.h> /* search the system include directories */
#include "config.h" /* search this file's directory first */
والفرق هو ترتيب البحث:
<الأقواس الزاوية>تبحث في مجلّدات التضمين القياسية للمترجم -/usr/include، ومجلّدات سلسلة الأدوات نفسها، إضافةً إلى ما تضيفه بـ-I. وهذه لترويسات المكتبات."علامات الاقتباس"تبحث أولًا في مجلّد الملف الذي يقوم بالتضمين، ثم ترجع إلى القائمة نفسها التي تستخدمها الأقواس الزاوية. وهذه للترويسات التي كتبتها.
وتعمل الصيغتان لأي نوع من الترويسات على معظم المترجمات، لكن العرف يحمل معنى: فالأقواس الزاوية تقول "هذه ترويسة شخص آخر"، وعلامات الاقتباس تقول "هذه لي". والخلط بينهما هو كيف ينتهي مشروع إلى تضمين ترويسة نظام قديمة بدل ترويسته الخاصة.
ولأن التضمين لصق نصّي، فإن تضمين الترويسة نفسها مرتين يلصقها مرتين - ولهذا تحتاج الترويسات إلى حراس تضمين. وذلك أول ما تصلحه صفحة ملفات الترويسة.
#define: استبدال نصّي لا أكثر
#define NAME replacement تخبر المعالج المسبق: من هنا فصاعدًا، حيثما ظهر الرمز NAME، ضع replacement بدلًا منه.
لاحظ ما لا يحدث هنا. فـ MAX_USERS ليس لها نوع. وهي غير مخزَّنة في أي مكان. ولا يمكن فحصها في منقّح. إنها قاعدة بحث واستبدال، وبعد المعالجة المسبقة يحتوي البرنامج حرفيًا على printf("%s allows %d users\n", "Coddy", 100);.
وهذا يعني أيضًا أن الاستبدال أعمى. فهذا يُترجَم، ويفعل شيئًا مفاجئًا:
#define SIZE 5 + 1
int arr[SIZE]; /* fine: int arr[5 + 1]; */
int total = SIZE * 2; /* 5 + 1 * 2 == 7, not 12 */
والإصلاح - أقواس حول كل شيء - هو القاعدة المحورية في صفحة الماكرو، إضافةً إلى الماكرو الذي يأخذ وسائط.
و#define بلا نصّ استبدال يعرّف الاسم على أنه "موجود لكنه فارغ". وهذا عديم الفائدة كاستبدال وجوهري كراية:
#define DEBUG /* defined, expands to nothing */
فتستطيع الشيفرة بعدها أن تسأل ما إذا كان DEBUG موجودًا بـ #ifdef.
رؤية خرج المعالج المسبق
أفضل طريقة لبناء الحدس هي النظر إلى ما أنتجه المعالج المسبق فعليًا. فـ gcc -E تتوقّف بعد المعالجة المسبقة وتطبع النتيجة:
gcc -E hello.c
ولملف يضمّن <stdio.h>، يكون ذلك من 700 إلى 30,000 سطر بحسب نظامك - ومعظمه محتويات الترويسة نفسها. ولرؤية جزئك وحده، خُذ النهاية:
gcc -E hello.c | tail -20
جرّبها على ملف مثل هذا:
#define SQUARE(x) ((x) * (x))
#define LIMIT 10
int main(void) {
int n = SQUARE(LIMIT);
return n;
}
تُظهر نهاية الخرج:
int main(void) {
int n = ((10) * (10));
return n;
}
كل ماكرو زال؛ ولم يبقَ إلا النصّ المستبدَل. وحين يسيء ماكرو التصرّف، يخبرك هذا الأمر بالسبب في ثوانٍ، وهو أفضل من التخمين. ورفيقان مفيدان: gcc -dM -E - < /dev/null يُدرِج كل ماكرو يعرّفه مترجمك مسبقًا، وgcc -E -P file.c يحذف ضجيج علامات الأسطر.
لماذا تشير الأخطاء إلى السطر الخاطئ
لأن المترجم يرى نصًّا متمدّدًا، يُبلَّغ عن الخطأ داخل ماكرو في الموضع الذي استُخدم فيه، لا حيث كُتب:
#define HALF(x) (x / 2
int main(void) {
int y = HALF(8); /* error reported here */
return 0;
}
القوس المفقود في سطر #define، لكن المترجم يشتكي من السطر الذي يحتوي على HALF(8)، وغالبًا برسالة عن رمز غير متوقّع لا معنى لها في السياق. وحين يبدو خطأ مستحيلًا، مدّد الملف بـ gcc -E واقرأ السطر الحقيقي.
والمترجمات الحديثة تساعد: فـ GCC وclang يطبعان ملاحظة "in expansion of macro" تشير رجوعًا إلى التعريف. ترجم بـ -Wall -Wextra لترى تلك الملاحظات فعلًا.
الماكرو المعرَّف مسبقًا
يوفّر المعالج المسبق بعض الماكرو بنفسه. وهي مفيدة فعلًا للتشخيص:
تتمدّد __FILE__ و__LINE__ إلى اسم الملف الحالي ورقم السطر، وهذا كيف تبلّغ ماكرو التوكيد والتسجيل عن موضع الخطأ. (و__func__ مختلفة قليلًا - فهي معرِّف حقيقي يوفّره المترجم لا ماكرو للمعالج المسبق، لكنها تُستخدم بالطريقة نفسها.)
وتعرّف المترجمات مسبقًا أيضًا ماكرو للمنصّات مثل __linux__ و_WIN32 و__APPLE__. والشيفرة التي يجب أن تختلف بحسب النظام تفحصها بـ #ifdef، وهو موضوع الترجمة الشرطية.
الخلاصة
المعالج المسبق صغير وغبيّ، وكلاهما عن قصد. فهو يمنحك ثلاث قدرات - جلب ملف، واستبدال نصّ، وإدخال الشيفرة أو إخراجها - ولا فحص أنواع إطلاقًا معها.
وتلك المقايضة هي سبب النصيحة المعيارية باللجوء إلى خصائص اللغة أولًا: استخدم const int أو enum بدل #define للثوابت حين تستطيع، ودالة حقيقية بدل ماكرو شبيه بالدالة. وحيث يكون المعالج المسبق هو الأداة الصحيحة فعلًا - الترويسات، ومفاتيح قابلية النقل، وتهيئة وقت الترجمة - فلا بديل عنه.
وتاليًا، يأخذ الماكرو الأمر #define على محمل الجدّ: الوسائط، وقواعد الأقواس، والفخاخ المرافقة لاستبدال نصّ داخل شيفرة لم تكتبها.
الأسئلة الشائعة
ما المعالج المسبق في لغة C؟
خطوة معالجة نصّية تُنفَّذ قبل الترجمة. وهي تطيع الأسطر التي تبدأ بـ # - فتلصق محتويات ملفات الترويسة مكان #include، وتستبدل الماكرو المعرَّف بـ #define، وتحذف الشيفرة أو تُبقيها وفق #if/#ifdef. ولا يرى المترجم إلا النتيجة، ولا يرى ملفك الأصلي أبدًا.
ما الفرق بين #include <stdio.h> و#include "myfile.h"؟
تبحث الأقواس الزاوية في مجلّدات التضمين النظامية للمترجم، حيث تعيش ترويسات المكتبة القياسية. وتبحث علامات الاقتباس في مجلّد الملف الحالي أولًا، ثم ترجع إلى المسارات النظامية. استخدم الأقواس الزاوية لترويسات المكتبات وعلامات الاقتباس للترويسات التي كتبتها.
كيف أرى ما أنتجه المعالج المسبق؟
نفّذ gcc -E file.c للتوقّف بعد المعالجة المسبقة وطباعة المصدر المتمدّد. وعلى ملف يضمّن <stdio.h> يكون الخرج آلاف الأسطر، فوجّهه بأنبوب: gcc -E file.c | tail -30 يعرض شيفرتك وحدها بكل ماكرو مستبدَل سلفًا.
هل #include جملة في لغة C؟
لا. فالتوجيهات ليست جزءًا من لغة C نفسها - لها صياغتها السطرية الخاصة، ولا تأخذ فاصلة منقوطة، وتزول بحلول وقت تحليل المترجم لبرنامجك. ولهذا يظهر الخطأ في ماكرو كخطأ محيّر على سطر يبدو سليمًا.