Dynamisch Referenzen erzeugen

Bisher haben wir Referenzen lediglich so erzeugt, dass wir sie mit Adressen von existierenden Instanzen beschrieben haben. Auch wenn es wünschenswert ist, möglichst viele Datenstrukturen des laufendem Programms derart zu erzeugen, lässt es sich nicht immer verhindern, dass Speicher dynamisch angefordert werden muss.

In dem Fall müssen wir mit Hilfe des New-Operators Speicher anfordern und diesen zu einer Referenz verarbeiten:

void DrawPoint( Point & p )
 
void function( void )
{
  Point * p = new Point( 1, 2 );
 
  DrawPoint( *p );
}

Wie wir sehen, ist es relativ einfach aus einem Zeiger eine Referenz zu machen. Und das ist die Gefahr bei Referenzen. Ein Zeiger enthält im Datentyp die Information, dass er ungültig sein könnte! Wo immer man einen Zeiger herbekommt, muss man erst sicherstellen, dass dieser Zeiger gültig ist:

void DrawPoint( Point & p )
 
void function( Point * p )
{
  if( p )
    DrawPoint( *p );
}

Zeiger können ungültig werden! Wird eine Referenz mit einer Instanz initialisiert, die später gelöscht wird, wird das Konzept der Referenzen gebrochen: Die Referenz zeigt dann auf eine Instanz, die freigegeben wurde. Entsprechend ist es günstig, wenn Arbeiten, die Zeiger erforderlich machen, möglichst an einer Stelle kontrolliert werden und die Freigabe von Zeilen so gestaltet ist, dass es weder Zeiger noch Referenzen auf die freizugebende Instanz mehr gibt.

eine dynamisch erstellte Referenz freigeben

Eine Referenz kann mit dem &-Operator wieder in eine Adresse konvertiert werden.

Point & CreatePointRef( int x, int y )
{
  // Dies ist vom Prinzip her falsch, da es sein könnte, dass der Point nicht erzeugt werden konnte.
 
  return * new Point( x, y );
}
 
void DeletePoint( Point & point )
{
  delete &point;
 
  // Ab hier ist die Referenz auf point ungültig!
}
 
void function( void )
{
  Point & point = CreatePointRef( 1, 2 );
 
  point.DrawPoint();
 
  DeletePoint( point );
 
  // Ab hier ist die Referenz auf point ungültig!
}

Das Beispiel zeigt, wie eine Referenz dazu verwendet werden kann, um einen Zeiger zu erhalten, den man freigeben kann. Das ist als Beispiel gedacht, falls man es benötigt - nicht als Beispiel für gute Programmierung.

In diesen Beispielen werden Pointer und Referenzen sehr stark vermischt. Diese Bereiche sind eben die gefährlichen Bereiche, bei denen dafür gesorgt werden muss, dass nichts schief geht. Wer eine Referenz herausgibt, muss dafür Sorge tragen, dass sie auf eine gültige Position zeigt. Wenn man dies nicht garantieren kann, so sollte man weiterhin mit Zeigern arbeiten und ungültige Zeiger mit NULL kenntlich gemacht werden.

Ein Verwendungsszenario

Grundsätzlich sollten Referenzen von einer steuernden Instanz erzeugt werden. Nennen wir eine Instanz der Klasse Text, die eine Instanz der Klasse TextZeile erzeugt. Also: Speicher anfordern, sicherstellen, dass der Speicher gültig ist und dann erst den Zeiger auf die Textzeile als Referenz an Funktionen übergeben, die die TextZeilen-Instanz auch nicht löschen. Nachdem sichergestellt ist, dass die Zeile existiert, kann eine Referenz in eine Liste aufgenommen werden. Ausschließlich die steuernde Instanz - der Text - darf die Textzeile wieder löschen. Alle Funktionen, die eine Referenz auf die Textzeile erhalten, können davon ausgehen, dass die Instanz existiert.

Zeiger werden durch Referenzen nicht abgelöst

Wichtig ist zu betonen, dass Zeiger durch Referenzen nicht abgelöst werden. Überall da, wo es erlaubt ist, einen Nullpointer zu übergeben, müssen weiterhin Zeiger verwendet werden, denn der Zeiger darf im Gegensatz zur Referenz ungültig (heißt mit NULL initialisiert) sein.

NULL-Pointer mit Referenzen ersetzen.

Da man keinen Nullpointer an eine Referenz übergeben darf, wird gelegentlich mit Null-Instanzen gearbeitet. Dies ist eine Instanz der gewünschten Klasse, dass jedoch keine Funktion oder Wert besitzt. Die Null-Instanz kann so als Marker dienen, um sich von gültigen Instanzen zu unterscheiden. Wird eine Funktion des Markers gerufen, geschieht nichts.

bool IsLastElementOfList( Node & node )
{
  return node.GetNext() == Node::Null;
}

Bei einem String kann ein leerer String als Null-Objekt begriffen werden. Es handelt sich um ein gültiges Objekt mit dem Algorithmen arbeiten können. Er kann beispielsweise nach der Länge gefragt werden oder ausgegeben werden, aber er unterscheidet sich klar von Strings, die einen Inhalt haben.

std::string const NullString("");
 
bool isStringNull( std::string & str )
{
  return str == NullString;
}

Das hat den Vorteil, dass man einen Algorithmus immer arbeiten lassen kann und nicht - wie bei einem Zeiger - erst prüfen muss, ob der Zeiger auf ein gültiges Objekt zeigt. Die Ausgabe eines leeren Strings funktioniert ja und gibt nichts aus.

Als Autor (Xin) bin ich von der Methode allerdings so nicht überzeugt. Das Null-Objekt ist verhältnismäßig selten interessant. In den meisten Fällen lösen Zeiger derartige Problem hier besser, da sie klar herausstellen, dass das verwendete Objekt ungültig sein könnte und im hier vorgestellten Beispiel kann eine Liste ein Ende haben. Das lässt sich mit einem Null-Pointer genauso gut abbilden, wie mit einer Null-Instanz, lediglich spart man beim Null-Pointer den Aufwand eine Null-Instanz zu erzeugen und ggfs. aufwendig dagegen zu vergleichen. Es gilt also zu überlegen, ob eine Null-Instanz einen Mehrwert liefert, weil sie in Algorithmen tatsächlich gebraucht werden oder nur zur Abfrage dienen, um einen Null-Zeiger zu verdecken. In dem Fall sollte ein Zeiger auch wirklich ein Zeiger sein.

In einer Liste muss der Next-Zeiger immer ein Zeiger sein, da der Zweck einer Liste bekanntlich ist, Elemente beliebig austauschen zu können. Eine Referenz kann nur einmalig initialisiert werden, den Operatoren, wie die Zuweisungen, auf eine Referenz beziehen immer auf die Instanz, die referenziert wird.