Poniższy kod powoduje wyciek pamięci:
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class base
{
void virtual initialize_vector() = 0;
};
class derived : public base
{
private:
vector<int> vec;
public:
derived()
{
initialize_vector();
}
void initialize_vector()
{
for (int i = 0; i < 1000000; i++)
{
vec.push_back(i);
}
}
};
int main()
{
for (int i = 0; i < 100000; i++)
{
unique_ptr<base> pt = make_unique<derived>();
}
}
Nie miało to dla mnie większego sensu, ponieważ pochodne klasy nie przydzielają surowej pamięci dynamicznej, a Unique_ptr sama się zwalnia. Rozumiem, że wywoływany jest domyślny destruktor tej klasy zamiast pochodnych, ale nie rozumiem, dlaczego jest to problem. Gdybym miał napisać wyraźny destruktor dla pochodnych, nie napisałbym niczego dla vec.
c++
inheritance
memory
allocation
Ignorancja bezwładności
źródło
źródło
~derived()
delegację do destruktora vec. Alternatywnie zakładasz,unique_ptr<base> pt
że poznasz pochodną destruktor. Bez metody wirtualnej tak nie jest. Podczas gdy unikatowi można przypisać funkcję usuwania, która jest parametrem szablonu bez reprezentacji środowiska wykonawczego, a funkcja ta nie ma zastosowania w tym kodzie.Odpowiedzi:
Kiedy kompilator wykonuje niejawne
delete _ptr;
wnętrzeunique_ptr
destruktora (gdzie_ptr
jest przechowywany wskaźnikunique_ptr
), wie dokładnie dwie rzeczy:_ptr
jest. Ponieważ wskaźnik jest wunique_ptr<base>
środku, oznacza_ptr
to , że jest tego typubase*
.To wszystko, co kompilator wie. Zatem, biorąc pod uwagę, że usuwa obiekt typu
base
, wywoła~base()
.Więc ... gdzie jest ta część, gdzie niszczy
dervied
obiekt, że faktycznie wskazuje? Ponieważ jeśli kompilator nie wie, że niszczy aderived
, to wcale nie wiederived::vec
, że istnieje , nie mówiąc już o tym, że należy go zniszczyć. Więc złamałeś obiekt, pozostawiając połowę z niego niezniszczonym.Kompilator nie może zakładać, że jakiekolwiek
base*
zniszczenie jest w rzeczywistościderived*
; w końcu może istnieć dowolna liczba klas pochodzących zbase
. Skąd miałby wiedzieć, na jaki typ ten konkretniebase*
wskazuje?Kompilator musi znaleźć odpowiedni wywoływacz Destruktor (tak,
derived
ma Destruktor. Jeśli nie jesteś= delete
Destruktorem, każda klasa ma Destruktor, niezależnie od tego, czy napiszesz jeden, czy nie). Aby to zrobić, będzie musiał użyć niektórych informacji przechowywanych wbase
celu uzyskania właściwego adresu kodu destruktora do wywołania, informacji ustawionej przez konstruktora faktycznej klasy. Następnie musi użyć tych informacji do przekonwertowaniabase*
wskaźnika na adres odpowiedniejderived
klasy (która może, ale nie musi mieć inny adres. Tak, naprawdę). A potem może wywołać ten destruktor.Ten mechanizm, który właśnie opisałem? Jest to powszechnie nazywane „wirtualną wysyłką”: inaczej dzieje się to za każdym razem, gdy wywołujesz funkcję oznaczoną,
virtual
gdy masz wskaźnik / odwołanie do klasy bazowej.Jeśli chcesz wywołać pochodną funkcję klasy, gdy wszystko, co masz, to wskaźnik / referencja klasy bazowej, funkcja ta musi zostać zadeklarowana
virtual
. Niszczyciele zasadniczo nie różnią się pod tym względem.źródło
Dziedzictwo
Głównym celem dziedziczenia jest współdzielenie wspólnego interfejsu i protokołu między wieloma różnymi implementacjami, tak że instancja klasy pochodnej może być traktowana identycznie jak każda inna instancja z dowolnego innego typu pochodnego.
W C ++ dziedziczenie niesie ze sobą również szczegóły implementacji, oznaczenie (lub brak oznaczenia) destruktora jako wirtualnego jest jednym z takich szczegółów implementacji.
Wiązanie funkcji
Teraz, gdy wywoływana jest funkcja lub którykolwiek z jej specjalnych przypadków, takich jak konstruktor lub destruktor, kompilator musi wybrać, która implementacja funkcji była przeznaczona. Następnie musi wygenerować kod maszynowy zgodny z tą intencją.
Najprostszym sposobem jest wybranie funkcji w czasie kompilacji i wysłanie wystarczającej ilości kodu maszynowego, aby niezależnie od jakichkolwiek wartości, podczas wykonywania tego fragmentu kodu, zawsze uruchamiał kod funkcji. Działa to świetnie, z wyjątkiem dziedziczenia.
Jeśli mamy klasę podstawową z funkcją (może to być dowolna funkcja, w tym konstruktor lub destruktor), a kod wywołuje na niej funkcję, co to oznacza?
Biorąc z twojego przykładu, jeśli
initialize_vector()
wywołałeś kompilator, musisz zdecydować, czy naprawdę chciałeś wywołać implementację znalezioną wBase
lub implementację znalezioną wDerived
. Istnieją dwa sposoby, aby to ustalić:Base
typu, miałeś na myśli implementację wBase
.Base
wpisanej wartości może byćBase
, lubDerived
że decyzja o tym, które wezwanie do wykonania, musi zostać podjęta w czasie wykonywania po wywołaniu (za każdym razem, gdy jest wywoływana).Kompilator w tym momencie jest zdezorientowany, obie opcje są jednakowo prawidłowe. To jest, kiedy
virtual
wchodzi w skład miksu. Gdy to słowo kluczowe jest obecne, kompilator wybiera opcję 2 opóźniającą decyzję między wszystkimi możliwymi implementacjami, aż kod będzie działał z rzeczywistą wartością. W przypadku braku tego słowa kluczowego kompilator wybiera opcję 1, ponieważ jest to normalne zachowanie.Kompilator może nadal wybrać opcję 1 w przypadku wirtualnego wywołania funkcji. Ale tylko jeśli może udowodnić, że tak jest zawsze.
Konstruktory i niszczyciele
Dlaczego więc nie określamy wirtualnego konstruktora?
Bardziej intuicyjnie, w jaki sposób kompilator wybierałby między identycznymi implementacjami konstruktora dla
Derived
iDerived2
? To jest dość proste, nie może. Nie ma żadnej wcześniejszej wartości, na podstawie której kompilator mógłby dowiedzieć się, co naprawdę było zamierzone. Nie ma wcześniejszej wartości, ponieważ jest to zadanie konstruktora.Dlaczego więc musimy określić wirtualny destruktor?
Bardziej intuicyjnie, w jaki sposób kompilator wybierałby implementacje dla
Base
iDerived
? Są to tylko wywołania funkcji, więc zachowuje się zachowanie wywołania funkcji. Bez deklarowanego wirtualnego destruktora kompilator zdecyduje się na bezpośrednie powiązanie zBase
destruktorem, niezależnie od typu środowiska uruchomieniowego wartości.W wielu kompilatorach, jeśli pochodne nie deklarują żadnych elementów danych ani nie dziedziczą po innych typach, zachowanie w
~Base()
będzie odpowiednie, ale nie jest gwarantowane. Działałoby to wyłącznie przypadkowo, podobnie jak stanie przed miotaczem ognia, który nie został jeszcze zapalony. Przez jakiś czas nic ci nie jest.Jedynym prawidłowym sposobem zadeklarowania dowolnego typu bazowego lub interfejsu w C ++ jest zadeklarowanie wirtualnego destruktora, aby wywołać prawidłowy destruktor dla dowolnej instancji hierarchii typów tego typu. Dzięki temu funkcja z największą wiedzą o instancji może poprawnie wyczyścić tę instancję.
źródło