Po co nam czysty wirtualny destruktor w C ++?


154

Rozumiem potrzebę posiadania wirtualnego destruktora. Ale dlaczego potrzebujemy czystego wirtualnego destruktora? W jednym z artykułów C ++ autor wspomniał, że używamy czystego wirtualnego destruktora, gdy chcemy stworzyć abstrakcję klasy.

Ale możemy uczynić klasę abstrakcyjną, sprawiając, że dowolny element członkowski działa jako czysto wirtualny.

Więc moje pytania są

  1. Kiedy naprawdę sprawiamy, że destruktor jest czysto wirtualny? Czy ktoś może podać dobry przykład w czasie rzeczywistym?

  2. Kiedy tworzymy klasy abstrakcyjne, czy dobrą praktyką jest uczynienie destruktora również czysto wirtualnym? Jeśli tak, to dlaczego?



14
@ Daniel- Podane linki nie odpowiadają na moje pytanie. Odpowiada, dlaczego czysty wirtualny destruktor powinien mieć definicję. Moje pytanie brzmi: dlaczego potrzebujemy czystego wirtualnego destruktora.
Mark

Próbowałem znaleźć przyczynę, ale już zadałeś to pytanie.
nsivakr

Odpowiedzi:


119
  1. Prawdopodobnie prawdziwym powodem, dla którego dozwolone są czyste wirtualne destruktory, jest to, że zakazanie ich oznaczałoby dodanie kolejnej reguły do ​​języka i nie ma potrzeby stosowania tej reguły, ponieważ zezwolenie na czysty wirtualny destruktor nie może powodować żadnych złych skutków.

  2. Nie, wystarczy zwykły stary wirtualny.

Jeśli tworzysz obiekt z domyślnymi implementacjami jego metod wirtualnych i chcesz uczynić go abstrakcyjnym bez zmuszania kogokolwiek do nadpisania określonego metody, możesz uczynić destruktor czystym wirtualnym. Nie widzę w tym sensu, ale jest to możliwe.

Zauważ, że ponieważ kompilator wygeneruje niejawny destruktor dla klas pochodnych, jeśli autor klasy tego nie zrobi, żadne klasy pochodne nie będą abstrakcyjne. Dlatego posiadanie czystego wirtualnego destruktora w klasie bazowej nie będzie miało żadnego znaczenia dla klas pochodnych. Spowoduje to, że klasa bazowa będzie tylko abstrakcyjna (dzięki za @kappa komentarz ).

Można również założyć, że każda klasa pochodna prawdopodobnie musiałaby mieć określony kod czyszczący i użyć czystego wirtualnego destruktora jako przypomnienia o jego napisaniu, ale wydaje się to wymyślone (i niewymuszone).

Uwaga: Destruktor jest jedyną metodą, która nawet jeśli jest czysto wirtualna, musi mieć implementację w celu utworzenia instancji klas pochodnych (tak, czyste funkcje wirtualne mogą mieć implementacje).

struct foo {
    virtual void bar() = 0;
};

void foo::bar() { /* default implementation */ }

class foof : public foo {
    void bar() { foo::bar(); } // have to explicitly call default implementation.
};

13
„tak, czyste funkcje wirtualne mogą mieć implementacje” W takim razie nie jest to czysto wirtualne.
GManNickG

2
Jeśli chcesz stworzyć abstrakcję klasy, czy nie byłoby łatwiej po prostu zabezpieczyć wszystkie konstruktory?
bdonlan

78
@GMan, mylisz się, bycie czystymi wirtualnymi oznacza, że ​​klasy pochodne muszą przesłonić tę metodę, jest to ortogonalne do posiadania implementacji. Sprawdź mój kod i skomentuj, foof::barjeśli chcesz się przekonać.
Motti

15
@GMan: C ++ FAQ lite mówi: „Zauważ, że można podać definicję czystej funkcji wirtualnej, ale to zwykle dezorientuje nowicjuszy i najlepiej tego unikać na później”. parashift.com/c++-faq-lite/abcs.html#faq-22.4 Wikipedia (ten bastion poprawności) również mówi podobnie. Uważam, że norma ISO / IEC używa podobnej terminologii (niestety moja kopia jest w tej chwili w użyciu) ... Zgadzam się, że jest myląca i generalnie nie używam tego terminu bez wyjaśnienia, gdy podaję definicję, zwłaszcza wokół nowszych programistów ...
leander

9
@Motti: To, co jest tutaj interesujące i wprowadza więcej zamieszania, to fakt, że czysty wirtualny destruktor NIE musi być jawnie zastępowany w klasie pochodnej (i utworzonej). W takim przypadku używana jest niejawna definicja :)
kappa

33

Wszystko, czego potrzebujesz do klasy abstrakcyjnej, to przynajmniej jedna czysta funkcja wirtualna. Każda funkcja się nada; ale tak się składa, że ​​destruktor jest czymś, co będzie miała każda klasa - więc zawsze jest dostępny jako kandydat. Co więcej, uczynienie destruktora czystym wirtualnym (w przeciwieństwie do zwykłego wirtualnego) nie ma żadnych behawioralnych skutków ubocznych poza uczynieniem klasy abstrakcyjną. W związku z tym wiele przewodników po stylach zaleca konsekwentne stosowanie czystego wirtualnego elementu destuctor, aby wskazać, że klasa jest abstrakcyjna - choćby z innego powodu niż zapewnia spójne miejsce, w którym ktoś czytający kod może sprawdzić, czy klasa jest abstrakcyjna.


1
ale nadal dlaczego zapewnić implementację czystego virtaul destructor. Co mogłoby się nie udać, robię destruktor jako czysty wirtualny i nie zapewniam jego implementacji. Zakładam, że deklarowane są tylko wskaźniki klas bazowych, dlatego destruktor klasy abstrakcyjnej nigdy nie jest wywoływany.
Krishna Oza,

4
@Surfing: ponieważ destruktor klasy pochodnej niejawnie wywołuje destruktor swojej klasy bazowej, nawet jeśli ten destruktor jest czysto wirtualny. Więc jeśli nie ma dla tego implementacji, nastąpi niezdefiniowane zachowanie.
a.peganz

19

Jeśli chcesz utworzyć abstrakcyjną klasę bazową:

  • którego nie można utworzyć (tak, jest to zbędne w przypadku terminu „streszczenie”!)
  • ale wymaga zachowania wirtualnego destruktora (zamierzasz przenosić wskaźniki do ABC zamiast wskaźników do typów pochodnych i usuwać za ich pośrednictwem)
  • ale nie potrzeba żadnego innego wirtualnego wysyłki zachowanie dla innych metod (być może nie żadne inne metody? Rozważmy prosty chroniony „zasób” pojemnik, który potrzebuje konstruktorów / destructor / zadanie, ale nie wiele więcej)

... najłatwiej jest uczynić klasę abstrakcyjną, czyniąc destruktor czystym wirtualnym i dostarczając dla niego definicję (treść metody).

Dla naszego hipotetycznego abecadła:

Gwarantujesz, że nie można go utworzyć instancji (nawet wewnątrz samej klasy, dlatego prywatne konstruktory mogą nie wystarczyć), otrzymujesz wirtualne zachowanie, które chcesz dla destruktora, i nie musisz znajdować i oznaczać innej metody, która tego nie robi Wirtualna wysyłka nie jest potrzebna jako „wirtualna”.


8

Z odpowiedzi, które przeczytałem na twoje pytanie, nie mogłem wydedukować dobrego powodu, aby faktycznie użyć czystego wirtualnego destruktora. Na przykład następujący powód w ogóle mnie nie przekonuje:

Prawdopodobnie prawdziwym powodem, dla którego dozwolone są czyste wirtualne destruktory, jest to, że zakazanie ich oznaczałoby dodanie kolejnej reguły do ​​języka i nie ma potrzeby stosowania tej reguły, ponieważ zezwolenie na czysty wirtualny destruktor nie może powodować żadnych złych skutków.

Moim zdaniem przydatne mogą być czyste wirtualne destruktory. Na przykład załóżmy, że masz w kodzie dwie klasy myClassA i myClassB i że myClassB dziedziczy po myClassA. Z powodów wymienionych przez Scotta Meyersa w jego książce „Bardziej efektywny C ++”, punkt 33 „Tworzenie abstrakcyjnych klas innych niż liście”, lepszą praktyką jest tworzenie klasy abstrakcyjnej myAbstractClass, z której dziedziczą myClassA i myClassB. Zapewnia to lepszą abstrakcję i zapobiega niektórym problemom wynikającym na przykład z kopiowaniem obiektów.

W procesie abstrakcji (tworzenia klasy myAbstractClass) może się zdarzyć, że żadna metoda myClassA lub myClassB nie będzie dobrym kandydatem do bycia czystą metodą wirtualną (co jest warunkiem abstrakcji myAbstractClass). W tym przypadku definiujesz destruktor klasy abstrakcyjnej jako czysty wirtualny.

Poniżej konkretny przykład z kodu, który sam napisałem. Mam dwie klasy, Numerics / PhysicsParams, które mają wspólne właściwości. Dlatego pozwoliłem im dziedziczyć po abstrakcyjnej klasie IParams. W tym przypadku nie miałem pod ręką żadnej metody, która mogłaby być czysto wirtualna. Na przykład metoda setParameter musi mieć tę samą treść dla każdej podklasy. Jedynym wyborem, jaki miałem, było uczynienie destruktora IParams czystym wirtualnym.

struct IParams
{
    IParams(const ModelConfiguration& aModelConf);
    virtual ~IParams() = 0;

    void setParameter(const N_Configuration::Parameter& aParam);

    std::map<std::string, std::string> m_Parameters;
};

struct NumericsParams : IParams
{
    NumericsParams(const ModelConfiguration& aNumericsConf);
    virtual ~NumericsParams();

    double dt() const;
    double ti() const;
    double tf() const;
};

struct PhysicsParams : IParams
{
    PhysicsParams(const N_Configuration::ModelConfiguration& aPhysicsConf);
    virtual ~PhysicsParams();

    double g()     const; 
    double rho_i() const; 
    double rho_w() const; 
};

1
Podoba mi się to użycie, ale innym sposobem „wymuszenia” dziedziczenia jest zadeklarowanie, że konstruktor IParamma być chroniony, jak zauważono w innym komentarzu.
rwols

4

Jeśli chcesz zatrzymać tworzenie instancji klasy bazowej bez dokonywania jakichkolwiek zmian w już zaimplementowanej i przetestowanej klasie pochodnej, zaimplementujesz czysty wirtualny destruktor w swojej klasie bazowej.


3

Tutaj chcę powiedzieć, kiedy potrzebujemy wirtualnego destruktora, a kiedy czystego wirtualnego destruktora

class Base
{
public:
    Base();
    virtual ~Base() = 0; // Pure virtual, now no one can create the Base Object directly 
};

Base::Base() { cout << "Base Constructor" << endl; }
Base::~Base() { cout << "Base Destructor" << endl; }


class Derived : public Base
{
public:
    Derived();
    ~Derived();
};

Derived::Derived() { cout << "Derived Constructor" << endl; }
Derived::~Derived() {   cout << "Derived Destructor" << endl; }


int _tmain(int argc, _TCHAR* argv[])
{
    Base* pBase = new Derived();
    delete pBase;

    Base* pBase2 = new Base(); // Error 1   error C2259: 'Base' : cannot instantiate abstract class
}
  1. Jeśli chcesz, aby nikt nie mógł bezpośrednio utworzyć obiektu klasy Base, użyj czystego wirtualnego destruktora virtual ~Base() = 0. Zwykle wymagana jest przynajmniej jedna czysta funkcja wirtualna, przyjmijmy virtual ~Base() = 0, że jest to funkcja.

  2. Kiedy nie potrzebujesz powyższej rzeczy, potrzebujesz tylko bezpiecznego zniszczenia obiektu klasy pochodnej

    Baza * pBase = new Derived (); usuń pBase; czysty wirtualny destruktor nie jest wymagany, tylko wirtualny destruktor wykona zadanie.


2

Z tymi odpowiedziami wchodzisz w hipotetyczne, więc dla jasności spróbuję przedstawić prostsze, bardziej przyziemne wyjaśnienie.

Podstawowe zależności w projektowaniu obiektowym są dwie: IS-A i HAS-A. Ja tego nie wymyśliłem. Tak się nazywają.

IS-A wskazuje, że określony obiekt identyfikuje się jako należący do klasy znajdującej się nad nim w hierarchii klas. Obiekt banana jest obiektem owocu, jeśli jest podklasą klasy owoców. Oznacza to, że wszędzie tam, gdzie można użyć klasy owoców, można użyć banana. Nie jest to jednak odruchowe. Nie można podstawić klasy bazowej dla określonej klasy, jeśli ta konkretna klasa jest wywoływana.

Has-a wskazał, że obiekt jest częścią klasy złożonej i że istnieje relacja własności. W C ++ oznacza to, że jest to obiekt składowy i jako taki na klasie będącej właścicielem spoczywa obowiązek pozbycia się go lub przekazania własności przed zniszczeniem samego siebie.

Te dwie koncepcje są łatwiejsze do zrealizowania w językach z pojedynczym dziedziczeniem niż w modelu wielokrotnego dziedziczenia, takim jak c ++, ale zasady są zasadniczo takie same. Komplikacja pojawia się, gdy tożsamość klasy jest niejednoznaczna, na przykład przekazanie wskaźnika klasy Banana do funkcji, która pobiera wskaźnik klasy Fruit.

Funkcje wirtualne są, po pierwsze, kwestią czasu wykonywania. Jest częścią polimorfizmu, ponieważ służy do decydowania, która funkcja ma być uruchomiona w momencie wywołania w uruchomionym programie.

Słowo kluczowe virtual jest dyrektywą kompilatora, która wiąże funkcje w określonej kolejności, jeśli istnieje niejasność dotycząca tożsamości klasy. Funkcje wirtualne są zawsze w klasach nadrzędnych (o ile wiem) i wskazują kompilatorowi, że powiązanie funkcji składowych z ich nazwami powinno odbywać się najpierw z funkcją podklasy, a następnie z funkcją klasy nadrzędnej.

Klasa Fruit może mieć wirtualną funkcję color (), która domyślnie zwraca „NONE”. Funkcja Color () klasy Banana zwraca „ŻÓŁTY” lub „BRĄZOWY”.

Ale jeśli funkcja pobierająca wskaźnik Fruit wywoła color () w wysłanej do niej klasie Banana - która funkcja color () zostanie wywołana? Funkcja normalnie wywołałaby Fruit :: color () dla obiektu Fruit.

W 99% przypadków nie byłoby to zamierzone. Ale jeśli Fruit :: color () zostałaby zadeklarowana jako wirtualna, wówczas Banana: color () zostałby wywołany dla obiektu, ponieważ prawidłowa funkcja color () byłaby powiązana ze wskaźnikiem Fruit w momencie wywołania. Środowisko uruchomieniowe sprawdzi, na który obiekt wskazuje wskaźnik, ponieważ został on oznaczony jako wirtualny w definicji klasy Fruit.

Różni się to od zastępowania funkcji w podklasie. W takim przypadku wskaźnik Fruit wywoła Fruit :: color (), jeśli jedyne, co wie, to to, że jest wskaźnikiem IS-A do Fruit.

Więc teraz pojawia się idea „czystej funkcji wirtualnej”. Jest to raczej niefortunne zdanie, ponieważ czystość nie ma z tym nic wspólnego. Oznacza to, że jest zamierzone, aby metoda klasy bazowej nigdy nie była wywoływana. Rzeczywiście, nie można wywołać czystej funkcji wirtualnej. Jednak nadal należy go zdefiniować. Podpis funkcji musi istnieć. Wielu programistów tworzy pustą implementację {} dla kompletności, ale kompilator wygeneruje ją wewnętrznie, jeśli nie. W takim przypadku, gdy funkcja jest wywoływana, nawet jeśli wskaźnik wskazuje na Fruit, zostanie wywołana Banana :: color (), ponieważ jest to jedyna dostępna implementacja color ().

Teraz ostatni element układanki: konstruktorzy i destruktory.

Czyste wirtualne konstruktory są całkowicie nielegalne. To właśnie się skończyło.

Ale czyste wirtualne destruktory działają w przypadku, gdy chcesz zabronić tworzenia instancji klasy bazowej. Tylko podklasy mogą być tworzone, jeśli destruktor klasy bazowej jest czysto wirtualny. Konwencja polega na przypisaniu go do 0.

 virtual ~Fruit() = 0;  // pure virtual 
 Fruit::~Fruit(){}      // destructor implementation

W takim przypadku musisz utworzyć implementację. Kompilator wie, że to właśnie robisz i upewnia się, że robisz to dobrze, lub bardzo narzeka, że ​​nie może połączyć się ze wszystkimi funkcjami, których potrzebuje do skompilowania. Błędy mogą być mylące, jeśli nie jesteś na dobrej drodze, jeśli chodzi o modelowanie hierarchii klas.

Więc w tym przypadku nie możesz tworzyć instancji Fruit, ale możesz tworzyć instancje Banana.

Wywołanie usunięcia wskaźnika Fruit, który wskazuje na instancję klasy Banana, najpierw wywoła Banana :: ~ Banana (), a następnie zawsze Fuit :: ~ Fruit (). Ponieważ bez względu na wszystko, kiedy wywołujesz destruktor podklasy, musi być zgodny z destruktorem klasy bazowej.

Czy to zły model? Jest to bardziej skomplikowane na etapie projektowania, tak, ale może zapewnić prawidłowe łączenie w czasie wykonywania oraz wykonywanie funkcji podklasy w przypadku niejasności co do tego, do której podklasy uzyskuje się dostęp.

Jeśli piszesz w C ++ tak, że przekazujesz tylko dokładne wskaźniki do klas bez wskaźników ogólnych ani niejednoznacznych, wtedy funkcje wirtualne nie są tak naprawdę potrzebne. Ale jeśli potrzebujesz elastyczności typów w czasie wykonywania (jak w Apple Banana Orange ==> Fruit), funkcje stają się łatwiejsze i bardziej wszechstronne z mniej redundantnym kodem. Nie musisz już pisać funkcji dla każdego rodzaju owocu i wiesz, że każdy owoc zareaguje na color () swoją własną, poprawną funkcją.

Mam nadzieję, że to rozwlekłe wyjaśnienie raczej utrwali koncepcję, a nie wprowadzi w błąd. Istnieje wiele dobrych przykładów, na które można spojrzeć, przyjrzeć się im wystarczająco dużo, uruchomić je i zadzierać z nimi, a otrzymasz to.


1

To temat sprzed dziesięciu lat :) Przeczytaj ostatnie 5 akapitów punktu 7 w książce „Efektywny C ++”, aby uzyskać szczegółowe informacje, zaczyna się od „Czasami wygodnie jest nadać klasie czysty wirtualny destruktor…”


0

Poprosiłeś o przykład i uważam, że poniższe uzasadnienie dla czystego wirtualnego destruktora. Z niecierpliwością czekam na odpowiedzi, czy to dobry powód ...

Nie chcę, aby ktokolwiek mógł wrzucić error_basetyp, ale typy wyjątków error_oh_shucksi error_oh_blastmają identyczną funkcjonalność i nie chcę pisać tego dwa razy. Złożoność pImpl jest konieczna, aby uniknąć ujawnienia std::stringmoim klientom i użyciastd::auto_ptr wymaga konstruktora kopiującego.

Nagłówek publiczny zawiera specyfikacje wyjątków, które będą dostępne dla klienta w celu rozróżnienia różnych typów wyjątków rzucanych przez moją bibliotekę:

// error.h

#include <exception>
#include <memory>

class exception_string;

class error_base : public std::exception {
 public:
  error_base(const char* error_message);
  error_base(const error_base& other);
  virtual ~error_base() = 0; // Not directly usable

  virtual const char* what() const;
 private:
  std::auto_ptr<exception_string> error_message_;
};

template<class error_type>
class error : public error_base {
 public:
   error(const char* error_message) : error_base(error_message) {}
   error(const error& other) : error_base(other) {}
   ~error() {}
};

// Neither should these classes be usable
class error_oh_shucks { virtual ~error_oh_shucks() = 0; }
class error_oh_blast { virtual ~error_oh_blast() = 0; }

A oto wspólna implementacja:

// error.cpp

#include "error.h"
#include "exception_string.h"

error_base::error_base(const char* error_message)
  : error_message_(new exception_string(error_message)) {}

error_base::error_base(const error_base& other)
  : error_message_(new exception_string(other.error_message_->get())) {}

error_base::~error_base() {}

const char* error_base::what() const {
  return error_message_->get();
}

Klasa wyjątku_string, utrzymywana jako prywatna, ukrywa std :: string z mojego publicznego interfejsu:

// exception_string.h

#include <string>

class exception_string {
 public:
  exception_string(const char* message) : message_(message) {}

  const char* get() const { return message_.c_str(); }
 private:
  std::string message_;
};

Mój kod następnie zgłasza błąd jako:

#include "error.h"

throw error<error_oh_shucks>("That didn't work");

Korzystanie z szablonu errorjest trochę nieodpłatne. Oszczędza trochę kodu kosztem wymagania od klientów wychwytywania błędów, takich jak:

// client.cpp

#include <error.h>

try {
} catch (const error<error_oh_shucks>&) {
} catch (const error<error_oh_blast>&) {
}

0

Może jest inny PRAWDZIWY PRZYPADEK czystego wirtualnego destruktora, którego właściwie nie widzę w innych odpowiedziach :)

Na początku całkowicie zgadzam się z zaznaczoną odpowiedzią: To dlatego, że zakazanie czystego wirtualnego destruktora wymagałoby dodatkowej reguły w specyfikacji języka. Ale nadal nie jest to przypadek użycia, do którego wzywa Mark :)

Najpierw wyobraź sobie to:

class Printable {
  virtual void print() const = 0;
  // virtual destructor should be here, but not to confuse with another problem
};

i coś takiego:

class Printer {
  void queDocument(unique_ptr<Printable> doc);
  void printAll();
};

Po prostu - mamy interfejs Printablei jakiś „kontener” przechowujący wszystko z tym interfejsem. Myślę, że tutaj jest całkiem jasne, dlaczegoprint() metoda jest czysto wirtualna. Może mieć jakąś treść, ale jeśli nie ma domyślnej implementacji, czysta wirtualność jest idealną "implementacją" (= "musi być dostarczone przez klasę potomną").

A teraz wyobraź sobie dokładnie to samo, z wyjątkiem tego, że nie jest to drukowanie, ale zniszczenie:

class Destroyable {
  virtual ~Destroyable() = 0;
};

A także może być podobny pojemnik:

class PostponedDestructor {
  // Queues an object to be destroyed later.
  void queObjectForDestruction(unique_ptr<Destroyable> obj);
  // Destroys all already queued objects.
  void destroyAll();
};

To uproszczony przypadek użycia z mojej prawdziwej aplikacji. Jedyną różnicą jest to, że zamiast metody „normalnej” użyto metody „specjalnej” (destructor) print(). Ale powód, dla którego jest to czysto wirtualny, jest nadal ten sam - nie ma domyślnego kodu dla metody. Nieco zagmatwany może być fakt, że MUSI istnieć efektywnie jakiś destruktor, a kompilator faktycznie generuje dla niego pusty kod. Ale z punktu widzenia programisty czysta wirtualność wciąż oznacza: „Nie mam żadnego domyślnego kodu, musi być dostarczony przez klasy pochodne”.

Myślę, że to nie jest żaden wielki pomysł, tylko więcej wyjaśnienia, że ​​czysta wirtualność działa naprawdę jednolicie - również w przypadku destruktorów.


-2

1) Gdy chcesz, aby klasy pochodne były czyszczone. To jest rzadkie.

2) Nie, ale chcesz, aby był wirtualny.


-2

musimy uczynić destruktor wirtualnym, ponieważ jeśli nie uczynimy go wirtualnym, to kompilator zniszczy tylko zawartość klasy bazowej, n wszystkie klasy pochodne pozostaną niezmienione, kompilator bacuse nie będzie wywoływał destruktora żadnego innego klasa z wyjątkiem klasy bazowej.


-1: Nie chodzi o to, dlaczego destruktor powinien być wirtualny.
Troubadour

Co więcej, w niektórych sytuacjach destruktory nie muszą być wirtualne, aby osiągnąć prawidłowe zniszczenie. Wirtualne destruktory są potrzebne tylko wtedy, gdy wywołujesz deletewskaźnik do klasy bazowej, podczas gdy w rzeczywistości wskazuje on na jego pochodną.
CygnusX1

Masz 100% racji. Jest to i było w przeszłości jednym z głównych źródeł przecieków i awarii w programach C ++, trzecim tylko po próbach robienia rzeczy ze wskaźnikami zerowymi i przekraczania granic tablic. Niewirtualny destruktor klasy bazowej zostanie wywołany na wskaźniku ogólnym, całkowicie pomijając destruktor podklasy, jeśli nie jest on oznaczony jako wirtualny. Jeśli istnieją dynamicznie tworzone obiekty należące do podklasy, nie zostaną one odzyskane przez podstawowy destruktor po wywołaniu funkcji delete. Dasz sobie spokój, a potem BLUURRK! (też trudno znaleźć)
Chris Reid,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.