Dobra, uporządkujmy trochę informacje.
Interfejsy i klasy abstrakcyjne w C++
W C++ domyślnie przekazujesz instancje klas przez wartości, a nie przez referencje.
Jakie są tego skutki:
class Klasa
{
public:
Klasa(int x) {
this->x = x;
}
int getX() {
return x;
}
void setX(int x) {
this->x = x;
}
private:
int x;
};
Klasa a(8);
Klasa b(9);
a = b;
b.setX(6);
Przypisanie b
do a
powoduje skopiowanie b
do a
.
a.getX() != b.getX()
Co zrobić, jeśli mamy klasę abstrakcyjną? Musimy do jej instancji odwoływać się przez wskaźnik lub referencję.
Wskaźnik w C++ to z grubsza to samo, co referencja w Javie.
Referencja w C++ to taki wskaźnik, którego nie możemy przestawiać i ogólnie korzystamy z niego jak ze zwykłej zmiennej, bez tych wszystkich gwiazdek i strzałek.
Czas życia zmiennych i zakresy
Nie możemy jednak zrobić czegoś takiego:
KlasaAbstrakcyjna *nowa_instancja()
{
KlasaAbstrakcyjnaImpl result;
return &result;
};
W C++ są 2 typy zmiennych: alokowane na stosie (stack) i na stercie (heap). W Javie jest podobnie - prymitywy, takie jak int
, są alokowane na stosie, a instancje klas są alokowane na stercie. Zmienne alokowane na stosie są niszczone po wyjściu poza zakres. Oznacza to, że po wykonaniu funkcji nowa_instancja
wskaźnik, który zwróciła, będzie wskazywać na wyczyszczoną pamięć.
Dzieje się tak, gdyż w C++ nie ma garbage collectora i wszystko jest domyślnie alokowane na stosie.
Żeby zmienna przeżyła zakończenie funkcji, musi zostać zaalokowana na stercie. Jednak pamięć zaalokowaną na stercie musimy czyścić ręcznie.
Tradycyjnie robiono to tak:
KlasaAbstrakcyjna* instancja = new KlasaAbstrakcyjnaImp();
.. // zrób coś ze zmienną instancja
delete instancja;
Bardzo łatwo zapomnieć o zniszczeniu zmiennej. Wtedy nastąpi wyciek pamięci.
Jednak C++ oferuje mechanizm bardzo ułatwiający wiele zadań, w tym zarządzanie pamięcią: destruktory. Destruktor to funkcja wywoływana po wyjściu zmiennej poza zakres, czyli tuż przed zniszczeniem obiektu.
class Destruktor
{
public:
~Destruktor()
{
cout << "Wywołano destruktor" << endl;
}
};
int main()
{
Destruktor d;
// wypisze "Wywołano destruktor" w konsoli
}
I teraz to połączmy: nie możemy korzystać z klas abstrakcyjnych alokując ich instancje na stosie, ale możemy wskaźniki opakować w klasy, które wyczyszczą pamięć za nas!
unique_ptr
i shared_ptr
Tu już sprawa jest dosyć prosta:
-
unique_ptr
wyczyści pamięć, kiedy zmienna wyjdzie poza zakres:
int main()
{
unique_ptr<KlasaAbstrakcyjna> instancja = make_unique<KlasaAbstrakcyjnaImpl>();
// <- tutaj `instancja` zostanie zniszczona
}
-
shared_ptr
zlicza referencje i czyści pamięć wtedy, kiedy żaden obiekt nie odwołuje się do danej instancji - działa więc podobnie do garbage collectora (ale nie tak samo, polecam poczytać o różnicach. Hasło: garbage collection vs reference counting
)
int main()
{
shared_ptr<KlasaAbstrakcyjna> instancja1 = make_shared<KlasaAbstrakcyjnaImpl>(); // 1 referencja
shared_ptr<KlasaAbstrakcyjna> instancja2 = instancja1; // 2 referencje
instancja1 = nullptr; // 1 referencja
instancja2 = nullptr; // <- 0 referencji: tutaj zaalokowana zmienna zostanie zniszczona
}
Oczywiście nie trzeba przypisywać nullptr
do zmiennych instancja
i instancja2
- shared_ptr
ma zdefiniowany destruktor, który zrobi to za nas.
Działa to dlatego, że instancje unique_ptr
i shared_ptr
są alokowane na stosie, więc są niszczone automatycznie.
vector
Kolekcje w STL-u nie są tak fajnie zaprojektowane jak w Javie (nie ma interfejsów i ich implementacji, tylko od razu implementacje. Na szczęście są szablony). W C++ można przeładowywać operatory, więc vector
ma przeładowany operator []
.
vector<int> liczby { 1, 2, 3, 4 };
liczby.push_back(5); // { 1, 2, 3, 4, 5 }
Zmienne dodawane do vectora są przekazywane przez wartość, więc są kopiowane.
Nie możemy jednak kopiować klas abstrakcyjnych. Dlatego powinniśmy zrobić coś takiego:
vector<shared_ptr<KlasaAbstrakcyjna>> instancje {
make_shared<KlasaAbstrakcyjnaImpl>(1),
make_shared<KlasaAbstrakcyjnaImpl>(2)
};
instancja[0]->zrobCos();
Zmienne pobierane z vectora są przekazywane przez referencję, więc kopiowanie nie następuje.
Dobra, mam nadzieję, że nic nie pomieszałem - od dłuższego czasu siedzę w Javie, więc mogłem coś pomylić. Powinno być OK, bo sprawdzałem kody w http://cpp.sh/ i wszystko się kompilowało ;)