اذهب إلى المحتوى

الماكرو Macro والمعالج المسبق Preprocessor في لغة سي C


Naser Dakhel

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

أثر المعيار

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

لم يكن هناك في تلك الفترة معيارٌ رسميٌ متفقٌ عليه يوحّد ما يفعله المعالج المسبق، وكانت إصدارات مختلفة منه مُطبّقة بصورةٍ مختلف على عدة أنظمة، وأصبحت عملية نقل البرنامج وتصديره إلى أنظمة أخرى مشكلةً كبيرة إذا استخدم ما يزيد عن الخصائص الأساسية للمعالج.

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

يعدّ استخدام المعالج المسبق سهلًا إذا استُخدم لمهمته الأساسية البسيطة في جعل البرامج سهلة القراءة والصيانة، ولكن يُفضّل ترك خصائصه المتقدمة لاستخدام الخبراء. بحكم تجربتنا، يُعد استخدام ‎#‎‎‎define ومجموعة تعليمات التصريف الشرطي conditional compilation (أوامر ‎#if) مناسبًا للمبتدئين، وإذا ما زلت مبتدئً في لغة سي، فاقرأ هذه المقالة مرةً واحدة لمعرفة إمكانيات المعالج المُسبق واستخدم التمارين للتأكد من فهمك، وإلا فنحن ننصح بخبرة لا تقل عن ستة أشهر في لغة سي حتى تستطيع فهم إمكانيات المعالج المسبق كاملةً، لذا لن نركز على منحك مقدمة سهلة هنا بل سنركز على التفاصيل الدقيقة فورًا.

كيف يعمل المعالج المسبق؟

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

012The_preprocessor.png

شكل 1: المعالج المسبق في لغة سي

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

لا يعي المعالج المسبق قوانين لغة سي الخاصة بالنطاق Scope، إذ تأخذ موجهات المعالج المسبق (مثل ‎#define) تأثيرها فور رؤيتها ويبقى تأثيرها موجودًا حتى الوصول إلى نهاية الملف الذي يحتوي هذه الموجهات، ولا ينطبق هنا هيكل البرنامج المتعلق بالكتل البرمجية. من المحبّذ إذًا استخدام موجهات المعالج المسبق بأقل ما أمكن، فكلما قلّ عدد الأجزاء التي لا تتبع قوانين النطاق "الاعتيادية" كلّما قلت إمكانية ارتكاب الأخطاء، وهذا ما نقصده عندما نقول أن تكامل المعالج المسبق ولغة سي C محدودٌ فيما بينهما.

يصف المعيار بعض القوانين المعقدة بخصوص كتابة موجهات المعالج المسبق، وبالأخص بالنسبة للمفاتيح Tokens، ويجب عليك معرفة القوانين كلها إذا أردت فهم موجهات المعالج المسبق، فالنص الذي يُعالج لا يعدّ سلسلةً من المحارف بل هو مُجزّأٌ إلى مفاتيح ومن ثم معلومات معالجة مُجزأة.

من الأفضل اللجوء إلى المعيار إذا أردت تعريفًا كاملًا بالعملية، إلا أننا سنتطرق إلى شرح بسيط؛ إذ سنشرح كل جزء موجود في القائمة التالية لاحقًا.

  1. اسم ملف الترويسة
  • > يمكن استخدام أي محرف هنا (باستثناء) <.
  1. مفتاح المعالج المسبق
  • اسم ملف الترويسة كما ذُكر سابقًا لكن فقط في حالة ذكره ضمن ‎#include
  • أو معرّف identifier مثل معرف لغة C أو كلمة مفتاحية
  • أو ثابت وهو أي عدد صحيح أو طبيعي ثابت
  • أو سلسلة نصية وهو سلسلة نصية سي اعتيادية
  • أو عامل وهو من أحد عوامل لغة سي
  • أو واحد من علامات الترقيم [ ] ( ) { } * , : = ; ... #
  • أو أي محرف غير فارغ (محرف فارغ مثل محرف المسافة) غير مذكور في اللائحة أعلاه

نقصد أي محرف (باستثناء) أي باستثناء المحرفين < أو محرف السطر الجديد.

الموجهات Directives

تبدأ موجّهات المعالج المسبق بالمحرف "#" دائمًا، وتُتبع بمحرف مسافة فارغة اختياريًا إلا أن هذا الاستخدام غير شائع، ويوضح الجدول التالي الموجهات المعرّفة في المعيار.

الموجّه المعنى
‎# include تضمين ملف مصدري
‎# define تعريف ماكرو
‎# undef التراجع عن تعريف ماكرو
‎# if تصريف شرطي
‎# ifdef تصريف شرطي
‎# ifndef تصريف شرطي
‎# elif تصريف شرطي
‎# else تصريف شرطي
‎# endif تصريف شرطي
‎# line التحكم بتقارير الأخطاء
‎# error عرض رسالة خطأ قسرية
‎# pragma تُستخدم للتحكم المعتمد على التنفيذ
# موجّه فارغ؛ دون تأثير

جدول 1 موجّهات المعالج المُسبق

سنشرح كل من معنى واستخدام الموجّهات بالتفصيل في الفقرات التالية. لاحظ أن المحرف # والكلمة المفتاحية التي تليه عنصرين مستقلين منفصلين، ويمكن إضافة مسافةٍ بيضاء بينهما.

الموجه الفارغ

هذا الموجّه بسيط، إذ ليس لإشارة # بمفردها على السطر أي تأثير.

موجه تعريف الماكرو ‎define

هناك طريقتان لتعريف الماكرو، أولهما تبدو مثل تابع والأخرى على النقيض، إليك مثالًا باستخدام الطريقتين:

#define FMAC(a,b) a here, then b

#define NONFMAC some text here

يعرّف التصريحان السابقان ماكرو ونصًا بديلًا له، إذ سيستخدم ليُستبدل بالماكرو المذكور ضمن كامل البرنامج، ويمكن استخدامهما بعد التصريح عنهما على النحو التالي مع ملاحظة أثر استبدال الماكرو الموضح في التعليقات:

NONFMAC
/* النص هنا */

FMAC(first text, some more)
/* النص الأول، ومزيدٌ من النص */

يُستبدل اسم الماكرو في الحالة التي لا يبدو فيها مثل دالة بالنص البديل ببساطة، وكذلك الأمر بالنسبة لماكرو من نوع دالة، وفي حال كان النص البديل يحتوي على معرّف يطابق اسم معامل من معاملات الماكرو، يُستخدم النص الموجود وسيطًا بدلًا من المعرِّف في النص البديل. يُحدَّد نطاق الأسماء المذكورة في وسطاء الماكرو بالكتلة التي تحتوي الموجه ‎#define.

تُهمل أي مسافات فارغة بعد أو قبل النص البديل ضمن تعريف الماكرو، وذلك لكلا الطريقتين على حد سواء.

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

يسمح المعيار للماكرو بغض النظر عن نوع أن يُعاد تعريفه في أي لحظة باستخدام موجّه ‎# define آخر، وذلك بفرض عدم تغيير نوع الماكرو وأن تكون المفاتيح التي تشكل التعريف الأساسي وإعادة التعريف متماثلة بالعدد والترتيب والكتابة بما فيها استخدام المسافة الفارغة، وتُعد المسافات الفارغة في هذا السياق متساوية، وطبقًا لذلك فالتالي صحيح:

# define XXX abc/*تعليق*/def hij
# define XXX abc def hij

وذلك لأن التعليق شكلٌ من أشكال المسافات الفارغة، وسلسلة المفاتيح للحالتين السابقتين هي:

# w-s define w-s XXX w-s abc w-s def w-s hij w-s

إذ تعني w-s مفتاح مسافة بيضاء.

استبدال الماكرو

أين سيتسبب اسم الماكرو باستبدال النص بالنص البديل؟ يحدث الاستبدال عمليًا في أي مكان يحدث التعرف فيه على المعرّف identifier مثل مفتاح منفصل ضمن البرنامج، عدا المعرف المتبوع بالمحرف "#" الخاص بموجّه المعالج المُسبق. يمكنك كتابة التالي:

#define define XXX

#define YYY ZZZ

ومن المتوقع أن يتسبب استبدال سطر ‎#define الثاني بالسطر ‎#xxx بخطأ.

يُستبدل المعرف المرتبط بماكرو بسيط عندما يُرى بمفتاح الماكرو البديل، ومن ثم يُعاد مسحه rescanned (سنتكلم عن ذلك لاحقًا) للعثور على أي استبدالات أخرى.

يمكن استخدام ماكرو الدالة مثل أي دالة أخرى اعتيادية، وذلك بوضع مساحات فارغة حول اسم الماكرو ولائحة الوسطاء وغيره، كما قد يحتوي على محرف سطر جديد:

#define FMAC(a, b) printf("%s %s\n", a, b)

FMAC ("hello",
      "sailor"
      );
/* ينتج ما سبق بالتالي */
printf("%s %s\n", "hello", "sailor")

يمكن أن تأخذ وسطاء الماكرو من نوع دالة أي تسلسل عشوائي للمفتاح، وتُستخدم الفاصلة "," لفصل الوسطاء عن بعضهم بعضًا، ولكن يمكن إخفاؤها بوضعها داخل أقواس "( )". توازن الأزواج المتطابقة من الأقواس داخل الوسيط بعضها بعضًا، وبالتالي ينهي القوس "(" استدعاء الماكرو إذا كان القوس ")" هو الذي بدأ باستدعائه.

#define CALL(a, b) a b

CALL(printf, ("%d %d %s\n",1, 24, "urgh"));
/* results in */
printf ("%d %d %s\n",1, 24, "urgh");

لاحظ كيف حافظنا على الأقواس حول الوسيط الثاني للدالة CALL عند الاستبدال، ولم تُزال من النص.

إذا أردت استخدام ماكرو مثل printt، لن يساعدك المعيار بهذا الخصوص عندما تختار عدد متغير من الوسطاء، فذلك غير مدعوم.

نحصل على سلوك غير معرف إذا لم يحتوي أحد الوسطاء على مفتاح معالج مسبق، والأمر مماثل إذا احتوت سلسلة مفاتيح المعالج المسبق التي تشكل الوسيط على موجّه معالج مسبق مغاير:

#define CALL(a, b) a b

/* كل حالة تنتج عن سلوك غير محدد */
CALL(,hello)
CALL(xyz,
#define abc def)

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

تتبع معالجة ماكرو الدالة الخطوات التالية:

  1. جميع وسطائها معرفة.
  2. إن كان أي من المفاتيح ضمن الوسيط مرشح لاستبدال بواسطة ماكرو، فسيُستبدل حتى الوصول للنقطة التي لا يمكن فيها إجراء المزيد من الاستبدالات، باستثناء الحالات المذكورة في البند الثالث التالي. لا يوجد هناك أي خطر بخصوص امتلاك الماكرو لعدد مختلف من الوسطاء بعد إضافة فاصلة إلى قائمة الوسطاء الأساسية، إذ يُحدد الوسطاء في الخطوة السابقة فقط.
  3. تُستبدل المعرفات التي تسمّي وسيط الماكرو في نص الاستبدال بسلسلة مفتاح مثل وسيطٍ فعلي، ويُهمل الاستبدال إذا كان المعرف مسبوقًا بإشارة "#" أو اثنتين "##" أو متبوعًا بالإشارتين "##".

التنصيص

هناك طريقةٌ خاصة لمعالجة الأماكن التي يسبق فيها وسيط الماكرو الإشارة "#" في نص الماكرو البديل، إذ تًهمل أي مسافة فارغة تسبق أو تلي قائمة الوسطاء الفعلية للمفتاح، ومن ثم تُحوّل قائمة المفتاح والإشارة # إلى سلسلة نصية واحدة، وتُعامل المسافات بين المفاتيح كأنها محارف مسافة في سلسلة نصية؛ ولمنع حدوث أي نتائج مفاجئة، يُسبق أي محرف " أو \ في السلسلة النصية الجديدة بالمحرف \.

إليك المثال التالي الذي يوضح الخاصية المذكورة:

#define MESSAGE(x) printf("Message: %s\n", #x)

MESSAGE (Text with "quotes");
/*
* النتيجة هي
* printf("Message: %s\n", "Text with \"quotes\"");
*/

لصق المفتاح Token pasting

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

إليك عملية تحصل على عدة مراحل يُستخدم فيها إعادة المسح لتوضيح لصق المفتاح:

#define REPLACE some replacement text
#define JOIN(a, b) a ## b

JOIN(REP, LACE)
becomes, after token pasting,
REPLACE
becomes, after rescanning
some replacement text

إعادة المسح

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

#define exit(x) exit((x)+1)

تصبح أسماء الماكرو التي لم تُستبدل مفاتيحٌ محمية من الاستبدال مستقبلًا، حتى لو وردت أي عمليات تالية لتبديلها، وهذا يدرأ الخطر عن حدوث التعادوية recursion اللانهائية في المعالج المسبق، وتنطبق هذه الحماية فقط في حالة نتج اسم الماكرو عن النص البديل، وليس النص المصدري ضمن البرنامج، إليك ما الذي نقصده:

#define m(x) m((x)+1)
/* هذا */
m(abc);
/* ينتج عن هذا بعد الاستبدال */
m((abc)+1);
/*
* على الرغم من أن النتيجة السابقة تبدو مثل ماكرو
* إلا أن القواعد تنص على لزوم عدم استبداله
*/

m(m(abc));
/*
* ‏تبدأ‫ m( الخارجية باستدعاء الماكرو,
* لكن تُستبدل الداخلية أولًا
* ‫لتصبح بالشكل m((abc)+1‪) 
* وتُستخدم مثل وسيط، مما يعطينا
*/
m(m((abc+1));
/*
* ويصبح بعد الاستبدال على النحو التالي
*/
m((m((abc+1))+1);

إذا لم يؤلمك دماغك بقراءة ما سبق، فاذهب واقرأ ما الذي يقوله المعيار عن هذا ونضمن لك أنه سيؤلمك.

ملاحظات

هناك مشكلة غير واضحة تحدث عند استخدام وسطاء ماكرو الدالة.

/* تحذير: هناك مشكلة في هذا البرنامج */
#define SQR(x)  ( x * x )
/*
* عند ورود المعاملات الصورية في النص البديل، تُستبدل بالمعاملات الفعلية للماكرو
*/
printf("sqr of %d is %d\n", 2, SQR(2));

المعامل الصوري formal parameter للماكرو SQR هو x، والمعامل الفعلي actual argument هو 2، وبالتالي سينتج النص البديل عن:

printf("sqr of %d is %d\n", 2, ( 2 * 2 ));

لاحظ استخدام الأقواس، فالمثال التالي قد يتسبب بمشكلة:

/* مثال سيء */
#define DOUBLE(y) y+y

printf("twice %d is %d\n", 2, DOUBLE(2));
printf("six times %d is %d\n", 2, 3*DOUBLE(2));

تكمن المشكلة في أن التعبير الأخير في استدعاء الدالة printf الثاني يُستبدل بالتالي:

3*2+2

وهذا ينتج عن 8 وليس 12. تنص القاعدة على أنه يجب عليك الحرص بخصوص الأقواس فهي ضرورية في حالة استخدام الماكرو لبناء تعابير. إليك مثالًا آخر:

SQR(3+4)

/* تصبح بالشكل التالي بعد الاستبدال */

( 3+4 * 3+4 )
/*  للأسف، ما زالت خاطئة!‏ */

لذا، يجب عليك النظر بحرص إلى الوسطاء الصورية عندما ترِد ضمن نصل بديل. إليك الأمثلة الصحيحة عن الدالتين SQR و DOUBLE:

#define SQR(x) ((x)*(x))
#define DOUBLE(x) ((x)+(x))

في جعبة الماكرو حيلةٌ صغيرة بعد لمفاجئتك، كما سيوضح لك المثال التالي:

#include <stdio.h>
#include <stdlib.h>
#define DOUBLE(x) ((x)+(x))

main(){
      int a[20], *ip;

      ip = a;
      a[0] = 1;
      a[1] = 2;
      printf("%d\n", DOUBLE(*ip++));
      exit(EXIT_SUCCESS);
}

مثال 1

لمَ يتسبب المثال السابق بمشاكل؟ لأن نص ماكرو البديل يشير إلى ‎*ip++‎ مرتين، مما يتسبب بزيادة ip مرتين، لا يجب للماكرو أن يُستخدم مع التعابير التي لها آثار جانبية، إلا إذا تحققت بحرص من أمانها.

بغض النظر عن هذه التحذيرات التي تخص الماكرو، إلا أنها تقدم خصائص مفيدة، وستُستخدم هذه الخصائص كثيرًا من الآن فصاعدًا.

موجه التراجع عن تعريف ماكرو undef

يُمكن أن يُهمل (يُنتسى) أي معرّف يعود لموجه ‎#define بكتابة:

#undef  NAME

إذ لا يولد ‎#undef خطأً إن لم يكن الاسم NAME معرفًا مسبقًا.

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

موجه تضمين ملف مصدري include

يمكن كتابة هذا الموجه بشكلين:

#include <filename>
#include "filename"

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

يمكن الاختلاف بين استخدام <> و " " حول اسم الملف بالمكان الذي سيُبحث فيه عن الملف؛ إذ يتسبب استخدام الأقواس في البحث في عددٍ من الأماكن المعرّفة بحسب التطبيق؛ بينما يتسبب استخدام علامتي التنصيص بحثًا في المكان المرتبط بمكان ملف الشيفرة المصدرية، وستُعلمك ملاحظات تطبيقك بما هو المقصود بكلمة "المكان" والتفاصيل المرتبطة بها، إذا لم تعود عملية البحث عن الملف باستخدام علامتي التنصيص بأي نتيجة، تُعاود عملية البحث من جديد وكأنك استخدمت القوسين.

تُستخدم الأقواس عمومًا عندما تريد تحديد ملفات ترويسة لمكتبة قياسية Standard library، بينما تُستخدم علامتي التنصيص لملفات الترويسة الخاصة بك، التي تكون مخصصة غالبًا لبرنامج واحد.

لا يحدد المعيار كيفية تسمية ملف بصورةٍ صالحة، إلا أنه يحدد وجوب وجود طريقة فريدة معرفة بحسب التطبيق لترجمة اسم الملفات من الشكل xxx.x (يمثّل كل x حرفًا)، إلى أسماء ملفات الشيفرة المصدرية، ويمكن تجاهل الفرق بين الأحرف الكبيرة والصغيرة من قِبل التطبيق، ويمكن أن يختار التطبيق أيضًا ستة محارف ذات أهمية فقط بعد محرف النقطة ..

يمكنك أيضًا الكتابة بالشكل التالي:

# define NAME <stdio.h>
# include NAME

للحصول على نتيجة مماثلة لهذا:

# include <stdio.h>

إلا أن هذه الطريقة تعقيدٌ لا داعي له، وهي معرضةٌ لبعض الأخطاء طبقًا للقواعد المعرفة بحسب التطبيق إذ تحدد هذه القواعد كيف سيُعالج النص بين القوسين < >.

من الأبسط أن يكون النص البديل للماكرو NAME سلسلةً نصية، على سبيل المثال:

#define NAME "stdio.h"

#include NAME

لا يوجد في حالتنا السابقة أي فرصة للأخطاء الناتجة عن التصرف المعرف بحسب التطبيق، إلا أن مسارات البحث مختلفة كما وضحنا سابقًا. سلسلة المفتاح في حالتنا الأولى التي تستبدِل NAME هي على النحو التالي (بحسب القوانين التي ناقشناها سابقًا):

<
stdio
.
h
>

أما في الحالة الثانية فهي من الشكل:

"stdio.h"

الحالة الثانية سهلة الفهم، لأنه يوجد لدينا فقط سلسلة نصية وهي مفتاح تقليدي لموجّه ‎# include، بينما الحالة الثانية معرفةٌ بحسب التطبيق، وبالتالي يعتمد تشكيل سلسلة المفاتيح لاسم ترويسة صالح على التطبيق.

أخيرًا، المحرف الأخير من الملف المُضمّن داخل موجه include يجب أن يكون سطرًا جديدًا، وإلا سنحصل على خطأ.

الأسماء مسبقة التعريف

الأسماء التالية هي أسماء مسبقة التعريف predefined names داخل المعالج المُسبق:

  • الاسم __LINE__: ثابت عدد صحيح بالنظام العشري، ويشير إلى السطر الحالي ضمن ملف الشيفرة المصدرية.
  • الاسم __FILE__: اسم ملف الشيفرة المصدرية الحالي، وهو سلسلة نصية.
  • الاسم __DATE__: التاريخ الحالي، وهو سلسلة نصية من الشكل:
Apr 21 1990

إذ يظهر اسم الشهر كما هو معرّف في الدالة المكتبية asctime وأول خانة من التاريخ مسافة فارغة إذان كان التاريخ أقل من 10.

  • الاسم __TIME__: وقت ترجمة الملف، وهو سلسلة نصية موافقة للشكل السابق باستخدام الدالة asctime، أي من الشكل "hh:mm:ss".
  • الاسم __STDC__: عدد صحيح ثابت بقيمة 1، ويُستخدم لاختبار اتباع المصرّف لضوابط المعيار، إذ يمتلك هذا العدد قيمًا مختلفة لإصدارات مختلفة من المعيار.

الطريقة الشائعة في استخدام هذه الأسماء المعرفة مسبقًا هي على النحو التالي:

#define TEST(x) if(!(x))\
      printf("test failed, line %d file %s\n",\
              __LINE__, __FILE__)

/**/

TEST(a != 23);

/**/

مثال 2

إذا كانت نتيجة TEST في المثال السابق خطأ، فستُطبع الرسالة متضمنةً اسم الملف ورقم السطر في الرسالة. إلا أن استخدام تعليمة if في هذه الحالات قد يتسبب ببعض من اللبس، كما يوضح المثال التالي:

if(expression)
      TEST(expr2);
else
      statement_n;

إذ سترتبط تعليمة else بتعليمة if المخفية التي ستُستبدل باستخدام الماكرو TEST، وعلى الرغم من أن حدوث هذا الشيء عند الممارسة مستبعد إلا أنه سيكون خطأً لعينًا صعب الحل والتشخيص إذا حدث لك، ولتفادي ذلك، يُحبّذ استخدام الأقواس وجعل محتوى كل تعليمة تحكم بتدفق البرنامج مثل تعليمة مركّبة بغض النظر عن طولها.

لا يمكننا استعمال أي من الأسماء __LINE__ أو __FILE__ أو __DATE__ أو __TIME__ أو __STDC__ أو أي من الأسماء المعرفة الأخرى ضمن موجه ‎#define أو ‎#undef.

ينص المعيار على أن أي اسم محجوز يجب أن يبدأ بشرطةٍ سفلية underscore وحرفٍ كبير، أو شرطتان، وبالتالي يمكنك استخدام أي اسم لاستخدامك الخاص، لكن انتبه من استخدام الأسماء المحجوزة في ملفات الترويسة التي ضمّنتها في برنامجك.

موجه التحكم بتقارير الأخطاء ‎ line

يُستخدم هذا الموجه في ضبط القيمة التي يحملها كل من __LINE__ و __FILE__، لكن ما المُستفاد من ذلك؟ توّلد العديد من الأدوات في الوقت الحالي شيفرةً بلغة سي C خرجًا لها، ويسمح هذا الموجه لهذه الأدوات بالتحكم برقم السطر الحالي إلا أن استخدامه محدودٌ لمبرمج لغة سي الاعتيادية.

يأتي الموجه بالشكل التالي:

# line number optional-string-literal newline

يضبط الرقم number قيمة __LINE__ وتضبط السلسلة النصية الاختيارية إن وُجدت قيمة __FILE__. في الحقيقة، ستُوسّع سلسلة المفاتيح التي تتبع الموجه ‎#line باستخدام ماكرو، ومن المفترض أن تشكّل موجهًا صالحًا بعد التوسعة.

التصريف الشرطي

يتحكم بالتصريف الشرطي عدد من الموجهات، إذ تسمح هذه الموجهات بتصريف أجزاء معينة من البرنامج اختياريًا أو تجاهلها حسب الشروط، وهذه الشروط هي: ‎#if و ‎#ifdef و ‎#ifndef و ‎#elif و ‎#else و ‎#endif إضافةً إلى عامل المُعالج المسبق الأحادي.

يمكننا استخدام الموجهات على النحو التالي:

#ifdef  NAME
/* صرّف هذا القسم إذا كان الاسم معرّفًا */
#endif
#ifndef NAME
/* صرّف هذا القسم إذا كان الاسم غير معرّفًا */
#else
/* صرّف هذا القسم إذا كان الاسم معرّفًا */
#endif

يُستخدم كل من ‎#ifdef و ‎#endif لاختبار تعريف اسم الماكرو، ويمكن استخدام ‎#else طبعًا مع ‎#ifdef‎#if أو ‎#elif أيضًا)، لا يوجد التباس حول استخدام ‎#else، لأن استخدام ‎#endif يحدّد نطاق الموجّه مما يبعد أي مجال للشبهات. ينص المعيار على وجوب دعم ثمان طبقات من الموجهات الشرطية المتداخلة، إلا أنه من المستبعد وجود أي حدّ عمليًّا.

تُستخدم هذه الموجهات لتحديد فقرات صغيرة من برامج سي التي تعتمد على الآلة التي تعمل عليها (عندما لا يكون بالإمكان جعل كامل البرنامج ذا أداء مستقل غير مرتبط بطبيعة الآلة التي يعمل عليها)، أو لتحديد خوارزميات مختلفة تعتمد على بعض المقايضات لتنفيذها.

تأخذ بنية ‎#if و ‎#elif تعبيرًا ثابتًا وحيدًا ذا قيمةٍ صحيحة، وقيم المعالج المسبق هذه مماثلةٌ للقيم الاعتيادية باستثناء أنها يجب أن تخلو من عوامل تحويل الأنواع casts. تخضع سلسلة المفتاح التي تشكّل التعبير الثابت لعملية استبدال بالماكرو، عدا الأسماء المسبوقة بموجه التعريف فلا تُستبدل. بالاعتماد على ما سبق ذكره، فالتعبير defined NAME أو defined ( NAME )‎ يُقيّمان إلى القيمة 1 إذا كان NAME معرّفًا، وإلى القيمة 0 إن لم يكن معرّفًا، وتُستبدل جميع المعرفات ضمن التعبير -بما فيها كلمات لغة سي المفتاحية- بالقيمة 0، ومن ثم يُقيّم التعبير. يُقصد بالاستبدال (بما فيه استبدال الكلمات المفتاحية) أن sizeof لا يمكن استخدامه في هذه التعابير للحصول على القيمة التي تحصل عليها في الحالة الاعتيادية.

تُستخدم القيمة الصفر -كما هو الحال في تعليمات سي الشرطية- للدلالة على القيمة "خطأ false"، وتدل أي قيمة أخرى على القيمة "صواب true".

يجب استخدام المعالج المسبق العمليات الحسابية وفق النطاقات المحددة في ملف الترويسة <limits.h>، وأن تُعامل التعابير ذات قيمة العدد الصحيح مثل عدد صحيح طويل والعدد الصحيح عديم الإشارة مثل عدد صحيح طويل عديم الإشارة، بينما لا يتوجب على المحارف أن تكون مساوية إلى القيمة ذاتها عند وقت التنفيذ، لذا من الأفضل في البرامج القابلة للنقل أن نتجنب استخدامها في تعابير المعالج المُسبق. تعني القوانين السابقة عمومًا أنه بإمكاننا أن نحصل على نتائج حسابية من المعالج المسبق التي قد تكون مختلفة عن النتائج التي نحصل عليها عند وقت التنفيذ، وذلك بفرض إجراء الترجمة والتنفيذ على آلات مختلفة. إليك مثالًا:

#include <limits.h>

#if ULONG_MAX+1 != 0
      printf("Preprocessor: ULONG_MAX+1 != 0\n");
#endif

      if(ULONG_MAX+1 != 0)
              printf("Runtime: ULONG_MAX+1 != 0\n");

مثال 3

من الممكن أن يُجري المعالج المسبق بعض العمليات الحسابية بنطاق أكبر من النطاق المُستخدم في البيئة المُستهدفة، ويمكن في هذه الحالة ألّا يطفح تعبير المعالج المسبق ULONG MAX+1 ليعطي النتيجة 0، بينما يجب أن يحدث لك في بيئة التنفيذ.

يوضح المثال التالي استخدام الثوابت المذكور آنفًا، وتعليمة "وإلّا else" الشرطية ‎#elif.

#define NAME    100

#if     ((NAME > 50) && (defined __STDC__))
/* افعل شيئًا */
#elif   NAME > 25
/* افعل شيئًا آخر */
#elif   NAME > 10
/* افعل شيئًا آخر */
#else
/* الاحتمال الأخير */
#endif

يتوجب التنويه هنا على أن موجهات التصريف الشرطية لا تتبع لقوانين النطاق الخاصة بلغة سي، ولهذا فيجب استخدامها بحرص، إلا إذا أردت أن يصبح برنامجك صعب القراءة، إذ من الصعب أن تقرأ برنامج سي C مع وجود هذه الأشياء كل بضعة أسطر، وسيتملكك الغضب تجاه كاتب البرنامج إذا صادفت شيفرة مشابهة لهذه دون أي موجه ‎#if واضح بالقرب منها:

#else
      }
#endif

لذلك يجب أن تعامل هذه الموجهات معاملة الصلصة الحارّة، استخدامها ضروري في بعض الأحيان، إلا أن الاستخدام الزائد لها سيعقّد الأمور.

موجه التحكم المعتمد على التنفيذ pragma

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

#pragma byte_align

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

يمكن أن يُفسّر هذا الأمر بحسب تفسير التطبيق له طبعًا، وإن لم يكن للتطبيق أي تفسير خاص به، فلن يأخذ الموجه أي تأثير، ولن يُعد خطأً، ومن المثير للاهتمام رؤية بعض الاستخدامات الخاصة لهذا النوع من الموجهات.

موجه عرض رسالة خطأ قسرية ‎ error

يُتبع هذا الموجه بمفتاح أو أكثر في نهاية السطر الخاص به، وتُشكّل رسالة تشخيصية من قبل المصرف تحتوي على هذه المفاتيح، ولا توجد مزيدٌ من التفاصيل عن ذلك في المعيار. يُمكن استخدام هذا الموجه لإيقاف عملية التصريف على آلة ذات عتاد صلب غير مناسب لتنفيذ البرنامج:

#include <limits.h>
#if CHAR_MIN > -128
#error character range smaller than required
#endif

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

الخاتمة

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

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

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

ترجمة -وبتصرف- لقسم من الفصل Structured Data Types من كتاب The C Book.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...