Istnieją dwie powszechnie stosowane techniki alokacji pamięci: alokacja automatyczna i alokacja dynamiczna. Zwykle dla każdego istnieje odpowiedni region pamięci: stos i sterta.
Stos
Stos zawsze przydziela pamięć w sposób sekwencyjny. Może to zrobić, ponieważ wymaga zwolnienia pamięci w odwrotnej kolejności (pierwsze wejście, ostatnie wyjście: FILO). Jest to technika alokacji pamięci dla zmiennych lokalnych w wielu językach programowania. Jest bardzo, bardzo szybki, ponieważ wymaga minimalnej księgowości, a następny adres do przydzielenia jest domyślny.
W C ++ nazywa się to automatycznym magazynowaniem, ponieważ magazyn jest zgłaszany automatycznie na końcu zakresu. Po {}
zakończeniu wykonywania bieżącego bloku kodu (ograniczonego za pomocą ), pamięć dla wszystkich zmiennych w tym bloku jest automatycznie gromadzona. Jest to także moment, w którym przywoływane są destruktory w celu oczyszczenia zasobów.
Sterta
Sterta pozwala na bardziej elastyczny tryb alokacji pamięci. Księgowość jest bardziej złożona, a alokacja wolniejsza. Ponieważ nie ma niejawnego punktu zwolnienia, musisz zwolnić pamięć ręcznie, używając delete
lub delete[]
( free
w C). Jednak brak niejawnego punktu zwolnienia jest kluczem do elastyczności stosu.
Powody korzystania z alokacji dynamicznej
Nawet jeśli korzystanie ze sterty jest wolniejsze i potencjalnie prowadzi do wycieków pamięci lub fragmentacji pamięci, istnieją bardzo dobre przypadki użycia dla alokacji dynamicznej, ponieważ jest ona mniej ograniczona.
Dwa kluczowe powody korzystania z alokacji dynamicznej:
Nie wiesz, ile pamięci potrzebujesz w czasie kompilacji. Na przykład podczas odczytywania pliku tekstowego do łańcucha zwykle nie wiesz, jaki rozmiar ma ten plik, więc nie możesz zdecydować, ile pamięci ma zostać przydzielone, dopóki nie uruchomisz programu.
Chcesz przydzielić pamięć, która pozostanie po opuszczeniu bieżącego bloku. Na przykład możesz chcieć napisać funkcję, string readfile(string path)
która zwraca zawartość pliku. W takim przypadku nawet jeśli stos może pomieścić całą zawartość pliku, nie można wrócić z funkcji i zachować przydzielonego bloku pamięci.
Dlaczego dynamiczna alokacja jest często niepotrzebna
W C ++ istnieje zgrabna konstrukcja zwana destruktorem . Ten mechanizm pozwala zarządzać zasobami, dopasowując czas życia zasobu do czasu życia zmiennej. Ta technika nazywa się RAII i jest wyróżnikiem C ++. „Zawija” zasoby w obiekty. std::string
jest doskonałym przykładem. Ten fragment kodu:
int main ( int argc, char* argv[] )
{
std::string program(argv[0]);
}
faktycznie przydziela zmienną ilość pamięci. std::string
Przydziela pamięć obiektów za pomocą sterty i uwalnia go w jego destruktora. W takim przypadku nie trzeba ręcznie zarządzać zasobami, a korzyści z dynamicznej alokacji pamięci są nadal możliwe.
W szczególności oznacza to, że w tym fragmencie:
int main ( int argc, char* argv[] )
{
std::string * program = new std::string(argv[0]); // Bad!
delete program;
}
istnieje niepotrzebna dynamiczna alokacja pamięci. Program wymaga więcej pisania (!) I stwarza ryzyko zapomnienia o zwolnieniu pamięci. Robi to bez widocznych korzyści.
Dlaczego warto korzystać z automatycznego przechowywania tak często, jak to możliwe
Zasadniczo ostatni akapit podsumowuje. Używanie automatycznego przechowywania tak często, jak to możliwe, sprawia, że twoje programy:
- szybciej pisać;
- szybciej po uruchomieniu;
- mniej podatne na wycieki pamięci / zasobów.
Punkty bonusowe
W cytowanym pytaniu pojawiają się dodatkowe obawy. W szczególności następująca klasa:
class Line {
public:
Line();
~Line();
std::string* mString;
};
Line::Line() {
mString = new std::string("foo_bar");
}
Line::~Line() {
delete mString;
}
Jest o wiele bardziej ryzykowny w użyciu niż następujący:
class Line {
public:
Line();
std::string mString;
};
Line::Line() {
mString = "foo_bar";
// note: there is a cleaner way to write this.
}
Powodem jest to, że std::string
poprawnie definiuje konstruktor kopii. Rozważ następujący program:
int main ()
{
Line l1;
Line l2 = l1;
}
Korzystając z oryginalnej wersji, ten program najprawdopodobniej ulegnie awarii, ponieważ używa delete
tego samego ciągu dwukrotnie. Korzystając ze zmodyfikowanej wersji, każda Line
instancja będzie posiadać własną instancję łańcuchową , każda z własną pamięcią i obie zostaną zwolnione na końcu programu.
Inne notatki
Szerokie zastosowanie RAII jest uważane za najlepszą praktykę w C ++ ze wszystkich powyższych powodów. Istnieje jednak dodatkowa korzyść, która nie jest od razu oczywista. Zasadniczo jest lepszy niż suma jego części. Cały mechanizm się komponuje . Skaluje się.
Jeśli użyjesz Line
klasy jako elementu konstrukcyjnego:
class Table
{
Line borders[4];
};
Następnie
int main ()
{
Table table;
}
przydziela cztery std::string
instancje, cztery Line
instancje, jedną Table
instancję i całą zawartość łańcucha, a wszystko jest automatycznie uwalniane automatycznie .