Weist in C malloc()
einen Speicherbereich im Heap zu und gibt einen Zeiger darauf zurück. Das ist alles was du bekommst. Der Speicher ist nicht initialisiert und Sie können nicht garantieren, dass alles Nullen oder etwas anderes ist.
In Java führt das Aufrufen new
eine Heap-basierte Zuweisung aus malloc()
, aber Sie erhalten auch eine Menge zusätzlichen Komfort (oder Overhead, wenn Sie dies bevorzugen). Beispielsweise müssen Sie die Anzahl der zuzuweisenden Bytes nicht explizit angeben. Der Compiler ermittelt dies für Sie anhand des Objekttyps, den Sie zuweisen möchten. Darüber hinaus werden Objektkonstruktoren aufgerufen (an die Sie Argumente übergeben können, wenn Sie steuern möchten, wie die Initialisierung erfolgt). Bei der new
Rückkehr erhalten Sie garantiert ein Objekt, das initialisiert wurde.
Aber ja, am Ende des Aufrufs sind sowohl das Ergebnis malloc()
als new
auch Zeiger auf einen Teil der Heap-basierten Daten.
Der zweite Teil Ihrer Frage fragt nach den Unterschieden zwischen einem Stapel und einem Haufen. Weitaus umfassendere Antworten finden Sie in einem Kurs über das Compiler-Design (oder in einem Buch darüber). Ein Kurs über Betriebssysteme wäre ebenfalls hilfreich. Es gibt auch zahlreiche Fragen und Antworten zu SO über die Stapel und Haufen.
Trotzdem gebe ich einen allgemeinen Überblick, von dem ich hoffe, dass er nicht zu ausführlich ist, und möchte die Unterschiede auf einem ziemlich hohen Niveau erklären.
Grundsätzlich liegt der Hauptgrund für zwei Speicherverwaltungssysteme, dh einen Heap und einen Stack, in der Effizienz . Ein sekundärer Grund ist, dass jeder bei bestimmten Arten von Problemen besser ist als der andere.
Stapel sind für mich als Konzept etwas leichter zu verstehen, daher beginne ich mit Stapeln. Betrachten wir diese Funktion in C ...
int add(int lhs, int rhs) {
int result = lhs + rhs;
return result;
}
Das obige scheint ziemlich einfach zu sein. Wir definieren eine Funktion mit dem Namen add()
und übergeben die linken und rechten Addends. Die Funktion fügt sie hinzu und gibt ein Ergebnis zurück. Bitte ignorieren Sie alle Randfälle wie Überläufe, die auftreten können. An dieser Stelle ist dies für die Diskussion nicht relevant.
Der add()
Zweck der Funktion scheint ziemlich einfach zu sein, aber was können wir über ihren Lebenszyklus sagen? Besonders die Speicherauslastung benötigt?
Am wichtigsten ist, dass der Compiler a priori (dh zur Kompilierungszeit) weiß , wie groß die Datentypen sind und wie viele verwendet werden. Die Argumente lhs
und rhs
sind jeweils sizeof(int)
4 Bytes. Die Variable result
ist auch sizeof(int)
. Der Compiler kann feststellen, dass die add()
Funktion 4 bytes * 3 ints
insgesamt 12 Byte Speicher verwendet.
Wenn die add()
Funktion aufgerufen wird, enthält ein Hardware-Register, das als Stapelzeiger bezeichnet wird, eine Adresse, die auf die Oberseite des Stapels zeigt. Um den Speicher zuzuweisen, den die add()
Funktion ausführen muss, muss der Funktionseintragscode lediglich eine einzelne Assembler-Anweisung ausgeben, um den Stapelzeigerregisterwert um 12 zu verringern. Auf diese Weise wird Speicher auf dem Stapel für drei Personen erstellt ints
, jeweils für lhs
, rhs
, und result
. Das Erhalten des Speicherplatzes, den Sie durch Ausführen eines einzelnen Befehls benötigen, ist ein enormer Geschwindigkeitsgewinn, da einzelne Befehle in der Regel in einem Takt ausgeführt werden (1 Milliardstel Sekunde einer 1-GHz-CPU).
Aus Sicht des Compilers kann auch eine Zuordnung zu den Variablen erstellt werden, die der Indizierung eines Arrays sehr ähnlich sieht:
lhs: ((int *)stack_pointer_register)[0]
rhs: ((int *)stack_pointer_register)[1]
result: ((int *)stack_pointer_register)[2]
Auch dies alles ist sehr schnell.
Wenn die add()
Funktion beendet wird, muss sie bereinigt werden. Dies geschieht durch Subtrahieren von 12 Bytes vom Stapelzeigerregister. Es ähnelt einem Aufruf von, verwendet free()
jedoch nur einen CPU-Befehl und nur einen Tick. Es ist sehr, sehr schnell.
Betrachten Sie nun eine Heap-basierte Zuordnung. Dies kommt ins Spiel, wenn wir nicht a priori wissen, wie viel Speicher wir benötigen werden (dh wir werden erst zur Laufzeit davon erfahren).
Betrachten Sie diese Funktion:
int addRandom(int count) {
int numberOfBytesToAllocate = sizeof(int) * count;
int *array = malloc(numberOfBytesToAllocate);
int result = 0;
if array != NULL {
for (i = 0; i < count; ++i) {
array[i] = (int) random();
result += array[i];
}
free(array);
}
return result;
}
Beachten Sie, dass die addRandom()
Funktion zur Kompilierungszeit nicht weiß, wie hoch der Wert des count
Arguments sein wird. Aus diesem Grund ist es nicht sinnvoll zu versuchen, so zu definieren, array
wie wir es tun würden, wenn wir es auf den Stapel legen würden:
int array[count];
Wenn count
es riesig ist, kann es dazu führen, dass unser Stack zu groß wird und andere Programmsegmente überschreibt. Wenn dieser Stapelüberlauf auftritt , stürzt Ihr Programm ab (oder schlimmer).
In Fällen, in denen wir nicht wissen, wie viel Speicher wir bis zur Laufzeit benötigen, verwenden wir malloc()
. Dann können wir einfach nach der Anzahl der benötigten Bytes fragen, wenn wir sie benötigen, und malloc()
prüfen, ob sie so viele Bytes verkaufen können. Wenn es geht, großartig, bekommen wir es zurück, wenn nicht, bekommen wir einen NULL-Zeiger, der uns sagt, dass der Aufruf malloc()
fehlgeschlagen ist. Insbesondere stürzt das Programm jedoch nicht ab! Natürlich können Sie als Programmierer entscheiden, dass Ihr Programm nicht ausgeführt werden darf, wenn die Ressourcenzuweisung fehlschlägt, aber die vom Programmierer initiierte Beendigung unterscheidet sich von einem falschen Absturz.
Jetzt müssen wir zurückkommen, um die Effizienz zu untersuchen. Der Stapelzuweiser ist superschnell - ein Befehl zum Zuweisen, ein Befehl zum Freigeben und wird vom Compiler ausgeführt. Denken Sie jedoch daran, dass der Stapel für Dinge wie lokale Variablen bekannter Größe gedacht ist, sodass er eher klein ist.
Der Heap-Allokator hingegen ist um mehrere Größenordnungen langsamer. Es muss in Tabellen nachgeschlagen werden, um festzustellen, ob genügend freier Speicher vorhanden ist, um die vom Benutzer gewünschte Speichermenge zu verkaufen. Diese Tabellen müssen nach dem Verkauf des Speichers aktualisiert werden, um sicherzustellen, dass niemand diesen Block verwenden kann (für diese Buchhaltung muss der Allokator möglicherweise zusätzlich zu dem, was er verkaufen möchte, Speicher für sich selbst reservieren). Der Allokator muss Sperrstrategien anwenden, um sicherzustellen, dass der Speicher threadsicher verkauft wird. Und wenn die Erinnerung endlich istfree()
d, was zu unterschiedlichen Zeiten und normalerweise in keiner vorhersehbaren Reihenfolge geschieht, muss der Allokator zusammenhängende Blöcke finden und sie wieder zusammenfügen, um die Haufenfragmentierung zu reparieren. Wenn das so klingt, als würde es mehr als eine einzige CPU-Anweisung erfordern, um all das zu erreichen, haben Sie Recht! Es ist sehr kompliziert und es dauert eine Weile.
Aber Haufen sind groß. Viel größer als Stapel. Wir können viel Speicher von ihnen erhalten und sie sind großartig, wenn wir zur Kompilierungszeit nicht wissen, wie viel Speicher wir benötigen. Wir tauschen also die Geschwindigkeit gegen ein verwaltetes Speichersystem aus, das uns höflich ablehnt, anstatt abzustürzen, wenn wir versuchen, etwas zu Großes zuzuweisen.
Ich hoffe, das hilft bei der Beantwortung einiger Ihrer Fragen. Bitte lassen Sie mich wissen, wenn Sie eine Erläuterung zu einem der oben genannten Punkte wünschen.