Istnieje ważna różnica między nimi.
Wszystko, co nie jest przydzielone, new
zachowuje się podobnie do typów wartości w języku C # (a ludzie często mówią, że te obiekty są przydzielane na stosie, co jest prawdopodobnie najczęstszym / oczywistym przypadkiem, ale nie zawsze jest prawdziwe. Dokładniej, obiekty przydzielone bez użycia new
mają automatyczne przechowywanie czas trwania
Wszystko przydzielone za pomocą new
jest przydzielane na stercie i zwracany jest do niego wskaźnik, dokładnie tak jak typy referencyjne w języku C #.
Wszystko przydzielone na stosie musi mieć stały rozmiar, określony w czasie kompilacji (kompilator musi poprawnie ustawić wskaźnik stosu, lub jeśli obiekt należy do innej klasy, musi dostosować rozmiar tej innej klasy) . Dlatego tablice w języku C # są typami referencyjnymi. Muszą być, ponieważ w przypadku typów referencyjnych możemy w czasie wykonywania decydować o wymaganej ilości pamięci. To samo dotyczy tutaj. Tylko tablice o stałym rozmiarze (rozmiar, który można określić w czasie kompilacji) mogą być przydzielane z automatycznym czasem przechowywania (na stosie). Tablice o dynamicznych rozmiarach muszą być przydzielane na stercie przez wywołanie new
.
(I tu kończy się wszelkie podobieństwo do C #)
Teraz wszystko, co przydzielono na stosie, ma „automatyczny” czas przechowywania (można faktycznie zadeklarować zmienną jako auto
, ale jest to ustawienie domyślne, jeśli nie określono innego typu pamięci, więc słowo kluczowe nie jest tak naprawdę używane w praktyce, ale właśnie tam pochodzi z)
Automatyczny czas przechowywania oznacza dokładnie, jak to brzmi, czas trwania zmiennej jest obsługiwany automatycznie. Natomiast wszystko przydzielone na stercie musi zostać ręcznie usunięte przez Ciebie. Oto przykład:
void foo() {
bar b;
bar* b2 = new bar();
}
Ta funkcja tworzy trzy wartości, które warto rozważyć:
W wierszu 1 deklaruje zmienną b
typu bar
na stosie (automatyczny czas trwania).
W linii 2 deklaruje bar
wskaźnik b2
na stosie (automatyczny czas trwania) i wywołuje new, przydzielając bar
obiekt na stercie. (dynamiczny czas trwania)
Kiedy funkcja powróci, wydarzy się: Po pierwsze, b2
wykracza poza zakres (kolejność niszczenia jest zawsze przeciwna do kolejności konstrukcji). Ale b2
to tylko wskazówka, więc nic się nie dzieje, pamięć, którą zajmuje, jest po prostu uwolniona. I co ważne, pamięć, na którą wskazuje ( bar
instancja na stercie) NIE jest dotykana. Uwolniony jest tylko wskaźnik, ponieważ tylko wskaźnik miał automatyczny czas trwania. Po drugie, b
wychodzi poza zakres, więc ponieważ ma on automatyczny czas trwania, wywoływany jest jego destruktor, a pamięć jest zwalniana.
A bar
instancja na stercie? Prawdopodobnie wciąż tam jest. Nikt nie zadał sobie trudu, aby go usunąć, więc wyciekła pamięć.
Z tego przykładu możemy zobaczyć, że wszystko, co ma automatyczny czas trwania, ma gwarancję, że zostanie wywołany jego destruktor, gdy wykroczy poza zakres. To się przydaje. Ale wszystko, co jest przydzielane na stosie, trwa tak długo, jak jest to potrzebne, i może być dynamicznie zmieniane, jak w przypadku tablic. To też jest przydatne. Możemy to wykorzystać do zarządzania przydziałami pamięci. Co jeśli klasa Foo przydzieli część pamięci na stercie w swoim konstruktorze i usunie tę pamięć w swoim destruktorze. Wtedy moglibyśmy uzyskać to, co najlepsze z obu światów, bezpieczne przydziały pamięci, które z pewnością zostaną uwolnione, ale bez ograniczeń zmuszania wszystkiego do umieszczenia na stosie.
Dokładnie tak działa większość kodu C ++. Spójrz na przykład na bibliotekę standardową std::vector
. Jest to zwykle przydzielane na stosie, ale można je dynamicznie zmieniać i zmieniać rozmiar. I robi to poprzez wewnętrzne przydzielanie pamięci na stercie, jeśli to konieczne. Użytkownik klasy nigdy tego nie widzi, więc nie ma szansy na wyciek pamięci lub zapomnienie o wyczyszczeniu przydzielonych zasobów.
Zasada ta nosi nazwę RAII (Resource Acquisition is Initialization) i można ją rozszerzyć na dowolny zasób, który należy nabyć i zwolnić. (gniazda sieciowe, pliki, połączenia z bazą danych, blokady synchronizacji). Wszystkie z nich można zdobyć w konstruktorze i uwolnić w destruktorze, więc masz gwarancję, że wszystkie zdobyte zasoby zostaną ponownie uwolnione.
Zasadniczo nigdy nie używaj new / delete bezpośrednio z kodu wysokiego poziomu. Zawsze zawiń go w klasę, która może zarządzać pamięcią za Ciebie i która zapewni, że zostanie ponownie uwolniona. (Tak, mogą istnieć wyjątki od tej reguły. W szczególności inteligentne wskaźniki wymagają new
bezpośredniego wywołania i przekazania wskaźnika do jego konstruktora, który następnie przejmuje kontrolę i zapewnia delete
prawidłowe wywołanie. Ale to nadal bardzo ważna zasada )