Übungspaket 20 Zeiger und Zeigervariablen Übungsziele: 1. Definition von Zeigervariablen 2. Verwendung von Zeigern 3. Arrays und Adressberechnungen Skript: Kapitel: 45 und 46 Semester: Wintersemester 2017/18 Betreuer: Kevin, Theo, Thomas und Ralf Synopsis: Zeiger! Bei diesem Wort bekommen viele Programmieranfänger Pusteln, Hautausschlag, hektische Flecken, Schweißausbrüche und dergleichen. Aber das muss nicht sein! Ein bisschen gezielt üben und schon ist es kinderleicht. . . Na ja, vielleicht nicht ganz kinderleicht, aber ohne nennenswerte Hürde. In diesem Übungspaket schauen wir uns erst einmal ganz einfach an, was Zeiger eigentlich sind und was wir mit derartigen Variablen machen können. Dabei steht vor allem die Frage nach dem Typ des Zeigers im Vordergrund. Zeiger und Funktionen heben wir uns für Übungspaket 21 auf. Am Ende der Übung mag sich der eine oder andere fragen, was man denn nun eigentlich von Zeigern hat, denn Zeiger sind erst einmal nur kompliziert. Die Antwort ist sehr einfach: Außer ein paar Vereinfachungen bei Array-Zugriffen eigentlich nicht viel. Die wirklich interessanten Dinge kommen erst in Übungspaket 21 und bei den dynamischen Datenstrukturen (Übungspakete 31 bis 33). Hierfür ist dieses Übungspaket eine sehr wichtige Vorbereitung. Teil I: Stoffwiederholung Aufgabe 1: Allgemeines zu Zeigern Erkläre kurz, was ein Zeiger ist und was in einer Zeigervariablen abgespeichert wird. Ein Zeiger ist nichts anderes als eine Adresse des Arbeitsspeichers. Implizit geht man meist davon aus, dass sich dort auch tatsächlich etwas befindet. Ein Zeiger zeigt“ also ” auf dieses Objekt. Entsprechend hat eine Zeigervariable eine Adresse als Inhalt. Aufgabe 2: Zeiger-Syntax Woran erkennt man eine Zeigerdefinition? Am * vor dem Namen Wie definiert man einen Zeiger? Typ * Name Wie weist man einem Zeiger etwas zu? Name=Wert; //’Wert’ ist eine Adresse Wie greift man auf den Zeigerinhalt zu? Name //liefert die gespeicherte Adr. Wie schreibt man etwas an die Stelle, auf die der Zeiger zeigt? Wie liest man von einer Stelle, auf die der Zeiger zeigt? Wie vergleicht man zwei Zeigervariablen miteinander? (Beispiel) Gibt es Werte, die eine Zeigervariable nicht annehmen darf? Gibt es Adressen, auf die man nicht zugreifen darf? Darf die Zeigervariable dennoch diese Werte annehmen? * Name = Wert * Name Name 1 != Name 2 Nein, alle Werte sind erlaubt Ja, beispielsweise 0 int *p=0; /*ok*/ *p=... /*Absturz*/ Aufgabe 3: Typen von Zeigervariablen Nehmen wir einmal an, eine Adresse des Arbeitsspeichers benötigt vier Bytes. Wie viele Bytes werden dann bei jedem Zugriff auf eine Zeigervariable im Arbeitsspeicher gelesen? 4 (vier) (four) (quattro) Gibt es Ausnahmen, bei der mehr oder weniger Bytes kopiert werden? Nein! (No) (non) Einführung in die Praktische Informatik, Wintersemester 2017/18 20-1 Wenn es nun wirklich so ist, dass es von dieser Regel keine Ausnahme gibt, warum in Gottes Namen legt dann das Lehrpersonal so viel Wert darauf, dass ein Zeiger auch einen Typ haben muss? Erkläre dies in eigenen Worten: Solange wir nur auf die Zeigervariablen zugreifen würden, wäre es in C tatsächlich egal, was für einen Typ diese Zeiger haben. Bei einer vier-Byte Architektur würden dann immer nur 32 Bit irgendwo hingeschrieben oder von irgendwo gelesen werden. Dies ändert sich, sobald wir auf diejenige Speicheradresse zugreifen, auf die der Zeiger zeigt. Zeigt beispielsweise ein Zeiger char *p auf ein Zeichen, so wird beim Zugriff *p = ’a’ ein Byte kopiert. Bei einem int *p-Zeiger wären dies hingegen *p = 4711 vier Bytes. Aber in beiden Fällen würden bei der Anweisung p = 0 vier Bytes an die Speicherstelle der Zeigervariablen p kopiert werden. Um hier also korrekt arbeiten zu können, muss der Compiler immer wissen, was sich am Ende des Zeigers befindet. Aufgabe 4: Zeiger und Arbeitsspeicher Vervollständige für das folgende Programm das skizzierte Speicherbildchen. 1 2 3 4 int i ; int * p ; p = & i; * p = 4711; Adresse 0xFFEE107C 0xFFEE1078 Variable int i: int *p: Wert 4711 0xFFEE107C Aufgabe 5: Zeiger, Arrays und Zeigeroperationen Erkläre nochmals kurz, was ein Array ist und was der Name des Arrays symbolisiert: Ein Array ist eine Zusammenfassung mehrerer Elemente des selben Typs, die alle nacheinander im Arbeitsspeicher abgelegt werden. Der Name des Arrays ist eine Konstante und symbolisiert die Anfangsadresse des Arrays. Zeiger, Arrays und int-Terme kann man eingeschränkt miteinander verknüpfen. Ergänze folgende Tabelle, in der n ein int-Ausdruck, a ein Array und p und q beliebige Zeiger sind. Ausdruck Alternative Effekt & a[ n ] p + n a[ n ] *(p + n) p - n p - q a + n & p[ n ] *(a + n) p[ n ] & p[ -n ] Bestimmt die Adresse des n-ten Elementes von a p plus n Elemente weiter oben“ ” Inhalt des n-ten Elementes von a Inhalt des n-ten Elementes von p p plus n Elemente weiter unten“ ” Bestimmt die Zahl der Basiselemente zwischen p und q 20-2 Wintersemester 2017/18, Einführung in die Praktische Informatik Teil II: Quiz Aufgabe 1: Variablen und Zeiger In dieser Quizaufgabe greifen wir nochmals das Miniprogramm der letzten Übungsaufgabe des ersten Teils auf. Demnach haben wir folgende Situation vorliegen: Adresse 0xFFEE107C 0xFFEE1078 1 int i = 4711; 2 int * p = & i ; Variable int i: int *p: Wert 4711 0xFFEE107C Ferner gehen wir wieder davon aus, dass sowohl eine int-Variable als auch ein Zeiger genau vier Bytes im Arbeitsspeicher belegen. Vervollständige folgende Tabelle. Gehe dabei immer davon aus, dass am Anfang jedes Ausdrucks alle Variablen wieder auf obige Initialwerte zurückgesetzt werden. Ausdruck Typ Ergebnis i i += 1 i-p += 1 --p p[ 0 ] & i & p i > 0 p > 0 p > 10 p > p + 1 int int int int int int int int int int int int 4711 4712 4711 0xFFEE1080 0xFFEE1078 4711 0xFFEE107C 0xFFEE1078 1 1 1 0 * * * ** Kommentar Dies ist einfach nur der Wert von i Zuweisungen sind auch Ausdrücke Post-Dekrement, Ausdruck: 4711, aber i = 4710 Ein Element ergibt hier vier Bytes Pre-Dekrement, ein Element, vier Bytes Zugriff auf das erste Array-Element Adresse der Variablen i Der Ort, an dem wir den Zeiger p finden Logisch wahr ergibt den Wert 1 wie oben wie oben Logisch falsch ergibt 0 Einführung in die Praktische Informatik, Wintersemester 2017/18 20-3 Aufgabe 2: Zeichenketten und Zeiger Diese Quizaufgabe ist ähnlich der vorherigen. Nur behandeln wir jetzt nicht normale“ Va” riablen sondern Zeichenketten. Zugegebenermaßen macht dies die Sache etwas komplexer. Wenn man sich aber langsam und Schritt für Schritt von innen nach außen vorarbeitet, ist die Sache doch recht überschaubar. Im Übrigen gehen wir wieder davon aus, dass alle Zeiger genau vier Bytes im Arbeitsspeicher belegen. Betrachten wir nun folgende Code-Zeilen: 1 char buf1 [] = " Fruehling " ; 2 char * buf2 = " Sommer " ; 3 char * buf3 [] = { " Herbst " , " Winter " }; Beschreibe kurz mit eigenen Worten, welchen Datentyp die drei Variablen haben: buf1 Ein Array vom Typ char buf2 Ein Zeiger auf ein char buf3 Ein Array mit zwei Zeigern auf char Vervollständige zunächst folgendes Speicherbildchen, denn es hilft drastisch beim Finden der richtigen Lösungen :-) Segment: Adresse 0xFE34 0xFE30 0xFE2C 0xFE28 0xFE24 0xFE20 20-4 Stack Variable char *buf3[ 1 ]: char *buf3[ 0 ]: char *buf2 : char buf1[] : Wert 0xF840 0xF838 0xF830 ’g’ ’\0’ ’h’ ’l’ ’i’ ’n’ ’F’ ’r’ ’u’ ’e’ Segment: Adresse 0xF844 0xF840 0xF83C 0xF838 0xF834 0xF830 Konstanten Wert ’e’ ’r’ ’\0’ ’W’ ’i’ ’n’ ’t’ ’s’ ’t’ ’\0’ ’H’ ’e’ ’r’ ’b’ ’e’ ’r’ ’\0’ ’S’ ’o’ ’m’ ’m’ Wintersemester 2017/18, Einführung in die Praktische Informatik Vervollständige nun folgende Tabelle: Ausdruck Typ Ergebnis buf1 char [] buf2 char * buf3 char ** sizeof( buf1 ) int sizeof( buf2 ) int sizeof( buf3 ) int buf1[ 3 ] char buf1 + 3 char * 0xFE20 0xF830 0xFE30 10 4 8 ’e’ 0xFE23 *(buf1 + 3) buf2[ 3 ] buf2 + 3 ’e’ ’m’ 0xF833 char char char * *(buf2 + 3) char buf3[ 0 ][ 2 ] char buf3[ 1 ][ 0 ] char & buf1 char ** & buf2 char ** & buf3 *(char * [2]) & buf1[ 4 ] char * & buf2[ 4 ] char * & buf3[0][2] char * ’m’ ’r’ ’W’ 0xFE20 0xFE2C 0xFE30 Kommentar Das komplette Array; Wert: Anfangsadresse Eine ganz gewöhnliche Zeigervariable Ein Array mit zwei char * Zeigern Genau neun Nutzzeichen plus ein Nullbyte Ein klassischer Zeiger Ein Array bestehend aus zwei Zeigern Genau eines der Zeichen, nämlich das vierte Anfangsadresse plus drei Bytes, also die Adresse von ’e’ Identisch mit buf1[ 3 ] Das vierte Zeichen der Zeichenkette Adresse in buf2 plus 3 Bytes, identisch mit & buf2[ 3 ], also die Adresse des zweiten ’m’ Identisch mit buf2[ 3 ] Das dritte Zeichen der ersten Zeichenkette Das erste Zeichen der zweiten Zeichenkette Der Ort an dem sich buf1 befindet Der Ort an dem sich buf2 befindet Der Ort an dem sich buf3 befindet 0xFE24 Adresse des 5. Zeichens (Stacksegment) 0xF834 Adresse des 5. Zeichens (Konstantensegment) 0xF83A Adresse des 3. Zeichens (Konstantensegment) Einführung in die Praktische Informatik, Wintersemester 2017/18 20-5 Teil III: Fehlersuche Aufgabe 1: Definition und Verwendung von Zeigern Das folgende Programm enthält einige einfache Variablen und einige Zeiger. Der gewünschte Typ der Zeiger geht immer unmittelbar aus dem Namen hervor. Ferner gehen die gewünschten Zeigeroperationen aus dem Kontext hervor. Ab Zeile 5 ist in jeder Zeile ein Fehler vorhanden. Finde und korrigiere diese, damit Dr. A. Pointer ruhig schlafen kann. 1 # include < stdio .h > 2 3 int main ( int argc , char ** argv ) 4 { 5 int i = 4711 , * i_ptr , * i_ptr_ptr *; 6 char c = ’x ’ , ** c_ptr ; 7 8 i_ptr = i ; 9 * i_ptr_ptr = & i_ptr ; 10 c_ptr = * c ; 11 12 printf ( " i =% d \ n " , i_ptr ) ; 13 printf ( " c = ’% c ’\ n " , c_ptr * ) ; 14 printf ( " i hat die Adresse % p \ n " , * i_ptr ) ; 15 printf ( " c hat die Adresse % p \ n " , & c_ptr ) ; 16 printf ( " i_ptr hat die Adresse % p \ n " , i_ptr ) ; 17 printf ( " i =% c \ n " , ** i_ptr_ptr ) ; 18 } Zeile Fehler Erläuterung Korrektur 5 *...* Bei der Definition müssen die Sternchen für die Zeiger vor dem Namen stehen. **i ptr ptr Da wir nur einen einfachen Zeiger haben wollen, darf vor dem Namen auch nur ein * stehen. *c ptr Da wir die Adresse der Variablen i benötigen, muss vor ihr der Adressoperator stehen. & i .............................................................................................................................................. 6 * zu viel .............................................................................................................................................. 8 & fehlt .............................................................................................................................................. 9 * falsch Der Stern vor dem Zeiger i ptr ptr ist in diesem Falle i ptr ptr unsinnig, er muss weg. .............................................................................................................................................. 10 20-6 * statt & Hier benötigen wir wieder die Adresse einer Variablen also ein & und nicht ein *. & c Wintersemester 2017/18, Einführung in die Praktische Informatik Zeile Fehler Erläuterung Korrektur 12 * fehlt Da wir hier auf den Inhalt derjenigen Speicherstelle zu- * i ptr greifen wollen, auf die i ptr zeigt, benötigen wir hier ein * zum Dereferenzieren. .............................................................................................................................................. 13 * an der falschen Stelle Der * zum Dereferenzieren muss vor dem Namen stehen nicht dahinter. * zu viel Wir wollen die Adresse von i ausgeben, die bereits in i ptr i ptr steht. Also dürfen wir nicht dereferenzieren, denn dann würden wir den Inhalt von i ausgeben. * c ptr .............................................................................................................................................. 14 .............................................................................................................................................. 15 & zu viel Hier gilt das gleiche wie ein Fehler weiter oben. Der Aus- c ptr druck & c ptr würde uns fälschlicherweise die Adresse der Variablen c ptr liefern. .............................................................................................................................................. 16 falsche Variable oder & fehlt Da wir die Adresse der Variablen i ptr ausgeben wollen, i ptr ptr dürfen wir nicht den Inhalt dieses Zeigers ausgeben, son- oder dern benötigen dessen Adresse. Die steht in i ptr ptr & i ptr oder kann mittels & i ptr gebildet werden. 17 falsches Format Hier ist eigentlich alles richtig. Nur muss das Format auf %d für Integer abgeändert werden. .............................................................................................................................................. %d Programm mit Korrekturen: 1 # include < stdio .h > 2 3 int main ( int argc , char ** argv ) 4 { 5 int i = 4711 , * i_ptr , ** i_ptr_ptr ; 6 char c = ’x ’ , * c_ptr ; 7 8 i_ptr = & i ; 9 i_ptr_ptr = & i_ptr ; 10 c_ptr = & c ; 11 12 printf ( " i =% d \ n " , * i_ptr ) ; 13 printf ( " c = ’% c ’\ n " , * c_ptr ) ; 14 printf ( " i hat die Adresse % p \ n " , i_ptr ) ; 15 printf ( " c hat die Adresse % p \ n " , c_ptr ) ; 16 printf ( " i_ptr hat die Adresse % p \ n " , i_ptr_ptr ) ; 17 printf ( " i =% d \ n " , ** i_ptr_ptr ) ; 18 } Einführung in die Praktische Informatik, Wintersemester 2017/18 20-7 Teil IV: Anwendungen Aufgabe 1: Definition von Variablen und Zeigern In dieser Aufgabe geht es lediglich darum, normale“ Variablen und Zeigervariablen zu ” definieren. Vervollständige dafür die folgende Tabelle. Wir gehen wieder davon aus, dass sowohl int-Variablen also auch Zeiger vier Bytes im Arbeitsspeicher belegen. Da die letzten beiden Definitionen recht schwierig sind, sollten sie mit den Betreuern besprochen werden. Was C-Definition sizeof(Variable) Eine int-Variable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eine char-Variable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein Zeiger auf eine int-Variable . . . . . . . . . . . . . . . . . . . . . . . . . . Ein Zeiger auf eine char-Variable . . . . . . . . . . . . . . . . . . . . . . . . Ein Array mit drei int-Elementen . . . . . . . . . . . . . . . . . . . . . . Ein Array mit drei int-Zeigern . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein Zeiger auf ein Array mit drei int-Elementen int i char c int *p char *p int a[ 3 ] int *a[ 3 ] int (*p)[ 3 ] 4 1 4 4 12 12 4 (ein Zeiger) (drei ints) (drei Zeiger) (nur ein Zeiger) Illustriere die letzten beiden Definitionen mittels eines kleinen Speicherbildchens und trage in die jeweiligen Variablen Phantasiewerte ein. Definition: int *a[3] Adresse 0xFFEE1028 0xFFEE1024 0xFFEE1020 Variable Wert a[ 2 ] : 0xFFFF3034 a[ 1 ] : 0xFFFF44CC a a[ 0 ] : 0xFFFF2D14 Anmerkung: Das Array a hat genau drei Elemente. Jedes dieser Elemente ist ein Zeiger, der seinerseits auf ein int zeigt. Definition: int (*p)[3] Adresse 0xFFEE1028 0xFFEE1024 0xFFEE1020 Variable [ 2 ] : [ 1 ] : [ 0 ] : 0xFFEE0300 p 20-8 Wert -1 815 4711 : 0xFFEE1020 Anmerkung: Die Variable p ist ein Zeiger auf ein Array. Dort befinden sich dann drei int-Werte. Bei diesem Array kann es sich um ein gewöhnliches“ oder ” ein dynamisch angelegtes Array (siehe auch Übungspaket 29) handeln. Wintersemester 2017/18, Einführung in die Praktische Informatik Aufgabe 2: Zeiger auf Zeiger Zeiger auf Zeiger, langsam wird’s ernst. Nehmen wir diesmal den Basistyp double. Aus irgendeinem unerfindlichen Grund brauchen wir hiervon eine einfache Variable. Nennen wir sie d wie double. Ferner brauchen wir noch einen Zeiger p auf diese Variable, einen Zeiger pp, der auf diesen Zeiger zeigt und schließlich einen Zeiger ppp der auf letzteren zeigt. Definiere die entsprechenden Variablen und vervollstängige unten stehendes Speicherbildchen. Bei Schwierigkeiten stehen die Betreuer für Diskussionen zur Verfügung. 1 2 3 4 5 6 7 8 9 double double double double d p pp ppp = = = = d; *p; ** pp ; *** ppp ; 3.141; & d; & p; & pp ; Adresse 0xFFEE100C 0xFFEE1008 // // // // // // // // the single variable a pointer to a double a pointer to a double pointer a pointer to a double ptr . ptr . giving it a well - known value p now points to d ; & d -> double * pp now points to p ; & p -> double ** ppp now points to pp ; & pp -> double *** Variable Wert ppp : 0xFFEE1008 pp : 0xFFEE1004 Adresse 0xFFEE1004 0xFFEE1000 Variable Wert p : 0xFFEE1000 d : 3.141 Vervollständige die folgenden Ausdrücke unter der Annahme, dass ein double acht und ein Zeiger vier Bytes im Arbeitsspeicher belegt. Ausdruck d p *p pp *pp **pp ppp *ppp **ppp ***ppp Typ double double double double double double double double double double Ergebnis sizeof(Ausdruck) ..... * ... ..... ** . * ... ..... *** ** . * ... ..... 8 ............................ 4 ............................ 8 ............................ 4 ............................ 4 ............................ 8 ............................ 4 ............................ 4 ............................ 4 ............................ 8 ............................ 3.141 . 0xFFEE1000 . . . . . . . . . . . 3.141 . 0xFFEE1004 . 0xFFEE1000 . . . . . . . . . . . 3.141 . 0xFFEE1008 . 0xFFEE1004 . 0xFFEE1000 . . . . . . . . . . . 3.141 ........... Zeige alle vier Möglichkeiten, dem Speicherplatz der Variablen d eine 2.718 zuzuweisen. 1. d = 2.718 2. *p = 2.718 3. **pp = 2.718 Einführung in die Praktische Informatik, Wintersemester 2017/18 4. ***ppp = 2.718 20-9 Aufgabe 3: Verwendung von Zeigern 1. Aufgabenstellung Definiere ein Array einer beliebigen Größe, eines beliebigen Typs. Schreibe zwei Programme. Das erste Programm soll alle Elemente dieses Arrays mittels einer forSchleife und einer Indexvariablen initialisieren. Schreibe ein zweites Programm, dass diese Initialisierung mittels einer for-Schleife und Zeigern (also nicht mit einer Indexvariablen) durchführt. 2. Pflichtenheft Aufgabe : Entwicklung zweier Programme, die ein Array mittels einer Indexvariablen bzw. mittels zweier Zeiger initialisieren. Eingabe : keine Ausgabe : keine Sonderfälle : keine 3. Kodierung Lösung mittels Indexvariable: 1 # define SIZE 10 2 # define INIT 0 3 4 int main ( int argc , char ** argv ) 5 { 6 int arr [ SIZE ]; 7 int i ; 8 for ( i = 0; i < SIZE ; i ++ ) 9 arr [ i ] = INIT ; 10 } Lösung mittels Zeigern: 1 # define SIZE 10 2 # define INIT 0 3 4 int main ( int argc , char ** argv ) 5 { 6 int arr [ SIZE ]; 7 int * p ; 8 for ( p = arr ; p < arr + SIZE ; p ++ ) 9 * p = INIT ; 10 } 20-10 Wintersemester 2017/18, Einführung in die Praktische Informatik