Version [22730]
Dies ist eine alte Version von ProzProg9Zeiger erstellt von RonnyGertler am 2013-03-28 20:46:40.
Prozedurale Programmierung - Kapitel 9 - Zeiger
Inhalte von Dr. E. Nadobnyh
C/C++ enthält ein umfangreiches Zeigerkonzept, welches einen direkten Zugriff auf den Speicherplatz ermöglicht.
Ein Zeiger ist eine Variable, die eine Adresse einer Variablen oder einer Funktion enthält.
Synonyme: Pointer, Pointervariable, Zeigervariable.
Man kann auf eine adressierte Variable oder Funktion indirekt über einen Zeiger zugreifen. Ein Zeiger adressiert eine Speicherstelle und gibt mit dem Typ an, wie diese Speicherstelle zu verwenden ist.
Bei Zeigern sind immer zwei unterschiedliche Typen verwickelt:
1) Zeigertyp (Pointertyp, Adresstyp) ist eigenen Datentyp des Zeigers und der Adresse.
2) Basistyp ist Typ der Variablen (des Objektes), auf die der Zeiger “zeigt“.
In C werden Zeiger auf eine Variable, auf eine Funktion, auf ein Feld und auf ein Zeiger definiert.
Zeiger auf eine Variable. Definition
Ein Zeiger wird wie jede andere Variable definiert: T* name;
Mit T* wird der Datentyp des Zeigers und mit T der Datentyp der adressierten Variable (Basistyp) bezeichnet.
Beispiel: int* p1; float* p2;
Die Zeigervariablen p1 und p2 haben die Datentypen „Zeiger auf int“ und „Zeiger auf float“.
Mit der Definition wird der Speicherplatz für den Zeiger reserviert. Ein Zeiger belegt bei einem 32-bit-Betriebs-system immer 4 Bytes.
Ein uninitialisierter Zeiger wird wie jede andere Variable mit dem zufälligen Wert oder mit 0 initialisiert. Die Verwendung des uninitialisierten Zeigers verursacht einen Fehler, der oft schwer zu finden ist.
Initialisierter Zeiger
Wie jede andere Variable erhält ein Zeiger einen sinnvollen Wert bei der Definition oder durch die Zuweisung.
Eine Variablenadresse kann z.B. mit dem Adressoperator erhalten werden.
Beispiel: int a=3; int* p=&a;
Nach der Zeigerinitialisierung sagt man "p zeigt auf a".
⇒ Demo 1.
Nullzeiger
Ein Zeiger, der die Adresse NULL enthält, wird Nullzeiger (Nullpointer) genannt. Er zeigt auf kein gültiges Datenobjekt.
Es ist sinnvoll, Zeigervariablen mit NULL zu initialisieren, z.B.: double* p=NULL;
NULL ist als eine Adresse mit dem Wert 0 in der Standardbibliothek definiert.
Man sollt auch immer NULL statt den Zahlenwert 0 verwenden, denn es ist nicht sicher, ob die Länge einer Integer-Variablen auch der Länge einer Adresse entspricht.
Inhaltsoperator
Der Inhaltsoperator (Dereferenzierungsoperator, Verweisoperator, Indirektionsoperator) wird mittels * bezeichnet.
Er bildet ein Sprachkonstrukt *x, der als ein Verweis benannt wird. Statt x kann eine Adresse, ein Zeigername, ein Feldname usw. verwendet werden.
Die Ausführung eines Inhaltsoperators ist vom Kontext abhängig:
1) Wenn ein Verweis in einem Ausdruck steht, wird er auswertet, d.h. durch ein Wert ersetzt.
2) Wenn ein Verweis links vom Gleichheitszeichen steht, wird er als Zuweisung interpretiert.
Achtung: Begriffsverwirrung bei Verweis vs. Referenz, Zeiger, Punkt-Operator!
Impliziter Inhaltsoperator
Zur Laufzeit wird für jede Variable eine Adresse zugeordnet. Dabei wird der Variablenname durch den Verweis ersetzt.
Wenn der Variable a die Adresse 0012FF88 zugeordnet wird, dann kann z.B. die Zuweisung a=a+4; zur Laufzeit folgendermaßen aussehen:
Der Verweis *(0012FF88) enthält den impliziten Inhaltsoperator * .
Inhaltsoperator und Zeiger
Die Verwendung des Inhaltsoperators *p bedeutet einen lesenden oder schreibenden Zugriff auf die Variable, deren Adresse im Zeiger p enthalten ist.
Beispiel:
int a=3, tmp;
int* p = &a; //statt a kann *p benutzt werden
tmp = *p; //lesender Zugriff auf die Variable a
*p=5; //schreibender Zugriff auf die Variable
int* p = &a; //statt a kann *p benutzt werden
tmp = *p; //lesender Zugriff auf die Variable a
*p=5; //schreibender Zugriff auf die Variable
Achtung: Das Zeichen * in der Zeiger-Definition int* ist ein Teil der Typbezeichnung aber kein Inhaltsoperator.
⇒ Demo 2
Inhaltsoperator und Priorität
Bei der Verwendung des Operators * muß man die Priorität und Assoziativität von Operatoren genau beachten. Dies erscheint zunächst etwas schwierig, da dieser Operator ungewohnt ist.
Beispiele:
int x=3;
int* px; // px ist ein Zeiger auf int
px = &x; // in px die Adresse von x speichern
*px; // Wert der Variablen x
*px + 1; // Wert von x plus 1
*(px+1); // Adresse in px erhöhen.
// Inhalt der Nachbar-Variable
*px += 1; // Inhalt von x erhöhen
(*px)++; // Inhalt von x inkrementieren
*px++; // wie *(px++); wegen Assoziativität
*++px; // wie *(++px); wegen Assoziativität
int* px; // px ist ein Zeiger auf int
px = &x; // in px die Adresse von x speichern
*px; // Wert der Variablen x
*px + 1; // Wert von x plus 1
*(px+1); // Adresse in px erhöhen.
// Inhalt der Nachbar-Variable
*px += 1; // Inhalt von x erhöhen
(*px)++; // Inhalt von x inkrementieren
*px++; // wie *(px++); wegen Assoziativität
*++px; // wie *(++px); wegen Assoziativität
9.2. Zeiger auf eine Variable. Details
Adresstyp T*
Jede Adresse hat einen bestimmten Typ, der als Adresstyp (auch Zeigertyp) bezeichnet wird.
Ein Adressoperator liefert eine Adresse vom Typ T*, wenn sein Argument vom Typ T ist.
Im folgenden Beispiel hat die Adresse den Typ int* :
int a=3; printf("%p", &a); //0012FF88
Einem Zeiger kann eine Adresse zugewiesen werden, die den gleichen Zeigertyp hat.
T a; T* p = &(a);
Ein Inhaltsoperator liefert einen Wert vom Typ T, wenn sein Argument vom Typ T* ist.
T* p; T a = *(p);
Legende: T – Datentyp, T* - Adresstyp.
Konvertierung von Adresstypen
Synonym: Typumwandlung von Zeigertypen.
Den Typ einer Adresse kann konvertiert werden. Der binäre Code der Adresse bleibt dabei unverändert. Die Konvertierung von Zeigertypen verwendet man z.B. in der objektorientierten Programmierung.
Eine implizite Zeigertypen-Konvertierung von eingebauten Datentypen ist nicht erlaubt, z.B.:
short b=4; short* pb=&b;
int* pa=pb; //Fehler: Konvertierung nicht möglich
Eine explizite Zeigertypen-Konvertierung ist nahezu immer ein logischer Fehler, z.B.:
pa=(int*)pb; //keine Fehlermeldung
⇒ Demo 3.
Zeiger und Konstanten
Wenn man einen Zeiger p benutzt, sind zwei Variablen beteiligt: der Zeiger und die Variable, auf die er zeigt (dereferenzierte Variable, Objekt).
Es gibt drei Möglichkeiten const zu verwenden:
1. Zeiger auf konstante Variable, z.B.:
const int* p;
int const* p;
int* const p ;
const int* const p;
int const* const p;
Zeiger als Parameter
Ein Zeiger als Parameter stellt einen IN-OUT-Parameter dar. Die adressierte Variable kann in der Funktion gelesen und beschrieben werden. Eine Übergabe per Zeiger ist ähnlich wie call-by-reference.
Syntax für Prototyp und Aufruf:
void f(T* p); T a; f(&a ); T- Datentyp (Basistyp).
Diese Übergabe ist aber tatsächlich call-by-value. Der Parameter p wird beim Aufruf mit dem Wert des Arguments, d.h. mit der Adresse der Variablen a initialisiert.
Beispiele
int main()
{ int x=3, y;
scanf(“%i“, &y);
swap(&x, &y);
return 0;
}
void swap(int* x, int* y)
{ int h = *x;
*x = *y;
*y = h;
}
{ int x=3, y;
scanf(“%i“, &y);
swap(&x, &y);
return 0;
}
void swap(int* x, int* y)
{ int h = *x;
*x = *y;
*y = h;
}
Rückgabe per Zeiger
Synonym: Zeiger als Rückgabewert
Eine Rückgabe per Zeiger ist ähnlich wie Rückgabe per Referenz. Der Aufrufer erhält die Adresse der Variablen (des Objektes) und kann direkt mit dem Original arbeiten.
Syntax für Prototyp und Aufruf:
T* f( ); T* a = f(); T- Datentyp.
Beispiel:
int main()
{ int* a=f();
return 0;
}
int b=5;
int* f( )
{ return &b;
}
{ int* a=f();
return 0;
}
int b=5;
int* f( )
{ return &b;
}
Eine Rückgabe per Zeiger ist tatsächlich die Rückgabe per Wert. Eine Adresse von Typ T* wird zurückgeliefert und kann einem Zeiger zugewiesen werden.
Bei der Manipulationen mit „großen“ Variablen (Objekten) kann man die Kopierarbeit reduzieren, wenn man statt Objekten Zeiger verwendet.
⇒ Demo 4.
Gefahren bei der Rückgabe per Zeiger:
Die Adresse von lokalen Variablen (Objekte) dürfen nicht zurückgegeben werden, weil sie nach dem Rückkehr verschwunden sind.
Negatives Beispiel :
Die lokale Variable existiert nur während der Funktionsausführung. Die Ausgabe ist unbestimmt, sie könnte zufällig auch 1234 lauten.
int main(void)
{ int* p = f();
printf("%d", *p);
return 0;
}
int* f(void)
{ int x = 1234;
return (&x);
}
{ int* p = f();
printf("%d", *p);
return 0;
}
int* f(void)
{ int x = 1234;
return (&x);
}
9.3. Adressarithmetik
Adressarithmetik
Als Adressarithmetik (Zeigerarithmetik, pointer arithmetic) bezeichnet man spezifische Adressoperationen mit beschränkte Möglichkeiten. Schwerpunkt ist die Behandlungvon Feldern.
Es sind natürlich nur Operationen erlaubt, die zu sinnvollen Ergebnissen führen:
1. Inkrement und Dekrement: ++, --.
2. Addition und Subtraktion mit ganzzahligem Wert.
3. Zeigersubtraktion.
4. Zeigervergleich: >, >=, <, <=, != und ==.
5. Alle anderen Operationen sind verboten.
Zeigersubtraktion und Vergleich ist nur dann sinnvoll, wenn beide Zeiger auf Elemente des gleichen Feldes zeigen.
Anmerkung: Arithmetische Operationen sind nicht erlaubt bei Zeigern auf Funktionen.
Name eines Feldelementes
In einem eindimensionalen Feld T a[N]; ist der Name eines Feldelementes aus einem Feldnamen a und einem Index i zusammengestellt: a[i]
a[i] ⇒ *(a + i).
Hier sind beteiligt:
* - ein Inhaltsoperator und
+ - eine Summe der Adressarithmetik.
Beispiel: int a[2], b[2]; int i=1;
a[i]=5; ⇒ *(a + i) = 5;
a[0] = b[0]; ⇒ *(a+0)=*(b+0); ⇒ *a=*b;
Die Addition der Adressarithmetik ist eine spezifische und wichtige Operation.
Die Adresse des einzelnen Elementes a[i] wird vom Compiler nach der folgenden Formel berechnet:
(int)a + i *x
Legende:
+ und * -normale Summe und Multiplikation, x -die Anzahl von Bytes, die Datentyp T des Feldelementes besitzt, x= sizeof(T).
⇒ Demo 5.
In einem zweidimensionalen Feld T a[N][M]; wird der Elementnamen a[i][j] vom Compiler zu äquivalenter Form umformuliert :
a[i][j] *(*(a + i) + j)
Legende:
* - ein Inhaltsoperator und
+ - eine Summe der Adressarithmetik.
Die Adresse des einzelnen Elementes a[i][j] wird vom Compiler nach der folgenden Formel berechnet:
(int)a + i*M*x + j*x
Legende:
+ und * -normale Summe und Multiplikation,
x -die Anzahl von Bytes, die den Datentyp T besitzt: x= sizeof(T).
9.4. Zeiger und eindimensionale Felder
Zeiger auf Feldelement
Zwischen Zeiger und Felder besteht in C/C++ eine enge Verwandtschaft.
1) Ein Feld T a[N] hat Elemente vom Datentyp T.
2) Ein Feldname ist eine Adresse auf erstes Element des Feldes und hat den Datentyp T*.
3) Ein Zeiger T* p kann als Zeiger auf ein Feldelement des Feldes T a[N] benutzt werden.
4) Folgende Paare sind äquivalent:
Adresse auf das ersten Element | a | &a[0] |
Zeiger-Zuweisung | p=a | p=&a[0] |
Zugriff auf Element über den Feldnamen | a[i] | *(a+i) |
Zugriff auf Element über den Zeiger | p[i] | *(p+i) |
Zeiger auf Feldelemente können bei der Übergabe eines Feldes als Parameter verwendet werden.
Dafür gibt es drei verschiedenen Schreibweise, die äquivalent sind:
void f( T a[n]); void f( T a[ ]); void f( T* a);
int main()
{ int b[4]; f( b, 4);
return 0;
}
void f( int* a, int n);
void f( int a[4], int n);
void f( int a[ ], int n);
{ int b[4]; f( b, 4);
return 0;
}
void f( int* a, int n);
void f( int a[4], int n);
void f( int a[ ], int n);
Vorsicht: Zwei Definitionen haben gleiche Syntax und können nur durch Kontext unterschieden werden:
- int b[4] steht nicht in der Parameterliste. Hier werden vier Variablen angelegt.
- int a[4] steht in der Parameterliste. Hier wird ein Zeiger definiert.
Zeiger als Laufvariable
Ein Zeiger kann elegant als eine Laufvariable einer Schleife bei der Feld-Bearbeitung verwendet werden.
Nach der folgende Addition enthält der Zeiger p die Adresse des Feldelementes a[i]:
T a[N]; T* p=a; p+=i ;
Beispiel:
const int n=4; int a[n];
int sum=0;
for(int* p=a; p<a+n; p++)
{ //Zeiger p als Laufvariable
sum+=*p;
}
int sum=0;
for(int* p=a; p<a+n; p++)
{ //Zeiger p als Laufvariable
sum+=*p;
}
Zeiger und Zeichenkette
Zeichenketten (nullterminierte C-Strings) sind Felder vom Typ char[ ], wobei das letzte Zeichen immer das Nullzeichen ‘\0‘ sein muss.
Zeichenketten können unterschiedlich definiert werden:
1) Ein benanntes char-Feld a, das mittels der Zeichen initialisiert wird, z.B.:
char a[ ] = {‘a‘, ‘b‘, ‘c‘, ‘\0‘}; oder char a[ ] = “abc“;
2)Ein namenloses char-Feld (Stringliteral) und ein Zeiger p der mit der Feldadresse initialisiert wird, z.B.:
const char* p = “qwert“;
Besonderheit: Bei einigen Systemen sind Stringliterale schreibgeschützt und daher muss der Zeiger als const definiert werden.
⇒ Demo 6 und 7
9.5. Besondere Zeiger
Zeiger auf Zeiger
Da Zeigervariablen selbst Datenobjekte sind, kann auch auf sie über Zeiger zugegriffen werden.
Syntax:
T p; //T - Datentyp
Beispiel:
int main()
{ int x= 10;
int* px= &x;
int** ppx= &px;
int y= **ppx; //Zugriff auf x
return 0;
}
{ int x= 10;
int* px= &x;
int** ppx= &px;
int y= **ppx; //Zugriff auf x
return 0;
}
Adresstyp T
Ein Adressoperator liefert eine Adresse vom Typ T, wenn sein Argument vom Typ T* ist.
T a; T* p= &a; T pp= &p;
Ein Inhaltsoperator liefert eine Adresse vom Typ T*, wenn sein Argument vom Typ T ist.
T* p= *pp; T a= *p;
Legende: T – Datentyp, T und T* - Adresstypen.
Beispiel:
int x= 10;
int* px1= &x;
int** ppx= &px1;
int* px2= *ppx;
int y= *px2; //Zugriff auf x
int* px1= &x;
int** ppx= &px1;
int* px2= *ppx;
int y= *px2; //Zugriff auf x
Zeiger auf ein Feld
Ein zweidimensionales Feld T aa[N][M] ist ein eindimensionales Feld mit N Zeilen. Jede Zeile ist selbst ein eindimensionales Feld. T- Typ eines Feldelementes. Der Datentyp einer Zeile ist T [ ][M]. Der Feldname aa ist eine Adresse vom Typ der Zeile.
C/C++ hat einen Zeiger auf ein Feld (Feldzeiger) vom Typ T[][M]. Dieser Zeiger kann hauptsächlich bei der Parameterübergabe verwendet werden, z.B.:
void f(int pp[ ][M])
{ cout<< sizeof(*pp)<<endl;
cout<< sizeof(int)*M;
}
int main()
{ int aa[N][M]; f(aa);
return 0;
}
{ cout<< sizeof(*pp)<<endl;
cout<< sizeof(int)*M;
}
int main()
{ int aa[N][M]; f(aa);
return 0;
}
Äquivalente Schreibweise:
void f(T pp[ ][M]); void f(T pp[N][M]); void f(T(*pp)[M]);
Adresstyp T [][M]
Ein Inhaltsoperator liefert eine Adresse vom Typ T[], wenn sein Argument vom Typ T [][M] ist.
void f( T pp[][M], T p[] ) { p = *pp; }
Legende:
T –Datentyp, T[][M] und T[] -Adresstypen.
Umgekehrt funktioniert es nicht. Ein Adressoperator liefert eine Adresse vom Typ T, wenn sein Argument vom Typ
T[] ist: T zz = &p;
⇒ Demo 8.
Zeiger auf Funktionen
- Jede kompilierte Funktion belegt einen Speicherbereich und besitzt ein Eingangspunkt.
- Der Name einer Funktion ist eine symbolische Bezeichnung der Adresse des Eingangspunktes (Funktionsadresse).
- In C/C++ gibt es einen Zeiger auf Funktionen (Funktionszeiger), der die Funktionsadresse enthalten kann.
- Ein Funktionszeiger kann verwendet werden, um die Übergabe einer Funktion als Parameter zu programmieren.
- Der Funktionszeiger wird dabei als ein formaler Parameter verwendet.
- Der Name einer Funktion wird dabei als ein aktueller Parameter verwendet.
Syntax:
T (*fp) (Parameterliste);
Legende:
fp – Funktionszeiger, T –Rückgabetyp.
Beispiel:
int main()
{
int (* fp) (int);
fp = f2;
cout<< f1(fp);
return 0;
}
int f2( int n ){ return n*2; }
int f3( int n ){ return n*3; }
int f1( int (*z) (int) )
{ return z(100);
}
{
int (* fp) (int);
fp = f2;
cout<< f1(fp);
return 0;
}
int f2( int n ){ return n*2; }
int f3( int n ){ return n*3; }
int f1( int (*z) (int) )
{ return z(100);
}
Komplexe Zeiger
Eigenschaften:
1) Es entsteht kein neuer Datentyp.
2) Kein Speicherplatz wird reserviert.
3) Für beliebige Datentypen T anwendbar.
4) Als Konvention in C sollten neue Typennamen gesamt oder mit dem erstem Buchstaben großgeschrieben werden.
5) Neue Typnamen dienen zur Verbesserung der Lesbarkeit von Programmen.
6) typedef –Vereinbarungen sollen in Headerdateien isoliert werden. Dann können Portabilitäts- und Wartungsprobleme vereinfacht werden.
9.6. Zeigerfelder
Grundidee
Zeigerfelder (Zeigervektoren, Pointer-Arrays) sind Felder deren Elemente Zeiger sind.
Syntax: T* a[M]; T- Datentyp,
Für viele Fälle ist die Arbeit mit Zeigern effektiver, als umfangreiche Datenbestände zu bearbeiten, z. B. werden beim Sortieren nur die Zeiger und nicht die Variablen (Objekte, Datenbestände) von „großen“ Datentypen selbst sortiert.
Ausgefranste Felder
Ausgefranste Felder (ragged arrays) sind mehrdimensionale Felder, deren Elemente wiederum aus Feldern (Teilfeldern) bestehen. Die einzelnen Teilfelder können unterschiedlich lang sein.
Ein ausgefranstes Feld wird mit Hilfe des Zeigerfeldes aufgefasst, wenn die Elemente des Zeigerfeldes wiederum Adressen von Teilfeldern (Zeilen) sind.
Beispiel:
char a [4], b [2], c [4]; Teilfelder
char* Z[3] = {a, b, c}; Zeigerfeld
Eigenschaften von ausgefransten Felder
1) Mehrdimensionale Felder und ausgefranste Felder werden gleichartig verwendet:
Z[i] -enthält die Adresse auf (i+1)-tes Teilfeld,
Z[i]+j -ist eine Adresse auf das (j+1)-te Element des (i+1)-ten Teilfeldes,
Z [i][j] -enthält das (j+1)-te Element des (i+1)-ten Teilfeldes.
2) Teilfelder können genau nach Bedarf reserviert werden, z.B. dreieckiges Feld.
3) Der Nachteil ist der zusätzliche Speicherbedarf für das Zeigerfeld.
⇒ Demo 9.
Kommandozeilenparameter
C/C++ bietet die Möglichkeit, beim Programmstart Kommandozeilenparameter an das Programm main zu übergeben:
void main(int argc, char *argv[]){...}
Die main-Funktion übernimmt Kommandozeilen durch folgende Parameter:
- int argc -die Argumentenanzahl (argument count),
- char *argv[] -ein Zeigerfeld auf C-Strings (arg. vector),
- argv[0] ist ein Pointer auf den Programmnamen selbst,
- argv[1] ist ein Pointer auf den ersten echten Kommandozeilen-Parameter,
- argv[argc-1] ist ein Pointer auf den letzten Parameter,
- argc > 1 wegen dem Programmnamen.
⇒Demo 10.
9.7. Dynamische Speicherreservierung
Grundidee
Es ist nicht immer möglich, den Speicherplatz exakt vorher zu planen, z.B. wenn die Größe eines Feldes nur zur Laufzeit bekannt ist. Es ist unökonomisch, den maximalen Speicherplatz zu reservieren.
C++ bietet zwei Operatoren:
- new -reserviert den Speicherplatz genau in der richtigen Menge und zum richtigen Zeitpunkt,
- delete -gibt den Speicherplatz frei, wenn er nicht mehr benötigt wird.
Dynamische Datenobjekte (Variablen, Felder) werden auch nicht über einen Namen, sondern nur über ihre Adresse angesprochen.
Diese Adresse entsteht bei der Speicherbelegung und wird normalerweise einem Zeiger zugewiesen.
Speicherreservierung
Für die dynamische Speicherverwaltung gibt es einen großen Adressbereich, der als Heap bezeichnet wird.
Der Zeiger p enthält die Adresse auf die namenlose int–Variable.
Die Lebensdauer dynamisch reservierter Datenobjekte ist nicht an die Ausführungszeit eines Blocks gebunden.
CategoryProzProg