10.4. Übergabe von Argumenten an eine Funktion

Ein besonderer Vorteil von Funktionen ist, daß ihr Code und ihre Daten nur für sie selbst zugänglich sind: Da stellt sich natürlich die Frage, wie Funktionen untereinander Daten austauschen sollen! Natürlich würden sich dafür globale Variablen anbieten, die ja stets für alle Funktionen verfügbar sind. Wir haben aber die Gründe gegen den häufigen Gebrauch globaler Variablen hinreichend dargestellt und darauf hingewiesen, daß dies nicht mit dem Konzept modularer Programmierung verträglich ist.

Nachdem der direkte Zugriff auf die Variablen einer Funktion von außen nicht möglich ist, muß sich der Datenaustausch über den einzig legitimen Zugriff auf den Code einer Funktion vollziehen: den Funktionsaufruf. Wenn eine Funktion eine andere aufruft, dann kann sie ihr bei dieser Gelegenheit Variablen als Argumente übergeben.

Dabei ist es zulässig, mehrere Argumente auf einmal zu übergeben. Damit eine Funktion überhaupt Argumente entgegennehmen kann, muß sie ihrerseits Variablen definieren, die den Wert dieser Argumente aufnehmen. Diese Variablen sind die Parameter einer Funktion. Sie haben die gleichen Eigenschaften wie alle anderen automatischen Variablen, werden aber nicht im Funktionskörper, sondern in der Parameterliste definiert.

Folgendes Beispiel definiert die Funktion pr_upper(), die ein Argument vom Typ char nimmt und dieses unter Berücksichtigung der deutschen Sonderzeichen als Großbuchstaben ausdruckt:


/* pr_upper.c */

#include <stdio.h>
#include <ctype.h>


/* Prototyp für pr_upper() */
void pr_upper(char c2upper);

int main(void){

 int iCount = 0;
 char szMsg[] = "pr_upper übersetzt "\
	"Kleinbuchstaben in Großbuchstaben\n";

 while( szMsg[iCount] != '\0' )
	 /* pr_upper() für jedes Zeichen des
	  * Strings szMsg aufrufen */
	    pr_upper( szMsg[iCount++]);
return 0;
 }

void pr_upper( char c2upper ){
 /* Die Parameterliste von pr_upper()
  * definiert eine Variable vom Typ char.
  * Diese steht in der Funktion pr_upper()
  * als lokale Variable zur Verfügung
  */
 if( c2upper >= 'a' && c2upper <= 'z' )
	putchar( c2upper - ('a' -'A'));
 else if( c2upper == 'ä')
	 putchar('Ä');
 else if( c2upper == 'ö')
	 putchar('Ö');
 else if( c2upper == 'ü')
	 putchar('Ü');
 else if( c2upper == 'ß')
	 printf("SS");
 else
	putchar( c2upper );
 }

Die Arbeitsweise von pr_upper.c ist relativ einfach: Die Funktion main() definiert die Zeichenkette szMsg und ruft in einer while-Schleife für jedes Zeichen aus szMsg die Funktion pr_upper() auf, um es — wenn möglich — als Großbuchstaben auszudrucken. Die Funktion pr_upper() selbst nimmt keine Änderung an der Variablen vor, die ihr als Argument übergeben wird. Wenn wir pr_upper() so modifizieren wollen, daß sie das übergebene Zeichen permanent in einen Kleinbuchstaben umwandelt, dann werden wir gleich mit einem überraschenden Ergebnis konfrontiert.

Wertübergabe vs. Adreßübergabe

Hier nun die Version von pr_upper, die allen Kleinbuchstaben den Wert des passenden Großbuchstabens zuweisen sollte:


/* ucase.c
 * ucase() soll das übergebene Zeichen in
 * einen Großbuchstaben umwandeln, falls ein
 * Kleinbuchstabe vorliegt. Da aber ucase()
 * das Argument über einen "call by value"

 * erhält, ändert sich der Wert der ursprüng-
 * lichen Variablen szMsg[Count] nicht!
 */

#include <stdio.h>

void ucase(char c2upper);

int main(void){

 int iCount = 0;
 char szMsg[] = "ucase übersetzt Kleinbuchstaben"\
	 " in Großbuchstaben\n";

 while( szMsg[iCount] != '\0' )
	 ucase( szMsg[iCount++]);

 printf("%s", szMsg);
 return 0;
 }

void ucase( char c2upper ){

 if( c2upper >= 'a' && c2upper <= 'z')
	c2upper -= ('a' -'A');
 else if( c2upper == 'ä')
	c2upper = 'Ä';
 else if( c2upper == 'ö')
	c2upper = 'Ö';
 else if( c2upper == 'ü')
	c2upper = 'Ü';
 }

Wenn sie dieses Beispielprogramm ausführen, dann werden Sie sich wahrscheinlich fragen, was Sie falsch gemacht haben: Die printf()-Anweisung in main() gibt nämlich die Zeichenkette szMsg auch nach der erwarteten Umwandlung ohne irgendwelche sichtbaren Änderungen am Bildschirm aus! Die Ursache dafür liegt in der Art und Weise, wie C Argumente an Funktionen übergibt. Bis auf wenige Ausnahmen erfolgt diese als Wertübergabe (engl. "call by value" ). Dabei erhält die aufgerufene Funktion eine Kopie der ursprünglichen Variablen. Alle Änderungen, die anschließend an den Parameter-Variablen vorgenommen werden, machen sich nur im Gültigkeitsbereich der aufgerufenen Funktion bemerkbar und gehen nach deren Beendigung verloren. Die als Argumente benutzten Variablen des Aufrufers bleiben deshalb unverändert!

Wenn Sie wollen, daß die aufgerufene Funktion Zugriff auf die Original-Variablen hat und deren Werte verändern kann, dann müssen Sie ihr die Speicheradressen dieser Variablen übergeben. Dieser Vorgang heißt mit dem englischen Fachausdruck "call by reference" . Die folgende Version von ucase.c benutzt diese Form der Argument-Übergabe, um sicherzustellen, daß ucase() die einzelnen Elemente der ursprünglichen Zeichenkette verändern kann:


/* ucase1.c
 * benutzt einen "call by reference"
 */

#include <stdio.h>

#include <ctype.h>     /* für islower()*/

void ucase(char *pc2upper);

int main(void){

 int iCount = 0;
 char szMsg[] = "ucase übersetzt "\
	"Kleinbuchstaben in Großbuchstaben\n";

 while( szMsg[iCount] != '\0' )
	/* Um die Adresse von szMsg[iCount] zu
	 * übergeben, benutzen wir den Adreß-
	 * operator
	 */
	 ucase( &szMsg[iCount++]);

 printf("%s", szMsg);
 return 0;
 }

/* ucase() erhält als Argument die Adresse
 * einer char-Variablen. Diese läßt sich am
 * besten in einem Pointer auf char speichern
 */
void ucase( char *pc2upper ){

 /* Mit Hilfe des Inhaltsoperators können wir
  * uns auf den Wert des Objekts beziehen, auf
  * das pc2upper zeigt. pc2upper zeigt auf
  * die Speicheradresse von szMsg[iCount].
  */
 if( islower( *pc2upper ))
	*pc2upper -= ('a' -'A');
 else if( *pc2upper == 'ä')
	*pc2upper = 'Ä';
 else if( *pc2upper == 'ö')
	*pc2upper = 'Ö';
 else if( *pc2upper == 'ü')
	*pc2upper = 'Ü';
}

Wozu brauchen wir denn plötzlich einen Parameter vom Typ "Pointer auf char" in der Funktion ucase()? Da in main() die Funktion ucase() mit dem Argument &szMsg[iCount++] — also der Adresse dieser char-Variablen - aufgerufen wird, benötigen wir auf seiten von ucase() eine Variable, die diesen Wert aufnehmen kann. Was würde sich dazu besser eignen als ein Pointer auf char? In der Funktion ucase() sind die Variablen szMsg[iCount] auch weiterhin unbekannt, weil sich deren Gültigkeit ja auf main() beschränkt. Trotzdem haben wir Zugriff auf deren Werte, weil wir einen Pointer auf diese Datenobjekte besitzen: Diesen müssen wir nur dereferenzieren, um ihnen einen neuen Wert zuzuweisen.

Arrays und Pointer als Argumente

Um gleich ein ganzes Array an die aufgerufene Funktion weiterzureichen, müssen Sie beim Funktionsaufruf den Namen dieses Arrays angeben. Nun wissen wir, daß der bloße Name eines Arrays ein Zeiger auf sein erstes Element ist: Dies ist der Grund, warum bei Feldern automatisch die Übergabe eines Adreßwerts, also ein "call by reference" erfolgt und die aufgerufene Funktion nie eine Kopie des ganzen Arrays erhält.

Es überrascht wenig, wenn bei dieser Gelegenheit auch in der Parameterliste der aufgerufenen Funktion ein Array angegeben wird. Dabei ist es Ihnen freigestellt, ob Sie dort die Größe des Arrays angeben oder nicht. Folgendes Beispiel implementiert eine Funktion str_lower(), die eine ganze Zeichenkette auf Kleinbuchstaben setzt:


/* str_lower.c */

#include <stdio.h>

#include <ctype.h>

void str_lower(char szConv[]);

int main(void){

char szMsg[] = "str_lower() setzt Zeichenketten"\
	" auf Kleinbuchstaben\n";

 str_lower( szMsg );

 printf("%s", szMsg);
 return 0;
 }


/* Die Angabe der Array-Größe ist bei
 * Parametern optional
 */
void str_lower(char szConv[]){

 int iCount = 0;

 while( szConv[iCount] != '\0' ){

	if( szConv[iCount] >= 'A' && szConv[iCount] <= 'Z')
		szConv[iCount] += ('a' -'A');
	else if( szConv[iCount] == 'Ä')
		 szConv[iCount] = 'ä';
	else if( szConv[iCount] == 'Ö')
	 	szConv[iCount] = 'ö';
	else if( szConv[iCount] == 'Ü')
		szConv[iCount] = 'ü';
	iCount++;
	}
 }

Diese Form der Zeichenketten-Umwandlung ist mit Sicherheit effizienter als jene in den vorgehenden Beispielen, wo für jedes Zeichen ein Funktionsaufruf erfolgte. Die Werte der ursprünglichen Zeichenkette werden in str_lower() verändert, da beim Funktionsaufruf eine Adresse übergeben wurde. Anstelle des Parameters char szConv[] können Sie natürlich auch hier einen Zeiger verwenden. Für den Compiler ist das einerlei: Ob ein Array mit oder ohne Größenangabe, ob Pointer — der Compiler wandelt bei Parametern Arrays ohnehin in Pointer um.

Da Arrays automatisch mit einem "call by reference

"

übergeben werden, fällt bei ihnen der Schutz vor versehentlichen Änderungen weg, den ein "call by value" für die Variablen des Aufrufers bietet. Wenn Sie allerdings sicherstellen wollen, daß die aufgerufene Funktion keine Änderungen an den Werten des ursprünglichen Arrays vornehmen kann, dann sollten sie als Parameter einen Pointer auf konstante Objekte definieren:


.
.
 str_lower( szMsg );

 printf("%s", szMsg);
 return 0;
 }

/* Die Elemente von szMsg können nun
 * nicht verändert werden */
void str_lower(const char *pszConv){
.
.

Liegt Ihnen allerdings daran, daß zwar die einzelnen Objekte, nicht aber der Wert des Pointers selbst verändert werden kann, dann müssen Sie einen konstanten Pointer vom Typ char definieren:


/* Verhält sich wie ein Array. Z.B. wäre
 * pszConv++ nicht möglich */
void str_lower(char *const pszConv){

Der Vollständigkeit halber sei noch erwähnt, daß bei Pointern natürlich immer eine Adresse weitergereicht wird und daher kein "call by reference" erfolgt. Zusätzliche Sorgfalt ist bei dieser Art von Argumenten erforderlich, sobald die dynamische Speicherverwaltung ins Spiel kommt. Ein Beispiel dazu finden Sie in Abschnitt 10.5. Pointer sind als Funktionsargumente besonders interessant, wenn Sie benutzerdefinierte Datentypen (Strukturen und Unionen) übergeben wollen: Um solche Datenobjekte weiterzureichen, verwenden geübte C-Programmierer fast ausschließlich Zeiger.

Kommandozeilen-Argumente für main()

An die Funktion main() können ebenso Argumente übergeben werden wie an jede beliebige "normale

"

Funktion. Im Unterschied zu anderen Funktionen wird main() aber gewöhnlich nicht im Programm selbst aufgerufen, sondern automatisch beim Programmstart ausgeführt. Die meisten Betriebssysteme bieten allerdings einem Programm zur Laufzeit eine Umgebung an, die die Übergabe von Kommandozeilen-Argumenten erlaubt; diese kann main() dann als Parameter übernehmen.

Es steht dem Programmierer allerdings nicht frei, main() nach seinem Gutdünken mit Parametern auszustatten. Sollen die Kommandozeilen-Argumente ignoriert werden, dann hat main() die Form int main(void). Andernfalls kennt sie zwei weitere Parameter: Der erste ist vom Typ int und speichert die Zahl der übergebenen Argumente. Diese werden im zweiten Parameter abgespeichert, einem Array aus Pointern auf char. Nach welchen Kriterien eingegebene Kommandos zerlegt werden und was als Trenner zwischen Argumenten gilt, hängt vom jeweiligen System ab. Im allgemeinen ist es aber z.B. möglich, daß Argumente Leerzeichen enthalten, wenn sie in Anführungszeichen oder Hochkommas gesetzt werden.

Für das erste Argument von main() hat sich der Name argc (für "argument count") eingebürgert, für das zweite argv (für "argument vector"). Es steht Ihnen natürlich frei, andere Namen zu vergeben, gute Gründe dafür gibt es aber nicht.

Das erste Kommandozeilen-Argument (also argv[0]) erhält stets den Namen des Programms: Deshalb hat argc zumindest immer den Wert 1. Wenn an das Programm beim Aufruf Argumente übergeben wurden, dann muß deshalb der Wert von argc größer sein als 1. Unter DOS und OS/2 umfaßt der Programmname Laufwerk und Pfad: Dies ist sehr praktisch, falls man während der Laufzeit das Programmverzeichnis kennen muß (um z.B. eine Konfigurationsdatei zu verwalten). Folgendes Beispiel implementiert einen einfachen Kommandozeilen-Rechner, der neben den vier Grundrechenarten auch die Modulo-Division beherrscht:


/* calc.c */

#include <stdio.h>
#include <stdlib.h>

void usage(char *pszProgName);

int main( int argc, char *argv[]){

long lResult;

 /* Wenn nicht genau 3 Argumente angegeben
  * wurden, ab zur Fehlerbehandlung. Die Zahl
  * 4 ergibt sich aus den 3 Argumenten plus
  * dem Programmnamen (argv[0])
  */
 if( argc != 4 )
	 usage(argv[0]);

 /* Wenn der 2. Operand den Wert null hat,
  * ebenfalls zur Fehlerbehandlung */
 if( atol(argv[3]) == 0)
	 usage(argv[0]);

 /* argv[2][0] steht für das 1.Zeichen des
  * 2.Arguments. Dabei soll es sich um den
  * Operator handeln
  */
 switch( argv[2][0]){
 /* Die beiden Operanden liegen nur als Zeichenketten
  * vor. Deshalb müssen sie in einen numerischen Wert
  * (vom Typ long) umgewandelt werden
  */
	case '+':
		 lResult = atol(argv[1]) + atol(argv[3]);
		 break;
	case '-':
		lResult = atol(argv[1]) —

 atol(argv[3]);
		 break;
	case '*':
		lResult = atol(argv[1]) * atol(argv[3]);
		break;
	case '/':
		lResult = atol(argv[1]) / atol(argv[3]);
		break;
	case '%':
	 	lResult = atol(argv[1]) % atol(argv[3]);
	 	break;
	default: usage(argv[0]);
	 }

 printf("Ergebnis: %ld\n", lResult);

 return 0;
}

void usage(char *pszProgName){

 /* An usage() wird als Argument der Name des
  * Programms übergeben. Das hat den Vorteil,
  * daß der Hilfetext auch dann noch stimmt,
  * wenn der Anwender das Programm umbenannt hat
  */
 printf("Aufruf: %s Operand1 Operator Operand2\n",
	pszProgName);
 printf("\tBeispiel: %s 3 * 5\n", pszProgName);
 puts("Zulässige Operatoren: +-*/%");
 puts("2.Operand darf nicht 0 sein!");

 exit(EXIT_FAILURE);
}

Wenn ein Programm eine bestimmte Anzahl an Kommandozeilen-Argumenten erwartet, dann kann es mit Hilfe von argc leicht überprüfen, ob der Aufruf durch den Anwender korrekt erfolgt. Im Falle von calc.c liegt nur dann ein richtiger Aufruf vor, wenn argc den Wert 4 aufweist; dieser ergibt sich aus den drei notwendigen Argumenten für die arithmetische Berechnung plus argv[0] für den Programmnamen.

Für die Fehlerbehandlung, die naturgemäß nur falsche Benutzereingaben berücksichtigt, wurde eigens die Funktion usage() definiert. Dies erweist sich als Vorteil, weil die Ausgabe des Hilfetextes mit anschließender Programmbeendigung unter mehreren Bedingungen erfolgen muß: Bei entsprechender Gelegenheit genügt dann der Aufruf von usage(). Diese Funktion nimmt ein Argument vom Typ char *. Wir übergeben ihr damit den Namen unseres Progamms, so daß sich der Hilfetext automatisch anpaßt, sobald der Anwender das Programm umbenennt.