Leseadressen in C++
In einer Deklaration wie »int * p« wird der Teil vor dem Stern »*« als Deklarationsspezifizierersequenz (»decl-specifier-seq«) bezeichnet.
In einer Deklarationsspezifizierersequenz können neben Typspezifizierern (wie »auto«, »int«, »double« oder »char«) auch Cv-Qualifizierer (wie »const«) in beliebiger Reihenfolge direkt hintereinander aufgelistet werden. Die Reihenfolge hat keine Bedeutung. Die Deklarationsspezifizierersequenz »auto const« bedeutet dasselbe wie die Deklarationsspezifizierersequenz »const auto«. Zur Vereinfachung beschränken wir uns ab hier auf eine Schreibweise, nämlich die mit dem nachgestellten »const«, also »auto const«.
Wenn T ein Typspezifizierer ist (wie »auto«, »int«, »double« oder »char«), dann nennen wir den Typ »T const *« einen Lesetyp.
Eine Adresse, deren Typ ein Lesetyp ist, nennen wir eine Leseadresse.
Ein Objekt, dessen Typ ein Lesetyp ist, nennen wir eine Leseobjekt. Ein Leseobjekt enthält eine Leseadresse.
Eine Leseadresse kann nur verwendet werden, um aus dem Referenten zu lesen (sie kann nicht verwendet werden, um etwas in den Referenten zu schreiben ).
Das folgende Programmbeispiel zeigt einen Lesezeiger »r« und eine Adresse »w«. Der Lesezeiger erlaubt es, das Objekt »i« durch Auswertung des Ausdrucks »*r« zu lesen, aber nur die Adresse »w« erlaubt es, mit »*w =« in das Objekt »i« zu schreiben. Würde versucht werden, mit »*r =« zu schreiben, so gäbe es eine Fehlermeldung.
main.cpp
#include <iostream>
#include <ostream>int main()
{ auto i{ 27 };auto const * r{ &i };
auto * w{ &i };::std::cout << *r << '\n';
*w = 40;
::std::cout << *r << '\n'; }
Protokoll
27
40
Der Lesezeiger kann geändert werden. Im folgenden Beispiel enthält »p« zuerst die Adresse von »i« und später die Adresse von »k«. Ein Lesezeiger ist also selber nicht notwendigerweise ein konstanter Zeiger.
main.cpp
#include <iostream>
#include <ostream>int main()
{ auto i{ 27 };
auto k{ 40 };
auto const * p{ &i };
::std::cout << *p << '\n';p = &k;
::std::cout << *p << '\n'; }
Protokoll
27
40
Die Existenz einer Leseadresse auf ein Objekt bedeutet nicht unbedingt, daß dieses Objekt konstant ist. Es kann durchaus möglich sein, daß das Objekt direkt oder über eine andere Adresse geändert werden kann.
Das folgende Programm zeigt, wie das Objekt »i« sowohl über »i =« als auch über »*q =« geändert werden kann, es kann nur nicht über »*p =« geändert werden.
main.cpp
#include <iostream>
#include <ostream>int main()
{ auto i{ 27 };
auto const * p{ &i };
auto * q{ &i }; ::std::cout << *p << '\n';
i = 40; ::std::cout << *p << '\n';
*q = 60; ::std::cout << *p << '\n'; }Protokoll
27
40
60
Wurde ein Objekt als konstant gekennzeichnet, so sind nur Leseaddressen dieses Objekts erlaubt!
main.cpp
#include <iostream>
#include <ostream>int main()
{ auto const i{ 27 };
auto const * p{ &i };
::std::cout << *p << '\n';
/* auto * q { &i };
error: initialization discards 'const' qualifier from pointer target type */ }Protokoll
27
Das voranstehende Programmbeispiel zeigt auch noch einmal, daß eine „Konstante“ (also hier »i«) in C kein richtiger Name für einen Wert ist (das wäre hier der Wert 27), sondern doch erst einmal ein Name für ein Objekt. Dieses Objekt enthält dann der Wert der Konstanten. Wenn dies nicht so wäre, dann könnte man nämlich nicht die Adresse einer Konstanten (also hier »&i«) angeben.
Wenn durch »const« einmal eine Einschränkung festgelegt wurde, dann verhindert das Typsystem von C++ es, daß diese Einschränkung unter Verwendung des Namen, der mit der Einschränkung versehen ist, wieder aufgehoben wird. Daher kann eine Leseadresse im allgemeinen nicht zur Initialisierung eines Zeigers, der kein Lesezeiger ist, verwendet werden.
main.cpp
#include <iostream>
#include <ostream>int main()
{ auto const i { 27 };
auto const * p { &i };
::std::cout << *p << '\n';
/* auto * w { p };
error: initialization discards 'const' qualifier from pointer target type */ }Protokoll
27
Zitate *
- »simple-declaration« (C++ 2017e, 10p1):
simple-declaration:
decl-specifier-seq init-declarator-list/opt ;- »decl-specifier-seq« (C++ 2017e, 10p1) (gekürzt):
decl-specifier-seq:
decl-specifier decl-specifier-seq- »decl-specifier« (C++ 2017e, 10.1p1) (gekürzt):
decl-specifier:
defining-type-specifier- »defining-type-specifier« (C++ 2017e, 10.1.7p1) (gekürzt):
defining-type-specifier:
type-specifier- »type-specifier« (C++ 2017e, 10.1.7p1) (gekürzt):
type-specifier:
simple-type-specifier
cv-qualifier- »simple-type-specifier« (C++ 2017e, 10.1.7p1) (gekürzt):
simple-type-specifier:
auto
char
int
double- »cv-qualifier« (C++ 2017e, 10.1.7p1) (gekürzt):
cv-qualifier:
const
Ergänzungen zu Zeigern
In den Unterabschnitten dieses Abschnitts finden sich vorläufig einige Ergänzungen zu Zeigern, für die jeweils weitere Lektionen geplant sind.
const-cast
Man kann eine const-Qualifikation durch einen cast entfernen, was man aber nie tun sollte. Das folgende Programm hat undefiniertes Verhalten, weil damit in ein const-Objekt geschrieben wird.
main.cpp
#include <iostream>
#include <ostream>int main()
{ auto const i{ 27 };
auto const * p{ &i };
::std::cout << *p << '\n';
/* auto * w { p };
error: initialization discards 'const' qualifier from pointer target type */
auto * w { const_cast< int * >( p ) };
*w = 99;
::std::cout << *p << '\n'; }Protokoll
27
99
- C++ Core Guidelines (2016-04-07)
- ES.50: Don't cast away const
- Type safety profile: … Type.3: Don't use const_cast to cast away const (i.e., at all).
»* const« — Konstante Zeiger
Ein konstanter Zeiger kann, wie jede andere konstante Variable, nicht verändert werden. Er muß aber deswegen nicht unbedingt auch ein Lesezeiger sein. Schreibzugriffe über einen konstanten Zeiger sind also nicht notwendigerweise verboten.
main.cpp
#include <iostream>
#include <ostream>int main()
{ auto i{ 27 };
auto j{ 33 };
auto * const p { &i };
::std::cout << *p << '\n';
*p = 45;
::std::cout << *p << '\n';
/* p = &j;
error: assignment of read-only variable 'p' */ }Protokoll
27
45
- Tabelle
Deklaration erlaubt verboten
int * p p = ..., *p = ...
int const * p p = *p =
int * const p *p = p =
int const * const p p = ..., *p = ...
»( int * p )« — Adreßparameter
Das folgende Programm zeigt eine Funktion, welche den Wert eines int-Objekts, dessen Adresse ihr übergeben wird, auf 0 setzt.
main.cpp
#include <iostream>
#include <ostream>void clear( int * const p ) { *p = 0; }
int main()
{ auto i{ 12 };
::std::cout << i << '\n';
clear( &i );
::std::cout << i << '\n'; }Protokoll
12
0
/ Zuweisung
Fügen Sie die Definition einer Funktion »assign« an Stelle der Ellipse »…« in das folgende Programm ein, welche den Wert ihres zweiten Arguments in das durch das erste Argument angegebene int-Objekt schreibt.
main.cpp
#include <iostream>
#include <ostream>…
int main()
{ auto i{ 7 };
::std::cout << i << '\n';
assign( &i, 34 );
::std::cout << i << '\n'; }Protokoll
7
34
/ Vertauschen
Fügen Sie die Definition einer Funktion »swap« (Aussprache: /swɑp/) an Stelle der Ellipse »…« in das folgende Programm ein, welche die Werte der beiden als Argumente angegebenen int-Objekte miteinander vertauscht.
main.cpp
#include <iostream>
#include <ostream>…
int main()
{ auto i{ 17 }; auto j{ 82 };
::std::cout << "i = " << i << ", j = " << j << '\n';
swap( &i, &j );
::std::cout << "i = " << i << ", j = " << j << '\n'; }Protokoll
i = 16, j = 82
i = 82, j = 16
»return p;« — Adreßrückgaben
Es ist zwar grundsätzlich möglich, eine Adresse zurückzugeben, aber das folgende Programm hat undefiniertes Verhalten, da die zurückgegebene Adresse zu einem Objekt gehört, das nur während der Existenz der Inkarnation von »f« (nur während der Dauer der Auswertung von »f()«) existiert.
main.cpp
#include <iostream>
#include <ostream>auto f( ){ auto i{ 9 }; return &i; }
int main()
{ ::std::cout << *f() << '\n'; }Protokoll
- (Programm stürzt ab)
»nullptr« — Die Nulladresse
Die Nulladresse ist ein Fluchtwert von Adreßtypen, das heißt, daß ihr Typ ein Adreßtyp ist, ihr Bedeutung aber nicht die Bedeutung einer Adresse ist. Sie kann durch Wandlung von »0« oder »nullptr« in einen Adreßtyp erhalten werden und ist falschartig.
Die Nulladresse darf nicht dereferenziert werden. Beim Schreiben von Programmen sollte »nullptr« statt »0« verwendet werden.
main.cpp
#include <iostream>
#include <ostream>int main()
{ int * n{ 0 };
int * m{ nullptr };
auto i{ 5 };
auto p{ &i };
::std::cout <<( n ? "n" : "-" )<< '\n';
::std::cout <<( m ? "m" : "-" )<< '\n';
::std::cout <<( n == m ? "=" : "-" )<< '\n';
::std::cout <<( n == p ? "=" : "-" )<< '\n';
::std::cout <<( m == p ? "=" : "-" )<< '\n'; }Protokoll
-
-
=
-
-
»::std::size_t« — Der Größentyp
Der Typ »::std::size_t« ist ein implementationsdefinierter vorzeichenloser ganzzahliger Typ, dessen Werte groß genug werden können, um die Größe jedes Objektes (gemessen in Byte) als Zahl zu umfassen.
main.cpp
#include <cstddef>
#include <iostream>
#include <ostream>int main()
{ auto const i{ 8 };
::std::size_t const n = sizeof i;
::std::cout << n << '\n'; }Protokoll
4