Variable Argumentenliste

In C ist es möglich Funktionen zu definieren, die eine beliebige Anzahl an Argumenten nehmen. Eine dieser Funktionen kennen wir bereits, nämlich printf. Der Aufruf von printf sieht so aus (aus der Manpage):

int printf(const char *restrict format, ...);

printf erhält also einen Formatparameter und eine variable Anzahl weiterer Parameter. Die Funktion geht format durch, auf der Suche nach Platzhaltern wie %d oder %s und ersetzt sie durch den passenden Parameter in den variablen Argumenten.

Schauen wir in der Manpage etwas weiter, finden wir einen zweiten Ruf, der interessant sein kann, nämlich:

int vprintf(const char *restrict format, va_list ap);

vprintf nimmt nun keine variable Argumentenliste mehr, sondern eine Struktur mit dem Typ va_list.

stdarg.h

Die va_list ist des Rätsels Lösung. In einer Funktion, die eine variable Argumentenliste bekommt, hat diese erstmal keinen Namen, d.h. auf die Argumente können wir nicht einfach zugreifen. Dafür hat die Standard C-Library die va_list für uns.

In einer solchen Funktion bekommen wir mittels va_start und va_arg den nächsten Parameter. Aber Obacht! Denn niemand sagt uns zur Laufzeit welchen Typ der Parameter eigentlich hat. Sollte das also nicht zusammenpassen, haben wir ein ziemliches Problem.

Machen wir das mal an einem Beispiel fest. Nehmen wir eine Funktion, die eine Liste von Integer-Parametern bekommt, und die Summe bilden soll. Mit zwei Parametern sähe das so aus:

int sum( int a, int b ) {
    return a+b;
}

Wollen wir nun die variablen Argumentenlisten verwenden, könnte das so aussehen. Wir nehmen hier den Wert 0 als Stoppzeichen. Das bedeutet, dass der Ruf immer mit einer 0 als letzten Parameter stattfinden muss, denn auch das Ende der Parameterliste wird uns nicht mitgeteilt. Aber zur Summe:

int sum ( int a, ... ) {
    va_list args;
    // Initialisierung auf a, damit sparen wir uns die Addition mit a
    int s = a;
    int x;
 
    // Falls a Null ist, brechen wir ab
    if ( a == 0 ) {
        return 0;
    }
 
    // "öffnen" der Argumentenliste
    va_start( args, a );
    do {
        // wir holen einen integer-Parameter aus der va_list
        x = va_arg( args, int );
 
        s += x;
    } while ( x != 0 );
 
    // Schließen der Argumentenliste
    va_end ( args );
    return s;
}

In dem Beispiel müssen wir, zunächst die va_list mittels va_start öffnen. Dabei geben wir den letzten benannten Parameter an. Damit ist auch klar, warum wir einen benannten Parameter brauchen (nämlich a).

Dann können wir beliebig viele Argumente aus der Parameterliste rausholen, mittels va_list. Aber obacht, stimmt der Typ nicht, oder ist die Parameterliste eigentlich schon zuende, werden wir zur Laufzeit nicht gewarnt und unser Programm wird mit ungültigen Daten weiterarbeiten. Daher ist die Abbruchbedingung wichtig, hier dass wir als letzten Parameter immer eine Null übergeben.

Am Ende der Funktion schließen wir die Parameterliste mit va_end.

Wenn wir diese Funktion ausführen, könnte da so aussehen:

int main () {
    printf ("Summe: %d", sum(1,2,3,4,5,6,7,8,9,0));
}

Die Ausgabe ist dann:

Summe: 45

Sollten wir die Parameterliste später nocheinmal brauchen, bspw. einmal um durchzugehen und die Länge zu bestimmen, einen Speicherbereich zu allozieren und dann ein weiteres Mal durchzugehen um das Array zu befüllen, können wir va_copy verwenden, was eine va_list in eine andere hinein kopiert. Dann können wir mit va_arg zweimal über die Liste iterieren.

Weitere Beispiele

Einige weitere Beispiele für Funktionen mit variabler Argumentenliste sind natürlich die printf-Varianten. Hier haben wir immer eine Variante mit variabler Argumentenliste und eine mit va_list als Parameter. Damit können wir einen Wrapper um printf schreiben, der ebenfalls eine Variable Parameterliste annimmt, indem wir die va_list erstellen, und das an vprintf übergeben.

printf kennt sowohl die Länge der va_list als auch die übergebenen Typen durch den Formatparameter. Darin enthalten sind durch die Platzhalter auch die Typinformationen zu den Parametern.

Ein weiteres Beispiel ist auf den Linux-Systemen evecl und dessen Varianten. Diese Funktionen sind dafür da, das aktuell laufende Programm durch ein Anderes zu ersetzen. Der Prozess bleibt dabei bestehen. In Verbindung mit fork wird exec verwendet um neue Prozesse zu starten.

Der Prototyp von execl sieht so aus:

int execl(const char *pathname, const char *arg, ...
          /*, (char *) NULL */);

Die Funktion erhält einen Pfadnamen von der zu ladenden ausführbaren Datei, einen einzigen Parameter als benannten Parameter und eine Liste von weiteren Paramtern. Der erste Paramter in den meisten C-Programmen ist oftmals einfach der Name der ausführbaren Datei. Oftmals wird argv[0] einfach als Programmname verwendet. In execl muss dieser Parameter immer angegeben werden.

Dann erhält execl eine Liste weiterer Parameter. Niemand kann zur Programmierzeit so genau wissen, wie viele das sind, schließlich kann ein Nutzer im Terminal beliebig viele Parameter angeben, in execl müssen sie aber durch einen Null-Pointer abgeschlossen werden. execl interpretiert jeden der Parameter als Zeiger auf ein char-Array, und kennt damit auch das Ende.

Diskussion

Die variablen Argumentenlisten in C sind recht einfach, dezent umständlich zu verwenden, und eigentlich auch nicht sicher. Jede Funktion, die sie verwendet braucht ein Abbruchkritierium und muss auch unbedingt den Typ der Parameter kennen. In der Manpage heißt es hierzu:

If there is no next argument, or if type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), random errors will occur.

Wenn man mit variablen Argumentenlisten in C arbeitet, sollte man sich genau vor Augen führen, was man da macht und ob man diese Funktion wirklich mit variablen Argumentenlisten implementieren muss und will, oder ob es andere sinnvollere Möglichkeiten gibt. Entscheidet man sich dafür, muss man sehr auf Typen und Länge der Argumentenliste achten.