ال vTable من أهم الأمور في الوراثة في السي++ . وهي عبارة بكل بساطة عن مصفوفة في الذاكرة كل عنصر فيها يعتبر عن مؤشر لتابع داخل الكلاس . مؤشر هذه المصفوفة موجود في أول ال Object لهذا الكلاس في الذاكرة .
عندما تقوم بكتابة كلاس يحتوي على هدة توابع . فرضا اعتبر الكلاس التالي :
CODE2
class base {
public:
void func1() {
printf("I am base::func1n");
}
void func2() {
printf("I am base::func2n");
}
void func3() {
printf("I am base::func3n");
}
};
base obj;
طبعا أنت تقر أن التابع func1 و func2 موجودين في الذاكرة بازاحات معينة ولتكن 400 و 500 على الترتيب .
الآن عندما تكتب مثلا :
CODE2
obj.func1();
فإن المترجم يفهم مباشرة العبارة السابقة على أنها استدعاء للتابع func1 من base ويقوم باستدعاء العنوان 400 . وسوف يطبع النتيجة :
I am base::func1
الآن إذا قمنا بوراثة الكلاس son من base بحيث يعيد الكلاس son تعريف التوابع func1 و func2 أما func3 فورثناه بدون تغيير :
CODE2
class son : public base {
public :
void func1() {
printf("I am son::func1n");
}
void func2() {
printf("I am son::func2n");
}
};
son obj;
base *ptr = &obj; // this is valid because base is a father of son
لاحظ كيف أصبح لدينا كائن في الذاكرة من نوع son ومن ثم حجزنا متحول من نوع مؤشر إلى base وأعيطناه عنوان ال son .
الآن إذا قمنا باستدعاء التابع func1 عن طريق obj فإننا نحصل على النتيجة :
I am son::func1
ومن ثم من ptr فإننا نحصل على النتيجة :
I am base::func1
النتيجة الاولى منطقية أما الثانية فلماذا نفذ التابع الخاطئ مع أنه لدينا كائن من son وليس base ..
في الحقيقة قام المترجم بفهم العبارة
Ptr->func1()
على أنها استدعاء للتابع func1 الذي داخل الكلاس base وقام بتكوين لغة الآلة التي تقوم باستدعاء الموقع 400 مباشرة .
في الحقيقة بما ان التابع func1 لا يصل إلى أي متحولات داخل الكلاس base فإنك حتى لو وضعت قيمة المؤشر ptr تساوي NULL فإن الكود يعمل بنجاح .
من أجل حل المشكلة السابقة نلجأ إلى ال vtable .. فكما لاحظت في الأمثلة السابق فإن عملية تحديد أيها تابع يجب أن يستدعى هي مسؤلية المترجم أي ال compiler وهذا يسمى ب early binding . أما في ال vtable كما سوف ترى فإن المترجم ليس له علاقة ويتم تحديد التابع الذي سوف يستدعى أثناء التنفيذ وهذه تسمى late binding ..
يتم بناء ال vtable لأي كلاس إذا تم اعطاء أي تابع ضمني داخل الكلاس الخاصية virtual . أي يكفي تابع واحد فقط من أجل بناء مصفوفة ال vtable .
لو فرضنا أننا قمنا بتغيير الكلاسين السابقين بحيث يصبحا يحتويان على مصفوفة ال vtable .. كالتالي :
CODE2
class base {
public:
virtual void func1() {
printf("I am base::func1n");
}
virtual void func2() {
printf("I am base::func2n");
}
virtual void func3() {
printf("I am base::func3n");
}
};
class son : public base {
public :
virtual void func1() {
printf("I am son::func1n");
}
virtual void func2() {
printf("I am son::func2n");
}
};
base obj_base;
son obj_son;
الآن ماذا أصبح لدينا : لو قمت بفحص المتحول obj في بيئة ال Visual C++ أثناء ال Debug فإنك سوف تحصل على التالي :
لاحظ أن أول حجرة في الكائن obj_base في الذاكرة هي عبارة عن متحول ضمني من نوع مؤشر إلى مصفوفة هذه المصفوفة تحتوي على ثلاثة عناصر بعدد توابع الvirtual في الكلاس . وأيضا لاحظ إلى ماذا يؤشر كل عنصر في المصفوفة .
ولاحظ الكائن obj_son الذي يحتوي أيضا على ثلاث عناصر لكنه يشارك نفس الكود تبع func3 مع الكلاس base .
ويمكن توضح الرسمة التالية أكثر :
يجب أن تنتبه إلى أمر وهو أن ترتيب وجود المؤشرات في ال vtable هو نفس ترتيب وجودها في ال class أي الذي يأتي أولا يتوضع أولا وهكذا .
الآن إذا قمنا بكتابة الكود التالي:
CODE2
base *father = &obj_son;
father->func2();
بما أن التابع func2 هو عبارة عن virtual فالمترجم لن يعرف أثناء الترجمة ألى أين يسند الاستدعاء ويقوم بتوليد الكود المناسب الذي يستخلص عنوان التابع من ال vtable . obj_son يحتوي على ال vtable التي رأيتها سابقا وعندما نقوم بأخذ عنوان هذا الكائن واسناده إلى متحول father الذي من نوع مؤشر إلى base فإن father في هذه الحالة يؤشر إلى vtable تبعة obj_son وبالتالي التابع func2 في هذه الحالة سوف ينفذ بطريقة صحيحة ويظهر النتيجة :
I am son::func2
بالنسبة لل COM فإنه أساس تعامل الCOM هو ال vtable .. إن كائنات الCOM تحتوي على Interfaces كل Interface هو عبارة عن Classe هذا الكلاس يحتوي على العديد من التوابع أو Methods . وعند استخدام هذا الCOM في أحد برامج ال client فإنها تحصل على أحد ال Interfaces التي تريد التعامل معها ومن هذه الواجهة تبدأ باستدعاء التوابع . ولكن كيف تعرف ال client أين موجود التابع من أجل استدعائه .. يتم هذا بأن تحتوي ال Interface أو الClass عند بنائه في الذاكرة على vtable . وال Client فقط تعرف ال prototype تبعة التوابع وأماكن وجودها أو ترتيبها في ال Class وبالتالي تعرف أين سوف تأخذ قيمة مؤشر التابع من ال vtable ..
طبعا بعض ال clients لا تدعم ال vtable مثل ال vbscript و jscript و ال asp و و و. ففي هذه الحالة يستخدم IDispatch من أجل تنفيذ التوابع .
بالنسبة إلى afx_msg فليس لها أي علاقة .. وهي لا شيئ .. أي تستبدل بلا شئ .. انظر تعريفها في الملف afxwin.h .
الآن بالنسبة إلى AFX_NOVTABLE فهي معرفة على أنها __declspec(novtable) إلى __declspec مستخدمة من أجل توسع للغة أو بيئة معينة ف Microsoft لها العديد من الأضافات على لغة السي++ أحدها novtable . وهي تستخدم بعد الأمر class من أجل عدم تكوين vtable للobject .. وهذا لا يعني أن طريقة التعامل هنا أصبحت early binding لا بل بقيت late binding ولكن لا يتم توليد vtable فقط . وهذا مفيد في حالة لدينا class في سلسلة الوراثة لا نستخدمه مباشرة وبالتالي لا يتم استخدام ال vtable خاصته وهذا يعني توفير في الذاكرة في حالة تم الغاء تكوين ال vtable لذاك الكلاس .
لشرح ذلك قم بوراثة كلاس آخر من البرنامج السابق ليصبح البرنامج كالتالي :
CODE2
class base {
public:
virtual void func1() {
printf("I am base::func1n");
}
virtual void func2() {
printf("I am base::func2n");
}
void func3() {
printf("I am base::func3n");
}
};
class __declspec(novtable) son : public base {
public :
virtual void func1() {
printf("I am son::func1n");
}
virtual void func2() {
printf("I am son::func2n");
}
};
class grandson : public son {
public :
virtual void func1() {
printf("I am grandson::func1n");
}
virtual void func2() {
printf("I am grandson::func2n");
}
};
base obj_base;
son obj_son;
grandson obj_grandson;
int main(int argc, char* argv[])
{
base *ptr;
ptr = &obj_son;
ptr->func1();
ptr = &obj_grandson;
ptr->func1();
return 0;
}
لاحظ كيفية وضع __declspec(novtable) بعد كلمة class .. وهذا يعني عدم تكوين مصفوفة ال vtable ..
قم بوضع break point في بداية البرنامج وادرس ال watch :

لاحظ أن المترجم عامل الكلاس son على أنه لديه vtable ولكن لاحظ مؤشر ال vtable يؤشر إلى vtable الأب وبالتالي لا يوجد مصفوفة جديدة تم حجزها خصوصي من أجل الكلاس son أما الكلاس الجديد فله vtable مثل العادة . في هذه الحالة يجب أن لا نستخدم الكلاس son أبدا لأنه قد يؤدي إلى مشاكل مثل المثال السابق في ال main ..
عندما تكتب أي COM باستخدام ال ATL فإنك سوف تجد أن الويزارد قام بوضع AFX_NOVTABLE عند الكلاس الذي تكتب فيه كود ال Interface لأنه في الحقيقة لانحتاج إلى vtable هنا وذلك بسبب أنك مطلقا وأبدا سوف تستخدم هذا الكلاس كobject وذلك بسبب أن ال ATL تسخدم كلاس آخر اسمه CComObject يرث من الكلاس تبعك والكلاس الناتج يتم منه بناء object بالذاكرة وبتم ارساله إلى ال client وهذا كله يتم في IClassFactory ... ومكنك الرجوع إلى كود ال Factory في ال ATL ...
هذا مالدي .. إذا عندك أي أسئلة فأنا جاهز ..