C-Tutorial (C oder C++, Vorwort, Installation, Kapitel 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)

Funktionen, cast-Operator

10.1. Unterprogramme (Funktionen) erstellen

Immer wieder kommt es vor, dass Code mehrmals benötigt wird (Stichwort Wiederverwendbarkeit). Nicht nur um Tipparbeit zu sparen, kann man diesen in einem Unterprogramm zusammenfassen. Unterprogramme bezeichnet man in C als Funktionen. Die Funktion kann dann bei Bedarf aufgerufen werden.

Für das Endergebnis (aus Anwendersicht!) ist es gleichgültig, ob Programmierzeilen in einer Funktion untergebracht werden, dieses dann aufgerufen wird, oder ob dieselben Zeilen "normal" im Quellcode stehen. Den großen Unterschied macht es aber für Sie als Programmierer. Funktionen helfen enorm, den Quellcode zu strukturieren, die Übersicht zu bewahren, und erhöhen - wie erwähnt - die Wiederverwendbarkeit Ihres Codes. Eine Funktion erledigt üblicherweise eine klar abgegrenzte Teilaufgabe. Nehmen Sie printf() als Beispiel her. Wie die Ausgabe am Bildschirm - technisch im Hintergrund - funktioniert, hat uns nicht gekümmert. Wird es auch in Zukunft nicht. Das hat irgendjemand einmal definiert. Wir greifen dabei auf bestehendes Stück Code zurück und verwenden es immer wieder. Der Zweck und die Aufgabe sind auch klar abgegrenzt (v.a. Text und Werte von Variablen in einem bestimmten Format ausgeben).

In C lassen sich Funktionen mit und ohne Rückgabewert unterscheiden. Sie müssen bei der Funktionsdeklaration und -definition den Rückgabetyp explizit angeben. Die Daten, die die Funktion zurückliefert, müssen von diesem Typ sein. Geben Sie void an, gibt es keinen Rückgabewert.

Ein typisches Anwendungsbeispiel für eine Funktion wäre eine mathematische Berechnung. Die Daten, die zur Berechnung nötig sind (quasi der Input; erinnern Sie sich an das EVA-Prinzip: Eingabe - Verarbeitung - Ausgabe), übergeben Sie an die Funktion als Parameter. Das waren in den bisherigen Beispielen die Werte, die zwischen den Klammern () standen, z.B. der Formatstring und die Variablen, die printf() ausgeben soll. Mit den gegebenen Parametern führt die - hier - mathematische Funktion dann die Berechnung durch, und liefert das Ergebnis als Rückgabewert zurück.

Ein Beispiel für eine Funktion ist getchar(), das ein Zeichen von der Tastatur einliest und zurückliefert. Testen Sie folgendes Beispiel. Es wird mit getchar() ein Zeichen eingelesen und in eingabe gespeichert. Danach werden das Zeichen und der ASCII-Code ausgegeben. Das Programm läuft in einer Endlosschleife, die Sie durch Drücken der Escape-Taste (ASCII-Code 27) und Enter beenden können.

#include <stdio.h>
 
int main()
{
  int eingabe;   /* getchar() liefert einen Integer zurueck */
 
  do
  {
    eingabe = getchar();
    printf ("Taste %c wurde gedrückt. ASCII-Code: %d\n",eingabe,eingabe);
  }
  while (eingabe != 27);
 
  return 0;
}

Das Programm hat - rein funktionell - zwei Schönheitsfehler: Welche Taste gedrückt wurde, wird ausgegeben (Echo), und vor allem muss jedesmal danach mit Enter bestätigt werden. Um die Arbeitsweise von Funktionen zu verstehen, ist das aber unerheblich. Eine elegantere Lösung bietet die Funktion getch(), die aber nicht zum C-Standard gehört und die - ebenfalls nicht-standardisierte - Headerdatei conio.h benötigt. Wenn Ihr Compiler eine conio.h-Datei zur Verfügung stellt, können Sie auch das nachfolgende Beispiel ausprobieren. Das gilt vor allem unter Windows. Bedenken Sie aber, dass dieser Code nicht portabel ist. Interessierte finden hier eine Lösung des getch()-"Problems".

#include <stdio.h>
#include <conio.h>
 
int main()
{
  char eingabe;
 
  do
  {
    eingabe = getch();
    printf ("Taste %c wurde gedrückt. ASCII-Code: %d\n",eingabe,eingabe);
  }
  while (eingabe != 27);
 
  return 0;
}

Interessant ist hierbei die Zeile:

eingabe = getchar();

bzw.

eingabe = getch();

Folgendes passiert: getchar() (Gleiches gilt analog für getch()) wird aufgerufen und ausgeführt. Nach der Ausführung liefert getchar() einen Wert zurück, der in eingabe gespeichert wird. Der Rückgabewert kann auch anders weiterverarbeitet werden, etwa indem er direkt an eine andere Funktion als Parameter übergeben wird (siehe unten).

Beim Erstellen (Definieren) einer Funktion müssen Sie sich an folgende Syntax halten:

Rückgabetyp Funktionsname (Parameterliste)
{
  Anweisung1;
  Anweisung2;
  ...

  [return Wert;]
}

Ganz zu Beginn steht der Datentyp des Wertes, der zurückgeliefert werden soll (Rückgabewert bzw. dessen Typ = Rückgabetyp). Soll die zurückgelieferte Zahl eine Ganzzahl sein, muss hier dementsprechend char, int, short oder long int stehen, bei einer Fließkommazahl etwa float oder double.

In der einfachsten Form, in der die Funktion nur Anweisungen ausführt und keinen Wert zurückliefert, wird der Typ void angegeben. Nach dem Rückgabetyp folgt der Name (Bezeichner) der Funktion, über den Sie die Funktion aufrufen können. Nach dem Bezeichner folgt die Parameterliste in Klammern. Diese gibt an, welche Werte (Daten) an die Funktion übergeben werden müssen. Wichtig ist, dass die Klammern stehen bleiben, auch wenn keine Parameter verwendet werden! Um dies zu signalisieren, kann zwischen die Klammern auch void geschrieben werden, wie ich auch schon im Hello-World-Beispiel gezeigt habe.

Diese erste Zeile der Funktion bezeichnet man als Funktionskopf. Dem Funktionskopf folgt der Anweisungsblock, in dem die Anweisungen stehen, die die Funktion ausführen soll. Von entscheidender Bedeutung ist die return-Anweisung, die die Funktion sofort beendet. Sie sorgt dafür, dass ein Wert zurückgeliefert wird. Bei void-Funktionen (liefern keinen Rückgabewert) kann auf return verzichtet werden. return kann dennoch in folgender Form vorkommen:

return;

Praktische Anwendung findet das bei void-Funktionen, die bei (Nicht-)Erfüllen einer bestimmten Bedingung (die die Funktion selbst prüft) frühzeitig beendet werden. Kurz gesagt: Wenn return folgt, ist die Ausführung der Funktion zu Ende.

Im nächsten Beispiel soll eine einfache Funktion zwei Zahlen addieren. Um der Funktion bekannt zu geben, welche Zahlen addiert werden sollen, werden beide Zahlen als Parameter angegeben. Die Parameter werden zwischen die Klammern geschrieben und jeweils durch Beistriche getrennt. Damit der Compiler die Parameterangabe akzeptiert, müssen Sie bei der der Definition (und Deklaration) der Funktion angeben, an welcher Stelle ein Wert welches Typs folgt. Bei der Definition muss auch ein Bezeichner angegeben werden, über den Sie die Variable ansprechen können.

Unsere Addieren-Funktion sieht nun folgendermaßen aus:

long int addieren (int a, int b)
{
  return (a + b);
}

Über die Variable a (übrigens eine lokale Variable!) kann auf den Wert zugegriffen werden, der als erster Parameter angegeben wurde. b bezeichnet den zweiten Parameter. Beide Parameter müssen Ganzzahlen vom Typ int sein.

Nachdem die Funktion ins Programm eingebaut wurde (siehe nächster Quellcode), kann Sie aufgerufen werden. Beispiel:

ergebnis = addieren (3,5);

Die Variable ergebnis erhält nach dieser Anweisung den Wert 8. Wie bereits oben kurz erwähnt, kann der zurückgelieferte Wert auch als Funktionsparameter verwendet werden. Ein Beispiel:

ergebnis = addieren (addieren(3,5) , 10); 

Hier passiert Folgendes: Die Funktion addieren() wird mit den Parametern 3,5 aufgerufen und das Ergebnis (8) wird weiterverwendet und als erster Parameter in die "äußere" addieren()-Funktion eingesetzt. Somit erhält ergebnis den Wert 18. Gleichwertig zu dieser Anweisung ist:

ergebnis = addieren (8 , 10);

Im nächsten Beispiel wurde die Funktion addieren() in ein Programm eingebaut:

#include <stdio.h>
 
long int addieren (int a, int b);
 
int main()
{
  long int ergebnis = addieren (1000, 4439);
 
  printf ("1000 + 4439 = %d\n", ergebnis);
 
  return 0;
}
 
long int addieren (int a, int b)
{
  return (a + b);
}

Möglicherweise sind Sie über die zweite Programmierzeile etwas verwundert. Lassen Sie diese versuchshalber weg, meldet Ihnen der Compiler einen Fehler. Der Grund dafür ist folgender: Eine aufzurufende Funktion muss entweder über der Funktion im Quellcode stehen, die diese aufruft, oder es erfolgt eine Funktions-Deklaration wie es hier der Fall ist. Die Zeile bezeichnet man als Funktionsprototyp.

Würde die Funktionsdefinition oberhalb der main()-Funktion stehen, wäre kein Prototyp nötig. Dieser ist nichts anderes als der Funktionskopf! Funktionsprototypen stehen am Anfang des Programmes bzw. am besten, in einer Include-Datei. Erinnern Sie sich: In einer Headerdatei stehen nur Deklarationen, dazu gehören auch Funktionsprototypen. Diese werden mittels #include eingefügt. Übrigens ist die Angabe der Variablenbezeichner bei der Deklaration nicht Pflicht! Ebenso gültig wären folgende zwei Versionen:

#include <stdio.h>
 
long int addieren (int, int);   /* nur Datentypen, keine Bezeichner */
 
int main()
{
  long int ergebnis = addieren (1000, 4439);
 
  printf ("1000 + 4439 = %d\n", ergebnis);
 
  return 0;
}
 
long int addieren (int a, int b)
{
  return (a + b);
}
#include <stdio.h>
 
/* addieren() wird oberhalb der Funktion definiert, die sie aufruft;
   in diesem Beispiel ist das main() */
 
long int addieren (int a, int b)
{
  return (a + b);
}
 
int main()
{
  long int ergebnis = addieren (1000, 4439);
 
  printf ("1000 + 4439 = %d\n", ergebnis);
 
  return 0;
}

Abschließend möchte ich anhand des Beispiels die Verwendung einer void-Funktion demonstrieren. Die Funktion funktioniert ähnlich wie die addieren()-Funktion oben. Mit einem feinen Unterschied: Nun wird der Wert nicht zurückgegeben, sondern am Bildschirm angezeigt. Die Funktion zeigeAddition() liefert gar keinen Wert zurück. Sie können also dann auch keinen Rückgabewert in Folge verarbeiten. Das muss nicht unbedingt ein Nachteil sein. Manchmal ist es einfach nicht nötig.

#include <stdio.h>
 
void zeigeAddition (int, int);   /* Funktionsprototyp */
 
void zeigeAddition (int a, int b)
{
  printf ("%d", (a + b));
}  /* kein return noetig! */
 
int main()
{
  printf ("1000 + 4439 = ");
  zeigeAddition (1000, 4439);   /* Funktion aufrufen */
  printf ("\n");   /* nur Zeilenumbruch */
 
  return 0;
}

10.2. Explizite Typumwandlungen mit dem cast-Operator

Mithilfe des Cast-Operators ( ) (nicht zu verwechseln mit dem Klammer-Operator!) lassen sich explizite Typumwandlungen (Typecasts) durchführen. Daneben gibt es implizite Typumwandlungen, die automatisch vom Compiler durchgeführt werden. Wenn Sie etwa einer long-Variable einen int-Wert zuweisen, wird dieser automatisch und verlustfrei (long ist ja bekanntlich größer) umgewandelt (= implizite Typumwandlung).

Ein praktisches Beispiel ist etwa die Division von Ganzzahlen, die ebenso wie die von Fließkommazahlen mit dem Operator / nur dann durchgeführt werden kann, wenn mindestens einer der beiden Operanden eine Fließkommazahl ist. Hat man nun zwei Operanden - etwa - vom Ganzzahlentyp int, so ist diese Division natürlich nicht durchführbar. Eine einfache Lösung wäre eine explizite Typumwandlung, die sich allgemein folgendermaßen anwenden lässt:

(neuer Typ) Ausdruck

Der Ausdruck ist üblicherweise eine Variable, kann jedoch auch eine Konstante (somit ein Wert, der direkt in den Quelltext geschrieben wurde) sein. Das letzte Beispiel ließe sich wie folgt realisieren:

ergebnis = (float) divident / (float) divisor;

Folgende Annahmen gelten dabei: ergebnis sei vom Typ float. divident und divisor seien vom Typ int. Die Cast-Konstrukte sorgen für die Typumwandlungen. Zweimal werden int-Werte zu float konvertiert. Dazu wird der Typ, in den umgewandelt werden soll (Zieltyp), in Klammer vor die Variable gesetzt. Es wäre auch ausreichend gewesen, nur divident ODER divisor umzuwandeln.

Achten Sie bei expliziten Datentypumwandlungen darauf, dass Konvertierungsfehler auftreten können. Das gilt insbesonders für Umwandlungen von einem größeren in einen kleineren Datentyp. Da der Zieltyp den Originalwert nicht aufnehmen kann, gehen dabei Daten verloren. Rundungsfehler können bei der Konvertierung von einem genaueren in einen ungenaueren Gleitkommatyp auftreten, etwa von double zu float. double besitzt eine höhere Genauigkeit, kann also mehr Nachkommastellen verlustfrei darstellen.

Vorheriges Kapitel Nächstes Kapitel