سؤال

لدي فهم قوي لمعظم نظريات OO ولكن الشيء الوحيد الذي يحيرني كثيرًا هو المدمرات الافتراضية.

اعتقدت أنه يتم استدعاء المدمر دائمًا بغض النظر عن كل كائن في السلسلة.

متى من المفترض أن تجعلها افتراضية ولماذا؟

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

المحلول

تكون أدوات التدمير الافتراضية مفيدة عندما يحتمل أن تقوم بحذف مثيل لفئة مشتقة من خلال مؤشر للفئة الأساسية:

class Base 
{
    // some virtual methods
};

class Derived : public Base
{
    ~Derived()
    {
        // Do some important cleanup
    }
};

هنا، ستلاحظ أنني لم أعلن عن وجود مدمر Base virtual.الآن، دعونا نلقي نظرة على المقتطف التالي:

Base *b = new Derived();
// use b
delete b; // Here's the problem!

نظرًا لأن مدمر Base ليس كذلك virtual و b هو Base* مشيرا إلى أ Derived هدف، delete b لديه سلوك غير محدد:

[في delete b] ، إذا كان النوع الثابت من يختلف الكائن المراد حذفه عن نوعه الديناميكي ، وهو ثابت يجب أن يكون النوع فئة أساسية للنوع الديناميكي للكائن المراد أن يكون حذفت و يجب أن يكون للنوع الثابت مدمر افتراضي أو السلوك غير محدد.

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

خلاصة القول، قم دائمًا بإنشاء مدمرات للفئات الأساسية virtual عندما يكون من المفترض أن يتم التلاعب بها بشكل متعدد الأشكال.

إذا كنت تريد منع حذف مثيل من خلال مؤشر الفئة الأساسية، فيمكنك جعل أداة تدمير الفئة الأساسية محمية وغير افتراضية؛من خلال القيام بذلك، لن يسمح لك المترجم بالاتصال delete على مؤشر الفئة الأساسية.

يمكنك معرفة المزيد حول الافتراضية ومدمر الفئة الأساسية الافتراضية في هذه المقالة من هيرب سوتر.

نصائح أخرى

ومنشئ الظاهري غير ممكن ولكن المدمر الظاهري ممكن. دعونا تجربة ....

#include <iostream>

using namespace std;

class Base
{
public:
    Base(){
        cout << "Base Constructor Called\n";
    }
    ~Base(){
        cout << "Base Destructor called\n";
    }
};

class Derived1: public Base
{
public:
    Derived1(){
        cout << "Derived constructor called\n";
    }
    ~Derived1(){
        cout << "Derived destructor called\n";
    }
};

int main()
{
    Base *b = new Derived1();
    delete b;
}

وإخراج رمز أعلاه ما يلي:

Base Constructor Called
Derived constructor called
Base Destructor called

وبناء كائن اشتقاق متابعة حكم البناء ولكن عندما وحذف "ب" المؤشر (مؤشر القاعدة) وجدنا أن فقط المدمر قاعدة يسمى. ولكن هذا يجب ألا يحدث. أن تفعل الشيء المناسب، لدينا لجعل المدمر قاعدة افتراضية. الآن دعونا نرى ما سيحدث في ما يلي:

#include <iostream>

using namespace std;

class Base
{ 
public:
    Base(){
        cout << "Base Constructor Called\n";
    }
    virtual ~Base(){
        cout << "Base Destructor called\n";
    }
};

class Derived1: public Base
{
public:
    Derived1(){
        cout << "Derived constructor called\n";
    }
    ~Derived1(){
        cout << "Derived destructor called\n";
    }
};

int main()
{
    Base *b = new Derived1();
    delete b;
}

وتغيير الانتاج على النحو التالي:

Base Constructor Called
Derived Constructor called
Derived destructor called
Base destructor called

وهكذا تدمير المؤشر الأساسي (الذي يأخذ مخصصات على كائن اشتقاق!) يتبع حكم الدمار، أي بمعنى أولا المشتقة، ثم قاعدة. من ناحية أخرى، لا يوجد شيء مثل منشئ الظاهري.

وتعلن تالفة الافتراضية في فئات أساسية متعددة الأشكال. وهذا هو البند 7 في سكوت مايرز الفعالة C ++ . مايرز يمضي لتلخيص أنه إذا كان فئة لديها <م> أي دالة الظاهري، فإنه يجب أن يكون المدمر الظاهري، وأن الطبقات ليست مصممة لتكون الفئات الأساسية أو ليست مصممة لاستخدامها polymorphically ينبغي <م> لا أعلن تالفة الظاهري.

انتبه أيضًا إلى أن حذف مؤشر الفئة الأساسية في حالة عدم وجود أداة تدمير افتراضية سيؤدي إلى سلوك غير محدد.شيء تعلمته مؤخرًا:

كيف يجب أن يتصرف تجاوز الحذف في C++؟

لقد كنت أستخدم C++ لسنوات وما زلت قادرًا على شنق نفسي.

تأكد من المدمر الظاهري كلما صفك هي متعددة الأشكال.

دعوة المدمر عبر مؤشر إلى الفئة الأساسية

struct Base {
  virtual void f() {}
  virtual ~Base() {}
};

struct Derived : Base {
  void f() override {}
  ~Derived() override {}
};

Base* base = new Derived;
base->f(); // calls Derived::f
base->~Base(); // calls Derived::~Derived

ودعوة المدمر الظاهري لا تختلف عن أي استدعاء دالة الظاهرية الأخرى.

لbase->f()، وسيتم ارسال الدعوة إلى Derived::f()، وأنه هو نفسه لbase->~Base() - وظيفتها الغالبة - سوف يطلق على Derived::~Derived()

والشيء نفسه يحدث عندما يتم استدعاء المدمر بشكل غير مباشر، على سبيل المثال delete base;. سوف البيان delete دعوة base->~Base() التي سيتم إرسالها إلى Derived::~Derived().

خلاصة الدرجة مع المدمر غير ظاهري

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

// library.hpp

struct Base {
  virtual void f() = 0;

protected:
  ~Base() = default;
};

void CallsF(Base& base);
// CallsF is not going to own "base" (i.e. call "delete &base;").
// It will only call Base::f() so it doesn't need to access Base::~Base.

//-------------------
// application.cpp

struct Derived : Base {
  void f() override { ... }
};

int main() {
  Derived derived;
  CallsF(derived);
  // No need for virtual destructor here as well.
}

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

لتكون بسيطة، المدمر الظاهري هو التدمير الموارد في الترتيب الصحيح، عند حذف مؤشر الفئة الأساسية لافتا إلى كائن الفئة مشتقة.

 #include<iostream>
 using namespace std;
 class B{
    public:
       B(){
          cout<<"B()\n";
       }
       virtual ~B(){ 
          cout<<"~B()\n";
       }
 };
 class D: public B{
    public:
       D(){
          cout<<"D()\n";
       }
       ~D(){
          cout<<"~D()\n";
       }
 };
 int main(){
    B *b = new D();
    delete b;
    return 0;
 }

OUTPUT:
B()
D()
~D()
~B()

==============
If you don't give ~B()  as virtual. then output would be 
B()
D()
~B()
where destruction of ~D() is not done which leads to leak

والكلمة الظاهري للالمدمر ضرورية عندما تريد أن تالفة مختلفة يتبع الترتيب الصحيح بينما يتم حذف الكائنات من خلال مؤشر الفئة الأساسية. على سبيل المثال:

Base *myObj = new Derived();
// Some code which is using myObj object
myObj->fun();
//Now delete the object
delete myObj ; 

وإذا كان لديك المدمر فئة مشتقة هو الظاهري ثم سيتم destrcuted الكائنات في النظام (مستمدة أولا الكائن ثم قاعدة). إذا كان لديك المدمر فئة مشتقة هو NOT الظاهري ثم كائن الفئة الوحيدة قاعدة سوف تحصل على حذف (لأن المؤشر من الفئة الأساسية "قاعدة * myObj"). لذلك سيكون هناك تسرب للذاكرة لكائن مشتقة.

ما هو المدمر الظاهري أو كيفية استخدام المدمر الظاهري

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

واطلع على عينة التالية مع بعض الدالات الظاهرية

والعينة كما اقول كيف يمكنك تحويل بريد إلكتروني إلى أعلى أو أقل

#include "stdafx.h"
#include<iostream>
using namespace std;
// program to convert the lower to upper orlower
class convertch
{
public:
  //void convertch(){};
  virtual char* convertChar() = 0;
  ~convertch(){};
};

class MakeLower :public convertch
{
public:
  MakeLower(char *passLetter)
  {
    tolower = true;
    Letter = new char[30];
    strcpy(Letter, passLetter);
  }

  virtual ~MakeLower()
  {
    cout<< "called ~MakeLower()"<<"\n";
    delete[] Letter;
  }

  char* convertChar()
  {
    size_t len = strlen(Letter);
    for(int i= 0;i<len;i++)
      Letter[i] = Letter[i] + 32;
    return Letter;
  }

private:
  char *Letter;
  bool tolower;
};

class MakeUpper : public convertch
{
public:
  MakeUpper(char *passLetter)
  {
    Letter = new char[30];
    toupper = true;
    strcpy(Letter, passLetter);
  }

  char* convertChar()
  {   
    size_t len = strlen(Letter);
    for(int i= 0;i<len;i++)
      Letter[i] = Letter[i] - 32;
    return Letter;
  }

  virtual ~MakeUpper()
  {
    cout<< "called ~MakeUpper()"<<"\n";
    delete Letter;
  }

private:
  char *Letter;
  bool toupper;
};


int _tmain(int argc, _TCHAR* argv[])
{
  convertch *makeupper = new MakeUpper("hai"); 
  cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" ";     
  delete makeupper;
  convertch *makelower = new MakeLower("HAI");;
  cout<<"Eneterd : HAI = " <<makelower->convertChar()<<" "; 
  delete makelower;
  return 0;
}

ومن العينة أعلاه يمكنك أن ترى أن المدمر لكل MakeUpper والطبقة MakeLower لا يسمى.

واطلع على عينة المقبلة مع المدمر الظاهري

#include "stdafx.h"
#include<iostream>

using namespace std;
// program to convert the lower to upper orlower
class convertch
{
public:
//void convertch(){};
virtual char* convertChar() = 0;
virtual ~convertch(){}; // defined the virtual destructor

};
class MakeLower :public convertch
{
public:
MakeLower(char *passLetter)
{
tolower = true;
Letter = new char[30];
strcpy(Letter, passLetter);
}
virtual ~MakeLower()
{
cout<< "called ~MakeLower()"<<"\n";
      delete[] Letter;
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
{
Letter[i] = Letter[i] + 32;

}

return Letter;
}

private:
char *Letter;
bool tolower;

};
class MakeUpper : public convertch
{
public:
MakeUpper(char *passLetter)
{
Letter = new char[30];
toupper = true;
strcpy(Letter, passLetter);
}
char* convertChar()
{

size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
{
Letter[i] = Letter[i] - 32;
}
return Letter;
}
virtual ~MakeUpper()
{
      cout<< "called ~MakeUpper()"<<"\n";
delete Letter;
}
private:
char *Letter;
bool toupper;
};


int _tmain(int argc, _TCHAR* argv[])
{

convertch *makeupper = new MakeUpper("hai");

cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" \n";

delete makeupper;
convertch *makelower = new MakeLower("HAI");;
cout<<"Eneterd : HAI = " <<makelower->convertChar()<<"\n ";


delete makelower;
return 0;
}

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

وأو زيارة الرابط

HTTPS: // web.archive.org/web/20130822173509/http://www.programminggallery.com/article_details.php؟article_id=138

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

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

#include <iostream>
using namespace std;

struct a
{
    ~a() {}

    unsigned long long i;
};

struct b : a
{
    ~b() {}

    unsigned long long j;
};

struct c : b
{
    ~c() {}

    virtual void m3() {}

    unsigned long long k;
};

struct d : c
{
    ~d() {}

    virtual void m4() {}

    unsigned long long l;
};

int main()
{
    cout << "sizeof(a): " << sizeof(a) << endl;
    cout << "sizeof(b): " << sizeof(b) << endl;
    cout << "sizeof(c): " << sizeof(c) << endl;
    cout << "sizeof(d): " << sizeof(d) << endl;

    // No issue.

    a* a1 = new a();
    cout << "a1: " << a1 << endl;
    delete a1;

    // No issue.

    b* b1 = new b();
    cout << "b1: " << b1 << endl;
    cout << "(a*) b1: " << (a*) b1 << endl;
    delete b1;

    // No issue.

    c* c1 = new c();
    cout << "c1: " << c1 << endl;
    cout << "(b*) c1: " << (b*) c1 << endl;
    cout << "(a*) c1: " << (a*) c1 << endl;
    delete c1;

    // No issue.

    d* d1 = new d();
    cout << "d1: " << d1 << endl;
    cout << "(c*) d1: " << (c*) d1 << endl;
    cout << "(b*) d1: " << (b*) d1 << endl;
    cout << "(a*) d1: " << (a*) d1 << endl;
    delete d1;

    // Doesn't crash, but may not produce the results you want.

    c1 = (c*) new d();
    delete c1;

    // Crashes due to passing an invalid address to the method which
    // frees the memory.

    d1 = new d();
    b1 = (b*) d1;
    cout << "d1: " << d1 << endl;
    cout << "b1: " << b1 << endl;
    delete b1;  

/*

    // This is similar to what's happening above in the "crash" case.

    char* buf = new char[32];
    cout << "buf: " << (void*) buf << endl;
    buf += 8;
    cout << "buf after adding 8: " << (void*) buf << endl;
    delete buf;
*/
}

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

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

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

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

class A
{
public:
    A() {}
    virtual void foo()
    {
        cout << "This is A." << endl;
    }
};

class B : public A
{
public:
    B() {}
    void foo()
    {
        cout << "This is B." << endl;
    }
};

int main(int argc, char* argv[])
{
    A *a = new B();
    a->foo();
    if(a != NULL)
    delete a;
    return 0;
}

هل طباعة:

This is B.

وبدون virtual فإنه سيتم طباعة:

This is A.

والآن يجب أن نفهم متى تستخدم تالفة الظاهري.

وعندما تحتاج إلى استدعاء المستمدة المدمر الفئة من الفئة الأساسية. تحتاج إلى إعلان الظاهري المدمر الفئة الأساسية في الفئة الأساسية.

إذا كنت تستخدم shared_ptr (shared_ptr فقط، وليس unique_ptr)، لم يكن لديك لديك المدمر الفئة الأساسية الظاهري:

#include <iostream>
#include <memory>

using namespace std;

class Base
{
public:
    Base(){
        cout << "Base Constructor Called\n";
    }
    ~Base(){ // not virtual
        cout << "Base Destructor called\n";
    }
};

class Derived: public Base
{
public:
    Derived(){
        cout << "Derived constructor called\n";
    }
    ~Derived(){
        cout << "Derived destructor called\n";
    }
};

int main()
{
    shared_ptr<Base> b(new Derived());
}

والإخراج:

Base Constructor Called
Derived constructor called
Derived destructor called
Base Destructor called

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

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

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