الجداول الافتراضية والمؤشرات الافتراضية للميراث الظاهري المتعدد واختيار النوع

StackOverflow https://stackoverflow.com/questions/3324721

سؤال

أنا في حيرة من أمري بشأن vptr وتمثيل الأشياء في الذاكرة، وآمل أن تتمكن من مساعدتي في فهم الأمر بشكل أفضل.

  1. يعتبر B يرث من A وكلاهما يحدد الوظائف الافتراضية f().مما تعلمته أن تمثيل كائن من الفئة B في الذاكرة يبدو كما يلي:[ vptr | A | B ]و ال vtbl الذي - التي vptr يشير إلى يحتوي على B::f().لقد فهمت أيضًا أن صب الكائن من B ل A لا يفعل شيئا سوى تجاهل B جزء في نهاية الكائن.هل هذا صحيح؟أليس هذا السلوك خاطئا؟نريد هذا الكائن من النوع A ينفذ A::f() طريقة وليس B::f().

  2. هل هناك عدد من vtables في النظام حسب عدد الفصول؟

  3. كيف سيكون أ vtable هل يبدو شكل الطبقة التي ترث من فئتين أو أكثر؟كيف سيتم تمثيل كائن C في الذاكرة؟

  4. نفس السؤال 3 ولكن مع الميراث الظاهري.

هل كانت مفيدة؟

المحلول

ما يلي ينطبق على دول مجلس التعاون الخليجي (ويبدو أنه صحيح بالنسبة إلى LLVM وصلة)، ولكن قد يكون ذلك صحيحًا أيضًا بالنسبة للمترجم الذي تستخدمه.كل هذه الأمور تعتمد على التنفيذ، ولا تخضع لمعايير C++.ومع ذلك، تقوم دول مجلس التعاون الخليجي بكتابة مستندها القياسي الثنائي الخاص بها، إيتانيوم أبي.

حاولت شرح المفاهيم الأساسية لكيفية وضع الجداول الافتراضية بكلمات أكثر بساطة كجزء من عملي مقالة عن أداء الوظيفة الافتراضية في C++, ، والتي قد تجدها مفيدة.فيما يلي إجابات لأسئلتك:

  1. الطريقة الأكثر صحة لتصوير التمثيل الداخلي للكائن هي:

    | 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 في هذه الحالة!

  2. عموما لا.يمكن أن تحتوي الفئة على عدة جداول افتراضية إذا ورثت من عدة قواعد، ولكل منها جدول افتراضي خاص بها.تشكل هذه المجموعة من الجداول الافتراضية "مجموعة جداول افتراضية" (انظر النقطة 1).3).

    يحتاج الفصل أيضًا إلى مجموعة من جداول البناء vtables، لتوزيع الوظائف الافتراضية بشكل صحيح عند إنشاء قواعد لكائن معقد.يمكنك قراءة المزيد في المعيار الذي ربطته.

  3. هنا مثال.يفترض C يرث من A و B, ، تعريف كل فئة virtual void func(), ، إلى جانب a,b أو c وظيفة افتراضية ذات صلة باسمها.

    ال C سيكون لديك مجموعة vtable من اثنين من vtables.سيتم مشاركة vtable واحد مع A (الجدول vtable الذي تنتقل إليه الوظائف الخاصة بالفئة الحالية يسمى "أساسي")، والجدول vtable for B سيتم إلحاق:

    | 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 في المجموعة، وسيكون لديك تقدير تقريبي لكيفية وضع البيانات داخل الكائن.يمكنك أن تقرأ عنها في القسم ذو الصلة المعيار الثنائي لدول مجلس التعاون الخليجي.

  4. تم وضع القواعد الافتراضية (بعضها) في نهاية مجموعة vtable.يتم ذلك لأن كل فئة يجب أن تحتوي على قاعدة افتراضية واحدة فقط، وإذا تم خلطها مع جداول افتراضية "عادية"، فلن يتمكن المترجم من إعادة استخدام أجزاء من جداول افتراضية تم إنشاؤها لإنشاء تلك الخاصة بالفئات المشتقة.قد يؤدي هذا إلى حساب الإزاحات غير الضرورية وسيؤدي إلى انخفاض الأداء.

    وبسبب هذا الموضع، تقدم القواعد الافتراضية أيضًا عناصر إضافية في جداولها الافتراضية: vcall الإزاحة (للحصول على عنوان التجاوز النهائي عند القفز من المؤشر إلى قاعدة افتراضية داخل كائن كامل إلى بداية الفئة التي تتجاوز الوظيفة الافتراضية) لكل وظيفة افتراضية محددة هناك.تضيف أيضًا كل قاعدة افتراضية vbase الإزاحات، التي يتم إدراجها في جدول vtable للفئة المشتقة؛أنها تسمح بالعثور على مكان بدء بيانات القاعدة الافتراضية (لا يمكن تجميعها مسبقًا نظرًا لأن العنوان الفعلي يعتمد على التسلسل الهرمي:توجد القواعد الافتراضية في نهاية الكائن، ويختلف التحول من البداية اعتمادًا على عدد الفئات غير الافتراضية التي ترثها الفئة الحالية.).

ووه، أتمنى ألا أكون قد أدخلت الكثير من التعقيد غير الضروري.على أية حال، يمكنك الرجوع إلى المعيار الأصلي، أو إلى أي مستند خاص بالمترجم الخاص بك.

نصائح أخرى

  1. هذا يبدو صحيحا بالنسبة لي.ليس من الخطأ كما لو كنت تستخدم مؤشرا، فأنت بحاجة فقط إلى تنفيذ تطبيقات الوظائف التي توفرها B بالإضافة إلى ذلك من Vtable (يمكن أن يكون هناك عدة VTable، اعتمادا على مترجم وتعقيد التسلسل الهرمي).
  2. أود أن أقول نعم، لكنه يعتمد تطبيق برنامج التحويل البرمجي حتى لا تضطر حقا إلى معرفة ذلك.
  3. و 4. اقرأ أبعد.
  4. أود أن أوصي بالقراءة الميراث المتعددتعتبر مفيدة ، إنها مقالة طويلة ولكنها تجعل الأمور أكثر وضوحا حول الموضوع كما يفسر تفاصيل رائعة كيف تعمل الميراث في C ++ (لا تعمل روابط الأرقام ولكنها متوفرة في أسفلالصفحة).

  1. إذا كان كائن B يرث من A ثم تمثل تمثيل الذاكرة ل B وما يلي:

    • المؤشر إلى الجدول الظاهري ل
    • متغيرات / وظائف محددة
    • المؤشر إلى الجدول الظاهري من B
    • B متغيرات / وظائف / تدخل محددة / تجاوز

    إذا كان لديك B * B= NEW B ()؛ (أ) B-> f () ثم:

    • إذا تم الإعلان عن F كدالة افتراضية، فإن تطبيق B يسمى لأن B من النوع B
    • إذا لم يتم الإعلان عن f كدالة افتراضية، فلن يكون هناك أي بحث في أنه لا يوجد بحث في VTable للتنفيذ الصحيح وسيتم استدعاء تنفيذ A.
  2. كل كائن سيكون له vtable الخاص (لا تأخذ هذا أمرا مفروغا منه، كما يجب أن أبحث عنه

  3. إلقاء نظرة على هذا للحصول على مثال لوضع vtable عند التعامل مع الميراث المتعددة

  4. شاهد هذا لمناقشة حول ميراث الماس والتمثيل vtable < / P>

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top