سؤال

class A                     { public: void eat(){ cout<<"A";} }; 
class B: virtual public A   { public: void eat(){ cout<<"B";} }; 
class C: virtual public A   { public: void eat(){ cout<<"C";} }; 
class D: public         B,C { public: void eat(){ cout<<"D";} }; 

int main(){ 
    A *a = new D(); 
    a->eat(); 
} 

أنا أفهم مشكلة الماس ، وفوق جزء من الكود ليس لديه هذه المشكلة.

كيف بالضبط الميراث الافتراضي يحل المشكلة؟

ما أفهمه:عندما أقول A *a = new D();, ، المترجم يريد أن يعرف ما إذا كان كائن من النوع D يمكن تعيينه إلى مؤشر من النوع A, ، لكن لديه مساران يمكن أن يتبعهما ، لكن لا يمكن أن يقرر بنفسه.

لذا ، كيف يحل الوراثة الافتراضية المشكلة (برنامج التحويل البرمجي للمساعدة يتخذ القرار)؟

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

المحلول

انت تريد: (يمكن تحقيقها مع الميراث الافتراضي)

  A  
 / \  
B   C  
 \ /  
  D 

و لا: (ماذا يحدث بدون ميراث افتراضي)

A   A  
|   |
B   C  
 \ /  
  D 

الميراث الافتراضي يعني أنه سيكون هناك حالة واحدة فقط من القاعدة A الفصل ليس 2.

نوعك D سيكون لديك مؤشران قابلتين (يمكنك رؤيتها في الرسم البياني الأول) ، واحد ل B وواحد ل C الذين يرثون فعليًا A. Dيتم زيادة حجم الكائن لأنه يخزن 2 مؤشرات الآن ؛ ومع ذلك هناك واحد فقط A حاليا.

لذا B::A و C::A هي نفسها وهكذا لا يمكن أن تكون هناك مكالمات غامضة من D. إذا لم تستخدم الميراث الظاهري ، فلديك الرسم البياني الثاني أعلاه. وأي مكالمة لعضو من A تصبح غامضة وتحتاج إلى تحديد المسار الذي تريد أن تأخذه.

Wikipedia لديها متهورة ومثال جيد آخر هنا

نصائح أخرى

مثيلات من الفصول المشتقة "تحتوي على" مثيلات من الفصول الأساسية ، بحيث تنظر في الذاكرة مثل ذلك:

class A: [A fields]
class B: [A fields | B fields]
class C: [A fields | C fields]

وبالتالي ، دون الميراث الافتراضي ، فإن مثيل الفئة D سيبدو:

class D: [A fields | B fields | A fields | C fields | D fields]
          '- derived from B -' '- derived from C -'

لذلك ، لاحظ اثنين من نسختين من البيانات. الوراثة الافتراضية تعني أنه يوجد مؤشر VTABLE داخل الفئة المشتقة في وقت التشغيل يشير إلى بيانات الفئة الأساسية ، بحيث تبدو حالات B و C و D مثل:

class B: [A fields | B fields]
          ^---------- pointer to A

class C: [A fields | C fields]
          ^---------- pointer to A

class D: [A fields | B fields | C fields | D fields]
          ^---------- pointer to B::A
          ^--------------------- pointer to C::A

لماذا إجابة أخرى؟

حسنًا ، تقول العديد من المنشورات على SO والمقالات في الخارج ، أن مشكلة الماس يتم حلها عن طريق إنشاء مثيل واحد A بدلا من اثنين (واحد لكل والد D) ، وبالتالي حل الغموض. ومع ذلك ، هذا لم يمنحني فهمًا شاملاً للعملية ، انتهى بي الأمر بمزيد من الأسئلة مثل

  1. ماذا إذا B و C يحاول إنشاء مثيلات مختلفة A على سبيل المثال ، استدعاء مُنشئ معلمات مختلفة (D::D(int x, int y): C(x), B(y) {}))؟ أي حالة A سيتم اختيارها لتصبح جزءًا من D?
  2. ماذا لو استخدمت الميراث غير الذروة ل B, ، ولكن افتراضي واحد ل C؟ هل يكفي إنشاء مثيل واحد A في D?
  3. هل يجب أن أستخدم دائمًا الميراث الافتراضي افتراضيًا من الآن فصاعدًا كقياس وقائي لأنه يحل مشكلة الماس المحتملة بتكلفة الأداء البسيطة وعدم وجود عيوب أخرى؟

عدم القدرة على التنبؤ بالسلوك دون تجربة عينات التعليمات البرمجية يعني عدم فهم المفهوم. فيما يلي ما ساعدني على لف رأس الميراث الافتراضي.

مضاعفة أ

أولاً ، لنبدأ بهذا الرمز دون الميراث الافتراضي:

#include<iostream>
using namespace std;
class A {
public:
    A()                { cout << "A::A() "; }
    A(int x) : m_x(x)  { cout << "A::A(" << x << ") "; }
    int getX() const   { return m_x; }
private:
    int m_x = 42;
};

class B : public A {
public:
    B(int x):A(x)   { cout << "B::B(" << x << ") "; }
};

class C : public A {
public:
    C(int x):A(x) { cout << "C::C(" << x << ") "; }
};

class D : public C, public B  {
public:
    D(int x, int y): C(x), B(y)   {
        cout << "D::D(" << x << ", " << y << ") "; }
};

int main()  {
    cout << "Create b(2): " << endl;
    B b(2); cout << endl << endl;

    cout << "Create c(3): " << endl;
    C c(3); cout << endl << endl;

    cout << "Create d(2,3): " << endl;
    D d(2, 3); cout << endl << endl;

    // error: request for member 'getX' is ambiguous
    //cout << "d.getX() = " << d.getX() << endl;

    // error: 'A' is an ambiguous base of 'D'
    //cout << "d.A::getX() = " << d.A::getX() << endl;

    cout << "d.B::getX() = " << d.B::getX() << endl;
    cout << "d.C::getX() = " << d.C::getX() << endl;
}

دعنا نذهب من خلال الإخراج. التنفيذ B b(2); يخلق A(2) كما هو متوقع ، نفس الشيء ل C c(3);:

Create b(2): 
A::A(2) B::B(2) 

Create c(3): 
A::A(3) C::C(3) 

D d(2, 3); يحتاج على حد سواء B و C, ، كل واحد منهم يخلق خاصة به A, ، لذلك لدينا ضعف A في d:

Create d(2,3): 
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3) 

هذا هو السبب d.getX() للتسبب في خطأ في التجميع كمترجم لا يمكن اختيار أي خطأ A مثيل يجب أن يتصل به. لا يزال من الممكن استدعاء الأساليب مباشرة لفئة الوالدين المختارة:

d.B::getX() = 3
d.C::getX() = 2

الافتراضية

الآن دعنا نضيف الميراث الافتراضي. باستخدام عينة الكود نفسها مع التغييرات التالية:

class B : virtual public A
...
class C : virtual public A
...
cout << "d.getX() = " << d.getX() << endl; //uncommented
cout << "d.A::getX() = " << d.A::getX() << endl; //uncommented
...

لنقفز إلى إنشاء d:

Create d(2,3): 
A::A() C::C(2) B::B(3) D::D(2, 3) 

يمكنك ان ترى، A يتم إنشاؤه مع مُنشئ الافتراض الذي يتجاهل المعلمات التي تم تمريرها من مُنشئين من B و C. مع اختفاء الغموض ، جميع المكالمات getX() إرجاع نفس القيمة:

d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42

ولكن ماذا لو كنا نرغب في الاتصال بمنشئ ParameTructor A؟ يمكن القيام به من خلال تسميته بشكل صريح من مُنشئ D:

D(int x, int y, int z): A(x), C(y), B(z)

عادة ، يمكن للطبقة استخدام منشئات الآباء المباشرين بشكل صريح فقط ، ولكن هناك استبعاد لحالة الميراث الافتراضي. اكتشاف هذه القاعدة "نقرت" بالنسبة لي وساعد في فهم الواجهات الافتراضية كثيرًا:

شفرة class B: virtual A يعني أن أي فئة ورثت من B مسؤول الآن عن إنشاء A في حد ذاته ، منذ ذلك الحين B لن تفعل ذلك تلقائيًا.

مع وضع هذا البيان في الاعتبار ، من السهل الإجابة على جميع الأسئلة التي أجريتها:

  1. أثناء D الخلق لا B ولا C مسؤول عن معايير A, ، الأمر متروك تمامًا D فقط.
  2. C سوف تفوض إنشاء A إلى D, ، لكن B سوف تنشئ مثيلها الخاص A وبالتالي إعادة مشكلة الماس
  3. إن تحديد معلمات الطبقة الأساسية في فئة الأحفاد بدلاً من الطفل المباشر ليس ممارسة جيدة ، لذلك يجب التسامح عند وجود مشكلة الماس وهذا التدبير أمر لا مفر منه.

المشكلة ليست طريق يجب أن يتبع المترجم. المشكلة هي نقطة النهاية من هذا المسار: نتيجة الممثلين. عندما يتعلق الأمر بكتابة التحويلات ، فإن المسار لا يهم ، فقط النتيجة النهائية لا تفعل ذلك.

إذا كنت تستخدم الميراث العادي ، فإن كل مسار له نقطة نهاية مميزة خاصة به ، مما يعني أن نتيجة الممثلين غامضة ، وهي المشكلة.

إذا كنت تستخدم الميراث الافتراضي ، فستحصل على تسلسل هرمي على شكل الماس: يؤدي كلا المسارين إلى نفس نقطة النهاية. في هذه الحالة ، لم تعد مشكلة اختيار المسار موجودة (أو ، على وجه التحديد ، لم تعد مهمة) ، لأن كلا المسارين يؤديان إلى نفس النتيجة. والنتيجة لم تعد غامضة - وهذا هو ما يهم. المسار الدقيق لا.

في الواقع يجب أن يكون المثال على النحو التالي:

#include <iostream>

//THE DIAMOND PROBLEM SOLVED!!!
class A                     { public: virtual ~A(){ } virtual void eat(){ std::cout<<"EAT=>A";} }; 
class B: virtual public A   { public: virtual ~B(){ } virtual void eat(){ std::cout<<"EAT=>B";} }; 
class C: virtual public A   { public: virtual ~C(){ } virtual void eat(){ std::cout<<"EAT=>C";} }; 
class D: public         B,C { public: virtual ~D(){ } virtual void eat(){ std::cout<<"EAT=>D";} }; 

int main(int argc, char ** argv){
    A *a = new D(); 
    a->eat(); 
    delete a;
}

... وبهذه الطريقة ، سيكون الإخراج هو الصحيح: "EAT => D"

الميراث الافتراضي يحل فقط ازدواجية الجد! ولكن لا تزال بحاجة إلى تحديد الطرق التي يجب أن تكون افتراضية من أجل الحصول على الأساليب بشكل صحيح ...

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