10.5. Verwendung von Rückgabewerten und Prototypen
Die Übergabe von Argumenten ermöglicht eine Kommunikation zwischen Funktionen - allerdings nur in einer Richtung. Auf diesem Wege kann bekanntlich zwar der Aufrufer den Wert von Variablen und Konstanten an die aufgerufene Funktion weitergeben, diese kann aber auf diesem Wege keine Daten zurückliefern.
Alle Funktionen, die einen Wert zurückgeben, benutzen dafür die return- Anweisung. Sie ist uns mittlerweile schon vertraut, da wir damit regelmäßig die Funktion main() beendet haben. Die Hauptfunktion eines C-Programms gibt per ANSI-Definition immer einen Wert vom Typ int zurück. Ihr Rückgabewert geht wie bei allen Funktionen an den Aufrufer zurück — in diesem Fall ans Betriebssystem. Dort kann der Rückgabewert in Batch-Dateien oder Shell-Scripts abgefragt werden, um Aufschluß über die erfolgreiche oder irreguläre Beendigung des Programms zu erhalten.
Wir wissen bereits aus dem Abschnitt 10.1, daß bei der Funktionsdefinition vor dem Namen der Datentyp des Rückgabewerts angegeben werden muß. Gibt eine Funktion keinen Wert zurück, dann sollten Sie das Schlüsselwort void verwenden: Beim Weglassen des Rückgabetyps (was aus Kompatibilitätsgründen mit Kerninghan und Ritchie leider auch unter ANSI-C noch möglich ist) geht der Compiler vom Typ int aus. Als Rückgabewerte kommen alle in C gültigen Datentypen in Frage. Soll die Basisadresse eines Arrays zurückgegeben werden, dann müssen Sie in der Definiton einen Pointer auf den Datentyp des Arrays angeben.
Im Gegensatz zu den Argumenten, von denen eine ganze Liste übergeben werden kann, akzeptiert die return-Anweisung nur einen Ausdruck. Bei Funktionen vom Typ void steht es Ihnen frei, return ohne Angabe eines Werts zu verwenden; dies ist aber nicht notwendig. Sollen von einer Funktion mehrere Daten zurückgegeben werden, dann müssen Sie sich mit zusammengesetzten Datentypen, wie z.B. Arrays oder Strukturen behelfen.
Folgendes Beispiel definiert eine Funktion zeller(), die mit Hilfe des Zeller-Algorithmus den Wochentag eines beliebigen Datums ermittelt. Als Rückgabewert liefert sie einen int-Wert in der Größe zwischen 0 und 6, wobei 0 für den Sonntag und 6 für den Samstag steht. Der Aufruf einer Funktion, die einen Rückgabewert liefert, kann innerhalb eines jeden gültigen Ausdrucks stehen — nur nicht auf der linken Seite einer Zuweisung. Entsprechend bereitet es keine Probleme, zeller() an der Argumentposition von PrintDay() aufzurufen:
/* zeller.c
* berechnet Wochentag für beliebiges Datum
*/
#include <stdio.h>
/* Funktionsprototypen */
int zeller( int iDay, int iMonth, int iYear);
void PrintDay( int iDow);
int main(void){
int iDay, iMonth, iYear;
puts("Bitte Datum eingeben, für das der "\
"Wochentag ermittelt werden soll!(tt/mm/jjjj)");
scanf("%2d%*c%2d%*c%4d", &iDay, &iMonth, &iYear);
/* PrintDay wird mit dem Rückgabewert
* von zeller() aufgerufen */
PrintDay( zeller(iDay, iMonth, iYear) );
return 0;
}
int zeller( int iDay, int iMonth, int iYear){
/* div. Hilfsvariablen definieren */
int iDivYear, iModYear, iResult;
if( iMonth < 3 ){
iMonth += 10;
iYear--;
}
else
iMonth -= 2;
iModYear = iYear % 100;
iDivYear = iYear / 100;
iResult = (26 * iMonth - 2)/ 10 + iDay + iModYear +\
(iModYear/4) + (iDivYear/4) —
2 * iDivYear;
return(iResult % 7);
}
void PrintDay( int iDow ){
switch( iDow ){
case 0: puts("Sonntag");
break;
case 1: puts("Montag");
break;
case 2: puts("Dienstag");
break;
case 3: puts("Mittwoch");
break;
case 4: puts("Donnerstag");
break;
case 5: puts("Freitag");
break;
case 6: puts("Samstag");
break;
default: puts("Ungültiger Wochentag!");
}
}
Unser letztes Beispiel zu den Rückgabewerten von Funktionen behandelt das Problem "verwaisten" Speichers. Ein solcher kann entstehen, wenn ein Zeiger zurückgegeben wird. Voraussetzung dafür ist, daß die aufgerufene Funktion mit Hilfe von malloc() Speicher anfordert, in dem sie ihre Daten ablegt. Speicherplatz, den die dynamische Speicherverwaltung zur Verfügung stellt, bleibt auch nach dem Verlassen der Funktion, in der er angefordert wurde, reserviert — und zwar so lange, bis er explizit mit free() wieder freigegeben wird. Im Gegensatz dazu sind Pointer, mit denen darauf zugegriffen werden kann, dann nicht mehr vorhanden. Versäumt es nun der Programmierer, den Wert dieser Pointer in einer Variablen des Aufrufers abzuspeichern, dann gibt es keine Möglichkeit mehr, auf den "ge-malloc-ten" Speicher zuzugreifen: Die automatische Pointer-Variable der gerufenen Funktion wurde ja nach deren Beendigung zerstört:
/* repl.c
* ersetzt Zeichenketten in einem Input-
* String.
* In main() wird Speicher freigegeben,
* den die Funktion replace() angefordert hat.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define SIZE 128
/* Funktionsprototyp */
char *replace(char *szInpString,
const char *szSuchbegriff,
const char *szErsatz );
/*******************************************
* Funktion main()
*******************************************/
int main(int argc, char **argv){
char szInpString[SIZE], *pszTemp;
/* Bei falschem Aufruf Info ausgeben */
if( argc != 3){
puts("Aufruf: repl String1 String2");
puts("\talle Vorkommnisse von String1 werden");
puts("\tin der Eingaben-Zeichenkette durch");
puts("\tString2 ersetzt\n");
exit(EXIT_FAILURE);
}
while( fgets( szInpString, SIZE, stdin ) != NULL){
if((pszTemp=strchr(szInpString, '\n')) != NULL)
*pszTemp = '\0';
/*-----------------> Achtung <-----------------
* Würde der Rückgabewert von replace() nicht in
* pszTemp gespeichert, sondern gleich an printf()
* übergeben - printf("%s\n", replace(....)) —
* dann könnte der in replace() mit malloc() ange-
* forderte Speicher nicht mehr freigegeben oder
* sonstwie weiterverwendet werden!
*/
pszTemp = replace(
szInpString, argv[1], argv[2] );
printf("%s\n", pszTemp);
free(pszTemp);
pszTemp = NULL;
}
return 0;
}
/***********************************************
* Funktion replace
*
* Argumente:
* Zeiger auf die Zeichenkette, in der ersetzt
* werden soll, Zeiger auf den Suchbegriff und
* Zeiger auf den String, mit dem dieser ersetzt
* werden soll.
* Rückgabewert:
* Zeiger auf die Zeichenkette, die
* Ersetzungen enthält.
*********************************************/
char *replace(char *szString,
const char *szSuchBegriff,
const char *szErsatz ){
char *pszResult = (char *) malloc(
SIZE *sizeof(char));
char *pszTemp = (char *) malloc(
SIZE * sizeof(char));
char *pszRest;
/* speichert Längen-Differenz zwischen
* szErsatz und szSuchbegriff */
int iDiff = strlen(szErsatz)-
strlen(szSuchBegriff);
int iAddMem = SIZE;
if(pszResult == NULL || pszTemp == NULL){
puts("Kein Speicher mehr frei!");
exit(EXIT_FAILURE);
}
strcpy( pszResult, szString );
/* Schleife läuft so lange, bis strstr()
* den Suchbegriff in pszResult findet.
*/
while( (pszRest = strstr(
pszResult, szSuchBegriff)) != NULL){
/* Folgender if-Block beschäftigt sich nur
* damit, mehr Speicher anzufordern, falls
* pszResult überzulaufen droht. Dies ist
* natürlich nur dann der Fall, wenn szErsatz
* länger als szSuchBegriff ist!
*/
if( strlen(pszResult) + iDiff >= iAddMem){
pszResult = (char *) realloc(
pszResult, iAddMem+=SIZE);
pszTemp = (char *) realloc(pszTemp, iAddMem);
if(pszResult == NULL || pszTemp == NULL){
puts("Kein Speicher mehr frei!");
exit(EXIT_FAILURE);
}
}
/* Den Ersetzvorgang erläutert die
* folgende Grafik */
strcpy( pszTemp, szErsatz );
strcat( pszTemp,
(pszRest + strlen(szSuchBegriff)) );
*pszRest = '\0';
strcat( pszResult, pszTemp);
}
free( pszTemp );
return(pszResult );
}
Das Programm repl ist in erster Linie für den Einsatz als Filter gedacht, also nach dem Muster cat datei|repl string1 string2 bzw. unter DOS type datei|repl string1 string2, wobei die Ausgabe in eine Datei umgeleitet werden kann. Um Zeichenketten von der Standardeingabe einzugeben, müssen Sie zum Schluß EOF eingeben, damit fgets() den Wert NULL liefert.
Zur regelkonformen Verwendung von Funktionen bedarf es neben der richtigen Definition und des passenden Aufrufs schließlich noch der Deklaration über Prototypen. Bestimmt ist Ihnen schon aufgefallen, daß in unseren Beispielen zwischen den #include-Anweisungen und dem Beginn von main() die Namen unserer selbstdefinierten Funktionen samt Rückgabetyp und Parameterliste zu finden waren.
Die Deklaration einer Funktion ist immer dann notwendig, wenn sie im Quellcode bereits vor ihrer Definition aufgerufen wird. Im Beispiel repl.c erfolgt der Aufruf von replace() bereits in main(), die Defintion von replace() findet aber erst im Anschluß an main() statt.
Wie ganz generell bei Deklarationen, wird auch im Fall von Funktionen kein Speicherplatz vergeben. Die Deklarationen benutzt der Compiler dazu, beim Aufruf einer Funktion zu überprüfen, ob die Anzahl und der Datentyp der Argumente korrekt ist. Bei Funktionen ohne Rückgabewert muß der Compiler zusätzlich sicherstellen, daß sie nicht innerhalb eines Ausdrucks aufgerufen werden, also z.B. als rvalue in einer Zuweisung auftreten.
Gegenüber Kerninghan und Ritchie erlaubt ANSI-C eine strengere Typenprüfung durch Verwendung von Prototypen. Deklarationen nach der alten Methode — die unter ANSI-C immer noch erlaubt sind - beschränken sich auf die Angabe des Rückgabetyps und des Namens. Die Deklaration unserer Funktion replace() würde nach dem alten "de facto"-Standard folgendermaßen aussehen:
char *replace();
Der ANSI-Standard empfiehlt aber dringend die Verwendung von Prototypen. Diese enthalten neben dem Typ des Rückgabewerts noch die Datentypen aller Argumente. Wenn eine Funktion keine Argumente nimmt, dann sollte für den Typ des Arguments das Schlüsselwort void verwendet werden. Hier ein weiteres Beispiel für einen Prototypen der Funktion replace():
char *replace(char *, const char *,const char *);
Die zusätzliche Angabe von Variablennamen — wie in repl.c — ist optional. Sie bietet aber den Vorteil, daß der Compiler bei eventuellen Fehlermeldungen den Namen des Arguments angeben kann, bei dem er einen Fehler entdeckt hat.
Es bleibt noch die Frage, wie der Compiler den jeweiligen Typ überprüfen kann, wenn wir Funktionen verwenden, die wir nicht selbst definiert haben. Dies betrifft in erste Linie die Bibliotheksfunktionen: Ihre Prototypen befinden sich in den diversen Header-Dateien, also jenen mit der Endung .h. Durch das Einfügen dieser Dateien mit Hilfe der #include-Anweisung werden dem Compiler die Prototypen dieser Funktionen zugänglich gemacht.