Domanda

Ho una solida conoscenza della maggior parte della teoria OO, ma l'unica cosa che mi confonde molto sono i distruttori virtuali.

Ho pensato che il distruttore venisse sempre chiamato, non importa cosa e per ogni oggetto nella catena.

Quando intendi renderli virtuali e perché?

È stato utile?

Soluzione

I distruttori virtuali sono utili quando si potrebbe potenzialmente eliminare un'istanza di una classe derivata attraverso un puntatore alla classe base:

class Base 
{
    // some virtual methods
};

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

Qui noterai che non ho dichiarato che il distruttore di Base è virtuale . Ora diamo un'occhiata al seguente frammento:

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

Poiché il distruttore di Base non è virtuale e b è un Base * che punta a un oggetto Derivato , < codice> elimina b ha comportamento indefinito :

  

[In elimina b ], se il tipo statico di   l'oggetto da eliminare è diverso dal suo tipo dinamico, statico   il tipo deve essere una classe base del tipo dinamico dell'oggetto da essere   eliminato e il tipo statico deve avere un distruttore virtuale o il   il comportamento non è definito .

Nella maggior parte delle implementazioni, la chiamata al distruttore verrà risolta come qualsiasi codice non virtuale, il che significa che verrà chiamato il distruttore della classe base ma non quello della classe derivata, con conseguente perdita di risorse.

Per riassumere, rendi sempre i distruttori delle classi base virtuali quando devono essere manipolati polimorficamente.

Se si desidera impedire la cancellazione di un'istanza tramite un puntatore della classe base, è possibile rendere il distruttore della classe base protetto e non virtuale; così facendo, il compilatore non ti permetterà di chiamare delete su un puntatore della classe base.

Puoi saperne di più sulla virtualità e sul distruttore di classe di base virtuale in questo articolo da Herb Sutter .

Altri suggerimenti

Un costruttore virtuale non è possibile ma è possibile il distruttore virtuale. Sperimentiamo ....

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

Il codice sopra riportato genera quanto segue:

Base Constructor Called
Derived constructor called
Base Destructor called

La costruzione dell'oggetto derivato segue la regola di costruzione ma quando cancelliamo il "b" pointer (puntatore di base) abbiamo scoperto che viene chiamato solo il distruttore di base. Ma questo non deve succedere. Per fare la cosa appropriata, dobbiamo rendere virtuale il distruttore di base. Ora vediamo cosa succede nel seguente modo:

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

L'output è stato modificato come segue:

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

Quindi la distruzione del puntatore di base (che prende un'allocazione sull'oggetto derivato!) segue la regola di distruzione, cioè prima la Derivata, poi la Base. D'altra parte, non c'è niente come un costruttore virtuale.

Dichiara i distruttori virtuali nelle classi base polimorfiche. Questo è l'articolo 7 di C ++ efficace di Scott Meyers . Meyers prosegue riassumendo che se una classe ha qualsiasi funzione virtuale, dovrebbe avere un distruttore virtuale e che le classi non progettate per essere classi base o non progettate per essere utilizzate polimorficamente non dovrebbero non dichiara distruttori virtuali.

Inoltre, tieni presente che l'eliminazione di un puntatore della classe base quando non è presente un distruttore virtuale comporterà comportamento indefinito . Qualcosa che ho imparato solo di recente:

Come dovrebbe comportarsi l'override dell'eliminazione in C ++?

Uso C ++ da anni e riesco ancora ad impiccarmi.

Rendi virtuale il distruttore ogni volta che la tua classe è polimorfica.

Chiamare distruttore tramite un puntatore a una classe base

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

La chiamata del distruttore virtuale non è diversa da qualsiasi altra chiamata di funzione virtuale.

Per base- > f () , la chiamata verrà inviata a Derived :: f () , ed è la stessa per base- > ; ~ Base () - la sua funzione prioritaria - verrà chiamato Derived :: ~ Derived () .

Lo stesso accade quando il distruttore viene chiamato indirettamente, ad es. elimina base; . L'istruzione delete chiamerà base- > ~ Base () che verrà spedito a Derivato :: ~ Derivato () .

Classe astratta con distruttore non virtuale

Se non hai intenzione di eliminare l'oggetto tramite un puntatore alla sua classe base, non è necessario disporre di un distruttore virtuale. Basta renderlo protetto in modo che non venga chiamato accidentalmente:

// 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.
}

Mi piace pensare alle interfacce e alle implementazioni delle interfacce. In C ++, l'interfaccia speak è pura classe virtuale. Destructor fa parte dell'interfaccia e dovrebbe essere implementato. Pertanto il distruttore dovrebbe essere puro virtuale. Che ne dici di costruttore? Il costruttore in realtà non fa parte dell'interfaccia perché l'oggetto viene sempre istanziato esplicitamente.

Per essere semplici, Il distruttore virtuale consiste nel distruggere le risorse in un ordine corretto, quando si elimina un puntatore della classe base che punta all'oggetto classe derivato.

 #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

La parola chiave virtuale per distruttore è necessaria quando si desidera che diversi distruttori debbano seguire l'ordine corretto mentre gli oggetti vengono eliminati tramite il puntatore della classe base. ad esempio:

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

Se il distruttore di classe derivato è virtuale, gli oggetti verranno distrutti in un ordine (prima l'oggetto derivato, quindi la base). Se il distruttore di classe derivato NON è virtuale, verrà eliminato solo l'oggetto della classe base (poiché il puntatore è di classe base " Base * myObj "). Quindi ci sarà perdita di memoria per l'oggetto derivato.

Che cos'è un distruttore virtuale o come usare un distruttore virtuale

Un distruttore di classe è una funzione con lo stesso nome della classe che precede con ~ che riallocherà la memoria allocata dalla classe. Perché abbiamo bisogno di un distruttore virtuale

Vedi l'esempio seguente con alcune funzioni virtuali

L'esempio spiega anche come convertire una lettera in maiuscolo o in basso

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

Dall'esempio sopra puoi vedere che il distruttore sia per la classe MakeUpper che per la classe MakeLower non viene chiamato.

Guarda l'esempio seguente con il distruttore virtuale

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

Il distruttore virtuale chiamerà esplicitamente il distruttore di runtime più derivato della classe in modo che sia in grado di cancellare l'oggetto in modo corretto.

O visita il link

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

I distruttori di classe di base virtuale sono "best practice" - dovresti sempre usarli per evitare (difficili da rilevare) perdite di memoria. Usandoli, puoi essere sicuro che tutti i distruttori della catena ereditaria delle tue classi vengano chiamati (nell'ordine corretto). Ereditare da una classe base usando il distruttore virtuale rende automaticamente anche il distruttore della classe ereditaria (quindi non è necessario riscrivere "virtuale" nella dichiarazione del distruttore della classe ereditaria).

Ho pensato che sarebbe stato utile discutere di " undefined " comportamento, o almeno il "crash" comportamento indefinito che può verificarsi durante l'eliminazione attraverso una classe di base (/ struct) senza un distruttore virtuale, o più precisamente nessuna vtable. Il codice seguente elenca alcune semplici strutture (lo stesso vale per le classi).

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

Non sto suggerendo se hai bisogno di distruttori virtuali o meno, anche se penso che in generale sia una buona pratica averli. Sto solo sottolineando il motivo per cui potresti finire con un arresto anomalo se la tua classe di base (/ struct) non ha una vtable e la tua classe derivata (/ struct) lo fa e tu elimini un oggetto tramite una classe di base (/ struct) puntatore. In questo caso, l'indirizzo che passi alla routine gratuita dell'heap non è valido e quindi il motivo dell'incidente.

Se esegui il codice sopra vedrai chiaramente quando si verifica il problema. Quando il puntatore this della classe base (/ struct) è diverso dal puntatore this della classe derivata (/ struct) si imbatterà in questo problema. Nell'esempio sopra, struct aeb non hanno vtables. le strutture ce d hanno vtables. Pertanto, un puntatore a o b a un'istanza di oggetto c o d verrà riparato per tenere conto della vtable. Se passi questo puntatore a o b per eliminarlo, si arresterà in modo anomalo a causa dell'indirizzo non valido per la routine gratuita dell'heap.

Se si prevede di eliminare le istanze derivate che hanno vtables dai puntatori della classe base, è necessario assicurarsi che la classe base abbia una vtable. Un modo per farlo è quello di aggiungere un distruttore virtuale, che potresti voler comunque ripulire correttamente le risorse.

Penso che il nocciolo di questa domanda sia sui metodi virtuali e sul polimorfismo, non specificamente sul distruttore. Ecco un esempio più chiaro:

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

Stampa:

This is B.

Senza virtuale verrà stampato:

This is A.

E ora dovresti capire quando usare i distruttori virtuali.

quando è necessario chiamare il distruttore di classe derivato dalla classe di base. devi dichiarare il distruttore di classe di base virtuale in classe di base.

Se usi shared_ptr (solo shared_ptr, non unique_ptr), non devi avere il distruttore di classe base virtuale:

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

uscita:

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

Qualsiasi classe ereditata pubblicamente, polimorfica o meno, dovrebbe avere un distruttore virtuale. Per dirla in altro modo, se può essere indicato da un puntatore della classe base, la sua classe base dovrebbe avere un distruttore virtuale.

Se virtuale, viene chiamato il distruttore di classe derivato, quindi il costruttore della classe base. Se non virtuale, viene chiamato solo il distruttore della classe base.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top