Im Weiteren lässt sich eine Variable vom Typ int* genauso benutzen wie etwa eine Variable vom Typ int[10], indem etwa etwas in der folgenden Art geschrieben werden kann:

int n; ... n = ... ... int* as = new int[n]; as[2] = 42;

Das funktioniert, weil der Compiler weiß, was er bei einer solchen Anweisung zu tun hat; er muss einfach die unter as gespeicherte Adresse laden, von dieser aus 8 Byte weiterspringen (wir wollen die 3. Stelle im Integer-Array, überspringen also 2 * 4 Byte) und an die entsprechende Stelle den Wert 42 schreiben. Schauen wir uns zum besseren Verständnis noch die konkrete Situation im Speicher an.

Natürlich benötigen wir nun 2 Speicherbereiche, nämlich den Stack und den Heap. Gehen wir von folgendem Code aus:

void f( int n ) { int* as = new int[n]; as[2] = 42; }

Wie wir aus einem früheren Artikel wissen, ergibt sich dafür für den Stack unmittelbar nach Aufruf der Funktion f der folgende Aufbau mit den zugehörigen Variablen-Positionen; nehmen wir für n zusätzlich einen Eingabewert von 5 an, leere Zellen enthalten beliebige Werte, die uns (noch) nicht weiter interessieren:

Adresse Inhalt Inhalt Inhalt Inhalt
123450 <– esp
123455 <– as [= ebp – 4]
123458 <– ebp
123462
123466 05 00 00 00 <– n
123490 <– bottom

Der Heap im (frei angenommenen) Adressbereich 345678 sieht folgendermaßen aus (er enthält momentan noch keine für uns relevanten Werte):

Adresse Inhalt Inhalt Inhalt Inhalt
345678
345682
345686
345690
345694

Gehen wir nun in der Funktion f einen Schritt weiter, nach Ausführung der Anweisung int* as = new int[n];. Nehmen wir an, dass der new-Ausdruck den gewünschten Speicher (für 5 Integer-Werte) auf dem Heap ab der Adresse 345678 reserviert hat; Der Stack sieht dann folgendermaßen aus:

Adresse Inhalt Inhalt Inhalt Inhalt
123450 <– esp
123455 4E 46 05 00 <– as [= ebp – 4]
123458 <– ebp
123462
123466 05 00 00 00 <– n
123490 <– bottom

Der Inhalt der Speicherzellen, welche as entsprechen (nämlich 4E 46 05 00) mag etwas seltsam aussehen, ist aber einfach zu verstehen: Zahlen werden im Speicher immer byteweise abgespeichert, und zwar auf regulären Prozessor-Architekturen in umgekehrter Reihenfolge der Bytes. Zur Notation greift man dabei meist auf die hexadezimale Schreibweise zurück, also die Notation der Zahlen in einem System zur Basis 16 (wir selber rechnen tagtäglich in einem System zur Basis 10; Binärzahlen sind zur Basis 2 notiert). Da wir nur über 10 Ziffern (0-9) verfügen, werden die fehlenden 6 “Ziffern” durch die Buchstaben A bis F dargestellt. Der Inhalt des Speichers entspricht also der Zahl 0x0005464E (das 0x markiert die hexadezimale Schreibweise), was genau der Zahl 345678 (die angenommene zurückgegebene Adresse) entspricht. So weit, so einfach.

Und wie sieht der Heap nun aus? Folgendermaßen (<-- *as markiert die Speicheradresse, welche in as gespeichert ist):

Adresse Inhalt Inhalt Inhalt Inhalt
345678 <– *as
345682
345686
345690
345694

Wer jetzt keine Änderung entdeckt, liegt richtig – im Heap hat sich noch nichts getan. Das Reservieren von Speicher allein ändert prinzipiell nichts am Inhalt des reservierten Speichers, zumindest dann, wenn Speicher für ein Array im Heap reserviert wird (es kann auch Speicher für andere Dinge reserviert werden, aber das werden wir später noch sehen). Für echte Änderungen muss noch etwas in den Speicher geschrieben werden, so wie es durch den Ausdruck as[2] = 42; passiert. Nach dessen Ausführung sieht der Heap folgendermaßen aus (im Stack hat sich natürlich nichts geändert):

Adresse Inhalt Inhalt Inhalt Inhalt
345678 <– *as
345682
345686 2A 00 00 00
345690
345694

1 / 2 / 3

Kommentare (8)

  1. #1 Sascha
    Juli 18, 2013

    Daran sieht man wieder, dass Mehrfachdeklarationen in einer Zeile immer problematisch sein können.
    Lieber ein paar Zeilen mehr, dafür aber sauber deklarierte Variablen.

  2. #2 Frank Wappler
    https://lang--lang.ist's.her...
    Juli 18, 2013

    Marcus Frenkel schrieb (Juli 17, 2013):
    > […] dass der Compiler veranlasst, ein Array der Länge n (die erst zur Laufzeit bekannt ist) auf dem Heap anzulegen, oder, um genauer zu sein: im Heap wird ein Speicherbereich der Länge n * 4 Bytes (ein int-Wert benötigt 4 Bytes Speicher) reserviert.

    > nehmen wir für n zusätzlich einen Eingabewert von 5 an

    > Nehmen wir an, dass der new-Ausdruck den gewünschten Speicher (für 5 Integer-Werte) auf dem Heap ab der Adresse 345678 reserviert hat; Der Stack sieht dann folgendermaßen aus: […]

    Cliffhanger:
    Was wäre, falls ein zweiter Speicherbereich (z.B. der Länge “k * 4 Bytes”) zu reservieren und zu nutzen sein soll, bevor der beschriebene “eine Speicherbereich der Länge n * 4 Bytes, ab Adresse 345678” wieder freigegeben wird?

    Wird vom Compiler ein weiterer “Adresse-Wert ausdrücklich angewiesen?
    Oder weist der Compiler (per Programm) lediglich an, dass und wie ein weiterer “Adresse-Wert erst zur Laufzeit errechnet und in den Stack geschrieben wird?

  3. #3 Marcus Frenkel
    Juli 18, 2013

    @Frank Wappler
    So ungefähr. Durch das new-Schlüsselwort wird dynamisch zur Laufzeit Speicher reserviert; *wo* das gemacht wird, hängt mehr oder weniger vom Betriebssystem ab, das wird noch nicht während der Kompilierungszeit bestimmt. Das Betriebssystem verwaltet die freien Speicherbereiche und sorgt dafür, dass der neue Speicher an einer passenden Stelle reserviert wird. Zur Zeit der Kompilierung sind also die Adressen der dynamisch reservierten Speicherbereiche noch nicht bekannt, immer erst zur Laufzeit – und potentiell liefert jedes “new” eine neue Adresse (falls nicht vorher etwas per “delete” freigegeben wurde natürlich).

  4. #4 rolak
    Juli 18, 2013

    new-Schlüsselwort

    ‘Schlüsselwort’ (also analog zu ‘class’) halte ich für unangemessen – ist doch ‘new’ nur ein leichter zu schreibender Deckel für die Speicherreservierung, zB getmem(_type_, _count_, _init_), eher so etwas wie ein überladenes Makro.

    *wo* das gemacht wird, hängt mehr oder weniger vom Betriebssystem ab, das wird noch nicht während der Kompilierungszeit bestimmt

    Falls es um den Ort des Speichers geht: Nein bis Jein, siehe unten – falls um den Prozeß der Reservierung geht: Nein, das ist schon eine ordinäre RTL-Prozedur, die sich darum kümmert.

    Das Betriebssystem verwaltet die freien Speicherbereiche und sorgt dafür, dass der neue Speicher an einer passenden Stelle reserviert wird.

    Falls nicht von vorneherein die maximale Heapgröße feststeht und beim Programmstart akquiriert wird, ist es ein zweistufiger Prozeß. Im bisher vom Programm beschlagnahmten Speicher für den Heap lebt eine Datenstruktur (Liste, Heap, …), die von new() und delete() gepflegt wird. Sollte der Platz für den Erfolg eines new() nicht ausreichen und sowohl Compiler als auch OS dies zulassen, kann durch Anfrage beim OS evtl das Programm seinen Heap vergößern (und selbstverständlich im gegenteiligen Falle verkleinern), doch generell verwaltet das Programm mittels des Compilers RTL seinen Heap selber.

    Wegen der Verwaltungsdaten paßt auch das Heapabbild des posts nicht ganz: Wenn 345678 die Adresse des ersten passenden freien Blocks ist, erhält as den Wert 345678+x, wobei das x je nach Verwaltungsaufwand verschieden ist.

    dass der Compiler veranlasst, ein Array .. auf dem Heap anzulegen

    Vielleicht wäre es sinnvoll, entwirrenderweise umzuformulieren in etwas wie

    dass der Compiler Code generiert, der zur Laufzeit Speicher für ein Array .. auf dem Heap reserviert

    Abgesehen von der Verwaltung des Variablenplatzes ergibt ein “int ivar = const” den Code

    mov ivar,const

    während “int *ivar = new _type_ [n]” irgendwas ergeben dürfte wie

    mov eax,n
    imul eax, SizeOf(_type_)
    push eax
    call _getmem
    inc esp,4
    mov ivar,eax

  5. #5 michael
    Juli 19, 2013

    Ein im Programmcode bewirkt schon etwas mehr als reine Speicherbereitstellung.

    von: https://www.cplusplus.com/reference/new/operator%20new%5B%5D/

    operator new[] can be called explicitly as a regular function, but in C++, new[] is an operator with a very specific behavior: An expression with the new operator on an array type, first calls function operator new (i.e., this function) with the size of its array type specifier as first argument (plus any array overhead storage to keep track of the size, if any), and if this is successful, it then automatically initializes or constructs every object in the array (if needed). Finally, the expression evaluates as a pointer to the appropriate type pointing to the first element of the array

  6. #6 rolak
    Juli 20, 2013

    Es ist schwer zu sehen, worauf sich Dein Korrektur-Kommentar womit bezieht, michael, doch für die im blogpost genannten Beispiele (Variationen über ‘int’) gibt es afaik keine Initialisierung. Und meine Wenigkeit geruhte, diese allgemein als Pseudoargument einzuführen.

  7. #7 Havok
    August 25, 2013

    Wann geht’s weiter? :-]

  8. #8 Marcus Frenkel
    August 25, 2013

    Bald! 😉
    Der nächste Artikel ist in Arbeit. In letzter Zeit dauert es immer etwas länger – ich bitte das zu entschuldigen. 😉