Die Computerseite von Eckart Winkler
Variable Parameteranzahl in C

 

In der Sprache C arbeitet man des öfteren mit Funktionen, bei denen die Anzahl der Argumente nicht fest vorgegeben ist. Es sind dies die Funktionen zur Eingabe von Tastatur und Ausgabe auf den Bildschirm (scanf und printf) oder zum Umgang mit Dateien (fscanf und fprintf) oder einfach zur Formatumwandlung (sscanf und sprintf).

Alle diese Funktionen sind nicht Bestandteil der Sprache C, sondern in einer der mitgelieferten Bibliotheken enthalten. Dies steht im Gegensatz zu Pascal, wo es ebenso derartige Funktionen gibt (read und write), diese aber Element des Sprachkerns sind. Als Konsequenz dieses Unterschieds ergibt sich, daß es mit den Mitteln der Sprache C möglich sein muß, Funktionen mit variabler Parameterzahl selbst zu schreiben.


Das Verfahren

Speziell in ANSI-C werden drei Makros angeboten, die sogar die Portabilität solcher Funktionen gewährleisten. Es sind dies va_start, va_arg und va_end. Zusätzlich ist der Datentyp va_list definiert, über den der Zugriff auf die Argumente möglich ist. Dies alles wird in der Include-Datei stdarg.h definiert, die daher in solchen Fällen immer mit eingebunden werden muß.

Zunächst die rein formelle Definition des Verfahrens: Diese soll anhand von folgendem Programm erläutert werden. Dort wird eine Funktion summe definiert, die die Summe einer beliebigen Anzahl von Integer-Werten berechnen und zurückgeben soll.

#include <stdio.h>
#include <stdarg.h>

int summe(int, ... );
int main(void);

int main()
{
  printf("%d\n", summe(1,2,3,4,0));
  printf("%d\n", summe(1,1,1,1,1,1,1,1,0));
  printf("%d\n", summe(-1,17,-16,0));
  printf("%d\n", summe(5,0));
  return(0);
}

int summe(int n0, ...)
{
  va_list args;
  int n, sum=n0;

  /* Parameterabfrage initialisieren */
  va_start(args, n0);

  for(;;)
  {
    /* Naechste Zahl holen */
    n = (int)va_arg(args, int);

    /* Zahl addieren, falls ungleich 0 */
    if ( n ) sum+=n; else break;
  }

  /* Parameterabfrage beenden, Summe zurueckgeben */
  va_end(args); return(sum);
}



Die Funktion, die eine variable Anzahl von Argumenten haben soll, muß mit mindestens einem festen Parameter definiert werden. Dies ist n0. n0 ist auch ein Integer-Wert, der in die Summe mit einbezogen werden soll. Er wird deshalb als Anfangswert für sum benutzt. Als letzter Parameter sind drei Punkte anzugeben. Diese stehen für die zusätzlichen Parameter, deren aktuelle Anzahl offengelassen wird. Analog zu diesem Funktionskopf wird auch der Prototyp geschrieben.

Innerhalb der Funktion muß nun ein Datenobjekt vom Typ va_list erklärt werden. Dieses ist zum Zugriff auf die zusätzlichen Parameter nötig. In obigem Programm wird hier die Variable args verwendet. Durch va_start wird args initialisiert. args ist als erstes Argument dieses Makros anzugeben, als zweites steht der letzte feste Parameter der Funktion selbst, also n0.

Nach der Initialisierung können die Argumente abgefragt werden. Es ist jedoch nicht möglich, deren Typen zu ermitteln. Darüber muß eine Vereinbarung mit dem Aufrufer der Funktion bestehen. Es muß in der Funktion somit bekannt sein, welcher Argumenttyp als nächstes zu erwarten ist. In der Funktion summe ist dies immer der Typ int. Dieser muß als zweiter Parameter des Makros va_arg angegeben werden, als erstes wieder die Variable args. Durch die Zuweisung n=(int)va_arg(args,int) wird der nächste Integer-Wert in die Variable n übertragen.


Ein Problem

Dies funktioniert auch dann, wenn der Aufrufer andere Datentypen angibt. Nur ergeben sich dann irgendwelche unerwarteten Werte. Ein Manko der Methode ist, daß man nicht feststellen kann, wann das Ende der Parameterliste erreicht ist. Hier muß wieder eine Vereinbarung mit dem Aufrufer bestehen. Diese besteht in dem Beispiel darin, daß zur Kennzeichnung des Endes eine 0 angegeben wird. In der Funktion summe wird n somit zu sum addiert, falls n ungleich 0 ist. Im anderen Fall n=0 wird die Schleife beendet. va_end beendet dann die Parameterabfrage, der Wert von sum wird zurückgegeben.

Im Hauptprogramm finden sich einige Aufrufe der Funktion summe. Als letztes Argument steht also immer die 0. Läßt man diese weg, kann es zum Absturz des Programm kommen. Mindestens wird aber ein unsinniger Wert resultieren.


Intern

Was passiert nun intern durch den Aufruf der Makros? Dazu muß man zunächst wissen, wie im allgemeinen die Parameter übergeben werden. Dies geschieht über den Stack (Stapelspeicher). Beim Aufruf einer Funktion werden die Parameter einfach auf dem Stack abgelegt. Und zwar kommt das erste Argument als unterstes auf den Stack, dann folgt das zweite, usw. Das letzte Argument liegt schließlich als oberstes.

Innerhalb der Funktion werden die Daten vom Stack geholt und verarbeitet. Dieses Abholen wird aber vom Compiler automatisch in den Code eingefügt, wenn er eine Referenz auf einen der Parameter entdeckt. Als Benutzer einer Hochsprache wie C sieht man davon also nichts.

Nichts anderes geschieht beim Aufruf einer Funktion mit variabler Parameterzahl. Der Unterschied besteht nur beim Ausführen des Codes der Funktion. Denn eine Referenz auf die variablen Parameter ist ja nicht möglich, da für diese keine Namen vergeben wurden. Hier hilft man sich mit einem Zeiger auf den Stack, im Beispielprogramm war dies args. Durch das Makro va_start wird args auf den ersten nicht fixen Parameter gesetzt.

Hierzu wird Kenntnis über das letzte fixe Argument benötigt. Dessen Adresse dient als Basisadresse. Zählt man zu ihr seine Größe, ergibt sich die Anfangsadresse der zusätzlichen Parameter. Weil also die Adressen der fixen Argumente den einzigen Anhaltspunkt für die Lage des Stacks im Speicher darstellen, ist die Angabe mindestens eines solchen unabdingbar.

Das Makro va_arg liefert den Wert vom Stack, auf den args gerade zeigt, und setzt args um die Größe des Wertes weiter. Der Typ des erwarteten Parameters, der ja bei va_arg angegeben werden muß, wird daher zunächst für die Cast-Operation benötigt, aber auch für das Weitersetzen von args. va_end schließlich setzt normalerweise args auf NULL.


Portable Makros

Eine mögliche Realisierung dieser Makros und des Datentyps va_list ist hier angegeben:

typedef char *va_list;

#define va_start(ap,v) ap=(va_list)&v+sizeof(v)
#define va_arg(ap,t) ((t*)(ap+=sizeof(t)))[-1]
#define va_end(ap) ap=NULL

Zunächst zu va_start. &v ist die Adresse des fixen Arguments. Sie wird durch Cast in den Typ va_list umgewandelt, damit die folgende Addition auf die korrekte Adresse führt. Addiert wird nämlich die Größe des fixen Arguments. Und die wird in Byte gemessen. Aus diesem Grund wurde va_list auch als char* deklariert. Addition einer Zahl n zu einem solchen Zeiger setzt diesen ja immer um n Bytes weiter.

In va_arg wird zunächst ap um die Größe des erwarteten Arguments, das ist t, weitergesetzt. Der ganze, in Klammern gesetzte, Ausdruck liefert den neuen, dadurch entstandenen Wert. Dieser wird durch Cast in den Typ t* umgewandelt. Zuletzt wird die dadurch entstandene Adresse als Array mit dem Index -1 angesprochen. dadurch erhält man den Wert, auf den der Zeiger ursprünglich gedeutet hat.

Zwei kleine Unschönheiten sind bei der variablen Parameterübergabe also zu beobachten. Zum einen kann der Typ der übergebenen Parameter nie festgestellt werden. Zum anderen ist auch das Ende der Parameterliste unbekannt. Es kann auch nicht ermittelt werden, da weitere, systemabhängige Daten auf dem Stack abgelegt werden.

Mit solchen Problemen haben aber auch die mitgelieferten "Standard"-Funktionen zu kämpfen, z.B. printf. printf kann mit einer beliebigen Zahl an Parametern aufgerufen werden. Die Vereinbarung, die mit dem Aufrufer besteht, findet sich in dem ersten Argument, dem Formatstring. Hier werden verschiedene Formatkennzeichner angegeben, jeweils durch ein Prozentzeichen eingeleitet. Zu jedem sollte im folgenden ein zugehöriger Parameter mit korrektem Datentyp angegeben werden. Natürlich hat der Aufrufer die Freiheit, sich daran nicht zu halten, dies bezahlt er jedoch mit unsinnigen Werten oder einem Absturz.


Ein Beispiel

Es ist nun recht interessant, sich die Implementation einer derartigen Funktion anzusehen. Im folgenden Programm wird die Funktion out definiert, eine stark abgemagerte Version von printf.

#include <stdio.h>
#include <stdarg.h>

int main(void);
void out(char*, ...);
void outstr(char*);
void outint(int);

int main()
{
  static char s1[]="abcdef";
  static char s2[]="uvwxyz";
  char ch='?';

  out("Dezimalzahl: %d\n", -123);
  out("Uhrzeit: %d:%d:%d\n", 17, 23, 45);
  out("Strings: (%s,%s)\n", s1, s2);
  out("Zeichen: '%c'\n", ch);
  return(0);
}

void out(char *form, ...)
{
  va_list args;
  char *pt;
  char ch;
  char *str;
  int n;

  /* Parameterabfrage initialisieren */
  va_start(args, form);

  for ( pt=form; *pt; pt++ )
  {
    /* Alle Zeichen ausser % ausgeben */
    if ( *pt!='%' ) { putchar(*pt); continue; }

    /* Formatkennzeichner abarbeiten */
    switch ( *(++pt) )
    {
      case 'c':
        ch = (char)va_arg(args, char);
        putchar(ch); break;
      case 's':
        str = (char*)va_arg(args, char*);
        outstr(str); break;
      case 'd':
        n = (int)va_arg(args, int);
        outint(n); break;
      case 0:
        return;
      default:
        putchar('%'); putchar(*pt); break;
    } /* switch */
  } /* for */

  /* Parameterabfrage beenden und Schluss */
  va_end(args); return;
}

void outstr(char *str)
{
  while ( *str ) putchar(*str++);
}

void outint(int n)
{
  char back[10];
  char *ph=back;
  int h;

  /* Gleich null? */
  if ( !n ) { putchar('0'); return; }

  /* Vorzeichen ? */
  if ( n<0 ) { putchar('-'); n=-n; }

  /* Ziffern rueckwaerts nach back schreiben */
  while (n) { h=n%10; n=n/10; *ph++=(char)('0'+h); }

  /* Nun Ziffern ausgeben */
  while (ph>back) putchar(*(--ph));
}

Am Anfang und Ende stehen wieder die bereits bekannten Aufrufe von va_start und va_end. Der Ablauf dazwischen läßt sich ganz grob wie folgt beschreiben:

Der Formatstring form wird von vorne bis hinten abgearbeitet. Jedes Zeichen des Strings wird einfach auf dem Bildschirm ausgegeben. Ausnahme ist das Prozentzeichen. Dieses leitet ja einen Formatkennzeichner ein. Zunächst muß also festgestellt werden, welchen. Wie bei printf steht c für ein Zeichen, s für einen String und d für eine Dezimalzahl. Entsprechend dieser Unterscheidung wird also durch va_arg der nächste Parameter geholt und auf dem Bildschirm ausgegeben.

Dies geschieht im Falle eines einzelnen Zeichens durch putchar. Beim String wird die weiter unten definierte Funktion outstr benutzt, bei einer Dezimalzahl outint. Diese haben mit der Parameterabfrage nichts zu tun und seien hier einmal vernachlässigt.

Mit diesem zweiten Beispiel ist somit gezeigt, daß die nicht fixen Parameter durchaus unterschiedliche Typen haben dürfen. Es funktioniert, solange sich der Aufrufer an die Konventionen hält, die mit der Definition der Funktion verbunden sind.

 

Übersicht Programmiersprache C Index