Dann sieht das zugehörige Speicherbild so aus (die Variablenzugehörigkeiten sind wieder entsprechend in Var1 und Var2 angegeben):

Adresse Inhalt Inhalt Inhalt Inhalt Var1 Var2
123450 <– esp
123454 <– p
<– p.f
<– p.f.numerator
p p.f
123458 <– p.f.denominator
123462 <– p.s
<– p.s.numerator
p.s
123466 <– p.s.denominator
123470 <– ebp
123490 <– bottom

Nun habe ich am Anfang des Artikels erwähnt, dass es bei der Komposition von strukturierten Datentypen zu Problemen kommen kann. Konkret geht es um die folgende Situation; nehmen wir eine Struktur Human an, die den Namen eines Menschen festhält, sowie die Eltern dieses Menschen (mit char* werden Zeichenketten beschrieben – die Details dahinter sollen an dieser Stelle nicht weiter interessieren, können aber theoretisch aus den Informationen der Artikel 7 und 8 hergeleitet werden)

struct Human { char* name; Human mother; Human father; };

Soweit sieht das ja eigentlich ganz logisch aus; das Problem ist nur, dass es so nicht machbar ist. Versucht man, diesen Code zu kompilieren, wird der Compiler unweigerlich Fehlermeldungen bringen, die leider nicht unbedingt auf das eigentliche Problem hinweisen; der Visual-Studio-Compiler von Microsoft wird etwa wenig aussagekräftig melden:

error C2460: 'Human::mother': Verwendet gerade definiertes 'Human'

Das Problem hier liegt natürlich in der Art des Speicherlayouts. Wie weiter oben beschrieben, benötigt eine Variable eines strukturierten Datentyps so viele Speicherzellen, wie die einzelnen Bestandteile des Datentyps benötigen. Für die Human-Struktur wäre das also einmal eine Speicherzelle für den Verweis auf den Namen (char* name) und der benötigte Speicher für die Mutter und den Vater. Die Mutter ist wieder ein Human, die wiederum eine Speicherzelle für den Namen und entsprechenden Speicher für deren Eltern benötigt; für die Eltern der Mutter (und natürlich des Vaters) gilt wieder das gleiche, und so weiter. Wir haben es also mit dem Problem einer unendlichen Rekursion zu tun: der benötigte Speicher wäre in unserem Beispiel unendlich groß, weil jeder Mensch einen Namen und zwei Eltern hätte.

Was uns fehlt, ist die Möglichkeit, einem Menschen nicht zwei Eltern zu geben (das widerspricht jetzt zwar auf den ersten Blick der Realität, aber biologisch gesehen ist das sogar korrekt: wenn wir weit genug in die Vergangenheit gehen, werden wir ein Wesen finden, welches keine zwei Eltern hatte). Um das zu verwirklichen, müssen wir die Datenstuktur für den Human etwas anders aufbauen, da momentan immer zwei Eltern gefordert sind.

Wünschenswert wäre eine Möglichkeit, die Eltern wahlweise zu definieren oder – wenn sie nicht vorhanden sind – undefiniert zu lassen. Mit einer “normalen” Variablendeklaration lässt sich das nicht erreichen, da gewöhnliche Variablen nicht in dem Sinne undefiniert sein können; ihnen kann zwar einfach kein Wert zugewiesen werden – der für sie theoretisch benötigte Speicher muss aber dennoch immer reserviert werden. Das Stichwort zur Lösung ist aber genau die Reservierung von Speicher: wie wir uns erinnern, ging es im Kapitel über den Heap schon einmal genau darum. Das Mittel der Wahl sind dann auch die bereits in diesem Artikel erwähnten Adressverweise, sprich: die Pointer.

Wir erinnern uns: eine Variable kann als ein Pointer deklariert werden, indem vor den Variablennamen ein * geschrieben wird; Pointer-Variablen verweisen nicht auf konkrete Werte, sondern auf eine Adresse, unter welcher der gesuchte Wert im Speicher zu finden ist. Zudem hat eine Pointer-Variable eine feste Größe und benötigt zur Speicherung immer genau 4 Byte, also eine Speicherzelle (auf 64-Bit-Systemen entsprechend 8 Byte).

Möchten wir nun, dass eine Variable auf keine Adresse verweist, so weisen wir ihr einen speziellen Wert zu, nämlich die 0. An dieser Adresse werden während des Programmablaufs niemals relevante Daten stehen, weswegen sie zur Markierung von derartigen Leerverweisen benutzt wird; man spricht dementsprechend auch von Null-Pointern.

1 / 2 / 3

Kommentare (2)

  1. #1 Nicolai
    November 13, 2013

    0 und nullptr drücken nicht das selbe aus – das eine kann als integraler Wert verwendet werden, das andere nur als Zeiger-Adresse und zur true / false Abfrage. Im Gegensatz zu NULL, das meistens als #define NULL 0 implementiert ist, ist der nullptr somit nur für Zeiger-Operationen gültig.

    Danke für die ausführlichen Artikel, ich finde es klasse, dass du ein wenig auf die Technik im Speicher eingehst. 🙂

    Kleine Frage: Kannst du grob erklären, wie ein Konstruktor übersetzt wird? Ich würde es mir wie eine normale Klassenfunktion vorstellen, Aufruf mit dem this-Pointer und den Argumenten, weiß aber nicht, ob das korrekt ist..

  2. #2 Marcus Frenkel
    November 13, 2013

    @Nicolai

    0 und nullptr drücken nicht das selbe aus – das eine kann als integraler Wert verwendet werden, das andere nur als Zeiger-Adresse und zur true / false Abfrage.

    Ich dachte mir schon, dass ein Kommentar dazu kommt. 😉
    Das gleiche sind sie natürlich nicht, aber sie drücken in Bezug auf die Nullpointer-Adresse das gleiche aus. Natürlich bestehen viele Unterschiede zwischen beiden Werten, aber im Kontext dieser einfachen Zuweisung sind sie äquivalent.

    Konstruktor kommt demnächst. So ein bisschen sind das wirklich normale Klassenfunktionen, die aber ein bisschen Extramagie beim Aufruf betreiben. Insbesondere werden hierbei zu Beginn des Konstruktors der Konstruktor der Basisklasse sowie die Konstruktoren alle Klassen-Attribute aufgerufen. Wo im Falle von RTTI die Typinformation gesetzt wird, weiß ich gerade gar nicht, das könnte aber auch im Konstruktor sein.