Es gibt zwei weit verbreitete Speicherzuweisungstechniken: automatische Zuweisung und dynamische Zuweisung. Üblicherweise gibt es für jeden einen entsprechenden Speicherbereich: den Stapel und den Heap.
Stapel
Der Stapel weist den Speicher immer sequentiell zu. Dies ist möglich, da Sie den Speicher in umgekehrter Reihenfolge freigeben müssen (First-In, Last-Out: FILO). Dies ist die Speicherzuweisungstechnik für lokale Variablen in vielen Programmiersprachen. Es ist sehr, sehr schnell, da es nur minimale Buchhaltung erfordert und die nächste zuzuweisende Adresse implizit ist.
In C ++ wird dies als automatischer Speicher bezeichnet, da der Speicher am Ende des Bereichs automatisch beansprucht wird. Sobald die Ausführung des aktuellen Codeblocks (begrenzt durch {}
) abgeschlossen ist, wird automatisch Speicher für alle Variablen in diesem Block gesammelt. Dies ist auch der Moment, in dem Destruktoren aufgerufen werden, um Ressourcen zu bereinigen.
Haufen
Der Heap ermöglicht einen flexibleren Speicherzuweisungsmodus. Die Buchhaltung ist komplexer und die Zuordnung ist langsamer. Da es keinen impliziten Freigabepunkt gibt, müssen Sie den Speicher manuell mit delete
oder delete[]
( free
in C) freigeben . Das Fehlen eines impliziten Freigabepunkts ist jedoch der Schlüssel zur Flexibilität des Heaps.
Gründe für die Verwendung der dynamischen Zuordnung
Selbst wenn die Verwendung des Heaps langsamer ist und möglicherweise zu Speicherverlusten oder Speicherfragmentierung führt, gibt es sehr gute Anwendungsfälle für die dynamische Zuordnung, da diese weniger begrenzt ist.
Zwei Hauptgründe für die Verwendung der dynamischen Zuordnung:
Sie wissen nicht, wie viel Speicher Sie zur Kompilierungszeit benötigen. Wenn Sie beispielsweise eine Textdatei in eine Zeichenfolge einlesen, wissen Sie normalerweise nicht, welche Größe die Datei hat. Daher können Sie erst entscheiden, wie viel Speicher zugewiesen werden soll, wenn Sie das Programm ausführen.
Sie möchten Speicher zuweisen, der nach dem Verlassen des aktuellen Blocks bestehen bleibt. Beispielsweise möchten Sie möglicherweise eine Funktion schreiben string readfile(string path)
, die den Inhalt einer Datei zurückgibt. In diesem Fall könnten Sie, selbst wenn der Stapel den gesamten Dateiinhalt enthalten könnte, nicht von einer Funktion zurückkehren und den zugewiesenen Speicherblock beibehalten.
Warum eine dynamische Zuordnung oft nicht erforderlich ist
In C ++ gibt es ein ordentliches Konstrukt, das als Destruktor bezeichnet wird . Mit diesem Mechanismus können Sie Ressourcen verwalten, indem Sie die Lebensdauer der Ressource an der Lebensdauer einer Variablen ausrichten. Diese Technik heißt RAII und ist das Unterscheidungsmerkmal von C ++. Es "verpackt" Ressourcen in Objekte. std::string
ist ein perfektes Beispiel. Dieser Ausschnitt:
int main ( int argc, char* argv[] )
{
std::string program(argv[0]);
}
weist tatsächlich eine variable Menge an Speicher zu. Das std::string
Objekt reserviert Speicher mithilfe des Heaps und gibt ihn in seinem Destruktor frei. In diesem Fall haben Sie nicht brauchen , um manuell alle Ressourcen verwalten und immer noch die Vorteile der dynamischen Speicherzuweisung bekam.
Insbesondere impliziert dies, dass in diesem Ausschnitt:
int main ( int argc, char* argv[] )
{
std::string * program = new std::string(argv[0]); // Bad!
delete program;
}
Es ist keine dynamische Speicherzuweisung erforderlich. Das Programm erfordert mehr Eingabe (!) Und birgt das Risiko, die Speicherfreigabe zu vergessen. Dies geschieht ohne offensichtlichen Nutzen.
Warum Sie die automatische Speicherung so oft wie möglich verwenden sollten
Grundsätzlich fasst der letzte Absatz es zusammen. Wenn Sie den automatischen Speicher so oft wie möglich verwenden, werden Ihre Programme:
- schneller zu tippen;
- schneller beim Laufen;
- weniger anfällig für Speicher- / Ressourcenlecks.
Bonuspunkte
In der genannten Frage gibt es zusätzliche Bedenken. Insbesondere die folgende Klasse:
class Line {
public:
Line();
~Line();
std::string* mString;
};
Line::Line() {
mString = new std::string("foo_bar");
}
Line::~Line() {
delete mString;
}
Ist tatsächlich viel riskanter als die folgende:
class Line {
public:
Line();
std::string mString;
};
Line::Line() {
mString = "foo_bar";
// note: there is a cleaner way to write this.
}
Der Grund ist, dass std::string
ein Kopierkonstruktor richtig definiert wird. Betrachten Sie das folgende Programm:
int main ()
{
Line l1;
Line l2 = l1;
}
Bei Verwendung der Originalversion stürzt dieses Programm wahrscheinlich ab, da es delete
zweimal dieselbe Zeichenfolge verwendet. Unter Verwendung der modifizierten Version, jede Line
Instanz eine eigene Zeichenfolge besitzen Instanz , jedes mit seinem eigenen Speicher und beide werden am Ende des Programms freigegeben.
Weitere Hinweise
Die umfassende Verwendung von RAII wird aus all den oben genannten Gründen als bewährte Methode in C ++ angesehen. Es gibt jedoch einen zusätzlichen Vorteil, der nicht sofort offensichtlich ist. Grundsätzlich ist es besser als die Summe seiner Teile. Der ganze Mechanismus setzt sich zusammen . Es skaliert.
Wenn Sie die Line
Klasse als Baustein verwenden:
class Table
{
Line borders[4];
};
Dann
int main ()
{
Table table;
}
Ordnet vier std::string
Instanzen, vier Line
Instanzen, eine Table
Instanz und den gesamten Inhalt der Zeichenfolge zu, und alles wird automatisch freigegeben .