Reihungen und Adressen (Zeiger) in C++
Diese Lektion zu Reihungen wurde vor den voranstehenden Lektionen zu Adressen geschrieben und wiederholt daher noch einmal einiges schon Behandeltes.
- Motivation: this-Adresse, char *, argv
Reihungen
Reihungen (englisch “arrays ” [Plural]) werden auch „Felder“ genannt. Jedoch nennt man Felder (englisch “fields ” [Plural]) von Klassen ebenfalls „Felder“. Um Verwechslungen mit diesen Feldern zu vermeiden, werden Reihungen hier „Reihungen“ genannt.
::std::vector, ::std::array
In C++ ist es oft besser, ::std::vector (veränderliche Größe) oder ::std::array (konstante Größe) zu verwenden. Diese beiden Klassen sind jedoch nicht das Thema dieser Lektion. Es läßt sich nicht ganz vermeiden, sich auch mit Reihungen zu beschäftigen, und Reihungen erlauben es, Adressen zu verstehen. Daher hat diese Lektion hier auch ihren Sinn. Sobald ::std::vector erlernt wurde, sollte dies aber als Behälter bevorzugt verwendet werden, wenn kein überzeugender Grund für die Verwendung einer Reihung besteht.
Definition
Mehrere Objekte des gleichen Datentyps können mithilfe nur eines Namens und einer zusätzlichen Kennzahl identifiziert werden. Hierzu wird eine Reihung (engl. array ) definiert. Die Zahl der Komponenten der Reihung wird bei der Variablendefinition in eckigen Klammern hinter dem Variablennamen angegeben.
main.cpp
int main(){ int a[ 2 ]; }
Die Variable "a" hat den Datentyp "int[ 2 ]", sprich „eine Reihung von zwei int-Objekten“. Ein int-Objekt ist ein Speicherplatz, der einen int-Wert speichern kann.
Eine Reihung ist ein Folge von Komponenten, die alle den gleichen Datentyp haben.
Im Speicher werden mit dieser Definition zwei Objekte vom Datentyp "int" angelegt.
Ein Objekt vom Datentyp "int[ 2 ]"
.--------------.
| int-Objekt 0 |
|--------------|
| int-Objekt 1 |
'--------------'
Eine Reihung ist also ein Objekt, das selber wieder Objekte enthält. Solch ein Objekt nennt man auch einen Aggregat. Die enthaltenen Objekte werden auch Komponenten genannt.
Die Objekte in der Reihung sind zunächst einmal namenlos (anonym), das heißt: Sie haben nicht notwendigerweise einen eigenen Trivialnamen. Indirekt kann der Name der Reihung verwendet werden, um sich auf eine Komponente der Reihung zu beziehen.
Die Zahl der Komponenten einer Reihung muß dem Übersetzer bekannt sein. Sie kann nicht erst zur Laufzeit bestimmt werden.
Inhalts-Operator
Ein Objekt wird durch eine Adresse gekennzeichnet. Die Adresse eines Objektes bietet neben den Variablennamen eine weitere Möglichkeit, ein Objekt anzugeben.
Wenn der Name einer Reihung in einem Ausdruck verwendet wird, dann gilt er meisten als Adresse der ersten Komponente der Reihung.
Adresse der ersten Komponente einer Reihung
.--------------.
a ---------> | int-Objekt 0 |
|--------------|
| int-Objekt 1 |
'--------------'
Um das Objekt an dieser Adresse zu notieren, wird der Dereferenzierungs "*" verwendet. Das Objekt bei der Adresse "B " wird als "*B " geschrieben. Der Zugriff auf das Objekt bei einer Adresse wird auch als Dereferenzierung bezeichnet. Die Adresse eines Objektes ist eine Bezugnahme auf das Objekt, welche durch die Dereferenzierung aufgelöst wird.
Die Reihungsvariable "a" selber ist eine Konstante: Sie bezeichnet immer das bei der Definition geschaffene Reihungsobjekt. Zuweisungen, die mit "a =" beginnen, sind nicht möglich. (Diese Schreibweise kann aber bei der Initialisierung einer Reihung vorkommen.)
Der Ausdruck "*a" bezeichnet jedoch den möglicherweise veränderlichen Inhalt der Komponente 0 der Reihung. Die erste Komponente der Reihung hat keinen Namen, der Ausdruck »*a« kann aber praktisch wie ein Name dieser Komponente verwendet werden.
Der Ausdruck "*a" kann wie eine Variable verwendet werden, sowohl auf der linken Seite der Zuweisung als auch auf der rechte oder als Argument oder Operator. Man sagt auch »*a« sei ein lvalue.
Die Reihung "a" und ihre Komponente *a
.--------------.
a ---------> | *a |
|--------------|
| |
'--------------'main.cpp
#include <iostream>
#include <ostream>
#include <cstdlib>int main()
{ int a[ 2 ];
*a = ::std::rand();
::std::cout << *a << '\n'; }
Bisher haben wir nur Zuweisungen betrachtet, auf deren linker Seite ein Variablenname steht. Nun sehen wir hier, daß auch der Operatorausdruck »*a« auf der linken Seite einer Zuweisung erlaubt sein kann.
Adressberechnungen
Die Adresse eines Objektes des Datentyps "int" hat den Datentyp "int *". Diesen Datentyp kann man als „int-Adresse“ lesen.
Wird eine Zahl zu einer Adresse addiert, so wird dem Speicherplatzbedarf entsprechend vieler Objekte des Basisdatentyps weitergezählt. Wird beispielsweise eine Zahl i zu einer Adresse vom Datentyp "int *" addiert, so wird um i Objekte des Datentyps "int" weitergezählt.
Der Ausdruck "a + 1" oder der gleichwertige Ausdruck "1 + a" bezeichnet also die Adresse des dem Objekte "*a" folgenden Objektes, da er durch die Addition gerade so weit erhöht wird, wie "*a" groß ist. Es wird also bei der Addition einer Zahl zu einer int-Adresse um die Größe eines Objektes vom Datentyp "int" weitergegangen, wenn ein int-Objekt beispielsweise 4 Byte umfaßt, wird also beispielsweise um 4 Byte weitergegangen. Wir können dank der Adressberechnungen daher die Adressen der zweiten und dritten Komponente der Reihung berechnen und mit dem Inhalts-Operator schließlich auch den Inhalt dieser Adressen erreichen.
Der Ausdruck "a + 0" hat denselben Wert wie der Ausdruck »a«.
Man beachte, daß es mit Reihungen nun möglich ist, zur Laufzeit auszurechnen, welches Objekt verwendet werden soll. Es kann also ein Zugriff auf ein bestimmtes Objekt in Abhängigkeit von einer Zahl erfolgen, wobei zu Schreibzeit noch nicht feststeht, auf welches Objekt zugegriffen wird.
Das Objekt "a" nach der Definition "int a[ 3 ];"
Addresse Objekt
.------------.
a -----> | *( a ) |---
|------------| |
a + 1 -----> | *( a + 1 ) | | 2 x int
|------------| V
a + 2 -----> | *( a + 2 ) |---
'------------'main.cpp
#include <iostream>
#include <ostream>
#include <cstdlib>int main()
{ int a[ 3 ];
::std::cout <<( *( a + 0 )= ::std::rand() )<< '\n';
::std::cout <<( *( a + 1 )= ::std::rand() )<< '\n';
::std::cout <<( *( a + 2 )= ::std::rand() )<< '\n';
::std::cout << *( a + 2 )<< '\n';
::std::cout << *( a + 1 )<< '\n';
::std::cout << *( a + 0 )<< '\n'; }::std::cout
41
18467
6334
6334
18467
41
Fehlerhafte Zugriffe
main.cpp
#include <iostream>
#include <ostream>
#include <cstdlib>
int main()
{ int a[ 1 ];
::std::cout <<( *( a + 1 )= ::std::rand() )<< '\n';
::std::cout << *( a + 1 )<< '\n'; }::std::cout
41
41
Der Index-Operator
Die Kennzahl eines Objektes in einer Reihung nennt man auch den Index des Objektes.
Die Dereferenzierung der Summe einer Reihung und einer Zahl wird so oft beim Zugriff au Reihungen verwendet, daß man dafür eine spezielle Abkürzung eingeführt hat: den Index-Operator "[]". Die Schreibweise "B [I ]" ist dabei eine Abkürzung für "*((B )+(I ))". Da die Addition kommutativ ist kann man dafür auch "I [B ]" schreiben. Beispielsweise kann man mit diesem Operator nun statt des Ausdrucks "*( reihung + 0 )" auch den Ausdruck "reihung[ 0 ]" oder den Ausdruck "0[ reihung ]" verwenden, statt des Ausdrucks "*( reihung + 1 )" kann man auch den Ausdruck "reihung[ 1 ]" oder den Ausdruck "1[ reihung ]" verwenden.
main.cpp
#include <iostream>
#include <ostream>
#include <cstdlib>int main()
{ int a[ 3 ];
::std::cout <<( a[ 0 ]= ::std::rand() )<< '\n';
::std::cout <<( a[ 1 ]= ::std::rand() )<< '\n';
::std::cout <<( a[ 2 ]= ::std::rand() )<< '\n';
::std::cout << a[ 2 ]<< '\n';
::std::cout << a[ 1 ]<< '\n';
::std::cout << a[ 0 ]<< '\n'; }::std::cout
41
18467
6334
6334
18467
41
Initialisierung
Zur Initialisierung einer Reihung kann die Schreibweise mit runden Klammern nicht verwendet werden. Die Werte der Komponenten müssen durch Komma getrennt in geschweiften Klammern angegeben werden.
main.cpp
#include <iostream>
#include <ostream>
#include <cstdlib>int main()
{ int a[ 2 ]{ 41, 18467 };
::std::cout << a[ 0 ]<< '\n';
::std::cout << a[ 1 ]<< '\n'; }::std::cout
41
18467
Es können auch nur die ersten Komponenten initialisiert werden.
main.cpp
#include <iostream>
#include <ostream>
#include <cstdlib>int main()
{ int a[ 4 ]{ 41, 18467 };
::std::cout << a[ 0 ]<< '\n';
::std::cout << a[ 1 ]<< '\n'; }::std::cout
41
18467
Bei Angabe von Initialisierungswerten kann die Angabe der Größe der Reihung entfallen. Dann wird die Größe so gewählt, daß gerade die initialisierten Komponenten hineinpassen.
main.cpp
#include <iostream>
#include <ostream>int main()
{ int a[]{ 41, 18467 };
::std::cout << a[ 0 ]<< '\n';
::std::cout << a[ 1 ]<< '\n'; }::std::cout
41
18467
sizeof
main.cpp
#include <iostream> /* ::std::cout */
#include <ostream> /* << */
#include <cstddef> /* ::std::size_t */int main()
{ int a[]{ 41, 18467 };
::std::cout << sizeof a << '\n';
::std::cout << sizeof *a << '\n';
::std::cout << sizeof a / sizeof *a << '\n';
::std::cout << sizeof( char )<< '\n';
constexpr ::std::size_t s = sizeof a;
::std::cout << s << '\n'; }::std::cout
8
4
2
1
8main.cpp
#include <iostream> /* ::std::cout */
#include <ostream> /* << */int main()
{ int a[ 1 ];
constexpr auto s = sizeof a;
::std::cout << s << '\n'; }::std::cout
4
Wertet Operanden nicht aus:
main.cpp
#include <iostream> /* ::std::cout */
#include <ostream> /* << */int f(){ ::std::cout << 'f' << '\n'; return 8; }
int main()
{ ::std::cout << sizeof f() << '\n';}- Protokoll
4
Adreßoperator
main.cpp
#include <iostream> /* ::std::cout */
#include <ostream> /* << */int main()
{ int i;
::std::cout << &i << '\n';
auto const p = &i;
::std::cout << p << '\n'; }::std::cout
0x22ff08
0x22ff08
Adresstypen
main.cpp
#include <iostream> /* ::std::cout */
#include <ostream> /* << */
int main()
{ int i = 2;
int const * const p = &i; // constexpr nicht möglich
int * const q = &i; // vereinfacht: int * q = &i;
::std::cout << i << '\n';
::std::cout << *p << '\n';
*q = 4;
::std::cout << i << '\n';
::std::cout << *p << '\n'; }::std::cout
2
2
4
4
Zeichenliterale
main.cpp
#include <iostream> /* ::std::cout */
#include <ostream> /* << */int main()
{ constexpr char const * p = "abc";
{ ::std::cout << p << '\n';
::std::cout << sizeof( "abc" )<< '\n'; }
{ constexpr auto p = "abc"; // kopieren!
::std::cout << p << '\n';
::std::cout << sizeof( p )<< '\n'; }
{ const char p[ 4 ]= "abc"; // kopieren!
::std::cout << p << '\n';
::std::cout << sizeof( p )<< '\n'; }}::std::cout
abc
4
abc
4
abc
4
implizite Reihung-Adress-Wandlung
#include <iostream> /* ::std::cout */
#include <ostream> /* << */
int main()
{ int a[ 32 ];
::std::cout << sizeof a << '\n';
::std::cout << sizeof( a )<< '\n';
::std::cout << sizeof( +( a ))<< '\n';
::std::cout << sizeof( a + 0 )<< '\n'; }
Das Zeichen „NUL“
*p++ bedeutet dasselbe wie *(p++).
#include <iostream> /* ::std::cout */
#include <ostream> /* << */int main()
{ char const * p = "abc";
::std::cout << static_cast< int >( *p++ )<< '\n';
::std::cout << static_cast< int >( *p++ )<< '\n';
::std::cout << static_cast< int >( *p++ )<< '\n';
::std::cout << static_cast< int >( *p++ )<< '\n'; }::std::cout
97
98
99
0
Die Zeichen sind a, b, c und NUL. NUL ist nicht mit dem Zeichen 0 zu verwechseln, welches die Kennzahl 48 hat.
- Zeichentabelle
NUL 0
0 48
a 97
b 98
c 99
Übungsaufgaben
/ Übungsaufgabe
In dem obigen Beispielprogramm wird die Anweisung »::std::cout << static_cast< int >( *p++ )<< '\n';« viermal wiederholt. Wiederholungen längerer Programmteile sollten vermieden werden. Schreiben Sie das Programm so um, daß es weiterhin alle vier Zeichen ausgibt, aber diese Anweisung nun in einer Funktion enthalten ist, so daß nur noch der Aufruf dieser Funktion viermal wiederholt wird. Die Deklaration »char const * p = "abc";« darf dabei auch verändert oder an eine andere Stelle verschoben werden. Das Programm soll weiterhin die vier Zeichen der Reihung »"abc"« in derselben Weise wie bisher ausgeben.
/ Übungsaufgabe
Verwenden Sie eine Schleife, um die Wiederholung der Anweisung »::std::cout << static_cast< int >( *q++ ) << '\n';« im Quelltext zu vermeiden. Das Programm soll weiterhin die vier Zeichen der Reihung »"abc"« in derselben Weise wie bisher ausgeben. Dieses Programm sollte dann auch mit anderen Zeichenfolgenliteralen zurechtkommen, selbst wenn diese eine andere Länge als vier haben.
/ Übungsaufgabe
Simulieren Sie 1000 Würfe eines sechsseitigen Würfels und geben Sie eine Tabelle aus, die angibt, wie oft jede Zahl (von 1 bis 6) geworfen wurde.
/ Übungsaufgabe
Fügen Sie zu der Tabelle noch eine Spalte mit den prozentualen Anteilen der sechs Häufigkeiten hinzu. Die Anzahl der Nachkommastellen kann auf zwei bis fünf (nach Ihrer Wahl) beschränkt werden.
/ Übungsaufgabe
Fügen Sie zu der Tabelle noch eine Spalte mit Sternen hinzu, deren Anzahl der Häufigkeit proportional ist. Der maximalen Häufigkeit sollen 40 Sterne entsprechen.
/ Übungsaufgabe
Simulieren Sie nun 1000 Würfe zweier Würfel mit 2 bis 12 Punkten pro Wurf aus. Die ausgegebene Tabelle soll weiterhin die Häufigkeit, den prozentualen Anteil und bis zu 40 Sterne enthalten.
Adressdifferenzen
#include <iostream> /* ::std::cout */
#include <ostream> /* << */int main()
{ constexpr char const * p = "abc";
const char * q = p;
::std::cout << static_cast< int >( *q++ )<< '\n';
::std::cout << static_cast< int >( *q++ )<< '\n';
::std::cout << static_cast< int >( *q++ )<< '\n';
::std::cout << static_cast< int >( *q++ )<< '\n';
::std::cout << q - p << '\n'; /* int */ }::std::cout
97
98
99
0
4
::std::string
In C++ ist es oft besser, ::std::string für Zeichenfolgen zu verwenden. Diese Klasse ist jedoch nicht das Thema dieser Lektion. Es läßt sich nicht ganz vermeiden, sich auch mit Texten von Typen wie »char *« zu beschäftigen. Daher hat diese Lektion hier auch ihren Sinn.
Stringfunktionen
strlen, strcpy, strncpy, strcat
- / Übungsaufgabe
- Schreiben Sie eine eigene Implementation von strlen und strcpy.
Die Nulladresse
nullptr
Zeiger und Adressen
Ein Objekt wie p, das Adressen speichert, wird in C auch „Zeiger“ genannt.
In C++ werden aber auch der Adressen selber als Zeiger angesehen.
Eine Adresse kann nicht 0 sein
Zeigerarithmetik
(char *)a+1
Wenn das Objekt o die Adresse p hat, dann sagt man auch „Der Zeiger p zeigt auf das Objekt o“ oder „p referenziert o“.
Funktionen und Funktionsadressen
Adreßparameter und Referenzparameter
Die Größe geht spätesten bei der Übergabe als Argument verloren, sie muß dann eventuell separat übergeben werden.
ua: ref auf Zeiger um Zeiger aend z koennen
ÜA: Eine Funktion gibt argc und argv aus, welche Argumente übergibt man ihr, damit sie die ersten beiden überspringt?
Dynamische Lebensdauer
Dynamik: malloc/free, new/delete, new[]/delete[], vector/array
, dynamische Arrays (Speicherverwaltung),
- /
- Sortieren
- Schreiben Sie ein C++-Programm, in dem eine Reihung vom Datentyp "int[ 3 ]" definiert wird. Das Programm soll drei Zahlen einlesen und in der Reihung speichern. Nun soll die Reihung so sortiert werden, daß größere Zahlen höhere Indizes haben. Schließlich soll die sortierte Reihung ausgegeben werden. (Die Sortierung und die Ausgabe müssen klar voneinander getrennt werden. Es soll also nicht während der Ausgabe sortiert werden, und die Ausgabe soll die erste, zweite und dritte Komponente hintereinander ausgeben.)
Konsole
12
189
7
7
12
189Struktogramm
.-----------------------------------.
| Die Reihung einlesen |
|-----------------------------------|
| Die Reihung aufsteigend sortieren |
|-----------------------------------|
| Die Reihung ausgeben |
'-----------------------------------'
Zusatzaufgabe Schreiben Sie solch ein Sortierprogramm für eine Reihung mit 10 Komponenten.
#include <iostream> /* ::std::cout */
#include <ostream> /* << */char const * mon( int const m, int const lang )
{ return
"Jan" "Feb" "Mar" "Apr" "Mai" "Jun" "Jul" "Aug" "Sep" "Okt" "Nov" "Dez"
"jan" "feb" "mar" "apr" "may" "jun" "jul" "aug" "sep" "oct" "nov" "dec"
+ 3 *( m - 1 )+ 3 * 12 * lang; }void print( char const * p ){ ::std::cout << p[ 0 ]<< p[ 1 ]<< p[ 2 ]<< '\n'; }
int main(){ print( mon( 5, 0 )); print( mon( 5, 1 )); }
Statische Mehrdimensionale Reihungen
Bei einem Parameter muß die Größe der inneren Reihung als Übersetzerkonstante festgelegt sein
Die Parameterliste der Funktion "main"
Zur Definition einer parameterlosen Funktion ist die empfohlene Schreibweise ein Paar leerer Klammern.
Richtige main-Definition in C++
int main(){ /* ... */ }
Ein erlaubte Alternative zur Angabe einer leeren Parameterliste ist es, das Schlüsselwort "void" in die runden Klammern zu schreiben. (Das ist allerdings eher ein Überbleibsel aus der Programmiersprache C, bei der eine leere Parameterliste angibt, daß über die Parameter nichts ausgesagt werden soll.)
Implementationen von C++ ist es erlaubt, beliebige Argumentlisten beim Aufruf von "main" zu verwenden. Allerdings muß jede Implementation neben der parameterlosen Definition noch die in der folgenden Auflistung angegeben Form mit zwei Parametern unterstützen, mit der die Funktion Informationen über Argumente des Programmes erhalten kann.
Richtige main-Definitionen in C++
int main(){ /* ... */ }
int main( void ){ /* ... */ } // Nicht empfohlen
int main( int argc, char * argv[] ){ /* ... */ }
Der nichtnegative Parameter "argc" spezifiziert die Zahl informativer Komponenten des Feldes "argv". Falls der Wert des Parameters "argc" positiv ist, dann gilt noch: Die Komponenten "argv[ 0 ]" bis "argv[ argc - 1 ]" können von der Umgebung verwendet werden, um dem Programm Informationen in der Form nullterminierter Multibytestrings mitzuteilen (z.B. über Kommandozeilenparameter). Dabei ist "argv[ 0 ]" der Name des Programmes oder der leere Text """". Der Ausdruck "argv[ argc ]" hat den Wert "0".
Im Falle der Definition mit Parametern gibt es auch einige erlaubte Varianten: Die Schreibweise "*argv[]" ist in einer Parameterliste zur Schreibweise "**argv" äquivalent. Die Namen der Parameter sind beliebig, sie können also auch anders lauten als "argc" und "argv" oder sogar entfallen (für jeden nicht benutzten Parameter). Eine erlaubte Variante wäre es noch, das Schlüsselwort "int" durch einen Typbezeichner zu ersetzen, der mithilfe des Schlüsselwortes "typedef" als Datentyp "int" definiert wurde, was aber nicht empfohlen wird.
Alle anderen Formen der Definition der Funktion "main" sind also entweder falsch oder nicht portabel.