Należy zauważyć, że w przypadku C ++ powszechne jest błędne przekonanie, że „trzeba ręcznie zarządzać pamięcią”. W rzeczywistości zwykle nie wykonuje się zarządzania pamięcią w kodzie.
Obiekty o stałym rozmiarze (z okresem użytkowania zakresu)
W zdecydowanej większości przypadków, gdy potrzebujesz obiektu, obiekt będzie miał określony czas życia w twoim programie i zostanie utworzony na stosie. Działa to dla wszystkich wbudowanych prymitywnych typów danych, ale także dla instancji klas i struktur:
class MyObject {
public: int x;
};
int objTest()
{
MyObject obj;
obj.x = 5;
return obj.x;
}
Obiekty stosu są automatycznie usuwane po zakończeniu funkcji. W Javie obiekty są zawsze tworzone na stercie i dlatego muszą być usuwane przez jakiś mechanizm, taki jak wyrzucanie elementów bezużytecznych. Nie dotyczy to obiektów stosu.
Obiekty zarządzające danymi dynamicznymi (z okresem istnienia zakresu)
Korzystanie z miejsca na stosie działa dla obiektów o stałym rozmiarze. Gdy potrzebujesz zmiennej ilości miejsca, na przykład tablicy, stosuje się inne podejście: Lista jest umieszczana w obiekcie o stałej wielkości, który zarządza pamięcią dynamiczną. Działa to, ponieważ obiekty mogą mieć specjalną funkcję czyszczenia, destruktor. Jest gwarantowane, że zostanie wywołany, gdy obiekt wykracza poza zasięg i robi coś przeciwnego do konstruktora:
class MyList {
public:
// a fixed-size pointer to the actual memory.
int* listOfInts;
// constructor: get memory
MyList(size_t numElements) { listOfInts = new int[numElements]; }
// destructor: free memory
~MyList() { delete[] listOfInts; }
};
int listTest()
{
MyList list(1024);
list.listOfInts[200] = 5;
return list.listOfInts[200];
// When MyList goes off stack here, its destructor is called and frees the memory.
}
W kodzie, w którym pamięć jest używana, nie ma żadnego zarządzania pamięcią. Jedyne, co musimy upewnić się, to to, że obiekt, który napisaliśmy, ma odpowiedni destruktor. Bez względu na to, jak opuszczymy zakres listTest
, czy to poprzez wyjątek, czy po prostu przez powrót z niego, destruktor ~MyList()
zostanie wywołany i nie będziemy musieli zarządzać żadną pamięcią.
(Wydaje mi się, że użycie binarnego operatora NOT w~
celu wskazania niszczyciela jest zabawną decyzją projektową . Gdy jest stosowany w liczbach, odwraca bity; tutaj analogicznie wskazuje, że to, co zrobił konstruktor, jest odwrócone.)
Zasadniczo wszystkie obiekty C ++, które potrzebują pamięci dynamicznej, używają tej enkapsulacji. Nazywa się to RAII („pozyskiwanie zasobów to inicjalizacja”), co jest dość dziwnym sposobem wyrażenia prostej idei, że obiekty dbają o własną zawartość; to, co nabywają, należy do posprzątania.
Obiekty polimorficzne i żywotność poza zasięgiem
Teraz oba te przypadki dotyczyły pamięci, która ma jasno określony czas życia: czas życia jest taki sam jak zakres. Jeśli nie chcemy, aby obiekt wygasał po opuszczeniu zakresu, istnieje trzeci mechanizm, który może zarządzać dla nas pamięcią: inteligentny wskaźnik. Wskaźniki inteligentne są również używane, gdy masz instancje obiektów, których typ zmienia się w czasie wykonywania, ale które mają wspólny interfejs lub klasę podstawową:
class MyDerivedObject : public MyObject {
public: int y;
};
std::unique_ptr<MyObject> createObject()
{
// actually creates an object of a derived class,
// but the user doesn't need to know this.
return std::make_unique<MyDerivedObject>();
}
int dynamicObjTest()
{
std::unique_ptr<MyObject> obj = createObject();
obj->x = 5;
return obj->x;
// At scope end, the unique_ptr automatically removes the object it contains,
// calling its destructor if it has one.
}
Istnieje inny rodzaj inteligentnego wskaźnika std::shared_ptr
do współdzielenia obiektów między kilkoma klientami. Usuwają zawarty obiekt tylko wtedy, gdy ostatni klient wykracza poza zakres, więc można ich używać w sytuacjach, w których nie wiadomo, ilu będzie klientów i jak długo będą używać obiektu.
Podsumowując, widzimy, że tak naprawdę nie wykonuje się żadnego ręcznego zarządzania pamięcią. Wszystko jest hermetyzowane, a następnie załatwiane za pomocą całkowicie automatycznego, opartego na zakresie zarządzania pamięcią. W przypadkach, gdy to nie wystarczy, używane są inteligentne wskaźniki, które zamykają surową pamięć.
Uznaje się za bardzo złą praktykę używanie surowych wskaźników jako właścicieli zasobów w dowolnym miejscu w kodzie C ++, surowe alokacje poza konstruktorami i surowe delete
wywołania poza destrukterami, ponieważ są one prawie niemożliwe do zarządzania, gdy wystąpią wyjątki, i generalnie są trudne do bezpiecznego użycia.
Najlepsze: działa dla wszystkich rodzajów zasobów
Jedną z największych zalet RAII jest to, że nie ogranicza się do pamięci. W rzeczywistości zapewnia bardzo naturalny sposób zarządzania zasobami, takimi jak pliki i gniazda (otwieranie / zamykanie) oraz mechanizmy synchronizacji, takie jak muteksy (blokowanie / odblokowywanie). Zasadniczo każdy zasób, który można uzyskać i musi zostać zwolniony, jest zarządzany w C ++ dokładnie w ten sam sposób i żadne z tych zarządzania nie jest pozostawione użytkownikowi. Wszystko jest zamknięte w klasach, które nabywają w konstruktorze i uwalniają w destruktorze.
Na przykład funkcja blokująca muteks jest zwykle napisana w C ++ w następujący sposób:
void criticalSection() {
std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
doSynchronizedStuff();
} // myMutex is released here automatically
Inne języki sprawiają, że jest to o wiele bardziej skomplikowane, wymagając od ciebie zrobienia tego ręcznie (np. W finally
klauzuli) lub rodzą wyspecjalizowane mechanizmy, które rozwiązują ten problem, ale nie w szczególnie elegancki sposób (zwykle w późniejszym okresie życia, gdy wystarczająca liczba ludzi ma cierpiał z powodu wady). Takimi mechanizmami są try-with-resources w Javie i instrukcja using w C #, które są przybliżeniami RAII C ++.
Podsumowując, wszystko to było bardzo powierzchownym kontem RAII w C ++, ale mam nadzieję, że pomoże czytelnikom zrozumieć, że zarządzanie pamięcią, a nawet zasobami w C ++ nie jest zwykle „ręczne”, ale w rzeczywistości w większości automatyczne.