Version [22729]
Dies ist eine alte Version von ProzProg9Zeiger erstellt von RonnyGertler am 2013-03-28 20:37:59.
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
CategoryProzProg
Diese Seite wurde noch nicht kommentiert.