Weitere Anwendungen von C-Zeigern: Der Zusammenhang von Feldern, Zeigern und Funktionen

Mit Hilfe von Zeigern kann man Funktionen realisieren, die Felder als Eingabewert beziehungsweise als R├╝ckgabewert besitzen. Es ist sogar m├Âglich, Zeiger auf Funktionen zu setzen und damit Funktionen als Eingabewerte anderer Funktionen einzusetzen.
Noch keine Stimmen abgegeben
Noch keine Kommentare

Einordnung des Artikels

In diesem Kapitel werden die Kenntnisse ├╝ber Felder, Zeiger und Funktionen zusammengef├╝hrt. Zahlreiche Aufgaben zu den hier vorgestellten Konzepten finden sich in Programmier-Aufgaben in C: Anwendungen von Feldern, Zeigern und Funktionen.

Einf├╝hrung

So wie Funktionen bisher beschrieben wurden, gilt f├╝r sie:

  • Der R├╝ckgabewert muss ein elementarer Datentyp sein oder void. Ein Feld ist nicht als R├╝ckgabewert erlaubt.
  • Jeder Eingabewerte einer Funktion muss ebenfalls einen elementaren Datentyp besitzen.
  • Dass eine Funktion mehrere R├╝ckgabewerte besitzt, ist eigentlich nicht erlaubt, kann aber mit Hilfe von Zeigern realisiert werden (call by value).

In diesem Kapitel wird gezeigt, wie man Zeiger einsetzen kann, um Funktionen zu realisieren, die Felder als Eingabewert beziehungsweise R├╝ckgabewert besitzen.

Zudem ist es m├Âglich, Zeiger auf Funktionen zu setzen und diesen Zeiger an eine weitere Funktion zu ├╝bergeben. Das hei├čt auch eine Funktion kann Eingabewert einer anderen Funktion sein; Letztere wird dann meist als Funktion h├Âherer Ordnung bezeichnet.

Die Beispiele in diesem Kapitel sind sehr einfach gehalten, um den Blick allein auf diese neuen Konzepte zu richten. Zahlreiche praktische Anwendungen werden in Programmier-Aufgaben in C: Anwendungen von Feldern, Zeigern und Funktionen vorgestellt.

Ein Feld als Eingabewert einer Funktion

Ein erstes Beispiel zur Einf├╝hrung

Das folgende Beispiel zeigt den naiven Zugang, wie man eine Funktion implementieren wird, die als Eingabewert ein Feld (array) erh├Ąlt (Deklaration in Zeile 1, Implementierung in Zeile 3 bis 6):

void testArray(int a []);

void testArray(int a[]){
    printf("size: %d \n", sizeof(a));       // Compiler-Warnung beachten!
    printf("length: %d \n", sizeof(a) / sizeof(int));       // Compiler-Warnung beachten!
}

Zeile 1: Um einen ersten Test auszuf├╝hren, wird eine m├Âglichst einfache Funktion testArray() deklariert; sie erh├Ąlt ein int-Feld als Eingabewert, aber hat keinen R├╝ckgabewert.

Zeile 3 bis 6: In der Implementierung steht lediglich, dass mit Hilfe von sizeof() berechnet wird, wie viele Bytes das int-Feld belegt und welche L├Ąnge es hat.

Zum Testen wird in main() ein Feld mit 5 Komponenten angelegt und testArray() aufgerufen:

int a [] = {0, 2, 4, 6, 8};
testArray(a);

Die Ausgaben lauten:

size: 4 
length: 1

Man erh├Ąlt andere Ausgaben, wenn die beiden printf()-Anweisungen in main() nach der Initialisierung von a aufgerufen werden:

int a [] = {0, 2, 4, 6, 8};
printf("size: %d \n", sizeof(a));
printf("length: %d \n", sizeof(a) / sizeof(int));

Jetzt erh├Ąlt man die Ausgaben:

size: 20 
length: 5

Dieser Widerspruch kl├Ąrt sich sofort auf, wenn man bedenkt:

  1. Der Name eines Feldes steht f├╝r einen Zeiger auf dessen erste Komponente (die erste Komponente besitzt den Index 0).
  2. Die Funktion sizeof() berechnet den vom Feld belegten Speicherplatz nur im G├╝ltigkeitsbereich des Feldes. Bei der ├ťbergabe des Feldes an eine Funktion wird nur ein Zeiger auf die erste Komponente ├╝bergeben und sizeof() kann nicht mehr auf das gesamte Feld zugreifen.

Man kann dies auch so formulieren:

Der Mechanismus call by value, mit dem bei einem Funktionsaufruf die aktuellen Parameter kopiert und an die Implementierung der Funktion ├╝bergeben werden, arbeitet ausschlie├člich bei elementaren Datentypen. Ist ein Feld aktueller Parameter, wird der Name des Feldes als Zeiger auf die erste Komponente interpretiert und diese Adresse wird an die Implementierung der Funktion ├╝bergeben.

Somit ist obige Deklaration von testArray() ├Ąquivalent zu folgender Deklaration:

void testArray(int * a);

Diese Deklaration ist sogar vorzuziehen, denn jetzt ist ausdr├╝cklich erkennbar, dass ein Zeiger ├╝bergeben wird; der Aufruf testArray(a); kann nicht das gesamte Feld an die Implementierung von testArray() weiterreichen.

Wie soll man ein Feld an eine Funktion ├╝bergeben?

Aus dem Beispiel des letzten Unterabschnittes kann man sofort folgern, wie man ein Feld an eine Funktion f() ├╝bergeben sollte:

  1. Mit dem Namen des Feldes wird der Zeiger auf die erste Komponente ├╝bergeben (Komponente mit Index 0). Im Argument von f() ben├Âtigt man daher einen Zeiger auf den Datentyp des Feldes (oben war es int * a ).
  2. Um innerhalb der Funktion auf die Komponenten des Feldes zuzugreifen, kann man die Zeigerarithmetik einsetzen: Mit a + i kann man auf die (i-1)-te Komponente von a zugreifen. Aber innerhalb von f() l├Ąsst sich mit Hilfe von sizeof() nicht mehr feststellen, wie viele Komponenten a hat. Daher sollte man als weiteren Eingabewert die L├Ąnge des Feldes an f() ├╝bergeben. Und diese L├Ąnge muss dort festgestellt werden, wo man noch mit sizeof() auf das gesamte Feld zugreifen kann.

Beispiel: Ausgabe der Komponenten eines Feldes

Es soll eine Funktion printArray() implementiert werden, die die Komponenten eines Feldes in einer Zeile ausgibt und dabei ein selbst zu bestimmendes Zeichen sep zwischen die Komponenten setzt. Nach obigen Ausf├╝hrungen lauten der Funktionskopf und die Implementierung etwa:

// Ausgabe der Komponenten eines Feldes a mit Trennungszeichen sep zwischen den Komponenten:
void printArray(int * a, int length, char sep);

void printArray(int * a, int length, char sep){
    for(int i = 0;  i < length; ++i) {
        printf("%d %c", a[i], sep);
    }
    puts("\n");
}

Ein Aufruf der Funktion k├Ânnte dann wie folgt geschehen (mit einem Leerzeichen als Trennungszeichen):

int a [] = {0, 2, 4, 6, 8};
printArray(a, sizeof(a) / sizeof(int), ' ');
// 0  2  4  6  8

Die drei aktuellen Parameter im Funktionsaufruf (Zeile 2) sind somit:

  1. Der Name a des Feldes, also der Zeiger auf die erste Komponente von a.
  2. Die L├Ąnge des Feldes, die man hier mit Hilfe von sizeof(a) / sizeof(int) berechnen kann.
  3. Das Leerzeichen ' ' als Trennungszeichen.

Beispiel: Berechnung eines gewichteten Mittelwertes

Befinden sich n Massen m1, m2, ..., mn an Positionen x1, x2, ..., xn, so kann man nach der Koordinate x des Schwerpunktes dieser Massenverteilung fragen. Zur Berechnung von x werden

  • die Positionen mit den jeweiligen Massen multipliziert,
  • diese Produkte xi ┬Ě mi aufsummiert und
  • anschlie├čend durch die Gesamtmasse geteilt:

x = (x1 ┬Ě m1 + ... + xn ┬Ě mn) / (m1 + ... + mn).

Damit man dies fehlerfrei und eindeutig durchf├╝hren kann, m├╝ssen einige Bedingungen erf├╝llt sein:

  1. Es m├╝ssen genau so viele Koordinaten wie Massen vorliegen.
  2. Die Massen k├Ânnen nicht negativ sein.
  3. Die Summe der Massen muss positiv sein.

Dies f├Ąllt dann wieder unter Pr├╝fung der Eingabewerte (siehe Aufgabe unten).

Soll dieser gewichtete Mittelwert mit Hilfe einer Funktion weightedMean() berechnet werden, so lautet die relevante Frage: Welcher Funktionskopf ist f├╝r diese Funktion zu w├Ąhlen?

Die Felder der Koordinaten und Massen sind sicher Zahlen mit Nachkommastellen, also ist double (oder float) der geeignete Datentyp; der R├╝ckgabewert, die Schwerpunktskoordinate, ist dann auch vom Datentyp double. Die Felder der Koordinaten und Massen werden wie im einf├╝hrenden Beispiel oben ├╝bergeben: als Zeiger auf die erste Komponente und einem int-Wert, der die L├Ąnge der Felder angibt:

double weightedMean(double * x, int length_x, double * m, int length_m);

Die Implementierung ist ÔÇô bis auf die Pr├╝fung der Eingabewerte ÔÇô sehr einfach:

double weightedMean(double * x, int length_x, double * m, int length_m){

    // TODO: Pr├╝fung der Eingabewerte
    
    double weightedSum = 0;
    double totalMass = 0;
    
    for(int i = 0;  i < length_x; ++i) {
        weightedSum += x[i] * m[i];
        m += m[i];
    }
    
    return weightedSum / m;
}

Ein Aufruf der Funktion weightedMean() k├Ânnte wie folgt aussehen:

double coords [] = {0, 2, 4, 6, 8};
double ms [] = {1, 1, 3, 1, 3};

double centerOfMass = weightedMean(coords, 5, ms, 5);

Aufgabe: Implementieren Sie die Pr├╝fung der Eingabewerte. Falls eine der oben genannten Bedingungen nicht erf├╝llt ist, soll eine entsprechende Fehlermeldung ausgegeben werden und der R├╝ckgabewert ist gleich null.

Ein Feld als R├╝ckgabewert einer Funktion

├ťbergabe des Feldes mit call by reference

Ein Feld kann nicht im Sinne von call by value der Eingabewert einer Funktion sein (das gesamte Feld kann nicht an die Funktion weitergereicht werden); dies ist nur bei elementaren Datentypen m├Âglich. Ebenso kann ein komplettes Feld nicht als R├╝ckgabewert an die aufrufende Einheit zur├╝ckkopiert werden. Es ist also nicht m├Âglich eine Funktion zu deklarieren, die ein Feld als R├╝ckgabewert besitzt.

Wie oben gezeigt wurde, kann man zwar einen Funktionskopf

void f(int a [])

angeben, beim Aufruf von f(a) wird aber lediglich der Zeiger auf die erste Komponente von a an f() ├╝bergeben.

Und berechnet eine Funktion ein Feld a und besitzt sie das return-statement

return a;

so wird damit ebenfalls nicht das gesamte Feld, sondern der Zeiger auf die erste Komponente zur├╝ckgegeben.

Um eine Funktion mit einem Feld als R├╝ckgabewert zu realisieren, muss man also nach einer anderen L├Âsung suchen. In Definition und einfache Anwendungen von C-Zeigern wurde bereits diskutiert, wie man Funktionen mit mehreren R├╝ckgabewerten simuliert: dazu wird der Mechanismus call by reference verwendet. Damit ist gemeint, dass man vor dem Aufruf einer Funktion die Variablen f├╝r die R├╝ckgabewerte deklariert, ihre Adressen an die Funktion ├╝bergibt und in der Funktion ihre Werte berechnet. Innerhalb der Funktion k├Ânnen dadurch Variablen gesetzt oder ver├Ąndert werden, die sich eigentlich in einem anderen G├╝ltigkeitsbereich befinden.

Diesen Mechanismus kann man auch f├╝r ein Feld einsetzen: das Feld, das eigentlich zur├╝ckgegeben werden soll, wird durch den Zeiger auf die erste Komponente und die L├Ąnge charakterisiert. ├ťbergibt man dies an die Funktion, kann man innerhalb der Funktion alle Komponenten des Feldes setzen. Der folgende Unterabschnitt soll ein einfaches Beispiel zeigen.

Beispiel: Berechnung der Quadrate eines Feldes

Es soll eine Funktion square() implementiert werden, die zu einem gegebenen int-Feld die Quadrate der Komponenten berechnet. Da beide Felder identische L├Ąnge haben, muss man der Funktion square() die zwei Adressen und eine L├Ąnge ├╝bergeben. Die Komponenten der Felder werden hier als int angenommen; der Funktionskopf lautet dann:

void square(int * x, int * y, int length);

Dabei soll x f├╝r die Eingabewerte und y f├╝r die "R├╝ckgabewerte" stehen. Einen echten R├╝ckgabewert gibt es hier nicht, daher ist der Datentyp des R├╝ckgabewertes gleich void.

Die Implementierung iteriert jetzt lediglich ├╝ber die Komponenten der Felder und berechnet jeweils das Quadrat von x[i] :

void square(int * x, int * y, int length){
    for (int i = 0; i < length; ++i) {
        y[i] = x[i] * x[i];
    }
    return;
}

Beim Aufruf der Funktion square() ist zu beachten, dass man das Feld y f├╝r die Quadrate mit gleicher L├Ąnge wie das Feld der x-Werte anlegt. Der Aufruf k├Ânnte dann lauten; zur Ausgabe wird die Funktion printArray() von oben eingesetzt:

int a[] = {1, 3, 5, 7, 9};
int b[5];

square(a, b, 5);
printArray(b, 5, ' ');
// 1  9  25  49  81

Zeiger auf Funktionen: Die Grundlage der funktionalen Programmierung

Ein erstes Beispiel: Zeiger auf eine Funktion

F├╝r eine Variable x eines elementaren Datentyps ist es m├Âglich, die Variable direkt ├╝ber ihren Namen x anzusprechen oder einen Zeiger p auf x zu definieren und mit diesem den Wert von x abzufragen oder zu ├Ąndern. Dies wurde bei der Definition von Zeigern diskutiert; das folgende Beispiel soll dies nochmals zeigen:

// Variable:
double x = 5;

// Zeiger auf x:
double * p = & x;

// Abfragen von x mit Zeiger:
printf("x = %f\n", * p);            // x = 5

// Ver├Ąndern von x ├╝ber Zeiger:
* p = 17;
printf("x = %f\n", x);          // x = 17

Ganz ├Ąhnlich kann man den Aufruf einer Funktion ├╝ber einen Zeiger realisieren.

Das folgende Beispiel soll die Syntax zeigen, wie man:

  • einen Zeiger auf eine Funktion definiert und dann
  • den Funktionsaufruf mit Hilfe des Zeigers realisiert.

Die Funktion f() mit dem Funktionskopf double f(double x); berechnet das Quadrat eines eingegebenen Wertes x (Deklaration in Zeile 1, Implementierung in Zeile 16 bis 18).

double f(double x);

int main(void) {

    // Zeiger auf f:
    double (* pf) (double) = f;
    
    // Funktionsaufruf mit x = 5:
    double y = pf(5);
    
    printf("y = %f\n", y);          // y = 17
    
    return EXIT_SUCCESS;
}

double f(double x){
    return x * x;
}

Der Zeiger auf die Funktion erh├Ąlt den Namen pf (Zeile 6). Entscheidend in der Definition des Zeigers ist, dass man den Funktionskopf der Funktion nachbildet, auf die er zeigen soll:

  1. Die Funktion f() besitzt als Eingabewert double und daher steht nach dem Namen des Zeigers double in runden Klammern (wie der Eingabewert einer Funktion).
  2. Der R├╝ckgabewert von f() ist double und daher steht vor dem Zeiger der Datentyp double.
  3. Initialisiert wird der Zeiger mit dem Namen der Funktion, auf die er zeigen soll. Dies ist ├Ąhnlich wie bei Feldern: es ist nicht n├Âtig, einen Adressoperator anzugeben, da der Name der Funktion bereits ein Zeiger ist.

Zus├Ątzlich muss (* pf) in runde Klammern eingeschlossen werden. L├Ąsst man die runden Klammern weg, erh├Ąlt man einen Syntax-Fehler (es wird versucht einen Zeiger auf eine Variable zu definieren).

Zeile 9: Der Zeiger pf kann jetzt wie die Funktion f() eingesetzt werden; das Argument wird in runde Klammern geschrieben. Zeile 9 ist somit gleichwertig zu double y = f(5);

Es soll ausdr├╝cklich betont werden: Zeiger auf Funktionen wurden nicht eingef├╝hrt, um Quelltexte wie im letzten Beispiel zu erm├Âglichen ÔÇô die Definition des Zeigers ist hier ├╝berfl├╝ssig, da man die Berechnung von y auch mit der Funktion f() erledigen kann. Interessant wird das Beispiel erst, wenn man einen Schritt weitergeht und den Zeiger auf eine Funktion an eine Funktion ├╝bergibt ÔÇô dies wird im n├Ąchsten Unterabschnitt gezeigt.

├ťbergabe eines Funktions-Zeigers an eine Funktion

Oft kann man beim Programmieren noch nicht entscheiden, welche Funktion aufgerufen werden soll, da erst w├Ąhren der Laufzeit des Programms entschieden wird, welche Funktion ben├Âtigt wird. Um dies zu realisieren, muss man die M├Âglichkeit haben, einer Funktion eine weitere Funktion als Eingabewert zu ├╝bergeben ÔÇô der Eingabewert ist nat├╝rlich ein Zeiger auf die Funktion. Welche Funktion mit dem Zeiger angesprochen wird kann etwa zuvor in einer if-else-Anweisung entschieden werden.

1. Beispiel: N├Ąherungsweise Integration

In Einf├╝hrung in die Programmiersprache C: Funktionen wurde eine Funktion integrate() entwickelt, die die Integrationsgrenzen a und b sowie die Anzahl der Teilintervalle N als Eingabewerte erh├Ąlt und daraus drei N├Ąherungswerte f├╝r ein Integral berechnet (Untersumme, Obersumme und Trapezregel). Der Funktionskopf lautete:

void integrate(double a, double b, int N);

Die Implementierung von integrate() hatte einen gro├čen Nachteil: Der Name der zu integrierende Funktion f() ist im Quelltext festgeschrieben. Mit anderen Worten: M├Âchte man eine andere Funktion integrieren, hat man 2 Alternativen zur Auswahl, die beide nicht dem "Geist des Programmierens" entsprechen:

  1. Entweder man ├Ąndert die Implementierung des Integranden f() ab.
  2. Oder man ver├Ąndert alle Stellen in der Implementierung von integrate(), an denen f() aufgerufen wird durch eine andere Funktion g() und implementiert anschlie├čend diese Funktion g().

Dem Problem angemessen ist dagegen: Die Funktion integrate() erh├Ąlt als Eingabewert eine beliebige Funktion, die lediglich den geeigneten Funktionskopf haben muss; falls eine Implementierung dieser Funktion vorliegt, kann auch das n├Ąherungsweise Integral berechnet werden.

2. Beispiel: Berechnung einer Wertetabelle

Der Quelltext, der zeigt wie man die ├ťbergabe einer Funktion an eine andere Funktion realisiert, soll an einem einfacheren Beispiel demonstriert werden. Dazu sind zwei Funktionen f() und g() implementiert, die beide einen double-Wert als Eingabewert und double als R├╝ckgabewert besitzen; die Funktionsk├Âpfe sind somit identisch:

double f(double x);
double g(double x);

Dabei soll gelten:

f(x) = x2 und g(x) = x3.

Ihre Implementierungen lauten also:

// Quadrat-Fkt x^2:
double f(double x){
    return x * x;
}

// x^3:
double g(double x){
    return x * x * x;
}

Jetzt soll die Funktion entwickelt werden, die eine Wertetabelle f├╝r eine der beiden Funktionen ausgibt. Dazu implementiert man zun├Ąchst eine Funktion tableOfValues(), die dies f├╝r eine fest vorgegebene Funktion erledigt (wie oben die Funktion integrate()).

Eingabewerte sind die x-Werte a und b, zwischen denen alle Funktionswerte berechnet werden sollen; dazu wird angenommen, dass a und b ganzzahlig sind und die Funktionswerte f├╝r

a, a + 1, a + 2, ..., b - 1, b

ausgegeben werden sollen. Und da weder die Wertetabelle noch die Funktionswerte als eigenes Objekt zur├╝ckgegeben werden, ist der Datentyp des R├╝ckgabewertes gleich void. Der Funktionskopf lautet somit:

void tableOfValues(int a, int b);

Eine m├Âgliche Implementierung lautet:

void tableOfValues(int a, int b){

    if (a > b) {
        int tmp = b;
        b = a;
        a = tmp;
    }

    for (int i = a; i <= b; ++i) {
        printf("x = %d || y = %f \n", i, f(i));
    }
    return;
}

Zeile 3 bis 7: Falls die Grenzen a und b falsch herum eingegeben wurden, werden sie vertauscht, damit die Wertetabelle mit dem kleineren x-Wert beginnt.

Zeile 10: Die entscheidende Stelle f├╝r die Diskussion dieses Abschnittes ist der Aufruf der Funktion f(). Sie muss irgendwo implementiert sein und ist in dieser Version dynamisch nicht ver├Ąnderbar ÔÇô dynamisch hei├čt hier w├Ąhrend der Laufzeit des Programms.

Diese Funktion tableOfValues() muss jetzt so umgeschrieben werden, dass man die Funktion, die in Zeile 10 aufruft, als Eingabewert an tableOfValues() ├╝bergibt. Dazu wird ein Zeiger auf eine Funktion fun gesetzt; sie muss den Funktionskopf wie die Funktionen f() und g() von oben besitzen. Der Funktionskopf f├╝r tableOfValues() muss somit lauten:

void tableOfValues(double (* fun) (double), int a, int b);

Der Zeiger bildet somit den Funktionskopf der Funktion nach, die an tableOfValues() ├╝bergeben werden soll.

Und die Implementierung:

void tableOfValues(double (* fun) (double), int a, int b){

    if (a > b) {
        int tmp = b;
        b = a;
        a = tmp;
    }

    for (int i = a; i <= b; ++i) {
        printf("x = %d || y = %f \n", i, fun(i));
    }
    return;
}

Man erkennt, dass sich an der Implementierung nichts ├Ąndert: Der Aufruf der Funktion fun() erfolgt wie in der ersten Version oben (jeweils Zeile 10).

Der Aufruf von tableOfValues() k├Ânnte dann wie folgt aussehen; mit Hilfe der Variable z soll der dynamische Aufruf von f() beziehungsweise g() simuliert werden:

// z gegeben
if (z > 0) {
    tableOfValues(f, -5, 5);
} else {
    tableOfValues(g, -5, 5);
}

F├╝r positives z ergibt sich folgende Ausgabe:

x = -5 || y = 25.000000 
x = -4 || y = 16.000000 
x = -3 || y = 9.000000 
x = -2 || y = 4.000000 
x = -1 || y = 1.000000 
x = 0 || y = 0.000000 
x = 1 || y = 1.000000 
x = 2 || y = 4.000000 
x = 3 || y = 9.000000 
x = 4 || y = 16.000000 
x = 5 || y = 25.000000

Funktionsk├Âpfe f├╝r Funktionen h├Âherer Ordnung

Zuletzt wurde ein Beispiel gezeigt, in dem die Funktion tableOfValues() als Eingabewert einen Zeiger auf eine Funktion fun erh├Ąlt, wobei alle Funktionen f() mit dem folgenden Funktionskopf zul├Ąssig sind:

double f(double);

Das Argument in tableOfValues(), das den Zeiger aufnimmt, muss diesen Funktionskopf nachbilden:

void tableOfValues(double (* fun) (double), int a, int b);

Das folgende Listing zeigt die verschiedenen M├Âglichkeiten f├╝r Funktionen f() und Funktionen h├Âherer Ordnung F() ÔÇô der R├╝ckgabewert von F() ist hier irrelevant und wird immer gleich void gesetzt:

// f(): kein R├╝ckgabewert, kein Eingabewert:
void f(void);
void F(void (*fun) (void));

// f(): kein R├╝ckgabewert, Eingabewert double:
void f(double x);
void F(void (*fun) (double));

// f(): R├╝ckgabewert double, kein Eingabewert:
double f(void);
void F(double (*fun) (void));

// f(): R├╝ckgabewert double, zwei Eingabewerte (jeweils double):
double f(double x, double y);
void F(double (*fun) (double, double));

Anstelle von double k├Ânnte nat├╝rlich jeder andere elementare Datentyp stehen.