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.