Objektnamen in C++
Ein Bindung ist eine Beziehung zwischen einem Namen und einem Objekt, wie sie bei der Definition einer Variablen hergestellt wird. Der Name steht danach für das Objekt und kann beispielsweise sowohl als linke Seite einer Zuweisung verwendet werden, um in das Objekt zu schreiben, als auch als rechte Seite einer Zuweisung, um aus dem Objekt zu lesen.
Das Referenzzeichen »&« kann in C++ verwendet werden, um eine Erzeugung eines Objekts und ein damit möglicherweise verbundenes Kopieren zu verhindern. Es ist damit möglich, einen Namen an ein schon vorhandenes Objekt zu binden.
Bei einer Referenzdeklaration wird ein neuer Name an ein schon vorhandenes Objekt gebunden. Das Objekt muß dazu durch einen Ausdruck angegeben werden, der ein vorhandenes Objekt angibt.
Objektnamensdeklarationen in C++
In dem folgenden Beispielprogramm wird zunächst eine Variable in der klassischen Weise definiert. Bei einer solchen Variablendefinition wird automatisch immer auch ein neues Objekt angelegt.
Es folgt dann die Definition eines reinen Objektnamens. Hierbei wird dem neuen Namen das Zeichen "&" vorangestellt.
Bei einer reinen Objektnamensdeklaration wird kein neues Objekt angelegt, sondern nur ein weiterer Name für ein schon vorhandenes Objekt vereinbart.
Daher muß bei einer Objektnamensdeklaration immer ein schon vorhandenes Objekt angegeben werden, an das dann (auch) der neue Name gebunden wird. In dem folgenden Beispiel wird der neue Name "w" an das Objekt der schon bestehenden Variablen "v" gebunden.
Danach ist der Name "w" an dasselbe Objekt gebunden wie zuvor schon der Name "v". Nun sind die Variable "v" und die Variable "w" beide Variablen mit dem gleichen Objekt, die sich nur noch in ihrem Namen unterscheiden, aber sonst absolut gleichberechtigt sind.
Wurde durch eine Variablendefinition oder eine Objektnamensdeklaration ein Name einmal an ein Objekt gebunden, so gilt diese Bindung danach unabänderlich im ganzen Gültigkeitsbereich des Namens. Derselbe Name kann also in seinem Gültigkeitsbereich (während derselben Lebenszeit) nicht an ein anderes Objekt gebunden werden. Hierin unterscheiden sich die durch eine Objektnamensdeklaration eingeführten Namen nicht von den bisher schon bekannten Variablennamen.
Die folgende Zeilen zeigen dann, daß es daher im folgenden egal ist, wann man die Variable "v" und wann die Variable "w" verwendet. Sie unterscheiden sich jetzt nur in dem Namen, aber nicht in ihrem Objekt. Man kann auch sagen, der Bezeichner "w" sei nun ein Synonym für den Bezeichner "v". Die Bezeichner sind, abgesehen davon, daß der eine »w« und der andere »v« lautet, absolut gleichberechtigt.
referenzen.cpp
#include <iostream>
#include <ostream>
#include <initializer_list>
int main()
{ std::cout << "Objektnamensdefinition\n";
int v{ 12 }; /* Variablendefinition und Initialisierung */
int & w{ v }; /* Namensdeklaration von w zum Objekt von v */
std::cout
<< & v << '\n' /* Ausgabe der Objektkennzahl (Adresse) */
<< & w << '\n' /* Ausgabe der Objektkennzahl (Adresse) */
<< w << '\n' /* Ausgabe des Objektwertes */
<< v << '\n'; /* Ausgabe des Objektwertes */
w = 7; /* Das Objekt wird auf 7 gesetzt. */
std::cout
<< w << '\n' /* Ausgabe des Objektwertes */
<< v << '\n'; /* Ausgabe des Objektwertes */
v = 4; /* Das Objekt wird auf 4 gesetzt. */
std::cout
<< w << '\n' /* Ausgabe des Objektwertes */
<< v << '\n'; /* Ausgabe des Objektwertes */
}std::cout
Referenzen
006BFDF4
006BFDF4
12
12
7
7
4
4
Man beachte den Alias-Effekt: Ein Schreibzugriff über den Namen "w" verändert nicht nur den über den Namen "w" erreichbaren Wert, sondern auch den über den Namen "v" erreichbaren Wert, da beide Namen sich ein Objekt teilen.
Da die Adressen (Objekte) beider Variablen gleich sind, kann man den Adreßoperator in der Objektnamensdefinition "& w = v" auch lesen als „die Adresse (d.h.: das Objekt) der Variablen "w" soll die von (dem Objekt von) der Variablen "v" sein.“
Nach der Definition von v [UML static structure diagram]
.----------------------.
| 12 : int |
| -------- |
| |
'----------------------'
^
| hat Wert ^
|
.--------------. .----------------------.
| v : Name | gebunden an > | 006BFDF4: int-Objekt |
| -------- |------------------->| -------------------- |
| | | |
'--------------' '----------------------'Nach der Definition von w [UML static structure diagram]
.----------------------.
| 12 : int |
| -------- |
| |
'----------------------'
^
| hat Wert ^
|
.--------------. .----------------------.
| v : Name | gebunden an > | 006BFDF4: int-Objekt |
| -------- |------------------->| -------------------- |
| | | |
'--------------' '----------------------'
.--------------. ^
| w : Name | gebunden an > |
| -------- |--------------------------------'
| |
'--------------'Nach der Definition von v [Behaelterdiagramm]
. .
|-------. _____ Wert | Objekt
| v |' '-. |
|-------'# ## '. |
| / ## # # \ |
| ; # # # ; |
| | # # | |
| ; # # ; |
| \ # # / |
' '. ### ######.' '
\ '-._____.-' /
'-------------------'Nach der Definition von w [Behaelterdiagramm]
. .
|-------. _____ Wert | Objekt
| v |' '-. |
|-------'# ## '. |
|-------.# # # \ |
| w |# # # ; |
|-------'# # | |
| ; # # ; |
| \ # # / |
' '. ### ######.' '
\ '-._____.-' /
'-------------------'
Namen, die über eine Namensdefinition an ein schon vorhandenes Objekt gebunden werden, nennt man auch „Referenzen“, dementsprechend wird die Objektnamensdefinition auch als „Referenzdefinition“ bezeichnet. Der Name „Referenz“ ist hier allerdings irreführend, wenn man dadurch verleitet wird, in einer Referenz etwas anderes zu sehen als in einer Variablen. Das letzte Programmbeispiel zeigte ja, daß ein durch eine Referenzdeklaration eingeführter Name nicht von einem zuvor durch eine Variablendefinition eingeführten Namen zu unterscheiden ist. Daher ist es besser, beide Namen als Objektnamen anzusehen, die sich nur in der Art ihres Zustandekommens unterscheiden, aber absolut gleichberechtigt sind, nachdem sie einmal gebildet wurden.
Da sich ein über eine Referenzdefinition eingeführter Name danach wie eine zusammen mit einem Objekt definierte Variable verhält, werden in diesem Text Referenzen auch manchmal als „Variablen“ bezeichnet. Eine Variable kann umgekehrt als eine Referenz auf ihr Objekt angesehen werden. Eine Referenz unterscheidet sich von einer anderen Variablen nur durch ihre Entstehung, aber nicht durch die Eigenschaften, die sie danach im Gebrauch hat. Allerdings können einer Referenz durch ausdrückliches Verlangen noch spezielle Eigenschaften gegeben werden, wie dies im folgenden Abschnitt beschrieben wird.
Objektnamen mit Nur-Lese-Zugriff
Variablen können sich in ihren Eigenschaften unterscheiden. In dem folgenden Beispiel wird die Variable "w" mit dem Schlüsselwort "const" vor dem Und-Zeichen "&" definiert. Das bedeutet, daß durch die Variable "w" keine Schreibzugriffe auf das Objekt möglich sein sollen. Anstatt dieser genauen Formulierung wird auch die Bezeichnung „konstante Referenz“ verwendet. Die Bezeichnung „konstante Referenz“ ist also immer so zu verstehen, daß über die verwendete Referenz keine Schreibzugriffe auf das Objekt möglich sind. (Es ist nicht so gemeint, daß die Referenz in dem Sinne konstant sei, daß sie nicht auf ein anderes Objekt geändert werden könne. Das ist zwar auch richtig, ist aber bei Variablen immer so. Es ist auch nicht notwendigerweise eine Referenz auf ein konstantes Objekt, weil das Objekt durch andere [nichtkonstante] Referenzen, ja durchaus noch verändert werden könnte, also nicht konstant sein muß.)
Im folgenden Programmbeispiel kann die Variable "w" wie bisher verwendet werden, um den Wert des Objektes zu bezeichnen. Es ist aber nicht mehr möglich, den Wert des Objektes über die Variable "w" zu verändern. Da dies aber eine Eigenschaft der Variablen "w" und nicht des Objektes selber ist, ist es über die Variable "v" weiterhin möglich, den Wert des Objektes zu verändern.
referenzen2.cpp
#include <iostream>
#include <ostream>
#include <initializer_list>
int main()
{ std::cout << "Referenzen\n";
int v( 12 ); /* Variablendefinition und Initialisierung */
int const & w( v ); /* Namensdefinition von w zum Objekt von v */
v = 7; /* Das Objekt wird auf 7 gesetzt. */
std::cout
<< w << '\n' /* Ausgabe des Objektwertes */
<< v << '\n'; /* Ausgabe des Objektwertes */
/* w = 4; */ /* Nicht erlaubt, da w nur fuer Lesezugriff */ }
Die wesentlichen Aspekte der Situation nach der Definition der Variablen "w" werden in dem folgenden Schaubild dargestellt.
Nach der Definition von w [UML static structure diagram]
.----------------------.
| 12 : int |
| -------- |
| |
'----------------------'
^
| hat Wert ^
|
.--------------. .----------------------.
| v : Name | Referenz > | 006BFDF4: int-Objekt |
| -------- |----------------------->| -------------------- |
| | Lesen und Schreiben | |
'--------------' '----------------------'
.--------------. ^
| w : Name | konstante Referenz > |
| -------- |------------------------------------'
| | ueber diese Bindung nur Lesen
'--------------'
Das folgende Diagramm stellt dieselbe Situation etwas ausführlicher dar.
Referenz [UML, static structure diagram]
.--------. .--------.
| v:Var- | .----------. .----------. | w:Var- |
| iable | * 1 | Eigen- | | const&: | 1 * | iable |
| ------ |<>------| schaft- | | Eigen- |------<>| ------ |
| | | ten | | schaft | | |
| | '----------' '----------' | |
| | .----------. .----------. | |
| | 1 1 | v : Name | | w : Name | 1 1 | |
| |<>------| -------- | | ------- |------<>| |
| | | | | | | |
| | '----------' '----------' | |
| | | | | |
| | | * | * | |
| | '----. .----' | |
| | V Referenz | | const Referenz V | |
| | | | | |
| | | 1 | 1 | |
| | V V | |
| | .-----------. | |
| | * 1 | O:Objekt | 1 * | |
| |--------------->| -------- |<--------------| |
| | | | | |
| | '-----------' | |
| | | | | | |
'--------' .--------------' | '--------------. '--------'
| 1 | 1 | 1
v v v
.---------. .---------. .---------.
| int:Typ | | Wert | | Eigen- |
| ------- | | | | schaf- |
| | | | | ten |
'---------' '---------' '---------'
C++ überwacht den Zugriffschutz, den eine konstante Referenz bietet: Es ist zwar möglich von einer Referenz eine konstante Referenz abzuleiten, aber nicht umgekehrt von einer konstanten Referenz eine (nicht-konstante) Referenz, da dieses es erlauben würde, den Zugriffsschutz zu umgehen.
referenzen2c.cpp
int main()
{ int v( 2 ); /* Variablendefinition und Initialisierung */
int & r( v ); /* Referenzdefinition */
int const & c( r ); /* konstante Referenzdefinition */
int & u( c ); /* nicht erlaubt */ }Fehlermeldungen
line 5: error: conversion from `int const' to `int &'
discards qualifiers
qualifiers dropped in binding reference of type "int &"
to initializer of type "int const"
Es ist natürlich nicht möglich, durch eine Referenz einen Schreibzugriff auf eine Konstante zu erhalten, da dies keinen Sinn ergeben würde, denn nur Objekte (Wertspeicher) können überhaupt verändert werden, keine Werte. In dem folgenden Programm wird eine Konstante "k" definiert. Zu solch einer Konstanten muß ja gar kein Objekt existieren. Demzufolge kann sie auch nicht zur Definition einer „weiteren Referenz“ auf ein Objekt verwendet werden.
Dies folgt der allgemeinen Regel, daß man ein veränderliches Objekt immer als Wert auffassen darf (nämlich als den momentanen Wert des Objektes), aber es umgekehrt nicht erlaubt ist, einen Wert als zeitlich veränderbares Objekt (als einen Wertspeicher) zu interpretieren.
referenzen3.cpp
#include <iostream>
#include <ostream>
#include <initializer_list>
int main()
{ std::cout << "Referenzen\n";
int const k( 2 ); /* Definition einer Konstanten */
int & w( k ); /* Nicht erlaubt */ }
Die Referenzdefinition in dem folgenden Beispiel ist offensichtlich nicht erlaubt, da der Wert "2" kein Objekt ist.
referenzen4.cpp
#include <iostream>
#include <ostream>
int main()
{ int & v( 2 ); /* Nicht erlaubt */ }
Nachdem nun so plastisch geschildert wurde, daß zu einem Wert kein Objekt gebildet werden kann, muß nun angefügt werden, daß es von dieser Regel in C++ doch auch eine Ausnahme gibt.
C++ erlaubt es nämlich einen Rechtswert zur Initialisierung von konstanten Referenzen zu verwendet. Da ein Rechtswert aber im allgemeinen nicht verändert werden kann, muß dann auch die Referenz konstant sein, also eine Referenz auf ein konstantes Objekt sein, aber immerhin auf ein Objekt. Woher soll aber solch ein konstantes Objekt kommen? Es wird in diesem Fall von C++ automatisch ein neues Objekt angelegt und mit dem gegebenen Rechtswert initialisiert! Dieses Objekt wird dann an die neue Variable gebunden.
referenzen5.cpp
#include <iostream>
#include <ostream>
#include <initializer_list>
int main()
{ int const & v( 2 ); /* erlaubt */
std::cout
<< &v << '\n'
<< v << '\n';
int const & w( 2 ); /* erlaubt */
std::cout
<< &w << '\n'
<< w << '\n'; }std::cout
006BFDF0
2
006BFDE8
2
Hier wurden zwei Objekte erzeugt und mit dem Wert "2" initialisiert.
Gibt es auch Referenzen auf Referenzen?
Eine sogenannte Referenz ist nichts weiter als eine Variable. Sie unterscheidet sich von anderen Variablen nur durch die Art der Entstehung ihrer Bindung: Bei einer Variablendefinition wird ein neuer Name an ein neues Objekt gebunden, bei einer Referenzdefinition wird ein neuer Name an ein schon existierendes Objekt gebunden.
Wird nun eine weitere Referenz unter Verwendung einer Referenz erstellt, so ist dies nur eine weitere Referenz auf das Objekt der vorgegebenen Referenz. Dies kann man schon daran erkennen, daß eine Referenz zum Objekt einer Variablen danach der ursprünglichen Variablen gleichzusetzen ist (abgesehen von Einschränkungen wie dem Nur-Lese-Zugriff). Daher ist eine Referenz, die unter Verwendung einer Referenz gebildet wird, nichts anderes als eine Referenz, die unter Verwendung einer Variablen mit demselben Objekt gebildet wird. Insofern gibt es also immer nur Referenzen auf Objekte, aber keine Referenzen auf Referenzen (oder Variablen).
referenzen6.cpp
#include <iostream>
#include <ostream>
#include <initializer_list>
#include <string> /* ::std::string */
using namespace ::std::literals;
int main()
{ int v( 4 ); /* Erzeugung eines Objekts, Bindung des Namens "v" */
int & w( v ); /* Bindung des Namens "w" an dasselbe Objekt */
int & u( w ); /* Bindung des Namens "u" an dasselbe Objekt */
std::cout
<< &v << ", "s << v << '\n'
<< &w << ", "s << w << '\n'
<< &u << ", "s << u << '\n'; }std::cout
0x254fdd4, 4
0x254fdd4, 4
0x254fdd4, 4
Übungsaufgaben
- Übungsaufgabe Vertauschen
Schreiben Sie eine Funktion "swap", welche die Inhalte zweier Objekte vom Datentyp "int" vertauscht.
swap.c
#include <iostream>
#include <ostream>
#include <initializer_list>
// hier "swap" definieren
int main()
{ int a( 1 ); int b( 7 );
swap( a, b ); ::std::cout << a << b << '\n'; }std::cout
71
Hinweis Überlegen Sie für jeden Parameter, ob er ein Variablenparameter oder ein Referenzparameter sein sollte. Bedenken Sie weiterhin für jeden Parameter, ob er ein konstanter oder nichtkonstanter Referenzparameter bzw. ein konstanter oder nichtkonstanter Variablenparameter sein sollte.
- Übungsaufgabe Zuweisung
Schreiben Sie eine Funktion "assign", welche einem Objekt einen Wert zuweist.
swap.c
#include <iostream>
#include <ostream>
#include <initializer_list>
// hier "assign" definieren
int main()
{ int a; assign( a, 7 ); ::std::cout << a << '\n'; }std::cout
7
Hinweis Überlegen Sie für jeden Parameter, ob er ein Variablenparameter oder ein Referenzparameter sein sollte. Bedenken Sie weiterhin für jeden Parameter, ob er ein konstanter oder nichtkonstanter Referenzparameter bzw. ein konstanter oder nichtkonstanter Variablenparameter sein sollte.
Exemplarnamen Referenzen zum Vermeiden des Kopierens
Eine Konstante wurde als Name für einen Wert eingeführt. Die Verwirklichung von Konstanten in C++ entspricht allerdings nicht immer dieser Erklärung, da jede bisher eingeführte Konstante sozusagen eine eigene Kopie ihres Wertes mit sich führt. Ob eine Konstante nun auf einen einmaligen Wert verweist oder eine eigene Kopie eines Wertes enthält ist allerdings in vielen Fällen gar nicht feststellbar, so daß der Unterschied bisher vernachlässigt wurde.
Wenn ein neuer Name für den Standardausgabestrom "::std::cout" vergeben werden soll, dann macht sich das Kopieren allerdings schon bemerkbar. Das Programm "outname" wurde mit der Absicht geschrieben, dem Standardausgabestrom "::std::cout" den neuen Namen "out" zu geben. Die Konstantendefinition verwendet den richtigen Datentyp "::std::ostream" für den neuen Namen "out", scheitert aber daran, daß die Konstantendefinition versucht, eine Kopie des Standardausgabestroms anzulegen, was in C++ nicht erlaubt ist.
outname_.cpp
#include <iostream> /* ::std::cout */
#include <ostream> /* ::std::ostream, << */
#include <initializer_list>
int main()
{ ::std::ostream const out( ::std::cout );
out << "Hallo!\n"; }Diagnose
"ios", line 398: error: "std::ios_base::ios_base(std::ios_base const &)" is inaccessible
1 error detected in the compilation of "outname_.cpp".
Das in diesem Fall verbotene Kopieren des Objektes kann verhindert werden, indem nicht mit einer Variablendefinition ein neues Objekt angelegt, sondern mit einer Referenzdeklaration nur ein neuer Name an das ja schon vorhandene Objekt gebunden wird.
In der Übersetzungseinheit "outname.cpp" wird der Name "out" mit Hilfe einer Referenzdeklaration an das Objekt "::std::cout" gebunden. Danach kann dann auch der Bezeichner "out" anstelle des Ausdrucks "::std::cout" verwendet werden.
outname.cpp
#include <iostream> // ::std::cout
#include <ostream> // ::std::ostream, <<
#include <initializer_list>
#include <string> /* ::std::string */
using namespace ::std::literals;
int main()
{ ::std::ostream & out{ ::std::cout }; out << "Hallo!\n"s; }::std::cout
Hallo!
Autoreferenzen
auto & iref = i; // int&
Exemplarnamen Referenzen zum Vermeiden des Kopierens
Eine Konstante wurde als Name für einen Wert eingeführt. Die Verwirklichung von Konstanten in C++ entspricht allerdings nicht immer dieser Erklärung, da jede bisher eingeführte Konstante sozusagen eine eigene Kopie ihres Wertes mit sich führt. Ob eine Konstante nun auf einen einmaligen Wert verweist oder eine eigene Kopie eines Wertes enthält ist allerdings in vielen Fällen gar nicht feststellbar, so daß der Unterschied bisher vernachlässigt wurde.
Wenn ein neuer Name für den Standardausgabestrom "::std::cout" vergeben werden soll, dann macht sich das Kopieren allerdings schon bemerkbar. Das Programm "outname" wurde mit der Absicht geschrieben, dem Standardausgabestrom "::std::cout" den neuen Namen "out" zu geben. Die Konstantendefinition verwendet den richtigen Datentyp "::std::ostream" für den neuen Namen "out", scheitert aber daran, daß die Konstantendefinition versucht, eine Kopie des Standardausgabestroms anzulegen, was in C++ nicht erlaubt ist.
outname_.cpp
#include <iostream> /* ::std::cout */
#include <ostream> /* ::std::ostream, << */
#include <initializer_list>
#include <string> /* ::std::string */
using namespace ::std::literals;
int main()
{ ::std::ostream const out( ::std::cout );
out << "Hallo!\n"s; }Diagnose
"ios", line 398: error: "std::ios_base::ios_base(std::ios_base const &)" is inaccessible
1 error detected in the compilation of "outname_.cpp".
Das in diesem Fall verbotene Kopieren des Objektes kann verhindert werden, indem nicht mit einer Variablendefinition ein neues Objekt angelegt, sondern mit einer Referenzdeklaration nur ein neuer Name an das ja schon vorhandene Objekt gebunden wird.
In der Übersetzungseinheit "outname.cpp" wird der Name "out" mit Hilfe einer Referenzdeklaration an das Objekt "::std::cout" gebunden. Danach kann dann auch der Bezeichner "out" anstelle des Ausdrucks "::std::cout" verwendet werden.
outname.cpp
#include <iostream> // ::std::cout
#include <ostream> // ::std::ostream, <<
#include <initializer_list>
#include <string> /* ::std::string */
using namespace ::std::literals;
int main()
{ ::std::ostream & out( ::std::cout ); out << "Hallo!\n"s; }::std::cout
Hallo!
Lebensdauer temporärer Objekten
Es wurde schon behandelt, daß ein temporäres Objekte ein Objekt initialisieren kann, indem sein Inhalt in das Objekt kopiert wird.
Wenn ein temporäres Objekt an eine konstante Referenz gebunden wird, so verlängert dies seine Lebensdauer auf die Lebensdauer der Referenz.
Weitere Themen zu Referenzen
- Wir können ::std::cout nicht kopieren, aber referenzieren
- Wenn Temporär an konstante Referenz gebunden wird, verlängert sich seine Lebenszeit
- Rückgabe von Referenzen
- decltype
#include <iostream>
#include <ostream>
int main()
{ int v;
decltype(v) w;
decltype((v)) ww; /* error: 'ww' declared as reference but not initialized */ }
Während »decltype« den Typ einer Variablen genau ergibt, ergibt »decltype« bei anderen Ausdrücken stets einen Referenztyp.
/ A
Schreiben Sie eine Funktion print_pair, die mit einem Referenzparameter ein ::std::pair<int,int> als Argument entgegennimmt und ausgibt. Rufen Sie diese Funktion dann von main aus mit einem ::std::pair<int,int>-Argument auf, um dieses auszugeben.
Zusatzanforderung A1: Schreiben Sie statt einer Funktion eine Funktionsschablone, welche ein beliebiges ::std::pair<T1,T2> als Argument entgegennimmt. (Dabei kann T1 und T2 dann jeweils einer der Typen int, double oder String sein.)