inheritance polymorphism - كيف تعلن واجهة في C++؟




geeksforgeeks (13)

بقدر ما يمكنني اختبار ، من المهم للغاية إضافة destructor الظاهري. أنا أستخدم كائنات تم إنشاؤها باستخدام new ودمرت مع delete .

إذا لم تقم بإضافة destructor الظاهري في الواجهة ، ثم يتم استدعاء destructor للفئة الموروثة.

class IBase {
public:
    virtual ~IBase() {}; // destructor, use it to call destructor of the inherit classes
    virtual void Describe() = 0; // pure virtual method
};

class Tester : public IBase {
public:
    Tester(std::string name);
    virtual ~Tester();
    virtual void Describe();
private:
    std::string privatename;
};

Tester::Tester(std::string name) {
    std::cout << "Tester constructor" << std::endl;
    this->privatename = name;
}

Tester::~Tester() {
    std::cout << "Tester destructor" << std::endl;
}

void Tester::Describe() {
    std::cout << "I'm Tester [" << this->privatename << "]" << std::endl;
}


void descriptor(IBase * obj) {
    obj->Describe();
}

int main(int argc, char** argv) {

    std::cout << std::endl << "Tester Testing..." << std::endl;
    Tester * obj1 = new Tester("Declared with Tester");
    descriptor(obj1);
    delete obj1;

    std::cout << std::endl << "IBase Testing..." << std::endl;
    IBase * obj2 = new Tester("Declared with IBase");
    descriptor(obj2);
    delete obj2;

    // this is a bad usage of the object since it is created with "new" but there are no "delete"
    std::cout << std::endl << "Tester not defined..." << std::endl;
    descriptor(new Tester("Not defined"));


    return 0;
}

إذا قمت بتشغيل التعليمات البرمجية السابقة دون virtual ~IBase() {}; ، سترى أن the destructor Tester::~Tester() لا يسمى أبدا.

كيف أقوم بإعداد فصل دراسي يمثل واجهة؟ هل هذا مجرد طبقة أساسية مجردة؟


لا يوجد مفهوم "واجهة" في حد ذاتها في C ++. قدمت AFAIK ، واجهات لأول مرة في جاوة للعمل حول عدم وجود الميراث متعددة. لقد تبين أن هذا المفهوم مفيد للغاية ، ويمكن تحقيق نفس التأثير في C ++ باستخدام طبقة أساسية مجردة.

الفئة الأساسية التجريدية هي فئة فيها وظيفة عضو واحد على الأقل (طريقة في لغة جافا) هي دالة ظاهرية خالصة يتم الإعلان عنها باستخدام البنية التالية:

class A
{
  virtual void foo() = 0;
};

لا يمكن إنشاء فئة أساسية مجردة ، أي لا يمكنك تعريف كائن من الفئة A. يمكنك فقط استخلاص الفئات من A ، ولكن أي فئة مشتقة لا توفر تطبيق foo() ستكون أيضًا مجردة. من أجل التوقف عن التجريد ، يجب أن توفر فئة مشتقة تطبيقات لكل الوظائف الظاهرية الخالصة التي ترثها.

لاحظ أن فئة أساسية مجردة يمكن أن تكون أكثر من واجهة ، لأنها يمكن أن تحتوي على أعضاء البيانات ووظائف الأعضاء التي ليست محض افتراضية. أي ما يعادل واجهة سيكون فئة أساسية مجردة دون أي بيانات مع وظائف افتراضية نقية فقط.

وكما أشار مارك رانسوم ، يجب أن توفر طبقة أساسية مجردة دمارًا افتراضيًا ، تمامًا مثل أي طبقة أساسية ، لهذه المسألة.


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

مشوش؟


    --- header file ----
    class foo {
    public:
      foo() {;}
      virtual ~foo() = 0;

      virtual bool overrideMe() {return false;}
    };

    ---- source ----
    foo::~foo()
    {
    }

السبب الرئيسي الذي تود القيام به هو إذا كنت تريد توفير أساليب واجهة ، كما فعلت ، ولكن جعل تجاوز الاختيارية.

لجعل الطبقة فئة الطبقة يتطلب طريقة افتراضية نقية ، ولكن جميع الأساليب الافتراضية الخاصة بك لديها تطبيقات افتراضية ، لذلك فإن الطريقة الوحيدة المتبقية لجعل الظاهري النقي هو destructor.

إن إعادة تنفيذ أحد العناصر المدمرة في الطبقة المشتقة لا يشكل صفقة كبيرة على الإطلاق - فأنا دائمًا أعيد تشغيل أداة تدميرية ، افتراضية أم لا ، في الطبقات المستمدة.


في C ++ 11 ، يمكنك بسهولة تجنب التوريث تمامًا:

struct Interface {
  explicit Interface(SomeType& other)
  : foo([=](){ return other.my_foo(); }), 
    bar([=](){ return other.my_bar(); }), /*...*/ {}
  explicit Interface(SomeOtherType& other)
  : foo([=](){ return other.some_foo(); }), 
    bar([=](){ return other.some_bar(); }), /*...*/ {}
  // you can add more types here...

  // or use a generic constructor:
  template<class T>
  explicit Interface(T& other)
  : foo([=](){ return other.foo(); }), 
    bar([=](){ return other.bar(); }), /*...*/ {}

  const std::function<void(std::string)> foo;
  const std::function<void(std::string)> bar;
  // ...
};

في هذه الحالة ، تحتوي واجهة على دلالات مرجعية ، أي يجب عليك التأكد من أن الكائن يتجاوز الواجهة (من الممكن أيضًا عمل واجهات مع دلالات القيمة).

هذا النوع من الواجهات له مزاياه وسلبياته:

  • أنها تتطلب ذاكرة أكثر من تعدد الأشكال القائم على الوراثة.
  • هم بشكل عام أسرع من التعدد القائم على التوارث.
  • في تلك الحالات التي تعرف فيها النوع النهائي ، فهي أسرع بكثير! (يقوم بعض المترجمين مثل gcc و clang بإجراء المزيد من التحسينات في الأنواع التي ليس لديها / ترث من الأنواع ذات الوظائف الظاهرية).

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

قل لدي تطبيق أتعامل فيه مع الأشكال الخاصة بي بشكل متعدد الأشكال باستخدام واجهة MyShape :

struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle

في التطبيق الخاص بك ، تفعل الشيء نفسه مع الأشكال المختلفة باستخدام واجهة YourShape :

struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...

الآن قل أنك تريد استخدام بعض الأشكال التي قمت بتطويرها في التطبيق الخاص بك. من الناحية المفاهيمية ، تحتوي الأشكال الخاصة بنا على نفس الواجهة ، ولكن لجعل الأشكال الخاصة بي تعمل في التطبيق الخاص بك ، سوف تحتاج إلى توسيع أشكالي كما يلي:

struct Circle : MyShape, YourShape { 
  void my_draw() { /*stays the same*/ };
  void your_draw() { my_draw(); }
};

أولاً ، قد لا يكون تعديل الأشكال الخاصة بي ممكنًا على الإطلاق. وعلاوة على ذلك ، فإن الميراث المتعدد يقود الطريق إلى كود السباغيتي (تخيل أن مشروع ثالث يأتي في ذلك باستخدام واجهة TheirShape ... ماذا يحدث إذا قاموا أيضا باستدعاء وظيفة السحب الخاصة بهم my_draw ؟).

تحديث: هناك اثنين من المراجع الجديدة حول تعدد الأشكال القائم على غير الوراثة:


جعل فئة مع وسائل افتراضية نقية. استخدم الواجهة عن طريق إنشاء فصل آخر يتجاوز تلك الطرق الافتراضية.

الطريقة الافتراضية الخالصة هي طريقة الفئة التي يتم تعريفها على أنها افتراضية ويتم تعيينها إلى 0.

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Child : public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};

للتوسع في الإجابة عن طريق bradtgmurray ، قد ترغب في إجراء استثناء واحد لقائمة الطريقة الظاهرية الخاصة بالواجهة عن طريق إضافة destructor افتراضي. هذا يسمح لك بتمرير الملكية إلى طرف آخر دون تعريض الطبقة المشتقة من الخرسانة. لا يحتاج المدمر إلى فعل أي شيء ، لأن الواجهة لا تحتوي على أي أعضاء ملموسين. قد يبدو من المتناقض تحديد وظيفة افتراضية ومضمنة ، لكن ثق بي - إنها ليست كذلك.

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Parent
{
    public:
        virtual ~Parent();
};

class Child : public Parent, public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};

لا يجب عليك تضمين جسد المدمر الافتراضي - فقد اتضح أن بعض المترجمين لديهم مشكلة في تحسين وجود مدمر فارغ وأنك أفضل حالا باستخدام الافتراضي.


ما زلت جديدة في تطوير C ++. لقد بدأت مع Visual Studio (VS).

ومع ذلك ، لا يبدو أن أحد ذكر __interface في VS ( . NET) . لست متأكدًا تمامًا مما إذا كانت هذه طريقة جيدة لإعلان واجهة. ولكن يبدو أن توفير إنفاذ إضافي (المذكورة في الوثائق ). بحيث لا تحتاج إلى تحديد virtual TYPE Method() = 0; بشكل صريح virtual TYPE Method() = 0; ، نظرًا لأنه سيتم تحويله تلقائيًا.

__interface IMyInterface {
   HRESULT CommitX();
   HRESULT get_X(BSTR* pbstrName);
};

ومع ذلك ، لا أستخدمها لأنني قلق بشأن توافق تجميع الأنظمة الأساسية ، نظرًا لأنها متوفرة فقط ضمن .NET.

إذا كان لدى أي شخص أي شيء مثير للاهتمام ، يرجى المشاركة. :-)

شكر.


إذا كنت تستخدم مترجم C ++ الخاص بـ Microsoft ، فيمكنك القيام بما يلي:

struct __declspec(novtable) IFoo
{
    virtual void Bar() = 0;
};

class Child : public IFoo
{
public:
    virtual void Bar() override { /* Do Something */ }
}

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


إضافة صغيرة لما هو مكتوب هناك:

أولا ، تأكد من أن المدمر الخاص بك هو أيضا الظاهري النقي

ثانيًا ، قد ترغب في أن ترث فعليًا (بدلاً من عادة) عندما تقوم بالتنفيذ ، فقط من أجل اتخاذ إجراءات جيدة.


هنا تعريف abstract class في معيار c ++

n4687

13.4.2

الفئة التجريدية هي فئة يمكن استخدامها فقط كفئة أساسية لبعض الصفوف الأخرى ؛ لا يمكن إنشاء كائنات من فئة مجردة باستثناء عناصر فرعية لفئة مشتقة منه. الفئة مجردة إذا كان لديها وظيفة افتراضية واحدة على الأقل.


class Shape 
{
public:
   // pure virtual function providing interface framework.
   virtual int getArea() = 0;
   void setWidth(int w)
   {
      width = w;
   }
   void setHeight(int h)
   {
      height = h;
   }
protected:
    int width;
    int height;
};

class Rectangle: public Shape
{
public:
    int getArea()
    { 
        return (width * height); 
    }
};
class Triangle: public Shape
{
public:
    int getArea()
    { 
        return (width * height)/2; 
    }
};

int main(void)
{
     Rectangle Rect;
     Triangle  Tri;

     Rect.setWidth(5);
     Rect.setHeight(7);

     cout << "Rectangle area: " << Rect.getArea() << endl;

     Tri.setWidth(5);
     Tri.setHeight(7);

     cout << "Triangle area: " << Tri.getArea() << endl; 

     return 0;
}

النتيجة: مساحة المستطيل: 35 مساحة المثلث: 17

لقد رأينا كيف حددت الطبقة المجردة واجهة من حيث getArea () وطبقتان أخرتان نفذتا نفس الوظيفة ولكن مع خوارزمية مختلفة لحساب المساحة الخاصة بالشكل.


بينما صحيح أن virtual هي المعيار الواقعي لتعريف واجهة ، دعونا لا ننسى النمط الكلاسيكي C-like ، الذي يأتي مع مُنشئ في C ++:

struct IButton
{
    void (*click)(); // might be std::function(void()) if you prefer

    IButton( void (*click_)() )
    : click(click_)
    {
    }
};

// call as:
// (button.*click)();

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

نصائح:

  • قد ترث من هذا كقاعدة أساسية (مسموح لكل من الظاهرين وغير الظاهريين) ، ثم click تعبئة في منشئ سليلك.
  • قد يكون لديك مؤشر الوظيفة كعضو protected ولديك مرجع عام و / أو برنامج ترحيبي.
  • كما ذكر أعلاه ، يسمح لك هذا بتشغيل التطبيق في وقت التشغيل. وبالتالي فهي طريقة لإدارة الدولة أيضًا. استنادًا إلى عدد التغييرات s في if s في الحالة البرمجية ، قد يكون هذا أسرع من switch() es أو s ( if المتوقع حدوث تحول في حوالي 3-4 if s ، ولكن دائمًا يتم قياسه أولاً.
  • إذا اخترت std::function<> فوق مؤشرات الدالة ، فقد تتمكن من إدارة جميع بيانات الكائن داخل IBase . من هذه النقطة ، يمكن أن يكون لديك مخططات القيمة لـ IBase (على سبيل المثال ، IBase std::vector<IBase> ). لاحظ أن هذا قد يكون أبطأ اعتمادًا على المترجم ورمز STL ؛ أيضًا أن التطبيقات الحالية لـ std::function<> تميل إلى أن يكون لها مقدار حمل عند مقارنتها بمؤشرات الدالة أو حتى الوظائف الظاهرية (قد يتغير ذلك في المستقبل).

To give a simple but clear answer, it helps to set the context : you use both when you do not want to provide full implementations.

The main difference then is an interface has no implementation at all (only methods without a body) while abstract classes can have members and methods with a body as well, ie can be partially implemented.





c++ inheritance interface abstract-class pure-virtual