← ST-Computer 11 / 1991

Verwaltung variabel langer Argumentlisten in C

Programmierpraxis

Normalerweise muss zu jeder C-Funktion eine eindeutige Parameterliste definiert sein. Es gibt aber auch Ausnahmen, bei denen Anzahl und Typ der Argumente nicht festgelegt sind. Prominentestes Beispiel dafĂŒr ist printf(). So etwas ist keine Hexerei, sondern mit ein paar einfachen Makros möglich.

Die Aufrufkonvention der Makros entspricht genau dem in [1] angegebenen Standard, sie könnten also mit etwas GlĂŒck sogar portabel sein. Initialisiert wird die Verwaltung einer Parameterliste durch va_start(), das einen Zeiger vom Typ vajist und zusĂ€tzlich den Namen des letzten fest angegebenen Parameters benötigt. Durch va_arg() holt man der Reihe nach die Argumente aus der Liste, wobei noch ihr Typ angegeben werden muß. Ist die Liste vollstĂ€ndig gelesen, sollte man noch mit va_end() de-initialisieren.

Wenn Sie sich das Demo in Listing 2 betrachten, sehen Sie, wie die Makros verwendet werden. Die Funktion r_sum() soll die Summe von einer nicht festgelegten Anzahl von Werten bilden, die zusĂ€tzlich noch von allen möglichen Datentypen sein können. Sie hat nur einen Parameter in der Definition angegeben, nĂ€mlich einen String, der Informationen darĂŒber enthĂ€lt, welche und wieviele Parameter folgen (‘c’ steht fĂŒr char, ‘i’ fĂŒr int usw.) Eine lokale Variable (pointer) vom Typ va_list wird als Listenzeiger verwendet. Dieser Zeiger wird nun mit va_start() initialisiert. Dann werden die Argumente der Reihe nach mit va_arg() geholt, wobei der im String angegebene Typ benutzt wird. Aber Vorsicht: bei der ArgumentĂŒbergabe findet eine automatische Typumwandlung statt! chars werden als ints ĂŒbergeben und floats als doubles. Das muß r_sum() natĂŒrlich berĂŒcksichtigen und bei ‘c’ und ‘f’ den entsprechenden Typ verwenden.

Der Stack und der Listenzeiger beim Aufruf von r_sum()

... und was dahintersteckt

Um den Trick mit den variablen Parameterlisten erklĂ€ren zu können, muß ich zuvor darauf eingehen, wie die ArgumentĂŒbergabe allgemein funktioniert. StĂ¶ĂŸt der C-Compiler auf einen Funktionsaufruf, so schiebt er alle Parameter auf den Stack, und zwar von rechts nach links. Dabei kĂŒmmert er sich ĂŒberhaupt nicht darum, welche Argumente bei der Funktionsdefinition angegeben sind. Das kann zu Chaos fĂŒhren, wenn Anzahl und Typen bei Aufruf und Definition nicht ĂŒbereinstimmen. Man kann diese Tatsache aber auch fĂŒr Tricks ausnutzen. Und genau das tun die varg-Makros. Wenn mindestens ein fester Parameter verwendet wird und die Typen der weiteren irgendwie bekannt sind, kann man mit etwas Zeigerakrobatik an ihre Werte kommen. Auch das anfangs erwĂ€hnte printf() hat einen festen Parameter: den Format-String, der auch Informationen ĂŒber Art und Anzahl der weiteren Argumente enthĂ€lt.

Wie funktioniert jetzt das ganze im Detail? va_start() nimmt sich die Adresse des letzten festen Parameters vor, an die wir mit dem ‘&’-Operator kommen. Ergebnis des ‘&’-Ausdrucks ist ein Zeiger auf den Typ, von dem der Parameter ist. Erhöht man diesen Zeiger um 1, rechnet der Compiler die LĂ€nge des bezeigten Typs dazu (nicht 1 Byte!). Jetzt haben wir einen Zeiger auf die Stelle genau hinter dem letzten Parameter, was auch der Anfang der variablen Liste sein muß. Das ist aber genau das, was wir wissen wollten, und wir merken uns diesen Zeiger im Listenzeiger. FĂŒr das konkrete Beispiel sehen Sie den Stack mit den Parametern und dem Listenzeiger (der kleine Pfeil) in Bild 1.

Wird jetzt va_arg() auf gerufen, bekommen wir den Typ mitgeteilt, den das Datum unter dem Zeiger hat. Wir verwandeln unseren Listenzeiger nun mit einem cast-Operator in einen Zeiger auf diesen Typ, derefenzieren mit und haben den gewĂŒnschten Wert. ZusĂ€tzlich muß der Listenzeiger noch auf die nĂ€chste Position weitergeschoben werden. Am einfachsten erscheint es, dazu den “++"-Operator (nachgestellt)zu benutzen. Aber das funktioniert leider nicht, weil der Compiler keine Manipulationen von umgewandelten Zeigern annimmt. Also mĂŒssen wir einen komplizierteren Weg einschlagen: Zuerst den Zeiger mit ‘+=’ erhöhen und dann erst mit dem cast umwandeln. Da der Zeiger jetzt hinter die Variable zeigt, erfolgt der Zugriff auf die gewĂŒnschten Daten mit *(zeiger-1).

Den Wert, den wir zum Listenzeiger addieren mĂŒssen, liefert uns sizeof(). Das Ergebnis ist eine LĂ€nge in Bytes, und damit diese korrekt addiert werden kann, muß unser Zeiger ein char-Pointer sein. Es wird ja, wie oben schon erwĂ€hnt, der Summand noch mit der GrĂ¶ĂŸe des bezeigten Typs multipliziert. Da die GrĂ¶ĂŸe von char eins ist, fĂ€llt die Multiplikation weg.

Das im Standard vorgesehene va_end ist bei dieser Implementierung eigentlich nicht notwendig, der VollstÀndigkeit halber aber doch vorhanden. Es ist ein Dummy, tut also gar nichts.

Betrachten wir weiter unser Beispiel. Als erstes wird va_arg mit „int“ aufgerufen. Zum Listenzeiger (pointer) wird die LĂ€nge des Typs „int“ addiert, also 2 Byte. Dann wird der Zeiger mit dem cast-Operator (int *) in einen integer-Pointer gewandelt. Von diesem wird 1 abgezogen, aber jetzt wird die 1 mit der LĂ€nge des Datentyps multipliziert! Das Ergebnis ist also ein Integer-Zeiger auf die Position vorder Addition. Wird jetzt der Zeiger mit derefe-renziert, erhĂ€lt man einen integer-Wert, und zwar die 5. Bei dem anschließenden Aufruf mit „long“ passiert genau dasselbe, nur mit LĂ€nge 4 statt 2, und so weiter fĂŒr die anderen Elemente der Liste.

Geht nur mit #define!

Besonders betonen möchte ich, daß man die va/'g-Makros nicht als Funktionen schreiben könnte. Man kann ja als Parameter nicht einen Datentyp ĂŒbergeben, genauso wenig den Namen einer Variablen. Bei #define aberfindet eine reine Textersetzung statt. Nachdem der PrĂ€prozessor alle ps, ls und ts durch die Symbole im Aufruf ersetzt hat, steht fĂŒr den Compiler ganz normaler C-Text da. WĂŒrden dieselben Parameter bei einem Funktionsaufruf stehen, wĂŒrde er sich beschweren, daß z.B. ‘char’ keine Variable ist, deren Wert er ĂŒbergeben kann. Der entscheidende Unterschied ist, daß Funktionsparameter bei der Übergabe ausgewertet werden. Bei #define werden sie ohne irgendwelche SyntaxprĂŒfung dort eingesetzt, wo die formalen Parameter standen. Und nur so kann man die Listenverwaltung implementieren!

Literatur:

[1]: Schildt, Herbert: C-Befehlsbibliothek, McGraw-Hill, 1988

/* varg.h */ /* Verwaltung variabel langer Argumentlisten */ /* by Roman Hodek (c) 1991 MAXON Computer */ typedef char *va_list; #define va_start(p,1) ( p=(va_list)(&(l)+1) ) #define va_arg(p,t) (*((t *)(p+=sizeof(t))-1)) #define va_end(p) p

Listing 1: Header-Datei mit den varg-Definitionen

/* varg - Demo */ /* (c) 1991 MAXON Computer */ #include "stdio.h" #include "varg.h" main () { double summe = 0.0, r_sum(); printf ("\33E"); summe += r_sum( "ilfc", 5, 80000L, 32, 88, 'A‘); summe += r_sum( "fdi", 99 1, (double)12.678456673, 15 ); printf( "\nSumme = %1f\n", summe ); getchar(); } double r_sum( typestr ) char *typestr; { va_list pointer; char c; double s = 0.0; va_start( pointer, typestr ); while( c=*typestr++ ) { switch( c ) { case 'c': case 'i': s += va_arg( pointer, int ); break; case 'l': s += va_arg( pointer, long ); break; case 'f': case 'd': s += va_arg( pointer, double ); break; } s += a; } va_end( pointer ); return( s ); }

Listing 2: Demo zur Verwendung von variablen Argumentlisten

Roman Hodek