تم إعداد ترجمة المقال خاصة لطلاب الدورة التدريبية "C ++ Developer" . هل هو مثير للاهتمام لتطوير في هذا الاتجاه؟ شاهد تسجيل فئة اختبار Google Framework Framework Practice !

الجزء 3 - الوراثة الافتراضية
في الجزءين الأول والثاني من هذه المقالة ، تحدثنا عن كيفية عمل vtables في أبسط الحالات ، ثم في الميراث المتعدد. الميراث الافتراضي يعقد الوضع أكثر.
كما تذكر ، الوراثة الافتراضية تعني أنه في فئة معينة لا يوجد سوى مثيل واحد من الفئة الأساسية. على سبيل المثال:
class ios ... class istream : virtual public ios ... class ostream : virtual public ios ... class iostream : public istream, public ostream
إذا لم تكن الكلمة الأساسية virtual
أعلاه ، iostream
فعليًا على حالتين من ios
يمكن أن يتسببان في حدوث صداع أثناء المزامنة وسيكون ببساطة غير فعال.
لفهم الوراثة الافتراضية ، سننظر في جزء التعليمات البرمجية التالي:
#include <iostream> using namespace std; class Grandparent { public: virtual void grandparent_foo() {} int grandparent_data; }; class Parent1 : virtual public Grandparent { public: virtual void parent1_foo() {} int parent1_data; }; class Parent2 : virtual public Grandparent { public: virtual void parent2_foo() {} int parent2_data; }; class Child : public Parent1, public Parent2 { public: virtual void child_foo() {} int child_data; }; int main() { Child child; }
دعونا استكشاف child
. سأبدأ بإلقاء قدر كبير من الذاكرة على وجه الدقة حيث يبدأ vtable Child
، كما فعلنا في الأجزاء السابقة ، ومن ثم تحليل النتائج. أقترح إلقاء نظرة سريعة على النتيجة هنا والعودة إليها عندما أفصح عن التفاصيل أدناه.
(gdb) p child $1 = {<Parent1> = {<Grandparent> = {_vptr$Grandparent = 0x400998 <vtable for Child+96>, grandparent_data = 0}, _vptr$Parent1 = 0x400950 <vtable for Child+24>, parent1_data = 0}, <Parent2> = {_vptr$Parent2 = 0x400978 <vtable for Child+64>, parent2_data = 4195888}, child_data = 0} (gdb) x/600xb 0x400938 0x400938 <vtable for Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400940 <vtable for Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400948 <vtable for Child+16>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400950 <vtable for Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400958 <vtable for Child+32>: 0xa0 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400960 <vtable for Child+40>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400968 <vtable for Child+48>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400970 <vtable for Child+56>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400978 <vtable for Child+64>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400980 <vtable for Child+72>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400988 <vtable for Child+80>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400990 <vtable for Child+88>: 0x00 0x0b 0x40 0x00 0x00 0x00 0x00 0x00 0x400998 <vtable for Child+96>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a0 <VTT for Child>: 0x50 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009a8 <VTT for Child+8>: 0xf8 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b0 <VTT for Child+16>: 0x18 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009b8 <VTT for Child+24>: 0x98 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c0 <VTT for Child+32>: 0xb8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009c8 <VTT for Child+40>: 0x98 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d0 <VTT for Child+48>: 0x78 0x09 0x40 0x00 0x00 0x00 0x00 0x00 0x4009d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e0 <construction vtable for Parent1-in-Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009e8 <construction vtable for Parent1-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x4009f0 <construction vtable for Parent1-in-Child+16>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x4009f8 <construction vtable for Parent1-in-Child+24>: 0x70 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a00 <construction vtable for Parent1-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a08 <construction vtable for Parent1-in-Child+40>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400a10 <construction vtable for Parent1-in-Child+48>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a18 <construction vtable for Parent1-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400a20 <typeinfo name for Parent1>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x31 0x400a28 <typeinfo name for Parent1+8>: 0x00 0x31 0x31 0x47 0x72 0x61 0x6e 0x64 0x400a30 <typeinfo name for Grandparent+7>: 0x70 0x61 0x72 0x65 0x6e 0x74 0x00 0x00 0x400a38 <typeinfo for Grandparent>: 0x50 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a40 <typeinfo for Grandparent+8>: 0x29 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a48: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a50 <typeinfo for Parent1>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400a58 <typeinfo for Parent1+8>: 0x20 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a60 <typeinfo for Parent1+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400a68 <typeinfo for Parent1+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a70 <typeinfo for Parent1+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400a78: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a80 <construction vtable for Parent2-in-Child>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a88 <construction vtable for Parent2-in-Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400a90 <construction vtable for Parent2-in-Child+16>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400a98 <construction vtable for Parent2-in-Child+24>: 0x90 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400aa0 <construction vtable for Parent2-in-Child+32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400aa8 <construction vtable for Parent2-in-Child+40>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x400ab0 <construction vtable for Parent2-in-Child+48>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ab8 <construction vtable for Parent2-in-Child+56>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00 0x400ac0 <typeinfo name for Parent2>: 0x37 0x50 0x61 0x72 0x65 0x6e 0x74 0x32 0x400ac8 <typeinfo name for Parent2+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400ad0 <typeinfo for Parent2>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400ad8 <typeinfo for Parent2+8>: 0xc0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400ae0 <typeinfo for Parent2+16>: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x400ae8 <typeinfo for Parent2+24>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400af0 <typeinfo for Parent2+32>: 0x03 0xe8 0xff 0xff 0xff 0xff 0xff 0xff 0x400af8 <typeinfo name for Child>: 0x35 0x43 0x68 0x69 0x6c 0x64 0x00 0x00 0x400b00 <typeinfo for Child>: 0xa0 0x10 0x60 0x00 0x00 0x00 0x00 0x00 0x400b08 <typeinfo for Child+8>: 0xf8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b10 <typeinfo for Child+16>: 0x02 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x400b18 <typeinfo for Child+24>: 0x50 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b20 <typeinfo for Child+32>: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b28 <typeinfo for Child+40>: 0xd0 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b30 <typeinfo for Child+48>: 0x02 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x400b38 <vtable for Grandparent>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x400b40 <vtable for Grandparent+8>: 0x38 0x0a 0x40 0x00 0x00 0x00 0x00 0x00 0x400b48 <vtable for Grandparent+16>: 0x80 0x08 0x40 0x00 0x00 0x00 0x00 0x00
واو ، هناك الكثير من المعلومات. يطفو سؤالان جديدان على الفور: ما هو VTT
وما هو البناء vtable for X-in-Child
؟ سوف نجيب عليها قريباً بما فيه الكفاية.
لنبدأ ببنية الذاكرة التابعة:
في الواقع ، لدى Child
مثيل واحد فقط من الأجداد. الشيء غير المميز هو أنه الأخير في الذاكرة ، على الرغم من أنه الأعلى في التسلسل الهرمي.
هنا هو هيكل vtable
:
أعلاه هناك مفهوم جديد - virtual-base offset
. قريبا سوف نفهم ما يفعله هناك.
بعد ذلك ، دعونا استكشاف هذه construction vtables
غريبة المظهر. هنا هو بناء vtable for Parent1-in-Child
:
في الوقت الحالي ، أعتقد أنه سيكون من المفهوم لوصف العملية أكثر من تكديس المزيد من الجداول بأرقام عشوائية عليك. لذلك:
تخيل أنك Child
. يطلب منك بناء نفسك في قطعة جديدة من الذاكرة. نظرًا لأنك ترث Grandparent
مباشرة (وهو ما يعنيه الميراث الافتراضي) ، فسوف تتصل مباشرة Parent1
مباشرة (إذا لم يكن الوراثة الافتراضية ، Parent1
، والذي بدوره سوف يطلق على مُنشئ Grandparent
). قمت بتعيين this += 32
بايت ، لأن هذا هو المكان الذي توجد فيه بيانات Grandparent
، وتدعو المنشئ. بسيط جدا
ثم حان الوقت لبناء Parent1
. يستطيع Parent1
أن يفترض بأمان أنه بحلول الوقت الذي يبني فيه نفسه ، فإن Grandparent
قد تم إنشاؤه بالفعل ، حتى يتمكن ، على سبيل المثال ، من الوصول إلى بيانات وطرق Grandparent
. لكن مهلا ، كيف يعرف أين يجد هذه البيانات؟ فهي ليست في نفس المكان مع المتغيرات Parent1
!
يدخل construction table for Parent1-in-Child
إلى مكان الحادث. هذا الجدول مخصص لإخبار Parent1
أين يمكن العثور على أجزاء من البيانات يمكنه الوصول إليها. this
يشير إلى بيانات Parent1
. يشير virtual-base offset
إلى حيث يمكنك العثور على بيانات Grandparent: الخطوة 32 بايت للأمام من هذا وستجد ذاكرة Grandparent
. هل حصلت عليه؟ يشبه الإزاحة الافتراضية الأساسية top_offset ، لكن بالنسبة للفصول الافتراضية.
الآن وقد فهمنا ذلك ، فإن بناء Parent2
هو نفسه بشكل أساسي ، فقط باستخدام construction table for Parent2-in-Child
. في الواقع ، Parent2-in-Child
على virtual-base offset
من 16 بايت.
دع المعلومات تنقع قليلاً. هل أنت مستعد للمتابعة؟ حسنا.
الآن دعنا نعود إلى VTT
. هنا هو هيكل VTT
:
VTT
تعني virtual-table table
، مما يعني أنه vtable. هذا هو جدول ترجمة يعرف ، على سبيل المثال ، ما إذا كان المُنشئ Parent1
لكائن فردي ، أو Parent1-in-SomeOtherObject
، أو Parent1-in-SomeOtherObject
. يظهر دائمًا بعد vtable
مباشرةً ، بحيث يعرف المترجم مكان العثور عليه. لذلك ، ليست هناك حاجة لتخزين مؤشر آخر في الكائنات نفسها.
فوه ... الكثير من التفاصيل ، لكنني أعتقد أننا غطينا كل ما أردت تغطيته. في الجزء الرابع سنتحدث عن تفاصيل vtables
ذات المستوى vtables
. لا تخطي ، لأن هذا ربما يكون الجزء الأكثر أهمية في هذه المقالة!
الجزء 4 - رمز تم إنشاؤه بواسطة المترجم
في هذه المرحلة من هذه المقالة ، تعلمنا كيف vtables
typeinfo
vtables
و typeinfo
مع الثنائيات الخاصة بنا وكيف يستخدمها المترجم. الآن سوف نفهم جزء العمل الذي يقوم به المترجم تلقائيًا.
الصانعين
منشئ أي فئة ، يتم إنشاء التعليمات البرمجية التالية:
- استدعاء الوالدين يبني ، إن وجدت ؛
- تحديد مؤشرات vtable ، إن وجدت ؛
- تهيئة الأعضاء وفقًا لقائمة المُهيئات ؛
- تنفيذ التعليمات البرمجية داخل أقواس المنشئ.
كل ما سبق يمكن أن يحدث بدون رمز واضح:
- تبدأ برامج إنشاء الوالدين تلقائيًا افتراضيًا ما لم ينص على خلاف ذلك ؛
- تتم تهيئة الأعضاء افتراضيًا إذا لم يكن لديهم قيمة أو إدخالات افتراضية في قائمة المهيئ ؛
- يمكن إنشاء علامة المنشئ بالكامل = افتراضي ؛
- فقط مهمة vtable مخفية دائمًا.
هنا مثال:
#include <iostream> #include <string> using namespace std; class Parent { public: Parent() { Foo(); } virtual ~Parent() = default; virtual void Foo() { cout << "Parent" << endl; } int i = 0; }; class Child : public Parent { public: Child() : j(1) { Foo(); } void Foo() override { cout << "Child" << endl; } int j; }; class Grandchild : public Child { public: Grandchild() { Foo(); s = "hello"; } void Foo() override { cout << "Grandchild" << endl; } string s; }; int main() { Grandchild g; }
لنكتب رمزًا زائفًا لمنشئ كل فصل:
بالنظر إلى هذا ، فليس من المستغرب أنه في سياق مُنشئ الفصل ، يشير vtable إلى vtable لهذه الفئة نفسها ، وليس إلى فئتها المحددة. هذا يعني أنه يتم حل المكالمات الافتراضية كما لو لم يكن هناك ورثة متاحون. وبالتالي ، فإن الخاتمة
Parent Child Grandchild
ماذا عن الوظائف الافتراضية الخالصة؟ إذا لم يتم تنفيذها (نعم ، فيمكنك تنفيذ وظائف افتراضية بحتة ، ولكن لماذا تحتاج إلى ذلك؟) ، من المحتمل أنك (ونأمل) الانتقال مباشرة إلى segfault. بعض المجمعين يهملون الخطأ ، وهو رائع.
تالفة
كما يمكنك أن تتخيل ، فإن المدمرات تتصرف بنفس طريقة سلوك المنشئات ، فقط بالترتيب العكسي.
فيما يلي تمرين سريع للتفكير: لماذا يغير المدمرون المؤشر vtable بحيث يشير إلى فئته الخاصة ، بدلاً من ترك المؤشر إلى فصل معين؟ الإجابة: بما أنه بحلول الوقت الذي تم فيه إطلاق المدمر ، فقد تم تدمير أي طبقة وراثية بالفعل. أساليب الاتصال في هذه الفئة ليست ما تريد القيام به.
يلقي ضمنا
كما رأينا في الجزأين الثاني والثالث ، فإن المؤشر إلى كائن فرعي لا يساوي بالضرورة المؤشر الأصلي لنفس المثيل (كما في حالة الوراثة المتعددة).
ومع ذلك ، بالنسبة لك (المطور) ، لا يوجد عمل إضافي لاستدعاء دالة تستقبل مؤشر أصل. وذلك لأن المحول البرمجي يغير this
ضمنيًا عند إلحاق المؤشرات والمراجع إلى فئات الأصل.
المصبوب الديناميكي (RTTI)
تستخدم القوالب الديناميكية جداول typeinfo
، والتي درسناها في الجزء الأول. يفعلون ذلك في وقت التشغيل ، typeinfo
إدخال typeinfo
بمؤشر واحد قبل ما يشير إليه المؤشر vtable
، ويستخدمون الفصل من هناك للتحقق مما إذا كان المدلى بها ممكنًا.
هذا ما يفسر تكلفة dynamic_cast عند استخدامها بشكل متكرر.
مؤشرات الطريقة
أخطط لكتابة منشور كامل حول مؤشرات الطرق في المستقبل. قبل ذلك ، أود التأكيد على أن المؤشر إلى طريقة تشير إلى دالة ظاهرية سوف يستدعي فعليًا أسلوبًا تم تجاوزه (على عكس المؤشرات إلى وظائف ليست أعضاء).
// TODO: ,
تحقق من نفسك!
يمكنك الآن أن تشرح لنفسك لماذا تتصرف جزء الكود التالي بالطريقة التي تتصرف بها:
#include <iostream> using namespace std; class FooInterface { public: virtual ~FooInterface() = default; virtual void Foo() = 0; }; class BarInterface { public: virtual ~BarInterface() = default; virtual void Bar() = 0; }; class Concrete : public FooInterface, public BarInterface { public: void Foo() override { cout << "Foo()" << endl; } void Bar() override { cout << "Bar()" << endl; } }; int main() { Concrete c; c.Foo(); c.Bar(); FooInterface* foo = &c; foo->Foo(); BarInterface* bar = (BarInterface*)(foo); bar->Bar(); // "Foo()" - WTF? }
بهذا تنتهي مقالتي المكونة من أربعة أجزاء. أتمنى أن تتعلم شيئًا جديدًا ، مثلي تمامًا.