Kiedy używać wirtualnych niszczycieli?

1485

Dobrze rozumiem większość teorii OO, ale jedną z rzeczy, która mnie bardzo dezorientuje, są wirtualne niszczyciele.

Myślałem, że destruktor zawsze jest wywoływany bez względu na wszystko i dla każdego obiektu w łańcuchu.

Kiedy masz je uczynić wirtualnymi i dlaczego?

Lodle
źródło
6
Zobacz: Virtual Destructor
Naveen
146
Każdy destructor w dół jest wywoływana nie wiem co. virtualupewnia się, że zaczyna się na górze zamiast na środku.
Mooing Duck
@MooingDuck to nieco mylący komentarz.
Euri Pinhollow
1
@FranklinYu to dobrze, że pytałeś, ponieważ teraz nie widzę żadnego problemu z tym komentarzem (oprócz próby udzielenia odpowiedzi w komentarzach).
Euri Pinhollow

Odpowiedzi:

1571

Wirtualne niszczyciele są przydatne, gdy potencjalnie możesz usunąć instancję klasy pochodnej za pomocą wskaźnika do klasy bazowej:

class Base 
{
    // some virtual methods
};

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

Tutaj zauważysz, że nie zadeklarowałem, że to Destruktor Bazy virtual. Teraz spójrzmy na następujący fragment:

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

Od destructor baza nie jest virtuali bjest Base*skierowana na Derivedprzedmiot, delete bma niezdefiniowanej zachowanie :

[In delete b], jeśli typ statyczny obiektu, który ma zostać usunięty, różni się od jego typu dynamicznego, typ statyczny powinien być klasą bazową typu dynamicznego obiektu, który ma zostać usunięty, a typ statyczny powinien mieć wirtualny destruktor lub zachowanie jest niezdefiniowane .

W większości implementacji wywołanie destruktora zostanie rozwiązane tak jak każdy kod niewirtualny, co oznacza, że ​​wywołany zostanie destruktor klasy bazowej, ale nie klasy pochodnej, co spowoduje wyciek zasobów.

Podsumowując, zawsze twórz niszczyciele klas podstawowych, virtualgdy mają być manipulowane polimorficznie.

Jeśli chcesz zapobiec usunięciu instancji za pomocą wskaźnika klasy bazowej, możesz sprawić, że niszczyciel klasy bazowej będzie chroniony i niewirusowy; w ten sposób kompilator nie pozwoli ci wywołać deletewskaźnika klasy bazowej.

Możesz dowiedzieć się więcej na temat wirtualności i wirtualnego niszczyciela klasy bazowej w tym artykule z Herb Sutter .

Luc Touraille
źródło
173
To by tłumaczyło, dlaczego miałem ogromne wycieki przy użyciu fabryki, którą zrobiłem wcześniej. Wszystko ma teraz sens. Dzięki
Lodle,
8
To zły przykład, ponieważ nie ma członków danych. Co jeśli Basei Derivedmieć wszystkie automatycznych zmiennych przechowywania? tzn. nie ma „specjalnego” ani dodatkowego niestandardowego kodu do wykonania w destruktorze. Czy można w ogóle przestać pisać jakiekolwiek niszczyciele? A może klasa pochodna nadal będzie miała przeciek pamięci?
bobobobo
28
Z artykułu Herb Sutter: „Wytyczna nr 4: Destruktor klasy podstawowej powinien być albo publiczny i wirtualny, albo chroniony i niewirusowy”.
Lody
3
Również z artykułu - „jeśli usuniesz polimorficznie bez wirtualnego destruktora, przywołasz przerażające widmo„ nieokreślonego zachowania ”, widmo, którego osobiście wolałbym nie spotkać nawet w umiarkowanie dobrze oświetlonym zaułku, dziękuję bardzo”. lol
Bondolin
219

Wirtualny konstruktor nie jest możliwy, ale wirtualny destruktor jest możliwy. Pozwól nam eksperymentować .......

#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;
}

Powyższy kod wyświetla następujące dane:

Base Constructor Called
Derived constructor called
Base Destructor called

Konstrukcja obiektu pochodnego jest zgodna z zasadą konstrukcji, ale kiedy usuwamy wskaźnik „b” (wskaźnik bazowy), stwierdziliśmy, że wywoływany jest tylko podstawowy destruktor. Ale to nie może się zdarzyć. Aby zrobić odpowiednią rzecz, musimy uczynić bazę destruktora wirtualną. Zobaczmy teraz, co dzieje się w następujący sposób:

#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;
}

Dane wyjściowe zmieniły się następująco:

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

Tak więc zniszczenie wskaźnika bazowego (który przyjmuje przydział dla obiektu pochodnego!) Odbywa się zgodnie z zasadą zniszczenia, tj. Najpierw Pochodna, a następnie Baza. Z drugiej strony nie ma wirtualnego konstruktora.

Tunvir Rahman Tusher
źródło
1
„wirtualny konstruktor nie jest możliwy” oznacza, że ​​nie musisz pisać własnego konstruktora. Konstrukcja obiektu pochodnego musi następować po łańcuchu konstrukcji od pochodnej do podstawy. Nie musisz więc pisać wirtualnego słowa kluczowego dla swojego konstruktora. Dzięki
Tunvir Rahman Tusher
4
@ Murkantilism, „wirtualnych konstruktorów nie da się zrobić” jest rzeczywiście prawdą. Konstruktora nie można oznaczyć jako wirtualny.
cmeub
1
@cmeub, Ale istnieje idiom umożliwiający osiągnięcie tego, czego chcesz od wirtualnego konstruktora. Zobacz parashift.com/c++-faq-lite/virtual-ctors.html
cape1232
@TunvirRahmanTusher czy mógłbyś wyjaśnić, dlaczego nazywa się Base Destructor?
rimalonfire,
@rimiro Jego automatycznie przez c ++. możesz kliknąć link stackoverflow.com/questions/677620/...
Tunvir Rahman Tusher
195

Deklaracja wirtualnych destruktorów w polimorficznych klasach podstawowych. To jest pozycja 7 w Effective C ++ Scott Meyers . Meyers podsumowuje, że jeśli klasa ma jakąkolwiek funkcję wirtualną, powinna mieć wirtualny destruktor, a klasy nie zaprojektowane jako klasy podstawowe lub nieprzeznaczone do użycia polimorficznego nie powinny deklarować wirtualnych destruktorów.

Bill jaszczurka
źródło
14
+ „Jeśli klasa ma jakąkolwiek funkcję wirtualną, powinna mieć wirtualny destruktor, a klasy, które nie zostały zaprojektowane jako klasy podstawowe lub nie zostały zaprojektowane do użycia polimorficznego, nie powinny deklarować wirtualnych destruktorów.”: Czy istnieją przypadki, w których sensowne jest złamać tę zasadę? Jeśli nie, czy sensowne byłoby, aby kompilator sprawdził ten warunek i wydał błąd, czy nie jest spełniony?
Giorgio
@Giorgio Nie znam żadnych wyjątków od reguły. Ale nie oceniłbym siebie jako eksperta w C ++, więc możesz opublikować to jako osobne pytanie. Ostrzeżenie kompilatora (lub ostrzeżenie z narzędzia analizy statycznej) ma dla mnie sens.
Bill the Jaszczurka
10
Klasy można zaprojektować tak, aby nie były usuwane za pomocą wskaźnika określonego typu, ale nadal mają funkcje wirtualne - typowym przykładem jest interfejs zwrotny. Nie usuwa się jego implementacji za pomocą wskaźnika interfejsu wywołania zwrotnego, ponieważ służy to tylko do subskrybowania, ale ma funkcje wirtualne.
dascandy
3
@dascandy Dokładnie - że albo wszyscy wielu innych sytuacjach, gdy używamy polimorficzne, ale nie wykonują zarządzanie pamięcią masową za pośrednictwem wskaźników - np utrzymujących automatycznych lub statyczną trwania obiektów, ze wskaźnikami używane tylko jako tras obserwacji. W takich przypadkach nie ma potrzeby / celu wdrażania wirtualnego destruktora. Ponieważ tylko tutaj cytujemy ludzi, wolę Suttera z góry: „Wytyczna nr 4: Destruktor klasy podstawowej powinien być publiczny i wirtualny, albo chroniony i niewirusowy”. To ostatnie zapewnia, że ​​każdy, kto przypadkowo spróbuje usunąć za pomocą wskaźnika bazowego, zobaczy błąd na swoich drogach
underscore_d
1
@Giorgio W rzeczywistości istnieje sztuczka, której można użyć i uniknąć wirtualnego wywołania destruktora: powiązanie przez stałe odniesienie obiektu pochodnego z bazą, np const Base& = make_Derived();. W takim przypadku Derivedzostanie wywołany destruktor wartości , nawet jeśli nie jest wirtualny, więc oszczędza się narzuty wprowadzone przez vtables / vpointers. Oczywiście zakres jest dość ograniczony. Andrei Alexandrescu wspomniał o tym w swojej książce Modern C ++ Design .
vsoftco
46

Należy również pamiętać, że usunięcie wskaźnika klasy bazowej, gdy nie ma wirtualnego destruktora, spowoduje niezdefiniowane zachowanie . Czego nauczyłem się niedawno:

Jak powinno się zachowywać przesłonięcie usuwania w C ++?

Używam C ++ od lat i nadal potrafię się zawiesić.

BigSandwich
źródło
Rzuciłem okiem na twoje pytanie i zobaczyłem, że zadeklarowałeś podstawowy destruktor jako wirtualny. Czy więc „usunięcie wskaźnika klasy bazowej, gdy nie ma wirtualnego destruktora spowoduje niezdefiniowane zachowanie”, pozostaje aktualne w odniesieniu do twojego pytania? Ponieważ w tym pytaniu po wywołaniu usunięcia klasa pochodna (utworzona przez jej nowego operatora) jest najpierw sprawdzana pod kątem zgodności z wersją. Ponieważ tam znalazł, został nazwany. Czy nie uważasz, że lepiej byłoby powiedzieć, że „usunięcie wskaźnika klasy bazowej, gdy nie ma destruktora, spowoduje niezdefiniowane zachowanie”?
ubuntugod
To właściwie to samo. Domyślny konstruktor nie jest wirtualny.
BigSandwich,
41

Uaktywnij destruktor wirtualny, ilekroć Twoja klasa jest polimorficzna.

tak
źródło
13

Wywoływanie destruktora za pomocą wskaźnika do klasy podstawowej

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

Wirtualne wywołanie destruktora nie różni się niczym od jakiegokolwiek wirtualnego wywołania funkcji.

Ponieważ base->f()wywołanie zostanie wysłane do Derived::f(), i to samo dotyczy base->~Base()- jego funkcji zastępującej -Derived::~Derived() zostanie wywołane.

To samo dzieje się, gdy destruktor jest wywoływany pośrednio, np delete base;. deleteWezwanie oświadczenie woli base->~Base(), które będą wysyłane doDerived::~Derived() .

Klasa abstrakcyjna z nie-wirtualnym destruktorem

Jeśli nie zamierzasz usuwać obiektu przez wskaźnik do jego klasy bazowej - nie ma potrzeby używania wirtualnego destruktora. Po prostu zrób to, protectedaby nie zostało przypadkowo nazwane:

// 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.
}
Abyx
źródło
Czy konieczne jest jawne deklarowanie ~Derived()we wszystkich klasach pochodnych, nawet jeśli jest to po prostu ~Derived() = default? Czy jest to sugerowane przez język (dzięki czemu można go bezpiecznie pominąć)?
Ponkadoodle,
@Wallacoloo nie, deklaruj to tylko wtedy, gdy jest to konieczne. Np. Wstawić protectedsekcję lub upewnić się, że jest wirtualna za pomocą override.
Abyx
9

Lubię myśleć o interfejsach i implementacjach interfejsów. W C ++ interfejs jest czystą klasą wirtualną. Destructor jest częścią interfejsu i powinien zostać zaimplementowany. Dlatego destruktor powinien być czysto wirtualny. A może konstruktor? Konstruktor tak naprawdę nie jest częścią interfejsu, ponieważ obiekt jest zawsze tworzony jawnie.

Dragan Ostojic
źródło
2
To inne spojrzenie na to samo pytanie. Jeśli myślimy w kategoriach interfejsów zamiast klasy bazowej vs klasy pochodnej, to jest naturalny wniosek: jeśli jest to część interfejsu, to uczyń go wirtualnym. Jeśli tak nie jest.
Dragan Ostojic,
2
+1 za stwierdzenie podobieństwa koncepcji interfejsu OO i czystej klasy wirtualnej C ++ . W odniesieniu do destruktora oczekuje się, że zostanie wdrożony : jest to często niepotrzebne. O ile klasa nie zarządza zasobem, takim jak surowa pamięć dynamicznie przydzielana (np. Nie za pomocą inteligentnego wskaźnika), uchwytem pliku lub uchwytem bazy danych, użycie domyślnego destruktora utworzonego przez kompilator jest w porządku w klasach pochodnych. I zauważ, że jeśli destruktor (lub dowolna funkcja) zostanie zadeklarowany virtualw klasie bazowej, to automatycznie znajdzie się virtualw klasie pochodnej, nawet jeśli nie zostanie zadeklarowany.
DavidRR
Pomija to kluczowy szczegół, że destruktor niekoniecznie jest konieczny częścią interfejsu. Można łatwo zaprogramować klasy, które mają funkcje polimorficzne, ale których osoba wywołująca nie zarządza / nie ma możliwości ich usunięcia. Zatem wirtualny destruktor nie ma sensu. Oczywiście, aby to zapewnić, nie-wirtualny - prawdopodobnie domyślny - destruktor powinien być niepubliczny. Gdybym musiał zgadywać, powiedziałbym, że takie klasy są częściej wykorzystywane wewnętrznie w projektach, ale to nie czyni ich mniej istotnymi jako przykład / niuans w tym wszystkim.
underscore_d
8

Wirtualne słowo kluczowe dla destruktora jest konieczne, jeśli chcesz, aby różne destruktory zachowywały odpowiednią kolejność podczas usuwania obiektów przez wskaźnik klasy bazowej. na przykład:

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

Jeśli Twój destruktor klasy podstawowej jest wirtualny, obiekty zostaną zniszczone w kolejności (najpierw obiekt pochodny, a następnie baza). Jeśli Twój niszczyciel klasy bazowej NIE jest wirtualny, tylko obiekt klasy podstawowej zostanie usunięty (ponieważ wskaźnik ma klasę podstawową „Base * myObj”). Zatem nastąpi wyciek pamięci dla obiektu pochodnego.

Mukul Kashmira
źródło
7

Mówiąc prościej, Virtual destructor polega na niszczeniu zasobów w odpowiedniej kolejności po usunięciu wskaźnika klasy bazowej wskazującego na obiekt klasy pochodnej.

 #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

Prakash GiBB
źródło
Brak podstawowego wirtualnego destruktora i wywołanie deletepodstawowego wskaźnika prowadzi do nieokreślonego zachowania.
James Adkison
@JamesAdkison, dlaczego prowadzi to do nieokreślonego zachowania?
rimalonfire
@rimiro Tak mówi standard . Nie mam kopii, ale link prowadzi do komentarza, w którym ktoś odwołuje się do lokalizacji w standardzie.
James Adkison,
@rimiro "Jeśli zatem usunięcie można wykonać polimorficznie za pomocą interfejsu klasy podstawowej, musi on zachowywać się wirtualnie i musi być wirtualny. Rzeczywiście, język tego wymaga - jeśli usuniesz polimorficznie bez wirtualnego destruktora, przywołasz przerażające widmo „niezdefiniowane zachowanie”, widmo, którego osobiście wolałbym nie spotkać nawet w umiarkowanie dobrze oświetlonej uliczce, dziękuję bardzo ”. ( gotw.ca/publications/mill18.htm ) - Herb Sutter
James Adkison
4

Destruktory wirtualnej klasy bazowej są „najlepszą praktyką” - zawsze należy ich używać, aby uniknąć (trudnych do wykrycia) wycieków pamięci. Korzystając z nich, możesz być pewien, że wszystkie destruktory w łańcuchu dziedziczenia twoich klas są wywoływane (w odpowiedniej kolejności). Dziedziczenie z klasy podstawowej za pomocą wirtualnego destruktora powoduje, że destruktor klasy dziedziczącej jest również automatycznie wirtualny (więc nie trzeba wpisywać „wirtualnego” w deklaracji dziedziczącego klasy destruktor).

Trantor
źródło
4

Jeśli używasz shared_ptr(tylko shared_ptr, a nie unique_ptr), nie musisz mieć wirtualnego destruktora klasy podstawowej:

#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());
}

wynik:

Base Constructor Called
Derived constructor called
Derived destructor called
Base Destructor called
Zhenxiao Hao
źródło
Chociaż jest to możliwe, zniechęcam każdego do korzystania z tego. Obciążenie wirtualnego destruktora jest maleńkie, a to tylko pozwala zepsuć, szczególnie przez mniej doświadczonego programistę, który tego nie wie. To małe virtualsłowo kluczowe może uratować cię od wielu cierpień.
Michal Štein
3

Co to jest wirtualny destruktor lub jak korzystać z wirtualnego destruktora

Destruktor klas to funkcja o tej samej nazwie co klasa poprzedzająca ~, która ponownie przydzieli pamięć przydzieloną przez klasę. Dlaczego potrzebujemy wirtualnego destruktora

Zobacz poniższy przykład z niektórymi funkcjami wirtualnymi

Próbka pokazuje również, jak przekonwertować literę na górną lub dolną

#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;
}

Z powyższej próbki widać, że destruktor zarówno dla klasy MakeUpper, jak i MakeLower nie jest wywoływany.

Zobacz następną próbkę z wirtualnym destruktorem

#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;
}

Wirtualny destruktor wywoła jawnie najbardziej pochodny destruktor czasu wykonywania klasy, aby był w stanie wyczyścić obiekt we właściwy sposób.

Lub odwiedź link

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

użytkownik2578542
źródło
2

kiedy trzeba wywołać destruktor klasy pochodnej z klasy podstawowej. musisz zadeklarować wirtualny destruktor klasy bazowej w klasie bazowej.

użytkownik2641018
źródło
2

Myślę, że sedno tego pytania dotyczy wirtualnych metod i polimorfizmu, a nie tylko destruktora. Oto wyraźniejszy przykład:

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;
}

Wydrukuje:

This is B.

Bez virtualtego wydrukuje:

This is A.

A teraz powinieneś zrozumieć, kiedy używać wirtualnych niszczycieli.

Gonjay
źródło
Nie, to tylko przegląda kompletne podstawy funkcji wirtualnych, całkowicie ignorując niuans kiedy / dlaczego destruktor powinien być jednym - co nie jest tak intuicyjne, dlatego OP zadał pytanie. (Ponadto, dlaczego tutaj niepotrzebny dynamiczny przydział? Po prostu zrób to B b{}; A& a{b}; a.foo();. Sprawdzanie NULL- które powinno być nullptr- przed wejściem delete- z niepoprawną motywacją - nie jest wymagane: delete nullptr;jest zdefiniowane jako brak operacji . Jeśli już, powinieneś to sprawdzić przed rozmową telefoniczną ->foo(), w przeciwnym razie może wystąpić niezdefiniowane zachowanie, jeśli newjakoś się nie powiedzie.)
underscore_d
2
Jest to bezpieczne, aby zadzwonić deletena NULLwskaźnik (czyli nie trzeba się if (a != NULL)strażnika).
James Adkison
@SaileshD Tak, wiem. Tak powiedziałem w moim komentarzu
James Adkison,
1

Pomyślałem, że korzystne byłoby omówienie „niezdefiniowanego” zachowania lub przynajmniej „awarii” niezdefiniowanego zachowania, które może wystąpić podczas usuwania przez klasę podstawową (/ struct) bez wirtualnego destruktora, a ściślej bez vtable. Poniższy kod zawiera kilka prostych struktur (to samo dotyczy klas).

#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;
*/
}

Nie sugeruję, czy potrzebujesz wirtualnych niszczycieli, czy nie, choć ogólnie uważam, że dobrą praktyką jest ich posiadanie. Podaję tylko powód, dla którego możesz zakończyć się awarią, jeśli twoja klasa podstawowa (/ struct) nie ma vtable, a twoja klasa pochodna (/ struct) ma i usuwasz obiekt za pomocą klasy podstawowej (/ struct) wskaźnik. W takim przypadku adres, który przekazujesz wolnej procedurze stosu, jest nieprawidłowy, a zatem przyczyna awarii.

Jeśli uruchomisz powyższy kod, zobaczysz wyraźnie, kiedy wystąpi problem. Kiedy ten wskaźnik klasy bazowej (/ struct) różni się od tego wskaźnika klasy pochodnej (/ struct), napotkasz ten problem. W powyższej próbce struktury a i b nie mają vtables. Struktury C i D mają vtables. W ten sposób wskaźnik a lub b do instancji obiektu ac lub d zostanie ustalony, aby uwzględnić tabelę vtable. Jeśli przekażesz ten wskaźnik a lub b, aby go usunąć, nastąpi awaria z powodu nieprawidłowego adresu wolnej procedury sterty.

Jeśli planujesz usunąć instancje pochodne, które mają vtable ze wskaźników klas bazowych, musisz upewnić się, że klasa bazowa ma vtable. Jednym ze sposobów na to jest dodanie wirtualnego destruktora, który i tak może być potrzebny do prawidłowego wyczyszczenia zasobów.

nickdu
źródło
0

Podstawowa definicja dotycząca virtual mówi, że określa, czy funkcja członka klasy może zostać zastąpiona w jej klasach pochodnych.

D-tor klasy jest wywoływany w zasadzie na końcu zakresu, ale istnieje problem, na przykład, gdy definiujemy instancję na stercie (alokacja dynamiczna), powinniśmy ją usunąć ręcznie.

Jak tylko instrukcja zostanie wykonana, wywoływany jest destruktor klasy bazowej, ale nie dla pochodnej.

Praktycznym przykładem jest to, że w polu sterowania trzeba manipulować efektorami, siłownikami.

Na końcu zakresu, jeśli nie zostanie wywołany destruktor jednego z elementów mocy (siłownika), będą miały fatalne konsekwencje.

#include <iostream>

class Mother{

public:

    Mother(){

          std::cout<<"Mother Ctor"<<std::endl;
    }

    virtual~Mother(){

        std::cout<<"Mother D-tor"<<std::endl;
    }


};

class Child: public Mother{

    public:

    Child(){

        std::cout<<"Child C-tor"<<std::endl;
    }

    ~Child(){

         std::cout<<"Child D-tor"<<std::endl;
    }
};

int main()
{

    Mother *c = new Child();
    delete c;

    return 0;
}
rekkalmd
źródło
-1

Każda klasa dziedziczona publicznie, polimorficzna lub nie, powinna mieć wirtualny destruktor. Innymi słowy, jeśli może to wskazywać wskaźnik klasy bazowej, jego klasa podstawowa powinna mieć wirtualny destruktor.

Jeśli jest wirtualny, wywoływany jest destruktor klasy pochodnej, a następnie konstruktor klasy podstawowej. Jeśli nie jest wirtualny, wywoływany jest tylko destruktor klasy podstawowej.

Syed H.
źródło
Powiedziałbym, że jest to konieczne tylko „jeśli może to wskazywać wskaźnik klasy bazowej” i może być publicznie usunięte. Ale myślę, że nie zaszkodzi nabrać nawyku dodawania wirtualnych lekarzy, na wypadek, gdyby mogli być później potrzebni.
underscore_d