ahmed.o.mohamed

[C] مُوجهات ما قبل المعالجة !

1 مشاركة في هذا الموضوع

فهرس الأسئلة :

  • ما هي فائدة الــ Preprocessor ؟
  • ما هو الــ MACRO ؟
  • كيف أستخدم ماكرو يحتوي على وسائط (Parameterized macro) ؟
  • ماذا تعني الكتابة #define MYMACRO ؟
  • لماذا تُوفر C هذا النوع من الكتابات الذي لا فائدة من ورائه ؟
  • كيف أعرف ما إذا كان ماكرو مُعَرَّف مُسبقا ؟
  • ما هي المشاكل التي قد تحدث نتيجة سوء استخدام الماكرو ؟
  • ما هو دور المؤثر # في تعريف الماكرو ؟
  • ما هو دور المؤثر ## في تعريف الماكرو ؟
  • ما هي فائدة #pragma ؟
  • متى أستخدم #error ؟
  • هل يمكننا استخدام sizeof مع #if ؟
  • ما هي الأسماء المُعَرَّفة (Predefined Names) و ما فائدتها ؟

ما هي فائدة الــ Preprocessor ؟

قبل الوصول إلى مرحلة الترجمة (Compilation), تتم معالجة الملفات المصدرية (Source Files) باستخدام الــ Preprocessor, الذي يتولى مهمة تنفيذ كافة الأوامر التي تبدأ بــ # مثل #include, #define, ..., بعد ذلك تبدأ مرحلة الترجمة : ترجمة الملف المصدر (Source file) إلى ملف كائن (File object).

ما هو الــ MACRO ؟

الماكرو عبارة عن تصريح DEFINE, قد يستقبل (أو لا يستقبل) بارامترات أو وسائط, يُستخدم الماكرو عادة لتعيين قيمة ثابتة في كل الكود, يمكن اللجوء إليها فيما بعد و شكله العام يكون كالتالي :

#define identifier replacement-text

عندما يظهر السطر السابق ضمن ملف معين, فإنه يتم استبدال كافة الكلمات المطابقة لـ identifier بالسلسة replacement-text بشكل تلقائي قبل أن تتم الترجمة, فمثلا يتم تحويل الكتابة التالية بعد المعالجة :

#define PI 3.14159

duoble CircleArea(float Radius) {
return PI * Radius * Radius;
}

إلى :

duoble CircleArea(float Radius) {
return 3.14159 * Radius * Radius;
}

كيف أستخدم ماكرو يحتوي على وسائط (Parameterized macro) ؟

كما قلنا سابقا, يستطيع الماكرو استقبال وسائط بشكل مشابه للدوال, القوس المفتوح يجب أن يتبع اسم الماكرو مباشرة (بدون Space) ثم بقية الوسائط, يليها قوس الإغلاق, ثم يأتي بعد ذلك مُحتوى الماكرو الذي يُمكن كتابته على أكثر من سطر, بشرط أن ينتهي كل سطر بــ backslash ماعدا الأخير, انظر المثال :

#include<stdio.h>
#define PRINT(x) print\
f("MESSAGE : \
%s\n", x)

int main() {
PRINT("Hello, world !");
return 0;
}

بعد انتهاء معالجة الماكرو ستتحول الكتابة السابقة إلى:

#include<stdio.h>

int main() {
printf("MESSAGE : %s\n", "Hello, world !");
return 0;
}

ماذا تعني الكتابة #define MYMACRO ؟

الكتابة السابقة تُعرِّف ماكرو باسم MYMACRO, يتم تحويله بعد عملية المعالجة إلى .. لاشيء !!

انظر المثال :

#define IN
#define OUT

int f(IN int n, OUT int * p1, OUT int * p2) {
*p1 = n - 1;
*p2 = n + 1;
return 2 * n;
}

بعد المعالجة :

int Func(int n, int * p1, int * p2) {
*p1 = n - 1;
*p2 = n + 1;
return 2 * n;
}

و لماذا تُوفر C هذا النوع من الكتابات الذي لا فائدة من ورائه ؟

لا تستعجل !, الكتابة السابقة مهمة جدا عندما نريد محاكاة الحلقات التكرارية باستخدام الماكرو, لأنها تُعتبر آلية جيدة لمنع الحلقات اللانهائية !

الإستدعاء المتكرر للماكرو يُولد حلقة تكرارية .. و عند استدعاء ماكرو عادي في نهاية الحلقة قد يُدخلنا هذا في حلقة لا نهائية, لذا من الأفضل استخدام الماكرو الصامت عند نهاية الإستدعاء كإشارة على نهاية الحلقة.

كيف أعرف ما إذا كان ماكرو مُعرف مُسبقا ؟

يُـمَكنك الشرط if defined(MYMACRO) أو ifdef MYMACRO من معرفة ما إذا كان الماكرو MYMACRO مُعرف أو لا, انظر المثال:

#include <stdio.h>
#if defined(UPPERCASE)
#define MESSAGE "HELLO, WORLD !"
#else
#define MESSAGE "Hello, world !"
#endif

int main() {
printf("%s\n", MESSAGE);
return 0;
}

في هذه الحالة, سيتم إبدال الكلمة MESSAGE بــ Hello, world (على التوالي HELLO, WORLD) إذا كان الماكرو UPPERCASE غير مُعَرَّف (على التوالي مُعرف).

بما أنه لم يتم تعريف الماكرو UPPERCASE في المثال السابق, سيقوم الــ Preprocessor بتجاهل (أو حذف) الجزء الأول من الإختبار, الموجود بين if defined و else.

ماهي المشاكل التي قد تحدث نتيجة سوء استخدام الماكرو ؟

النقاط الثلاثة التالية تُمثل أشهر المشاكل :

  1. عدم الاستخدام الصحيح للأقواس :
    ليكن الماكرو SQUARE ذو التعريف :
    #define SQUARE(x) x * x


    هو المسؤول عن حساب مساحة المربع من خلال القيمة التي تُرسل له. إذا تم إرسال البارامتر 9+1 مثلا على الماكرو السابق فإن النتائج ستكون خاطئة و غير متوقعة لدى البعض !, لأن SQUARE(9 + 1) سيتم إبدالها بــ 9 + 1 * 9 + 1 الذي يكافئ في لغة C _حسب أولوية المؤثرات_ الكتابة 9 + (1 * 9) + 1 !
    و بالتالي الخطأ نتج بسبب تقديم أولوية الضرب على الجمع في هذه الحالة, لذا يُنصح دائما باستخدام الأقواس عند إجراء عمليات كهذه :

    #define SQUARE(x) ((x) * (x))


  2. التأثيرات الجانبية (Side effects) :
    ليكن الماكرو MAX ذو التعريف :
    #define MAX(x, y) ((x) > (y) ? (x) : (y))


    هو المسؤول عن إيجاد أكبر العددين x و y.
    عندما نستدعي الماكرو MAX هكذا k = MAX(3, 2) فإن قيمة k ستكون 3 و هذا شيء بديهي !
    لكن عندما نستدعي MAX هكذا k = MAX(++i, j), حيث i و j متغيرين من النوع int, يحملان نفس القيمة (و لتكن 2 مثلا), فإن قيمة k ستكون 4 !, بالرغم من أنها يجب أن تكون 3 !!
    الخطأ في هذه الحالة نتج عن طريقة ترجمة الكتابة k = MAX(++i, j), لأن الــ Preprocessor حَوَّلها إلى :

    k = ((++i) > (j) ? (++i) : (j))


    و بالتالي, من الطبيعي جدا أن يُصبح k=4 لأن المتغير i تمت زيادته مرتين.

  3. أسماء الوسائط (Parameter name) :
    هذا النوع من الأخطاء عادة ما يحدث عند الإعلان عن الماكرو هكذا :
    #define ERR_PRINT_INT(n) fprintf(stderr, "%d\n", n)


    عند تمرير القيمة 10 إلى الماكرو, سيُصبح :

    fprintf(stderr, "%d\10", 10) !!


ما هو دور المؤثر # في تعريف الماكرو ؟

المؤثر # يسمح بتحويل الوسيط (مهما كان) إلى سلسلة محارف, انظر المثال:

#include <stdio.h>
#define TOSTR(x) #x
#define NNNNN 99999

int main() {
printf("%s\n", TOSTR(10000)); /* --> "10000" */
printf("%s\n", TOSTR(1 + 1)); /* --> "1 + 1" */
printf("%s\n", TOSTR(n + 1)); /* --> "n + 1" */
printf("%s\n", TOSTR(float)); /* --> "float" */
printf("%s\n", TOSTR(NNNNN)); /* --> "NNNNN" */
return 0;
}

كما نُلاحظ, يتم تحويل الكتابة TOSTR(NNNNN) إلى #NNNNN مع الأخذ في الإعتبار أن المعالج سيقوم بتحويل الوسيط السابق إلى "NNNNN" و ليس "99999", لكي نحصل على الكتابة الأخيرة بدل الأولى, يُمكننا استخدام الحيلة التالية:

#define TOSTR(x) __STR(x)
#define __STR(x) #x

في هذه الحالة, سيتم تحويل الكتابة TOSTR(NNNNN) إلى __STR(99999) التي سيتم تحويلها ثانية إلى "99999".

ما هو دور المؤثر ## في تعريف الماكرو ؟

يستقبل المؤثر ## نصين و يقوم بدمجهما, الماكرو الآتي:

#define CAT(x, y) x##y

يُحول CAT(C, 90) إلى C90, , انظر المثال :

#include <stdio.h>
#include <wchar.h>
#define WIDESTR(x) L##x
#define AU_REVOIR "Au revoir"

int main(void) {
wprintf(WIDESTR("%s\n"), WIDESTR("Bonjour")); /* --> wprintf(L"%s\n", L"Bonjour"); */
return 0;
}

على عكس ما سبق, سيتم تحويل الكتابة WIDESTR(AU_REVOIRE) إلى L##AU_REVOIR و من ثم إلى LAU_REVOIR !, للحصول على الكتابة L"Au revoir" بدلا من الكتابة السابقة, يمكننا استخدام الحيلة التالية:

#define WIDESTR(x) ___WSTR(x)
#define ___WSTR(x) L##x

في هذه الحالة, سيتم تحويل الكتابة WIDESTR(AU_REVOIR) إلى ___WSTR("Au revoir") التي سيتم تحويلها ثانية إلى L"Au revoir".

ما هي فائدة #pragma ؟

تكمن فائدة التوجيه #pragma في إرسال أمر معين إلى المترجم (أحد خيارات الترجمة أو الربط, ...), هذه الأوامر تكون دائما خاصة بالمترجم. ينص المعيار القياسي لــ C على أن المترجم يُمكنه تجاهل الأمر المُرسل عندما لا يستطيع التعرف عليه, كما يُمكنه أيضا إرسال تحذير (Warning) ليكون المُستخدم على علم بما يحدث.

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

متى أستخدم #error ؟

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

يعتبر البعض أن هذا التوجيه ما هو إلا محاكاة كلاسيكية للــ Exception الموجودة في C++, فمثلا إذا أردنا ترجمة كود معين بشرط أن يكون حجم char هو 8 bits فيمكنا كتابة :

#include <limits.h>
...
#if (CHAR_BIT != 8)
#error Ce programme requiert que la taille d'un char soit de 8 bits.
#endif
...

هل يمكننا استخدام sizeof مع #if ؟

لا !, لأن sizeof تتم معالجتها أثناء الترجمة و ليس قبلها.

الطريقة (أو الحيلة) التالية تسمح بتوليد خطأ مباشر أثناء الترجمة عندما لا يتحقق الشرط, في هذا المثال سيتم توليد خطأ عندما تكون sizeof(int) تختلف عن sizeof(long) :

/* Si sizeof(int) != sizeof(long), le tableau assert_int_long aura une taille negative, */
/* ce qui ne sera pas apprecie par le compilateur ... */
static int assert_int_long[sizeof(int) == sizeof(long) ? 1 : -1];

ما هي الأسماء المُعَرَّفة (Predefined Names) و ما فائدتها ؟

هي مجموعة من المختصرات Macros جاهزة, و يُمكن تمييزها بالرمزين Underscore في بداية و نهاية اسم المختصر. الجدول التالي يُبين جميع الأسماء المُعرفة :

post-219439-028901100 1345677390_thumb.p

و هذا مثال حول طريقة الاستخدام :

#include<stdio.h>

int main() {
printf("Line: %d\n", __LINE__);
printf("File: %s\n", __FILE__);
printf("Time of compilation: %s\n", __TIME__);
printf("Date of compilation: %s\n", __DATE__);
printf("AINSI C(False = 0, True = 1): %d\n", __STDC__);
return 0;
}

تم تعديل بواسطه أحمد الشنقيطي
إضافة أسئلة جديدة.
0

شارك هذا الرد


رابط المشاركة
شارك الرد من خلال المواقع ادناه

  • يستعرض القسم حالياً   0 members

    لا يوجد أعضاء مسجلين يشاهدون هذه الصفحة .