الجداول الافتراضية والمؤشرات الافتراضية للميراث الظاهري المتعدد واختيار النوع
-
28-09-2020 - |
سؤال
أنا في حيرة من أمري بشأن vptr وتمثيل الأشياء في الذاكرة، وآمل أن تتمكن من مساعدتي في فهم الأمر بشكل أفضل.
يعتبر
B
يرث منA
وكلاهما يحدد الوظائف الافتراضيةf()
.مما تعلمته أن تمثيل كائن من الفئة B في الذاكرة يبدو كما يلي:[ vptr | A | B ]
و الvtbl
الذي - التيvptr
يشير إلى يحتوي علىB::f()
.لقد فهمت أيضًا أن صب الكائن منB
لA
لا يفعل شيئا سوى تجاهلB
جزء في نهاية الكائن.هل هذا صحيح؟أليس هذا السلوك خاطئا؟نريد هذا الكائن من النوعA
ينفذA::f()
طريقة وليسB::f()
.هل هناك عدد من
vtables
في النظام حسب عدد الفصول؟كيف سيكون أ
vtable
هل يبدو شكل الطبقة التي ترث من فئتين أو أكثر؟كيف سيتم تمثيل كائن C في الذاكرة؟نفس السؤال 3 ولكن مع الميراث الظاهري.
المحلول
ما يلي ينطبق على دول مجلس التعاون الخليجي (ويبدو أنه صحيح بالنسبة إلى LLVM وصلة)، ولكن قد يكون ذلك صحيحًا أيضًا بالنسبة للمترجم الذي تستخدمه.كل هذه الأمور تعتمد على التنفيذ، ولا تخضع لمعايير C++.ومع ذلك، تقوم دول مجلس التعاون الخليجي بكتابة مستندها القياسي الثنائي الخاص بها، إيتانيوم أبي.
حاولت شرح المفاهيم الأساسية لكيفية وضع الجداول الافتراضية بكلمات أكثر بساطة كجزء من عملي مقالة عن أداء الوظيفة الافتراضية في C++, ، والتي قد تجدها مفيدة.فيما يلي إجابات لأسئلتك:
الطريقة الأكثر صحة لتصوير التمثيل الداخلي للكائن هي:
| vptr | ======= | ======= | <-- your object |----A----| | |---------B---------|
B
يتضمن فئتها الأساسيةA
, ، فهو يضيف فقط اثنين من أعضائه بعد نهايته.الصب من
B*
لA*
في الواقع لا يفعل شيئًا، فهو يُرجع نفس المؤشر، وvptr
بقي على حاله.ولكن باختصار، لا يتم استدعاء الوظائف الافتراضية دائمًا عبر vtable.في بعض الأحيان يتم استدعاؤها تمامًا مثل الوظائف الأخرى.وهنا شرح أكثر تفصيلا.يجب عليك التمييز بين طريقتين لاستدعاء وظيفة العضو:
A a, *aptr; a.func(); // the call to A::func() is precompiled! aptr->A::func(); // ditto aptr->func(); // calls virtual function through vtable. // It may be a call to A::func() or B::func().
الشيء هو أنه معروف في وقت التجميع كيف سيتم استدعاء الدالة:عبر vtable أو مجرد مكالمة عادية.والشيء هو ذلك يُعرف نوع تعبير الصب في وقت الترجمة, وبالتالي يختار المترجم الوظيفة المناسبة في وقت الترجمة.
B b, *bptr; static_cast<A>(b)::func(); //calls A::func, because the type // of static_cast<A>(b) is A!
لا يبدو حتى داخل vtable في هذه الحالة!
عموما لا.يمكن أن تحتوي الفئة على عدة جداول افتراضية إذا ورثت من عدة قواعد، ولكل منها جدول افتراضي خاص بها.تشكل هذه المجموعة من الجداول الافتراضية "مجموعة جداول افتراضية" (انظر النقطة 1).3).
يحتاج الفصل أيضًا إلى مجموعة من جداول البناء vtables، لتوزيع الوظائف الافتراضية بشكل صحيح عند إنشاء قواعد لكائن معقد.يمكنك قراءة المزيد في المعيار الذي ربطته.
هنا مثال.يفترض
C
يرث منA
وB
, ، تعريف كل فئةvirtual void func()
, ، إلى جانبa
,b
أوc
وظيفة افتراضية ذات صلة باسمها.ال
C
سيكون لديك مجموعة vtable من اثنين من vtables.سيتم مشاركة vtable واحد معA
(الجدول vtable الذي تنتقل إليه الوظائف الخاصة بالفئة الحالية يسمى "أساسي")، والجدول vtable forB
سيتم إلحاق:| C::func() | a() | c() || C::func() | b() | |---- vtable for A ----| |---- vtable for B ----| |--- "primary virtual table" --||- "secondary vtable" -| |-------------- virtual table group for C -------------|
سيبدو تمثيل الكائن في الذاكرة بنفس الطريقة التي يبدو بها جدول vtable.فقط أضف أ
vptr
قبل كل جدول vtable في المجموعة، وسيكون لديك تقدير تقريبي لكيفية وضع البيانات داخل الكائن.يمكنك أن تقرأ عنها في القسم ذو الصلة المعيار الثنائي لدول مجلس التعاون الخليجي.تم وضع القواعد الافتراضية (بعضها) في نهاية مجموعة vtable.يتم ذلك لأن كل فئة يجب أن تحتوي على قاعدة افتراضية واحدة فقط، وإذا تم خلطها مع جداول افتراضية "عادية"، فلن يتمكن المترجم من إعادة استخدام أجزاء من جداول افتراضية تم إنشاؤها لإنشاء تلك الخاصة بالفئات المشتقة.قد يؤدي هذا إلى حساب الإزاحات غير الضرورية وسيؤدي إلى انخفاض الأداء.
وبسبب هذا الموضع، تقدم القواعد الافتراضية أيضًا عناصر إضافية في جداولها الافتراضية:
vcall
الإزاحة (للحصول على عنوان التجاوز النهائي عند القفز من المؤشر إلى قاعدة افتراضية داخل كائن كامل إلى بداية الفئة التي تتجاوز الوظيفة الافتراضية) لكل وظيفة افتراضية محددة هناك.تضيف أيضًا كل قاعدة افتراضيةvbase
الإزاحات، التي يتم إدراجها في جدول vtable للفئة المشتقة؛أنها تسمح بالعثور على مكان بدء بيانات القاعدة الافتراضية (لا يمكن تجميعها مسبقًا نظرًا لأن العنوان الفعلي يعتمد على التسلسل الهرمي:توجد القواعد الافتراضية في نهاية الكائن، ويختلف التحول من البداية اعتمادًا على عدد الفئات غير الافتراضية التي ترثها الفئة الحالية.).
ووه، أتمنى ألا أكون قد أدخلت الكثير من التعقيد غير الضروري.على أية حال، يمكنك الرجوع إلى المعيار الأصلي، أو إلى أي مستند خاص بالمترجم الخاص بك.
نصائح أخرى
- هذا يبدو صحيحا بالنسبة لي.ليس من الخطأ كما لو كنت تستخدم مؤشرا، فأنت بحاجة فقط إلى تنفيذ تطبيقات الوظائف التي توفرها B بالإضافة إلى ذلك من Vtable (يمكن أن يكون هناك عدة VTable، اعتمادا على مترجم وتعقيد التسلسل الهرمي).
- أود أن أقول نعم، لكنه يعتمد تطبيق برنامج التحويل البرمجي حتى لا تضطر حقا إلى معرفة ذلك.
- و 4. اقرأ أبعد.
أود أن أوصي بالقراءة الميراث المتعددتعتبر مفيدة ، إنها مقالة طويلة ولكنها تجعل الأمور أكثر وضوحا حول الموضوع كما يفسر تفاصيل رائعة كيف تعمل الميراث في C ++ (لا تعمل روابط الأرقام ولكنها متوفرة في أسفلالصفحة).
-
إذا كان كائن B يرث من A ثم تمثل تمثيل الذاكرة ل B وما يلي:
- المؤشر إلى الجدول الظاهري ل
- متغيرات / وظائف محددة
- المؤشر إلى الجدول الظاهري من B
- B متغيرات / وظائف / تدخل محددة / تجاوز
- إذا تم الإعلان عن F كدالة افتراضية، فإن تطبيق B يسمى لأن B من النوع B
- إذا لم يتم الإعلان عن f كدالة افتراضية، فلن يكون هناك أي بحث في أنه لا يوجد بحث في VTable للتنفيذ الصحيح وسيتم استدعاء تنفيذ A.
-
كل كائن سيكون له vtable الخاص (لا تأخذ هذا أمرا مفروغا منه، كما يجب أن أبحث عنه
-
إلقاء نظرة على هذا للحصول على مثال لوضع vtable عند التعامل مع الميراث المتعددة
-
شاهد هذا لمناقشة حول ميراث الماس والتمثيل vtable < / P>
إذا كان لديك B * B= NEW B ()؛ (أ) B-> f () ثم: