Menu
flag Ar iconالعربيةdown icon

الماكرو في لغة C: ‎#define والماكرو الشبيه بالدوال وقواعد الأقواس

الماكرو الشبيه بالكائنات والشبيه بالدوال بـ ‎#define - لماذا يحتاج كل وسيط والجسم كله إلى أقواس، وفخّ التقييم المتعدّد، والماكرو متعدّد الأسطر بـ do-while(0)، ومتى تكون دالة أو const هي الأداة الأفضل.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

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

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

الماكرو الشبيه بالكائنات

أبسط صيغة تعرّف اسمًا يقف مقام قيمة:

وعرفًا تكون هذه الأسماء UPPER_SNAKE_CASE ليتبيّن القارئ من نظرة واحدة أن الرمز ماكرو لا متغيّر. ويهمّ ذلك العرف هنا أكثر من المعتاد، لأن الماكرو يتجاهل النطاق: فـ #define داخل دالة ما يزال ينطبق على بقية الملف، وسيعيد بسرور كتابة متغيّر بالاسم نفسه في دالة أخرى.

و#undef NAME تزيل تعريفًا، فيمكن إعادة تعريف اسم لاحقًا دون تحذير من إعادة التعريف.

الماكرو الشبيه بالدوال

ضع قائمة وسائط مباشرةً بعد الاسم - بلا مسافة قبل القوس المفتوح - فيأخذ الماكرو وسائط:

وقاعدة المسافة حقيقية: فـ #define SQUARE (x) ((x)*(x)) تعرّف ماكرو شبيهًا بالكائنات اسمه SQUARE يبدأ نصّ استبداله بـ (x). ولن يذكر خطأ المترجم الذي يتبع شيئًا عن المسافات.

قواعد الأقواس

وهما قاعدتان، وكلتاهما موجودة لأن الجسم يُلصَق داخل تعبير لا تراه وأنت تكتب الماكرو.

القاعدة 1: قوّس كل وسيط. وبدونها، يمزّق ترتيب الأولوية وسيطًا هو نفسه تعبير:

#define SQUARE_BAD(x) x * x

SQUARE_BAD(2 + 3)   /* becomes 2 + 3 * 2 + 3  ==  11, not 25 */

القاعدة 2: قوّس الجسم كله. وبدونها، يمزّق التعبير المحيط النتيجة:

#define DOUBLE_BAD(x) (x) + (x)

10 / DOUBLE_BAD(5)  /* becomes 10 / (5) + (5)  ==  7, not 1 */

شغّل الفشلين جنبًا إلى جنب مع نسختيهما المُصلَحتين:

النسختان السيّئتان تُترجَمان دون تحذير واحد وتنتجان أرقامًا خاطئة. والعادة التي ينبغي بناؤها آلية: غلّف كل وسيط، ثم غلّف النتيجة. فـ ((a) > (b) ? (a) : (b)) تبدو مزعجة، والإزعاج هو المقصود.

فخّ التقييم المتعدّد

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

يتمدّد الماكرو إلى ((i++) > (j) ? (i++) : (j)). فتزيد المقارنةُ i إلى 6 وتقارن 5 بـ 3؛ ثم يزيد الفرع الصحيح i مجددًا إلى 7 ويعطي 6. فتكون m هي 6 وi هي 7 - ولا واحدة منهما ما ينبغي أن يعطيه "الأكبر بين i وj". ودالة max(i++, j) ستكون صحيحة، لأن الوسيط يُقيَّم مرة واحدة قبل الاستدعاء.

والفخّ نفسه يوقع بكل ما له أثر جانبي: فـ SQUARE(rand()) تستدعي rand() مرتين وتضرب عددين مختلفين. وCHECK(read_byte()) تستهلك بايتين.

ودفاعان، بترتيب الأفضلية:

  1. استخدم دالة حقيقية. وإن احتجتها لعدة أنواع، فاكتب واحدة لكل نوع، أو استخدم دالة static inline في ترويسة.
  2. وإن كان لا بدّ أن يكون ماكرو، فوثّق بصوت عالٍ أن الوسائط تُقيَّم أكثر من مرة، وأبقِ مواضع الاستدعاء خالية من الآثار الجانبية.

الماكرو متعدّد الأسطر وdo-while(0)

يمكن لجسم الماكرو أن يمتدّ على أسطر إن انتهى كل سطر عدا الأخير بشرطة عكسية. والنسخة الساذجة تبدو سليمة:

#define LOG_TWICE(msg) \
    printf("%s\n", msg); \
    printf("%s\n", msg)

ثم تنكسر بصمت، في الموضع الوحيد الذي يهمّ:

if (verbose)
    LOG_TWICE("hello");
else
    printf("quiet\n");

بعد الاستبدال تملك if أول printf فقط، ويُنفَّذ الثاني دون شرط، ولم يعد لـ else جملة if مقابلة - وهذا خطأ ترجمة لا تشير رسالته إلى مكان مفيد. وتغليف الجسم بأقواس معقوفة عادية ليس أفضل: فالفاصلة المنقوطة ; بعد LOG_TWICE("hello") تصبح جملة فارغة تُنهي if، فتنكسر else مجددًا.

والاصطلاح الذي ينجح هو do { ... } while (0):

do { ... } while (0) جملة واحدة، وتنفّذ جسمها مرة واحدة بالضبط، وتتطلّب فاصلة منقوطة بعدها - فيُقرأ موضع الاستدعاء كاستدعاء دالة عادي ويتصرّف كذلك في كل سياق تدفّق تحكّم. وانتبه للشرطات العكسية: فمسافة شاردة بعد شرطة عكسية تُنهي المتابعة وتنتج خطأً محيّرًا.

الماكرو المعرَّف مسبقًا وأنماط التوكيد

يعرّف المعالج المسبق عدة ماكرو بنفسه، وهي سبب استحالة أن تكون بعض الأشياء دوالّ:

تتمدّد __FILE__ و__LINE__ حيث تُكتب، فوضعهما داخل ماكرو يلتقط موضع المستدعي. ولا تستطيع دالة فعل ذلك - فداخل دالة ستبلّغان دائمًا عن ملف دالة التسجيل وسطرها. وهذا بالضبط كيف يبلّغ ماكرو assert المعياري عن موضع التعبير الفاشل.

وأسماء أخرى مفيدة معرَّفة مسبقًا: __DATE__ و__TIME__ و__STDC_VERSION__ (مثلًا 201710L لـ C17).

عاملان يستحقّان المعرفة

داخل جسم الماكرو، يحوّل # وسيطًا إلى ثابت نصّي ("التنصيص")، ويلصق ## الرموز معًا ("لصق الرموز").

تصبح SHOW(width * height) هي printf("width * height" " = %d\n", (width * height)); - والثوابت النصّية المتجاورة يضمّها المترجم، فيطبع استدعاء واحد نصَّ التعبير وقيمته معًا. وهي حيلة تنقيح تستحق التذكّر.

و## أندر ويظهر غالبًا في ماكرو توليد الشيفرة: فـ #define MAKE_VAR(n) int var_##n تحوّل MAKE_VAR(3) إلى int var_3. استخدمه باعتدال؛ فالمعرِّفات التي يبنيها المعالج المسبق لا يمكن البحث عنها بالاسم، ما يجعل التنقّل في الشيفرة صعبًا.

الماكرو مقابل الدوال مقابل const

استخدم أضعف أداة تؤدّي المهمّة:

  • متغيّر const - const double PI = 3.14159; له نوع، ويطيع النطاق، ويظهر في المنقّح، ولا يمكن لترويسة غير ذات صلة إعادة تعريفه. فضّله للقيم المستخدمة في وقت التشغيل.
  • enum - enum { MAX_USERS = 100 }; يعطي ثابتًا صحيحًا مسمّى يعمل حيث تطلب C ثابتًا في وقت الترجمة، بنوع ونطاق. جيّد لحدود الأعداد الصحيحة.
  • دالة static inline - مفحوصة الأنواع، وتقيّم كل وسيط مرة واحدة بالضبط، وتضمّنها المترجمات الحديثة تمامًا كما يفعل ماكرو. وهذا البديل الصحيح لكل ماكرو شبيه بالدوال تقريبًا.
  • ماكرو - حين تحتاج __LINE__، وحين يجب أن يعمل الشيء قبل وجود الأنواع (أحجام المصفوفات في الشيفرة القديمة)، وحين تبدّل الشيفرة بـالترجمة الشرطية، أو حين تولّد شيفرة متكرّرة.

وموضع واحد يبقى فيه الماكرو بلا منازع هو تحديد أحجام المصفوفات، لأن النتيجة يجب أن تكون ثابتًا في وقت الترجمة:

ولهذا تحفّظه المعروف: فهو صحيح لمصفوفة حقيقية فقط. مرّر مؤشّرًا - وهو ما تصبح إليه المصفوفة حين تُسلَّم إلى دالة - فتقيس sizeof المؤشّرَ، معطيةً إجابة خاطئة وصامتة بثقة.

تنقيح ماكرو

حين يسيء ماكرو التصرّف، لا تحدّق فيه. مدّده:

gcc -E program.c | tail -30

يخبرك النصّ المستبدَل فورًا بما إذا كانت المشكلة قوسًا مفقودًا، أو تقييمًا مزدوجًا، أو اسمًا يتصادم مع شيء آخر. وترجم بـ -Wall -Wextra أيضًا - فـ GCC وclang يضيفان ملاحظات "in expansion of macro" تربط سطر الخطأ المبلَّغ عنه رجوعًا إلى التعريف.

والتالي: ملفات الترويسة، حيث يجتمع #define و#include ليتيحا لمشروع واحد أن يمتدّ على عدة ملفات مصدر دون لصق التصريحات نفسها مرتين.

الأسئلة الشائعة

ما الماكرو في لغة C؟

قطعة نصّ مسمّاة يستبدلها المعالج المسبق في مصدرك قبل الترجمة. فـ #define MAX 100 تجعل كل MAX لاحقة تصبح 100؛ و#define SQUARE(x) ((x) * (x)) تأخذ وسائط وتستبدلها في نمط. وليس للماكرو أنواع ولا يطيع قواعد النطاق - فهو استبدال نصّي.

لماذا يحتاج الماكرو في C إلى كل هذه الأقواس؟

لأن الجسم يُلصَق داخل شيفرة محيطة ثم يُحلَّل كوحدة. فـ #define SQUARE(x) x * x تحوّل SQUARE(2 + 3) إلى 2 + 3 * 2 + 3، وهي 11 لا 25. وتغليف كل وسيط والجسم كله - ((x) * (x)) - يجعل النصّ المستبدَل يتجمّع كما قصدت بصرف النظر عمّا حوله.

ما الفرق بين الماكرو والدالة في لغة C؟

الدالة تُترجَم مرة واحدة، وتُفحَص أنواعها، وتقيّم كل وسيط مرة واحدة بالضبط. أما الماكرو فيُلصَق عند كل استخدام، ولا يفحص شيئًا، وقد يقيّم وسيطًا عدة مرات - فـ MAX(i++, j) يمكن أن تزيد i مرتين. فضّل الدوال (وconst/enum للثوابت)؛ واستخدم الماكرو لما لا تستطيعه الدوال، كالتقاط __LINE__ أو توليد شيفرة.

لماذا يُغلَّف الماكرو متعدّد الأسطر بـ do { ... } while (0)؟

ليتصرّف كجملة واحدة. فجسم { ... } المجرّد ينكسر حين تتبعه فاصلة منقوطة داخل if/else (إذ تُنهي الفاصلة if مبكرًا)، وتسلسل الجمل العادي ينكسر داخل if بلا أقواس معقوفة. أما do { ... } while (0) فجملة واحدة تقبل فاصلة منقوطة ختامية وتنفّذ جسمها مرة واحدة بالضبط.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن