1. Einführung
2. Pointer Grundlagen
2.1 Fragen
2.2 Aufgaben
3. Pointer & Felder
3.1 Fragen
3.2 Aufgaben
4. Pointer & Funktionen
4.2 Fragen
4.3 Aufgaben
5. Lösungen
6. Download

4. Pointer & Funktionen
Beim Programmieren in C wird viel mit Funktionen gearbeitet, weil es den Quelltext übersichtlicher macht und es möglich ist, Teilprogramme von anderen Programmierern schreiben zu lassen. Dazu müssen nur die zu übergebenen Variablen und der Rückgabewert abgesprochen werden. Außerdem besteht die Möglichkeit Funktionen in anderen Programmen noch einmal zu benutzen. Es ist nicht zu vergessen, dass
	void main(void){…}
auch eine Funktion ist! In diesem Fall werden keine Variablen an sie übergeben und sie liefert keinen Rückgabewert. (Die main-Funktion wird von nun an nicht jedes Mal extra als Funktion erwähnt.)

Vielleicht hat sich der eine oder andere Leser dieser Lerneinheit schon gefragt, warum es diese Pointer denn nun gibt, wenn man die Speicherinhalte mit den Variablennamen und über die Pointer ansprechen kann.

Man kommt in C um Pointer nicht herum wenn,
  1. man mit Funktionen arbeitet, die Speicherinhalte direkt manipulieren
  2. man mehrere Rückgabewerte in einer Funktion hat
  3. es um dynamische Speicherverwaltung geht

Die ersten beiden Punkte werden an dieser Stelle anhand von Beispielen erläutert und der dritte Punkt wird erst im 2. Semester in Softwaretechnik behandelt.

4.1 Funktionen, die Speicherinhalte direkt manipulieren
Wenn man mit Funktionen arbeitet, ist es wichtig, dass man sich den Gültigkeitsbereich der verwendeten Variablen klar macht. Wenn man das nicht tut, kann es zu ungewollten Effekten kommen, was folgendes Beispiel (Quelltext 4.1) verdeutlicht. Es soll eine int Variable definiert und initialisiert werden und dann mit einer Funktion um eins erhöht werden. Um die Variable zu prüfen, soll eine Ausgabe auf dem Bildschirm vorgenommen werden.
01 #include <stdio.h>            	/*Header einbinden        */ 
02 
03 void pluseins(int i);         	/*Prototyp der Funktion   */
04 int main(void)                	/*Beginn, main - Funktion */
05 {
06     int i=0;                  	/*dekl.und initialisieren */
08     printf("i ist: %d\n",i);  	/*Testausgabe - Bildschirm*/ 
09     pluseins(i);              	/*Funktionsaufruf         */
10     printf("i ist: %d\n",i);  	/*Testausgabe - Bildschirm*/      
11     fflush(stdin);            	/*Tastaturpuffer löschen  */
12     getchar();                	/*auf Tastatur warten     */
13     return 0;                 	/*0 zurückgeben an Konsole*/
14 }
15 
16 void pluseins(int i)          	/*Definition der Funktion */
17 {
18     i++;                      	/*i um eins erhöhen       */
19 }
 Quelltext 4.1 mit anschließender Bildschirmausgabe
Es wurde beim Programmieren fälschlicherweise angenommen, dass es sich bei der Variable i im ganzen Quelltext um ein und die selbe Variable handelt. Das stimmt aber nicht. In Zeile 9 wird die Funktion aufgerufen und die Variable wird der Funktion „übergeben“. Diese Übergabe bedeutet aber nicht, dass sich die Variable i aus der main – Funktion nun in der pluseins – Funktion befindet. Es ist vielmehr so, dass die pluseins – Funktion sich den Wert i aus der main – Funktion „anschaut und kopiert“ und dann mit einer eigenen i – Variable weiterarbeitet. Diese Art des Funktionsaufrufes wird Call-by-Value genannt. Man könnte auch sagen, dass die i – Variable der main – Funktion von der i – Variable der pluseins – Funktion überdeckt wird. In Bild 4.1 ist der Programmablauf einmal grafisch dargestellt.
Damit das Programm tut, was es soll (eine 0 und dann eine 1 ausgeben), ist es nötig den Quelltext zu verändern. Die pluseins – Funktion wird so verändert, dass sie den um eins erhöhten Wert zurückgibt. Anschließend wird der Variable i der neue Wert zugewiesen.
01 #include <stdio.h>            	/*Header einbinden        */
02 int pluseins(int i);          	/*Prototyp der Funktion   */
03 int main(void)                	/*Beginn, main - Funktion */
04 {
05     int i=0;                  	/*dekl.und initialisieren */
06     printf("i ist: %d\n",i);  	/*Testausgabe - Bildschirm*/  
07     i = pluseins(i);          	/*Funktionsaufruf         */    
08     printf("i ist: %d\n",i);  	/*Testausgabe - Bildschirm*/     
09     fflush(stdin);                /*Tastaturpuffer löschen  */ 
10     getchar();               	/*auf Tastatur warten     */
11     return 0;                 	/*0 zurückgeben an Konsole*/
12 }
13 
14 int pluseins(int i)           	/*Definition der Funktion */
15 {
16     i++;                      	/*i um eins erhöhen       */
17     return i;                 	/*i zurückgeben           */   
18 }
 Quelltext 4.2 mit anschließender Bildschirmausgabe
Diese Ausgebe sieht schon besser aus, das Programm tut, was es soll! In Zeile 9 wird die i – Variable der Funktion übergeben. Dann „kopiert“ sich die Funktion die Variable und erhöht sie um 1. Bei return i; gibt die Funktion den Wert 1 zurück und in der main – Funktion wird der Variable i der zurückgegebene Wert zugewiesen. In Bild 4.2 ist der Programmablauf noch einmal dargestellt.
Das Programm funktioniert zwar, aber die Lösung ist nicht so elegant. Denn man könnte in Zeile 9 anstelle des Funktionsaufrufes gleich i++ schreiben und die Ausgabe auf den Bildschirm würde genauso aussehen. Mit anderen Worten, die pluseins – Funktion ist in diesem Beispiel überflüssig.

Nehmen wir aber einmal an, die Variable i soll wie im Quelltext 4.1 direkt in der pluseins - Funktion geändert werden und es soll nicht mit einer Kopie einer Variablen gearbeitet werden. Das funktioniert nur mit Pointern!

In Quelltext 4.3 ist die beschriebene Vorgehensweise beachtet worden. Es werden zunächst eine int – Variable und eine Pointer – Variable auf den Datentypen int deklariert. Dann wird dem Pointer int_pt die Adresse von i zugewiesen. Mit printf wird nun überprüft, ob der Pointer wirklich auf die Variable i zeigt. Im folgenden Schritt wird die pluseins – Funktion aufgerufen und es wird eine Adresse im Arbeitsspeicher an die Funktion übergeben. Diese „schaut“ sich die Adresse an und „kopiert“ sie sich in eine eigene Pointervariable, welche nur in der pluseins – Funktion gültig ist. Da nun in der lokalen Pointervariable eine Speicheradresse im Arbeitsspeicher steht, kann diese Speicherstelle (welche der Variable i aus der main – Funktion entspricht) von der Funktion aus manipuliert werden. Der Wert wird ausgelesen, um 1 erhöht und wieder zurück geschrieben. Um zu prüfen, ob auch alles geklappt hat, wird noch einmal eine Ausgabe auf den Bildschirm vorgenommen. Diese Art des Funktionsaufrufes wird Call-by-Adress genannt. Das Beispiel ist in Quelltext 4.3 zu sehen.
01 #include <stdio.h>                  	/*Header einbinden */
02
03 void pluseins(int *int_pt);         	/*Prototyp         */
04 int main(void)                      	/*Beginn, main - Funktion */
05 {
06     int i=0;                        	/*deklarieren und initialisieren */
07     int *int_pt;                    
08     int_pt= &i;                     
09     printf("i ist: %d\n",*int_pt);  	/*Testausgabe      */
10     pluseins(int_pt);               	/*Funktionsaufruf  */
11     printf("i ist: %d\n",i);        	/*Testausgabe      */
12     fflush(stdin);                  	/*Tast.puf.löschen */
13     getchar();                      	/*auf Tast. Warten */
14     return 0;                       	/*0 zurückgeben    */
15 }
16 
17 void pluseins(int *int_pt )        	/*Definition der Funktion  */
18 {
19     *int_pt += 1;			/* *int_pt um 1 erhöhen  */
20 }
 Quelltext 4.3 mit anschließender Bildschirmausgabe
In Bild 4.3 ist der Programmablauf noch einmal grafisch dargestellt.
4.2 Funktionen mit mehreren Rückgabewerten
Wenn man sich einmal einen Funktionsprototypen anschaut kann man erkennen, dass zwar mehrere Variablen an eine Funktion übergeben werden können, aber es nur möglich ist einen Wert zurückzugeben. Als Beispiel ist nun der Prototyp einer Funktion gezeigt, die zwei int – Zahlen voneinander abzieht und das Ergebnis als Rückgabewert zurückliefert.
	int minus(int gross, int klein);			
	Rückgabewert Funktionsname(erste Variable, zweite Variable);
In diesem Fall ist es kein Problem, dass man nur einen Wert zurückgeben kann. Aber es gibt auch Fälle in denen man zwei Werte zurückgeben will, weil man z.B. den Widerstand und die Leistung von einem übergebenen Strom und einer übergebenen Spannung in einer Funktion errechnet hat. Dies funktioniert nur über Pointer. Für dieses beschriebene Beispiel würde ein Funktionsprototyp wie folgt aussehen:
	void berechnung(double u, double i, double *p_pt, double *r_pt);
Man übergibt der berechnung – Funktion die Spannung und den Strom wie bei Call – by – Value und die Speicheradressen für die Leistung und den Widerstand wie bei Call – by – Adress. Dabei ist es wichtig, dass die beiden Pointer auf gültige Speicherbereiche zeigen. In der Funktion werden der Widerstand und die Leistung errechnet und über Pointer in die Variablen r_err und p_err hineingeschrieben. Im Quelltext 4.4 ist diese Art von Funktionsaufruf schrittweise dargestellt.
01 # include <stdio.h>
02 
03 void berechnung(double u, double i, double *p_pt, double *r_pt);
04 int main (void)
05 {
06     double u_mess = 0, i_mess = 0;    
07     double p_err = 0, r_err = 0;
08     double *p_err_pt = NULL;
09     double *r_err_pt = NULL;
10     
11     /*Pointer setzen*/
12     p_err_pt = &p_err;
13     r_err_pt = &r_err;
14         
15     /* vom Messgerät gemessene Spannung und gemessener Strom */
16     u_mess = 12.5;
17     i_mess = 1.2;
18     
19     /*Leistung und Widerstand in der Funktion errechnen*/ 
20     berechnung(u_mess, i_mess, p_err_pt, r_err_pt);
21     
22     /*Ausgabe*/
23     printf("Gemessene Werte:\n");
24     printf("Spannung: %.2f Strom: %.2f\n\n", u_mess, i_mess);
25     if(i_mess != 0)
26     {
27         printf("Errechnete Werte:\n");
28         printf("Widerstand: %.2f Leistung: %.2f\n", r_err, p_err);
29     }
30     else printf("Division durch 0 nicht moeglich!\n");
31 
32     fflush(stdin);
33     getchar();
34     return 0;
35 }
36 
37 void berechnung(double u, double i, double *p_pt, double *r_pt)
38 {
39     /*Widerstand ausrechnen, wenn strom != null ist und Speichern*/
40     if (i!=0)
41     {
42         *r_pt = u/i;
43     }
44        
45     /*Leistung errechnen und Speichern*/
46     *p_pt = u * i;
47 }
 Quelltext 4.4
Hat man mehrere Variablen eines gleichen Datentyps, werden diese oft zu Feldern oder Arrays zusammengefügt. Im Abschnitt „Pointer & Felder“ wurde die Arbeitsweise von Pointern im Zusammenhang mit Feldern bereits ausführlich erläutert. Dieses Wissen wird jetzt benötigt, da man in Funktionen auch nur über Pointer auf Datenfelder direkt zugreifen kann. Es ist im Prinzip eine Variation von „Funktionen mit mehreren Rückgabewerten“.

Die Arbeit mit Funktionen ist denkbar einfach: man übergibt der Funktion die erste Speicheradresse des Datenfeldes und eventuell die Anzahl der Feldelemente. In der Funktion muss jetzt nur noch mittels Pointerarithmetik durch das Feld "gewandert" werden und die Werte des Datenfeldes können von der Funktion aus direkt verändert werden. Da sich dieser Satz zwar leicht liest, die Vorstellung darüber aber etwas schwierig ist, ist in folgendem Quelltext diese Funktionsweise einmal Schritt für Schritt aufgelistet. Die feldbeispiel – Funktion erhöht den Inhalt der Feldelemente um eins und gibt sie dann zurück.

01 #include <stdio.h>
02 
03 void feldbeispiel(int *anfangsadresse, int anzahlfeldelemente);
04 int main(void)
05 {
06     int feld[5]={1,2,3,4,5};
07     int *feld_anfang;
08     
09     feld_anfang=feld; /*oder: feldanfang = &feld[0];*/
10     feldbeispiel(feld_anfang, 5);
11 }
12 
13 void feldbeispiel(int *anfangsadresse, int anzahlfeldelemente)
14 {
15     int i=0;
16     
17     for(i=0;i<anzahlfeldelemente;i++)
18     {
19         *(anfangsadresse+i) = *(anfangsadresse+i)+1;
20     }
21 }
 Quelltext 4.5
4.3 Dynamische Speicherplatzverwaltung, Pointer & Strukturen
Dynamische Speicherplatzverwaltung gehört zwar nicht direkt in diese Lerneinheit und auch eigentlich nicht unbedingt an diese Stelle. Da sie aber ein wesentlicher Bestandteil von Softwaretechnik im 2. Semester ist und dort überwiegend mit Komplexen Datentypen hantiert wird, ist hier ein Beispiel gezeigt, wie man über Pointer auf eine Komplexe Variable (Struktur) zugreift. Außerdem kommen wir am Ende dieses Abschnittes wieder bei Funktionen an. Ein Komplexer Datentyp besteht aus einem Verbund unterschiedlicher Datentypen. Z.B. hat der Datentyp reifen einen preis und einen hersteller.
01 	typedef struct
02 	{
03 	   short preis;
04 	   char hersteller[40];
05 	}reifen;
Bei dieser Schreibweise heißt der Datentyp in der main – Funktion einfach reifen. (Bsp: reifen fahrradreifen;) Es gibt allerdings noch eine zweite Schreibweise.
01 	struct reifen
02 	{
03 	   short preis;
04 	   char hersteller[40];
05 	};
In diesem Fall heißt der Datentyp struct reifen. (Bsp. struct reifen fahrradreifen;) In dieser Lerneinheit wird die erste Schreibweise benutzt. Da das Thema an dieser Stelle nur kurz angeschnitten wird, ist im folgenden Quelltext 4.6 gezeigt wie man einen komplexen Datentypen erzeugt und mit Inhalt füllt. Dies wird einmal mit Pointer und einmal ohne Pointer gemacht. Es ist zu beachten, dass es zwei Schreibweisen gibt, um mit Pointern und Strukturen zu arbeiten. Zum Einen der Punktoperator (zuerst im Beispiel) und den Pfeiloperator (zweite Variante im Beispiel). Der Einfachheit halber ist zu empfehlen, den Pfeiloperator zu verwenden.
01 #include<stdio.h>
02 #include<string.h>
03 
04 typedef struct
05 {
06     short preis;
07     char hersteller[40];
08 }reifen;
09 
10 int main(void)
11 {
12     /*Variablen deklarieren*/
13     reifen fahrradreifen = {0, "Hersteller" };
14     reifen autoreifen = {0,"Hersteller" };
15     reifen *pt_f, *pt_a;
16         
17     /*Pointer setzen*/
18     pt_f=&fahrradreifen;
19     pt_a=&autoreifen;
20 
21     /*Werte setzen ohne Pointer (Strings über strcpy)*/
22     fahrradreifen.preis = 10;
23     strcpy(fahrradreifen.hersteller, "Continental");
24     autoreifen.preis = 100;
25     strcpy(autoreifen.hersteller, "GoodYear"); 
26     
27     /*Werte setzen mit Pointer*/
28     (*pt_f).preis = 9;
29     strcpy((*pt_f).hersteller, "Maxxis");
30     (*pt_a).preis = 99;
31     strcpy((*pt_a).hersteller, "Michelin");
32     
33     /*Werte über Pointer setzen 2*/
34     pt_f->preis = 8;
35     strcpy(pt_f->hersteller, "Vittoria");
36     pt_a->preis = 88;
37     strcpy(pt_a->hersteller, "Blackhawk");
38     
39     fflush(stdin);
40     getchar();
41     return 0;
42 }
 Quelltext 4.6
Felder von Strukturen lassen sich ganz einfach wie gewöhnliche Felder "durchwandern". Hätte man z.B. ein Feld von 10 fahrradreifen – Variablen, könnte man mit einer Zählervariable durch das Feld "wandern" und die Inhalte wie im folgenden Quelltext 4.7 gezeigt, bearbeiten. Dieser Quelltext zeigt die Variante, bei der der Zeiger "verbogen" wird. Es ist aber auch die Variante möglich, in der der Pointer direkt durch das Feld "wandert".
01 #include<stdio.h>
02 #include<string.h>
03 
04 typedef struct
05 {
06     short preis;
07     char hersteller[40];
08 }reifen;
09 
10 int main(void)
11 {
12     /*Variablen deklarieren*/
13     int i=0;
14     reifen fahrradreifen[10];
15     reifen *pt_f;
16         
17     /*Pointer setzen*/
18     pt_f=&fahrradreifen[0];
19     
20     for(i=0;i<10;i++)
21     {
22       (*(pt_f+i)).preis = 9;
23       strcpy((*(pt_f+i)).hersteller, "Maxxis");    
24     }
25 
26     for(i=0;i<10;i++)
27     {
28       (pt_f+i)->preis = 8;
29       strcpy((pt_f+i)->hersteller, "Vittoria");
30     }        
31     
32     fflush(stdin);
33     getchar();
34     return 0;
35 }
 Quelltext 4.7
Da nun der Umgang mit Strukturen und Pointern gezeigt wurde kann man sagen, dass der Umgang in Funktionen mit Strukturen genau so funktioniert, wie mit gewöhnlichen Variablen oder Feldern. Es wird Ausschließlich eine (Anfangs-) Adresse an eine Funktion übergeben und diese speichert die Daten, die in der Funktion erzeugt werden direkt an diese Speicherstelle (oder "wandert" gegebenen Falls durch das Feld). Der Quelltext 4.8 zeigt dies noch einmal. Es wird ein Feld von 10 fahrradreifen deklariert. In der Funktion fuellen wird das Feld mit leeren Werten gefüllt.
01 #include<stdio.h>
02 #include<string.h>
03 
04 typedef struct
05 {
06     short preis;
07     char hersteller[40];
08 }reifen;
09 
10 void fuellen(reifen *pt, int anzahl);
11 
12 int main(void)
13 {
14     /*Variablen deklarieren*/
15     int anzahl=10;
16     reifen fahrradreifen[10];
17     reifen *pt;
18         
19     /*Pointer setzen*/
20     pt=&fahrradreifen[0];
21 
22     fuellen(pt, anzahl);
23     
24     fflush(stdin);
25     getchar();
26     return 0;
27 }
28 
29 void fuellen(reifen *pt, int anzahl)
30 {
31     int i;    
32     for(i=0;i<anzahl;i++)
33     {
34       (pt+i)->preis = 0;
35       strcpy((pt+i)->hersteller, "default");
36     }
37 }
 Quelltext 4.8