موضوعنا الليله يتحدث حول التقليل من استخدام الـ Preprocessor في سي++ وأستخدام طرق أخرى للوصول الى الهدف ، وخاصه الـ #define والتي كما سنرى بعد قليل مساوئها أكثر من ميزاتها ..
وبشكل عام دائما حاول الأعتماد على الCompiler وليس الـ Preprocessor ، أي ( Prefer Compiler Than Preprocessor )
في البدايه قبل أن نذكر لماذا علينا أن نقلل من أستخدام تعليمات الـ Preprocessor ، علينا أن نتحدث عن ماهيه الـ Preprocessor ، ودوره في لغه سي++ .
الـ Preprocessor هو برنامج يقوم بوظيفه معينه قبل أن يبدأ المترجم عمله ، هذا البرنامج Preprocessor لديه تعليمات خاصه به تسمى Preprocessor Directive ، وفي سي++ أي جمله تبدأ بالعلامه # تعتبر Preprocessor Directive .
بعدما يبدأ عمل الـ Preprocessor ويقوم بمعالجه الـ Directive ، سوف ينتهي عمله ويبدأ بعدها المترجم بالعمل ، طبعا المترجم لن يرى أي Preprocessor Directive وذلك لأن Preprocessor قد عالج تلك الأوامر ، أي أن المترجم سوف يرى حاصل العمليات بعد تشغيل الـ Preprocessor .
وظيفه الـ Preprocessor غالبا تكون في أربعه أمور :
1- تعريف ثوابت Constants
2- تعريف ماكروز Macros
3- عمل تضمين لملفات الرأس Inclusion Guards
4- في عمليات compilation Control وتستخدم للتأكد من عدم اضافه ملف رأس أكثر من مره ، ومسح تعريف ثابت .
في الحاله الأولى ، عندما أريد أن أعرف ثابت سوف أقول :
#define MAX 30
أما في الحاله الثانيه ، لتعريف ماكروز (شبيه بالداله ، ولكنه أسرع في التنفيذ أولا لأنه لن يكون موجود في ملف Object الناتج لأنه كما ذكرنا أن الPreprocessor سوف يعمل له Substitute ، بعكس الداله التى لها ثقل Overhead حيث هناك Call والتي تسبب عملتي Push و pop ) :
#define MIN(x,y) (x) < (y) ? (x) : (y)
الماكروز أعلاه يقوم بارجاع العدد الأصغر من عددين .
أما الحاله الثالثه ، فدائما ما نستخدمها لكي نصل للـ Deceleration للدوال التي نستخدمها والتي تكون في ملف رأس أخر ، مثل :
#include <iostream>
الحاله الأخير وهي للتأكد من أن من سيستخدم هذا الملف لا يقوم بعمل Include أكثر من مره ، وذلك باستخدام الـ Inclusion Guards :
#ifndef WAJDY.HPP #define WAJDY.HPP // here Your Source #endif
بالنسبه للحاله الثالثه والرابعه ، فلن يمكنك الأستغناء عنها أبدا ، لأنها ضروريه جدا ، فلا يمكن أستدعاء داله موجوده في كلاس أخر الأ بعد عمل include لهذا الكلاس .. أما بالنسبه للحالتين الأولى والثانيه فهناك مساوئ في أستخدامهم ويجب أن نتجنبهم بقدر الأمكان واستخدام حلول سي++ الأكثر فعاليه ، كما سنرى بعد قليل .
لماذا لا يجب أن نستخدم #define لكي نعرف الثوابت ؟
السبب هو أن الـ Preprocessor أول ما يرى الثابت سوف يقوم بتغيير الأسم Substitue مباشره بالقيمه الخاصه بالثابت ، وبالتالي عندما يبدأ المترجم عمله سوف يرى القيمه فقط ، لنفرض أنك عرفت الثابت MAX بالقيمه 10 في الملف A.h ، المهم في الملف الأخر قمت باستخدام MAX وحصلت مشكله في هذا الثابت ، الرساله التي ستأتيك من المترجم هي خطأ في 10 وليس MAX (لأن المترجم لا يراه) ، تخيل أنك تعمل في مشروع كبير مكون من عشرات الملفات وعده مبرمجين يعملوا في المشروع ، هل تستطيع معرفه ماذا يقصد بالرقم 10 ؟
يمكن الأستغناء عن تعريف الثوابت بـ #define عن طريق const :
const int MAX = 10 ;
هنا وصلنا لنفس الهدف وبشكل أكثر كفائه ، حيث في حاله الخطأ سوف يكون واضح هو MAX ، أيضا الثوابت لا تدخل جدول الرموز Symbol Table الا في حال حصلت على عنوانها ، أو أحتاج المترجم الى الوصول الى عنوان الثابت فهنا في هذه الحاله تدخل الجدول .
لكن عند التعامل مع الـ const يجب أعطائها قيمه مباشره ، وهنا علينا سوف نقع في ثلاثه خيارات :
- ثابت عادي
- ثابت معرف داخل كلاس
- مؤشر الى ثابت
لتعريف الثابت العادي (أقصد في أي داله) :
const int MAX = 10 ;
أما الثابت المعرف داخل الكلاس ، يجب أعطائه قيمه في داله البناء في مرحله الـ Initializing والا سوف يكون هناك رساله error :
class Demo { private : const int MAX ; public : Demo () : MAX(0) { } };
جرب أعطي الثابت قيمه داخل الـ Body في داله البناء وسوف تجد المترجم يشتكي منك .
الان التعريف السابق ، يعني أن كل كائن سوف يحتوي على ثابت خاص به ، وفي الكثير من الأحيان نحن لا نريد ذلك ، مثلا كنا نريد عمل كلاس به مصفوفه من خانات ثابته ، فالأفضل عمل الثابت لجميع الكائنات (جميعا تتشارك في هذا الثابت) وهو عن طريق static .طبعا يمكن أن نعرف الثابت Global ونستخدم هذا الثابت داخل الكلاس ، ولكن سوف يهز مبدأ الحمايه والكبسله ، لذلك يفضل أن نعرف هذا الثابت داخل الكلاس ويكون static :
-
class Stack { private : static const int MAX = 10 ; int arrat[MAX]; };
بشكل عام في سي++ ، جميع الأعلانات عن المتغيرات الـ Static داخل الكلاس تكون Declare أعلان فقط من غير قيمه ، ويتم اعطاء القيمه من خارج الكلاس . ماعدا في حاله واحده هي static const to Integral Data Type أي لثابت من نوع صحيح أو حرفي char وهنا في هذه الحاله سوف يتم اعتباره Declare & Definition في نفس اللحظه .
-ربما قد تشتكي المترجمات القديمه من الكود السابق ، وهنا عليك بتعريف هذا الثابت بالخارج واعطائه قيمه هناك-
أو يمكنك أستخدام الـ Enum Hack (وهي أفضل ) حيث هي مشابه للـ #define من ناحيه أنك لا تستطيع أخذ عنوانها ، ومشابه لـ const في طريقه العمل حيث لها Range محدد .
class Stack { private : enum { MAX = 10 } ; int arrat[MAX]; };
باختصار هنا ، أستخدم const أفضل بكثير من الـ #define ، هذا الكلام في تعريف الثوابت .
نأتي الأن الى تعريف الـ Macro ؟
أولا في الماكروز هناك شروط لكي يعمل بشكل صحيح ، هي :
لا يحتوي بعد أسم الماكروز على مسافه (في حال كانت هناك معاملات مرسله)
يجب أن يكون كل متغير مرسل في قوس حتى نتعامل بشكل صحيح في حاله كان الأستدعاء عباره عن Expression .
مثال :
#include <iostream> using namespace std ; #define MIN(x,y) (x) < (y) ? (x) : (y) int main () { int z = MIN(3,4); cout << z << endl; return 0; }
سوف يطبع العدد الأصغر وهو 3 ، دعنا الأن نحاول ان نخل في الشروط لنرى مدى الخطوره ..
في حاله غيرنا الماكروز (أضفنا مسافه بعد أسم الماكرو MIN ) :
#define MIN (x,y) (x) < (y) ? (x) : (y)
الأن عندما يقوم الـ Preprocessor بعمل Substitute للسطر :
MIN(3,4
أي سيغير MIN بمحتوى الماكرو ويصبح السطر :
(x,y) (x) < (y) ? (x) : (y) (3,4)
وسوف يكون خطأ بالترجمه ، وهو أن x غير معرفه ..
الحاله الثانيه وهي بدون عمل أقواس ، سيتم فهم اي تعبير بشكل خاطئ :
شاهد المثال التالي :
#include <iostream> using namespace std ; #define CUB(x) ( (x) * (x) * (x) ) #define CUB2(x) x*x*x int main () { int x = 3 ; int a = CUB(x); int b = CUB2(x); cout << a << " " << b << endl; a = CUB(x+2) ; b = CUB2(x+2); cout << a << " " << b << endl; return 0; }
الأن في المره الأولى (في كلا الماكروين ) سوف يطبع 27 مرتين ، وهو صحيح . -لا يوجد expression –
في المره الثانيه ، حيث كان هناك عمليع جمع ، الماكرو CUB جاب النتيجه صحيحه لأنه يحتوي على أقواس ، أما الثاني طبع 17 وهي خاطئه ، حيث فهم CUB2 السطر كالتالي :
x*x*x تستبدل كل x بـ x+2 :
3+2*3+2*3+2
والأولويه في العمليات الحسابيه للضرب :
2+6+6+3
12 + 5 = 17 !!
الى هنا قد تقول سوف أستخدم ماكروز يحتوي على أقواس ومن غير مسافات ، هل هناك عيب أخر ؟ نعم في بعض الأحيان سوف تكون النتيجه غير التي تتوقعها تماما ، ليكن لدينا الماكرو التالي :
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
يقوم هذا الماكروز باستدعاء داله f بالعدد الأصغر المرسل للماكرو ..
ولدينا :
int a = 5, b = 0;
وأستدعائين :
-
CALL_WITH_MAX(++a, b); CALL_WITH_MAX(++a, b+10);
في الأستدعاء الأول سوف يحصل زياده لa مرتين . مره عند عمليه المقارنه ، ومره أخرى في الجزء true في الجمله ، حيث قيمه a أكبر من b وبالتالي تحقق الشرط ، وذهب للجزء (a) وهنا ستتم الزياده للمره الثانيه (حيث تستبدل a بـ a++ ) .
في الأستدعاء الثاني ، سوف تحصل زياده لـ a مره واحده فقط في عمليه المقارنه .
بذلك تكون رأيت مشاكل الماكرو والتي يمكن حلها كلها بعمل داله inline (داله صغيره حبوبه ، لا تأخذ مساحه ويتم عمله Substitue لها أيضا ولكن بواسطه المترجم) . ويا حبذا لو كانت هذه الداله عباره عن Template Based (لا نعرف ما نوع القيمه المرسله) ، وياحبذا لو كانت القيمه المرسله هي Reference To Const وبالتالي لا يكون هناك استدعائات كثيره لCopy Constructor
template<typename T> inline void callWithMax(const T& a, const T& b) { f(a > b ? a : b); }
هكذا نكون أنهينا مشاكل الMacro بهذه الدوال الinline وبشكل أكثر كفائه ، وأكثر حمايه حيث في الماكروز لا يوجد أي مراعاه لـ Data Type فهي Not Type Safe بعكس الدوال العاديه سواء inline أو لا .
قبل أن أنهي ، أحب أوضح ماكروز جيد وهو الـ Stringizing حيث يقوم بطباعه ما تمرره لهذا الماكروز :
#define WRITESTRING(x) cout << #x WRITESTRING(This is a string);
قبل الترجمه سوف يصبح السطر الثاني :
cout << "This is a string";
مثال بسيط :
#include <iostream> using namespace std ; #define print(x,y) cout << #x << " : " << y << endl; int main () { int b = 3 ; print(value of b,b); print(b*2,b*2); return 0; }
فقط يسهل بعض الأحيان وخاصه لطباعه نص وبعدها رقم .
الخلاصه في موضوع الماكرو:
- عند تعريف الثوابت أستخدم const
- لتعريف الدوال الصغيره (ماكروز) أستخدم الدوال الخطيه Inline Function .