Verwaltung variabel langer Argumentlisten in C

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
Aus: ST-Computer 11 / 1991, Seite 74

Links

Copyright-Bestimmungen: siehe Über diese Seite