Problem czyszczenia
Załóżmy, że piszesz program, który musi przesłać pewne dane przez sieć. Jednak nawiązanie połączenia z serwerem jest drogie, dlatego chcesz zebrać kilka danych, a następnie wysłać je wszystkie na raz. Taka klasa może mieć następującą strukturę:
// This example won't compile because it is (intentionally) incomplete
class NetworkData
{
private:
std::string m_serverName{};
DataStore m_dataQueue{};
public:
NetworkData(std::string_view serverName)
: m_serverName { serverName }
{
}
void addData(std::string_view data)
{
m_dataQueue.add(data);
}
void sendData()
{
// connect to server
// send all data
// clear data
}
};
int main()
{
NetworkData n("someipAddress");
n.addData("somedata1");
n.addData("somedata2");
n.sendData();
return 0;
}Jednak NetworkData istnieje potencjalny problem. Polega na sendData() wywołaniu jawnym przed zamknięciem programu. Jeśli użytkownik NetworkData zapomni o tym, dane nie zostaną wysłane na serwer i zostaną utracone po zamknięciu programu. Możesz teraz powiedzieć: „no cóż, nie jest trudno o tym pamiętać!” i w tym konkretnym przypadku będziesz miał rację. Rozważmy jednak nieco bardziej złożony przykład, na przykład tę funkcję:
bool someFunction()
{
NetworkData n("someipAddress");
n.addData("somedata1");
n.addData("somedata2");
if (someCondition)
return false;
n.sendData();
return true;
}W tym przypadku, jeśli someCondition Jest true, to funkcja powróci wcześniej i sendData() nie zostanie wywołana. Łatwiej jest popełnić ten błąd, ponieważ sendData() wywołanie jest obecne, program po prostu nie wyznacza ścieżki do niego we wszystkich przypadkach.
Uogólniając ten problem, klasy korzystające z zasobu (najczęściej pamięci, ale czasami plików, baz danych, połączeń sieciowych itp.) często muszą zostać jawnie wysłane lub zamknięte, zanim korzystający z nich obiekt klasy zostanie zniszczony. W innych przypadkach możemy chcieć przeprowadzić pewne rejestry przed zniszczeniem obiektu, na przykład zapisać informacje w pliku dziennika lub wysłać fragment danych telemetrycznych na serwer. Terminu „sprzątanie” często używa się w odniesieniu do dowolnego zestawu zadań, które klasa musi wykonać, zanim obiekt klasy zostanie zniszczony, aby zachować się zgodnie z oczekiwaniami. Jeśli musimy polegać na użytkowniku takiej klasy, aby upewnić się, że funkcja wykonująca czyszczenie zostanie wywołana przed zniszczeniem obiektu, prawdopodobnie napotkamy gdzieś błędy.
Ale dlaczego w ogóle wymagamy od użytkownika, aby się tym zajął? Jeśli obiekt ulega zniszczeniu, wiemy, że w tym momencie należy przeprowadzić sprzątanie. Czy to czyszczenie powinno odbywać się automatycznie?
Destruktory na ratunek
W lekcji 14.9 -- Wprowadzenie do konstruktorów omówiliśmy konstruktory, które są specjalnymi funkcjami składowymi wywoływanymi, gdy tworzony jest obiekt typu klasy niezagregowanej. Konstruktory służą do inicjowania zmiennych składowych i wykonywania wszelkich innych zadań konfiguracyjnych wymaganych do zapewnienia, że obiekty klasy są gotowe do użycia.
Analogicznie, klasy mają inny typ specjalnej funkcji składowej, która jest wywoływana automatycznie, gdy obiekt klasy niezagregowanej zostanie zniszczony. Ta funkcja nazywa się a destruktor. Destruktory zaprojektowano tak, aby umożliwić klasie wykonanie niezbędnego czyszczenia, zanim obiekt klasy zostanie zniszczony.
Nazewnictwo destruktorów
Podobnie jak konstruktory, destruktory mają określone zasady nazewnictwa:
- Destruktor musi mieć taką samą nazwę jak klasa, poprzedzoną tyldą (~).
- Destruktor nie może przyjmować argumentów.
- Destruktor nie ma typu zwracanego.
Klasa może mieć tylko jeden destruktor.
Generalnie nie należy jawnie wywoływać destruktora (ponieważ zostanie on wywołany automatycznie, gdy obiekt zostanie zniszczony), ponieważ rzadko zdarzają się przypadki, w których chciałbyś wyczyścić obiekt więcej niż raz.
Destruktory mogą bezpiecznie wywoływać inne funkcje składowe, ponieważ obiekt zostanie zniszczony dopiero po destruktorze wykonuje.
Przykład destruktora
#include <iostream>
class Simple
{
private:
int m_id {};
public:
Simple(int id)
: m_id { id }
{
std::cout << "Constructing Simple " << m_id << '\n';
}
~Simple() // here's our destructor
{
std::cout << "Destructing Simple " << m_id << '\n';
}
int getID() const { return m_id; }
};
int main()
{
// Allocate a Simple
Simple simple1{ 1 };
{
Simple simple2{ 2 };
} // simple2 dies here
return 0;
} // simple1 dies hereTen program daje następujący wynik:
Constructing Simple 1 Constructing Simple 2 Destructing Simple 2 Destructing Simple 1
Zauważ, że gdy każdy Simple obiekt zostanie zniszczony, wywoływany jest destruktor, który wypisuje komunikat. „Destructing Simple 1” jest drukowane po „Destructing Simple 2”, ponieważ simple2 został zniszczony przed zakończeniem funkcji, podczas gdy simple1 nie został zniszczony do końca main().
Pamiętaj, że zmienne statyczne (w tym zmienne globalne i statyczne zmienne lokalne) są konstruowane podczas uruchamiania programu i niszczone przy zamykaniu programu.
Udoskonalanie danych sieciowych program
Wracając do naszego przykładu na początku lekcji, możemy wyeliminować potrzebę jawnego wywoływania przez użytkownika sendData() poprzez destruktor wywołujący tę funkcję:
class NetworkData
{
private:
std::string m_serverName{};
DataStore m_dataQueue{};
public:
NetworkData(std::string_view serverName)
: m_serverName { serverName }
{
}
~NetworkData()
{
sendData(); // make sure all data is sent before object is destroyed
}
void addData(std::string_view data)
{
m_dataQueue.add(data);
}
void sendData()
{
// connect to server
// send all data
// clear data
}
};
int main()
{
NetworkData n("someipAddress");
n.addData("somedata1");
n.addData("somedata2");
return 0;
}Dzięki takiemu destruktorowi nasz NetworkData obiekt zawsze wyśle wszystkie posiadane dane, zanim obiekt zostanie zniszczony! Czyszczenie odbywa się automatycznie, co oznacza mniejsze ryzyko błędów i mniej rzeczy do przemyślenia.
Niejawny destruktor
Jeśli obiekt typu klasy niezagregowanej nie ma destruktora zadeklarowanego przez użytkownika, kompilator wygeneruje destruktor z pustą treścią. Ten destruktor nazywany jest destruktorem ukrytym i w rzeczywistości jest po prostu symbolem zastępczym.
Jeśli twoja klasa nie musi wykonywać żadnego czyszczenia po zniszczeniu, dobrze jest w ogóle nie definiować destruktora i pozwolić kompilatorowi wygenerować niejawny destruktor dla twojej klasy.
Ostrzeżenie dotyczące std::exit() funkcja
W lekcji 8.12 -- Halts (wyjście z programu wcześnie), omówiliśmy funkcję std::exit() , której można użyć do natychmiastowego zakończenia programu. Gdy program zostanie natychmiast zakończony, program po prostu się zakończy. Zmienne lokalne nie są najpierw niszczone, dlatego nie zostaną wywołane żadne destruktory. Zachowaj ostrożność, jeśli w takim przypadku polegasz na destruktorach, które wykonają niezbędne prace porządkowe.
Dla zaawansowanych czytelników
Nieobsługiwane wyjątki również spowodują zakończenie działania programu i mogą nie rozwinąć stosu przed wykonaniem tej czynności. Jeśli nie nastąpi odwinięcie stosu, destruktory nie zostaną wywołane przed zakończeniem programu.

