Null Pointer

Adressen des Speichers können als vorzeichenlose Ganzzahlen aufgefasst werden, welche in Pointern gespeichert werden. Die Adresse 0 ist somit genauso wie jede andere Ganzzahl eine potentiell gültige Adresse. Allerdings ist diese Adresse in allen gängigen Systemen für systemspezifische Dinge reserviert, und ein Zugriff auf diese Adresse führt heutzutage aufgrund verschiedener Sicherheitsmassnahmen zu einem Absturz des Programmes.

Im C-Standard wurde festgelegt, dass die Adresse 0 explizit als ungültig gilt. In den Standard-Bibliotheken werden entsprechende Makros festgelegt. In C wird das Makro NULL verwendet und in C++ ab dem C11++ Standard das Keyword nullptr. Beide dienen dazu, um einen Pointer als ungültig oder uninitialisiert zu markieren.

Details

Pointer-Variablen werden oftmals direkt bei der Definition mit einer gültigen Adresse initialisiert. Diese Initialisierung ist jedoch nicht in jedem Falle möglich und wird auch nicht automatisch ausgeführt. So kann es sein, dass sich in einer nicht-initialisierten Pointer-Variablen ein Wert befindet, welcher eine ungültige Adresse beschreibt. Bei einem Zugriff auf diese Adresse würde das Programm möglicherweise abstürzen.

Beispielsweise werden Pointer-Variablen verwendet, um dynamisch alloziierte Speicherblöcke (mittels malloc) zu speichern. Wann genau während des Programmablaufes eine solche Allokation stattfindet, ist frei wählbar. Zur Kennzeichnung des Pointers, ob er bereits eine gültige Adresse beinhaltet oder nicht, wird der Null-Pointer verwendet. Ein Null-Pointer hat explizit die semantische Bedeutung, dass der Pointer auf keine gültige Adresse zeigt.

Sowohl in C als auch in C++ wird als Null-Pointer in den Standard-Bibliotheken das Makro NULL bereitgestellt. Das Makro NULL ist grundsätzlich in der stddef-Bibliothek definiert, wird jedoch an so vielen Stellen gebraucht, dass es in anderen Standard-Bibliotheken ebenfalls definiert wird. Beim Programmieren wird das Makro NULL am ehesten durch Einbinden der stdio-Bibliothek verfügbar gemacht. In jedem Falle ist das Makro definiert als ein void-Pointer mit dem Wert 0.

Seit dem C++11-Standard existiert in C++ für den Null-Pointer ein neues Keyword: nullptr. Dieses ist genauso wie NULL definiert als Pointer-Typ mit dem Wert 0, aber es ist kein Makro, sondern ein eigenes Keyword und damit Bestandteil der Sprache selbst. Nach wie vor existieren jedoch einige Compiler, welche diesen Standard noch nicht unterstützen, beziehungsweise werden aufgrund älterer Codes manchmal auch ältere Standards erzwungen.

Bei Verwendung von älteren Standards sollte stets das Preprozessor-Makro NULL als Null-Pointer verwendet werden. Es wird beim Programmieren in reinem C oder C++ strengstens davon abgeraten, andere Makros oder auch einfach die Zahl 0 zu verwenden. Insbesondere gehören dazu Keywords wie NUL, NIL, Nil, nil, Null, null oder zero. Diese Begriffe und Keywords werden für andere Dinge oder von anderen Sprachen verwendet.

Ein Pointer kann auch als boolscher Wert aufgefasst werden, welcher false ergibt, wenn der Pointer ein Null-Pointer ist und true in jedem anderen Falle. Diese Eigenschaft wird sehr häufig bei Bedingungen von Kontrollstrukturen verwendet. Beispielsweise wird im folgenden Code geprüft, ob die Allokation von Speicher erfolgreich war:









a Allocated.
#include <stdio.h>
#include <stdlib.h>

int main(){
  int* a = NULL;
  int* b = NULL;
  a = (int*)malloc(10 * sizeof(int));
  b = (int*)malloc(1000000000 * sizeof(int));
  if(a) {printf("a Allocated.\n");}
  if(b) {printf("b Allocated.\n");}
  free(a);
  free(b);
  return 0;
}

In diesem Beispiel war die Allokation von b nicht erfolgreich, da zuwenig Speicher vorhanden war. Es ist zu beachten, dass die free-Funktion explizit auch einen Null-Pointer annimmt, ohne dass ein Fehler auftritt.

Schlussbemerkung: Es ist zu beachten dass ein Null-Pointer in C zwar den Wert 0 hat, jedoch grundsätzlich nichts mit dem deutschen Begriff Null zu tun hat. Viel mehr ist das Wort null englisch aufzufassen mit der Bedeutung Ungültig.

Billion dollar mistake?

Die Verwendung von Null-Pointer führt manchmal zu hitzigen Diskussionen im Studium und selbst in der Berufswelt ergibt sich oft ein Zwist zwischen ideomatischer Programmierung und Praktikabilität. Dies deswegen, da das Konzept des Null-Pointers zwar sehr einfach, aber ebenso fehleranfällig ist. Heutige Informatik-Wissenschaften lehren oft, dass Programmiersprachen auch ohne das Konzept einer ungültigen Adresse designt werden können und somit die Frage nach korrekter Adressierung vollständig dem Compiler überlassen werden kann.

Da heute solche Konzepte bekannt sind und auch in modernen Sprachen usus sind, stellt sich die Frage, ob die scheinbar einfache Lösung aus einer Zeit, als C erfunden wurde, auf lange Sicht hin nicht möglicherweise Unmengen an zusätzlichem Aufwand für Fehlersuche notwendig machte. Werden sämtliche weltweit aufgewendeten Debugging-Zeiten über die vergangenen fünfzig Jahre zusammengezählt, welche aufgrund von Null-Pointern verursacht werden, wird schnell klar, weswegen hier von einem Billion-Dollar mistake geredet wird. Die Frage ist, ob diese Unterstellung gerechtfertigt ist.

Die Antwort darauf muss von jeder Person selbst gegeben werden. Wer es gewohnt ist, mit Null-Pointer zu arbeiten, wird sie auch weiterhin verwenden und diejelnigen, welche sie verachten werden sie auch in Zukunft verachten. Die Meinung des Autors ist die folgende:

Null-Pointer sind kein Billion Dollar mistake, aus dem einfachen Grunde, da es zwar möglich ist, sie zu machen, es jedoch die wohl einfachsten und sogar hilfreichsten Bugs sind, die es überhaupt gibt. Aus Erfahrung kann berichtet werden, dass wenn ein Programm abstürzt, es dies mit hoher Wahrscheinlichkeit aufgrund eines falschen Pointers tut, sehr häufig, da der Pointer Null ist. Ein einfacher Durchlauf mittels eines Debuggers - was oftmals nicht mehr als ein paar Sekunden dauert - reicht, um die präzise Stelle des Problems ausfindig zu machen. Ebenso schnell ist normalerweise die Lösung gefunden. Und das Schreiben von Unit-Tests, um solche Fehler zu detektieren, ist entweder trivial oder gar oftmals nicht mal notwendig, da sie im Verlaufe des normalen Programmablaufes sowieso auftreten.

Weitaus problematischer sind Bugs, welche aufgrund von Automatismen entstehen, sprich beispeislweise, wenn Pointer automatisch mittels eines scheinbar gültigen Wertes initialisiert werden. Sprachen, welche keine Null-Pointer erlauben, konzipieren ihre Compiler und Interpreten dergestalt, dass selbst ein nicht-definiertes Objekt dennoch gültig ist. Die Semantik dessen, was diese initialen Objekte jedoch bedeuten, wird dem Benutzer überlassen.

Die Häufigkeit, wie oft ein Objekt nicht korrekt initialisiert wird, ist bei beiden Sprach-Paradigmen etwa gleich hoch. Dass also das Ansprechen einer nicht vollständig initialisierten Variable zu einem Fehler führen kann, ist ebenfalls etwa gleich hoch.

Das Problem der automatischen Initialisierung ist nun, dass selbst mittels eines Debuggers nicht immer sofort eruiert werden kann, was genau hinter einem scheinbar gültigen Object steckt, wie, wann und wo es initialisiert wurde. Derselbe Fehler, welcher mittels eines Null-Pointers zum sofortigen Absturz führt, kann somit mittels eines automatisch gesetzten Wertes zu stundenlanger Fehlersuche führen, da das Symptom des Problems möglicherweise erst an einer komplett anderen Stelle im Code in Erscheinung tritt und somit nur mittels geschickter Detektivarbeit und möglicherweise umfangreichem Wissen im aktuellen Bereich des Source-Codes die eigentliche Ursache ausfindig gemacht werden kann.

Solche und ähnliche Fehler werden vom Autor als Semantik-Versteckspiel bezeichnet. Zugunsten von syntaktischer Korrektheit und blinder, ideomatischer Regelbefolgung werden potentielle Fehlerquellen als korrekten Code getarnt, dergestallt, dass der Compiler, statische Analysetools und selbst Unit-Tests nicht mehr anschlagen. Dass das Problem jedoch immer noch latent vorhanden ist, wird ignoriert. Folgendes Beispiel soll die Problematik stark vereinfacht veranschaulichen:

Must compile as cpp






Forgotten function














remote file
-----------
local file











Inch to cm:
1234.0
Error
#include <stdio.h>

typedef struct Unit Unit;
struct Unit{
  float scale;
};

void setUnitScale(Unit* unit, float scale){
  unit->scale = scale;
}

Unit standardUnit = { 1.f };

typedef struct SuperMegaSafeScaler SuperMegaSafeScaler;
struct SuperMegaSafeScaler{
  Unit* unit = &standardUnit;
};

typedef struct BillionDollarScaler BillionDollarScaler;
struct BillionDollarScaler{
  Unit* unit = NULL;
};

// ---------------------------------

float GetLength1(float length, SuperMegaSafeScaler scaler){
  return scaler.unit->scale * length;
}

float GetLength2(float length, BillionDollarScaler scaler){
  return scaler.unit->scale * length;
}

int main(){
  SuperMegaSafeScaler safeScaler;
  BillionDollarScaler nullScaler;
  printf("Inch to cm:\n");
  printf("%f\n", GetLength1(1234., safeScaler));
  printf("%f\n", GetLength2(1234., nullScaler));
  return 0;
}

Eigentlich wäre bei diesem Beispiel die Idee, mittels einer automatischen Umrechnung Inch in Centimeter umzurechnen. Dummerweise wurde jedoch vergessen, die Einheit mit dem korrekten Skalierungsfaktor (2.54) durch einen Aufruf von setUnitScale zu initialisieren. Während die erste printf Zeile ein Resultat ausgibt, stürzt das Programm bei der zweiten Zeile aufgrund eines Null-Pointers-Zugriffs ab. Dass das Programm jedoch bereits bei der ersten Zeile fehlerhaft ist, ist nicht ersichtlich.

Dieses Beispiel ist sehr simpel aufgebaut und der Fehler ist hier mehr oder weniger offensichtlich. Es ist jedoch zu beachten, dass der Code oberhalb der Trennlinie potentiell sich weit weg von der main-Funktion befinden kann, möglicherweise verschachtelt in noch viel mehr Objekten, möglicherweise in einem nicht-einsichtbaren Bereich des Codes oder möglicherweise gar in einem vorcompilierten Modul. Herauszufinden, dass das Unit-Objekt nicht gültig ist kann dadurch zu einer Tortur werden. Solche Fehler können teilweise jahrelang unentdeckt bleiben und zu Inkonsistenzen führen während des Programmablaufes.

Der Autor hat über die vielen Programmierjahre selbst gelernt, dass eines der wichtigsten Qualitätsmerkmale von Code die Wartbarkeit ist, auf Englisch Maintainability. Es gibt einige Konstrukte moderner Programmierung, welche in der Theorie vielleicht optimaler sind im Bereich der Laufzeit, dem Speicherverbrauch oder gar den gültigen Programmier-Paradigmen, doch sind sie in der Praxis leider schwierig zu warten. Die Praktikabilität und die Fähigkeit, den Code schnell zu entziffern und ihn somit flexibel umgestalten zu können ohne eine komplette Neukonzipierung ist leider ein sehr prominenter Teil der Software-Entwicklung, welcher bei rein ideologischer Planung manchmal ignoriert wird.

Die Nutzung von Null-Pointern ist - kurz gesagt - einfach ungeheuer praktisch.