ich war hier: ProzProg9Zeiger
 (image: http://wdb.fh-sm.de/uploads/QualipaktLehre/BMBF_Logo_klein.jpg)

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;

 (image: https://ife.erdaxo.de/uploads/ProzProg9Zeiger/pp55.gif)

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:

 (image: https://ife.erdaxo.de/uploads/ProzProg9Zeiger/pp56.gif)

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  


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  


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).

 (image: https://ife.erdaxo.de/uploads/ProzProg9Zeiger/pp57.gif)

Es gibt drei Möglichkeiten const zu verwenden:

1. Zeiger auf konstante Variable, z.B.:

const int* p;
int const* p;

Eine Veränderung des Zeigers ist möglich.

2. Konstanter Zeiger auf Variable, z.B.:
int* const p ;

Eine Veränderung der Variablen ist möglich.

3. Konstanter Zeiger auf konstante Variable, z.B.:

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;
}



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;  
}


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);  
}



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]

Für eingebaute Datentypen T transformiert der Compiler diesen Namen zu äquivalenter Form:

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);


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;
   }



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.

 (image: https://ife.erdaxo.de/uploads/ProzProg9Zeiger/pp58.gif)

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;
}



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



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;
}


Ä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);
}  


Komplexe Zeiger


 (image: https://ife.erdaxo.de/uploads/ProzProg9Zeiger/pp59.gif)


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.

 (image: https://ife.erdaxo.de/uploads/ProzProg9Zeiger/pp60.gif)


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

 (image: https://ife.erdaxo.de/uploads/ProzProg9Zeiger/pp61.gif)


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 einzelnen Parameter sind nach dem Programmnamen jeweils durch Leerzeichen getrennt anzugeben.

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.

 (image: https://ife.erdaxo.de/uploads/ProzProg9Zeiger/pp62.gif)

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.

Mit dem Operator new[ ] kann ein Feld dynamisch reserviert werden:

Syntax: T* p = new T [N]; T-Datentyp.

Beispiel: double* p= new double[10];

Der Zugriff auf die Feldelemente geschieht genau wie beim nicht dynamischen Feld:

   double a[10];    
   a[4]= 4.4;  oder   *(a+3)= 3.3;  //a- ist die Adresse
   p[4]= 4.4;  oder   *(p+3)= 3.3;  //p- enthält die Adresse


⇒ Demo 11


Speicherfreigabe


Synonym: Löschen oder Zerstörung des Datenobjektes.

Der delete- Operator gibt den reservierten Platz frei, damit er bei neuen Reservierungen wieder belegt werden kann.

Syntax:
delete p; oder delete[] p;
p - Zeiger auf reserviertes Datenobjekt.

Wenn dynamische Datenobjekte nicht mehr gebraucht werden, müssen sie freigegeben werden. Ein nicht mehr zugängliches Datenobjekt wird als Speicherleck (memory leak) bezeichnet.

Beispiel: int* p= new int; p= new int;

Wenn ein Programm mit dem Speicherleck immer mehr Speicher reserviert, wird bei genügend langer Laufzeit an erschöpften Systemreserven scheitern.

⇒ Demo 12


CategoryProzeduraleProgrammierung
Diese Seite wurde noch nicht kommentiert.
Valid XHTML :: Valid CSS: :: Powered by WikkaWiki