Parameter

Willemers Informatik-Ecke

Diese Seite basiert auf Inhalten aus meinem Buch "Einstieg in C++", seinerzeit erschienen im Verlag Galileo Computing. Das Buch ist inzwischen vergriffen.


Das Nachfolgebuch heißt C++. Der Einstieg und ist bei Wrox im Verlag Wiley-VCH erschienen.

Die Inhalte dieser Website unterliegen meinem Urheberrecht und dürfen trotz inhaltlicher Überschneidungen mit dem Buch dank der freundlichen Genehmigung des Verlags Wiley-VCH hier erscheinen.

Schnittstelle

Funktionen können Parameter haben. Sie dienen dazu, Werte an Funktionen durchzureichen. Diese Möglichkeit macht die Arbeit mit Funktionen um ein Vielfaches flexibler. Um aus der aufgerufenen Funktion auf die Werte des Aufrufers zuzugreifen, könnten Sie natürlich auch globale Variablen verwenden. Durch die Parameter wird aber offen dokumentiert, welche Informationen eine Funktion von außen bekommt. Man spricht auch von der Schnittstelle einer Funktion.

Die Parameterdefinition einer Funktion gleicht einer Variablendefinition. Der Typ der Parameter ist wichtig, um zu prüfen, ob die Aufrufparameter mit den Parametern der Funktion auch kompatibel sind. Über die Parameternamen wird von innen aus der Funktion heraus auf die Parameter zugegriffen. Zur Veranschaulichung wird die Funktion trennlinie() so erweitert, dass der Aufrufer angeben kann, wie viele Bindestriche ausgegeben werden sollen.

[Funktion trennlinie mit Parameter]

void trennlinie(int stellen)
{
    int i;
    for (i=0; i<stellen; i++)
    {
        cout << "-";
    }
    cout << endl;
}
...
int main()
{
    int breite = 45;
    trennlinie(breite);
    cout << "Programm zur Ermittlung..." << endl;
    trennlinie(45);

Variable

Der Parameter stellen ist aus Sicht der Funktion eine ganz gewöhnliche lokale Variable, in die beim Funktionsaufruf der Wert des Aufrufers hineinkopiert worden ist. Da hier eine Kopie angefertigt wird, greift die Funktion nicht auf die übergebenen Variablen zu. Die Funktion erreicht lediglich die lokale Variable stellen. Diese Variable kann innerhalb der Funktion gelesen werden. Da sie lokal ist, könnte sie sogar verändert werden. Solche Änderungen an der Variablen stellen innerhalb der Funktion schlagen nicht auf die als Parameter verwendete Variable breite durch.

Der Aufrufer merkt nicht, was in der Funktion mit der Variablen geschieht. Der Wert, der bei einem Aufruf an die Funktion übergeben wird, wird kopiert. So ist es möglich, auch eine Konstante (hier 45) als Parameter zu übergeben.

Seiteneffekt

Der Datenaustausch zwischen dem Aufrufer und der Funktion sollte immer über Parameter erfolgen. Auch wenn es manchmal bequemer erscheint, eine globale Variable zu verwenden, sollten Sie dies nur in wohl begründeten Ausnahmesituationen tun. Der Zugriff von Funktionen auf globale Variablen nennt man Seiteneffekt. Dieser führt dazu, dass Zusammenhänge unübersichtlich werden. Darum sollten Sie ein solches Verhalten einer Funktion ausgiebig kommentieren.

Wertübergabe an Parameter

Parameter werden als Werte an die Funktion übergeben. Das bedeutet, dass die Inhalte der Variablen, die der Aufrufer an Funktionen übermittelt, aus dessen Sicht nicht innerhalb der Funktion verändert werden können. Im Beispiel könnte in der Funktion trennlinie() der Wert der Variablen stellen verändert werden. Dies würde auch innerhalb der Funktion Wirkung zeigen. Der Wert 45, mit dem die Funktion aufgerufen wurde, wird nicht verändert. Genausowenig ändert sich die Variable breite, die beim ersten Aufruf als Parameter übergeben wird.

Parametervariablen sind lokale Variablen der Funktion, die beim Funktionsaufruf
durch eine Kopie der übergebenen Werte initialisiert werden.

Hintergrund

Die Parameter werden vor dem Funktionsaufruf zu der Rücksprungadresse auf den Stack kopiert. Dort kann man auf sie während der Laufzeit der Funktion wie auf alle anderen lokalen Variablen zugreifen. Beim Verlassen der Funktion wird der Speicher, den die Funktion verwendet hat, wieder freigegeben. Das betrifft die lokalen Variablen, die Parametervariablen und der Speicher, der für die Rücksprungadresse verwendet wurde.

In Abbildung (graffunction2) wird noch einmal der Syntaxgraph von Abbildung (graffunction) dargestellt. In Abbildung (grafparameter) wird dieser um die Funktionsparameter ergänzt.

An dieser Stelle erscheint im Buch die Abbildung "Syntaxgraph Funktion" (graffunction2)

An dieser Stelle erscheint im Buch die Abbildung "Syntaxgraph Parameter" (grafparameter)

Parameter ohne Namen

Überraschend ist, dass Sie auch einen Typ ohne passenden Variablennamen in die Parameterdefinition schreiben können. Naheliegenderweise kann dann innerhalb der Funktion nicht auf diesen Parameter zugegriffen werden. Es gibt aber Fälle, in denen Ihnen die Schnittstelle einer Funktion vorgegeben ist und Sie nur die Implementierung schreiben sollen. Im Zuge dessen kann sich herausstellen, dass ein Parameter gar nicht gebraucht wird. Da eine Änderung der Schnittstelle Auswirkungen auf andere Programmteile hat, bleibt vielleicht der überflüssige Parameter stehen. Das wiederum kann zu einer Warnung des Compilers führen, der der Meinung ist, dass alle Parameter in der Funktion verwendet werden sollten. Damit die Warnung verschwindet, können Sie den Variablennamen des Parameters weglassen. Sie signalisieren dem Compiler damit, dass Sie diesen Parameter absichtlich nicht verwenden.

Zusammenfassung

Eine Funktion wird über ihren Namen aufgerufen. Der Aufrufer übermittelt seine Daten über die Parameter an die Funktion. Dabei werden die Daten des Aufrufers in die Parametervariablen kopiert. Liefert die Funktion Daten an den Aufrufer zurück, verwendet sie dazu den Befehl return. Die Abbildung (grafparas) zeigt dies schematisch.

An dieser Stelle erscheint im Buch die Abbildung "Schnittstelle einer Funktion" (grafparas)

Prototypen

Wenn eine Funktion im Quelltext erst hinter ihrem Aufruf steht, kann der Compiler nicht prüfen, ob der Aufruf korrekt ist. Darüber wird er sich mit Warnungen oder Fehlermeldungen beschweren. Wenn es nicht möglich oder umständlich ist, die Funktionen nach ihren Aufrufen zu sortieren, dann bleibt Ihnen die Möglichkeit, die Funktion vor ihrem ersten Aufruf zu deklarieren. Eine Deklaration gleicht einer Funktionsdefinition, allerdings wird statt des Funktionskörpers in geschweiften Klammern einfach ein Semikolon gesetzt.

Beispiel

Im folgenden Beispiel wird die Funktion trennlinie() zunächst deklariert. Dann folgt in main() der Aufruf der Funktion trennlinie(), und erst zum Schluss wird die Funktion implementiert.

[Prototyp]

void trennlinie(int AnzahlStriche);

int main()
{
    int breite = 45;
    trennlinie(45);
    cout << "Programm zur Ermittlung..." << endl;
    trennlinie(breite);
}

void trennlinie(int stellen)
{
    ...
}

Bei Prototypen können die Variablennamen weggelassen werden oder, wie oben zu sehen ist, zur besseren Dokumentation geändert werden. Für den Compiler sind der Funktionsname, der Rückgabetyp und die Typen der Parameter relevant.

Zeiger als Parameter

Beim Aufruf einer Funktion werden die übergebenen Werte in die Parametervariablen kopiert. Damit befinden sie sich auf einer Einbahnstraße vom Aufrufer zur Funktion. In manchen Fällen ist es aber erwünscht, dass Funktionen Variablen der aufrufenden Funktion verändern können. Das erreicht man mit einem recht simplen Trick. Anstatt die Variable selbst zu übergeben, wird die Adresse der Variablen verwendet. Diese Adresse wird dann in eine Zeigervariable kopiert, die die Funktion als Parameter definiert hat. Wird in der Funktion über diesen Zeiger referenziert, kann auf die Variable zugegriffen werden, deren Adresse übergeben wurde und so deren Inhalt geändert werden.

Beispiel

In der folgenden Funktion inkrementiere() wird eine Integer-Variable des Aufrufers erhöht. Dazu muss der Aufrufer die Adresse der Variablen übergeben, die er durch Voranstellen eines Ampersand (Kaufmännisches Und) ermittelt. Da der Aufrufer die Adresse einer Integer-Variablen übergibt, muss der Parametertyp ein Integer-Zeiger sein.

[Veränderlicher Parameter]

void inkrementiere(int *ziel)
{
    *ziel += 1;
}

int main()
{
    int meinWert = 5;
    inkrementiere(&meinWert); // danach ist meinWert 6
}
Nach dem Aufruf von inkrementiere() wird der Inhalt der Variablen meinWert 6 sein. Beim Aufruf wird durch das kaufmännische Und deutlich, dass die Funktion Zugriff auf diese Variable erhält. Der Parameter ist ein Zeiger. Dadurch wird deutlich, dass die Funktion einen Zugriff auf eine Integer-Variable bekommt.

An dieser Stelle steht im Buch die Abbildung "Zeiger als Parameter" (grafpointerpara)

Die Grafik zeigt, wie der Aufrufer den Parameter ziel durch die Adresse der Variablen meinWert bedient und wie die Funktion mit *ziel den übergebenen Zeiger nutzt, um auf den Inhalt der Variablen meinWert zugreifen zu können.

Damit eine Funktion eine Variable des Aufrufers verändern kann, wird
deren Adresse als Argument an die Funktion übergeben. Der entsprechende
Parameter wird als Zeiger auf den Typ der Variablen definiert. 
Über diesen Zeiger kann innerhalb der Funktion der Inhalt der 
Variablen des Aufrufers verändert werden.

Konstante Zeiger

Effizienz

Neben der Möglichkeit, Variablen des Aufrufers zu verändern, hat die Verwendung von Zeigern und Adressen bei großen Datenstrukturen den Vorteil, dass erheblich weniger Daten kopiert werden müssen. Ein Zeiger ist heutzutage auf einem PC vier Byte groß. Insofern werden Zeiger auch manchmal verwendet, obwohl ein Zugriff auf die Daten des Aufrufers nicht beabsichtigt ist. Um dies gegenüber dem Aufrufer zu dokumentieren, kann der Zeiger als konstant deklariert werden.
An anderer Stelle wurde bereits gezeigt, dass Sie Zeigervariablen in zwei Varianten als Konstante deklarieren können. Diese Varianten gelten natürlich auch bei Parametern.

const int *konstantesZiel;

Konstantes Ziel

Wenn das Schlüsselwort const vor dem Typ steht, bedeutet das, dass die Aufrufvariable über diesen Zeiger nicht verändert werden darf. Der Zeiger selbst darf aber innerhalb der Funktion inkrementiert werden, um beispielsweise durch ein Array zu laufen.

int * const konstanterZeiger;

Konstanter Zeiger

Wenn das Schlüsselwort const vor dem Namen der Zeigervariablen steht, bedeutet das, dass der Zeiger konstanterZeiger selbst nicht verändert werden darf, also beispielsweise nicht inkrementiert werden darf. Dagegen ist die Veränderung von Aufrufvariablen damit möglich:

const int * const komplettKonstant;

Die letzte Variante kombiniert beides und verhindert sowohl das Inkrementieren des Zeigers als auch den Zugriff auf die Aufrufvariable.

Die Verwendung von const hat dokumentierenden Nutzen. Es sagt dem Aufrufer, was die Funktion mit dem Zeiger tut oder nicht tut. Diese Zusage wird darüber hinaus vom Compiler überwacht.

Arrays als Parameter

Sie können auch Arrays als Parameter an Funktionen übergeben. Ein Array kann auf verschiedene Art und Weise als Parameter für eine Funktion deklariert werden. Gehen wir beispielsweise davon aus, dass ein Integer-Array mit fünf Elementen übergeben werden soll. Dann sind folgende Funktionsdeklarationen möglich.

void fa(int a[5]);
void fb(int a[]);
void fc(int *a);

gpHinweis

Bei der Funktion fa besagt die Parameterdefinition, dass die Funktion ein Array mit fünf Integer-Werten erwartet. Man sollte vermuten, dass diese fünf Elemente als Wert an die Funktion kopiert werden. Leider verhält es sich aber anders. Es wird nicht das Array kopiert, sondern die Adresse des Arrays übergeben. Änderungen, die innerhalb der Funktion an dem Array vorgenommen werden, schlagen direkt auf das Array in der Aufruffunktion durch. Der Compiler prüft nicht einmal, ob das übergebene Array wirklich fünf Elemente hat. Der Parameter von fb erwartet ein Array als Parameter. Da zwischen den eckigen Klammern kein Wert steht, kann man schon erahnen, dass hier nicht kontrolliert wird, wie groß das Array tatsächlich ist. Auch hier führt eine Änderung am Array innerhalb der Funktion zu einer Änderung am Original-Array. Der Parameter von fc ist ein Zeiger auf eine Integer-Variable. Wie zu erwarten, lassen sich hier auch Arrays übergeben. Alle drei Parameter sind also kompatibel.

Dokumentationscharakter

Auch wenn die Parameter der drei Funktionen keinen technischen Unterschied bewirken, sollten Sie die Möglichkeit nutzen, Ihre Parameter durch die rechteckigen Klammern als Array zu dokumentieren. Dann wird selbst ein flüchtiger Leser davon ausgehen, dass Sie hier ein Array erwarten und nicht über einen Zeiger auf eine Variable des Aufrufers zugreifen wollen.

const

Soll eine Veränderung des Arrays innerhalb der Funktion verhindert werden, muss dem Parameter das Schlüsselwort const vorangestellt werden. Dann wird der Compiler jede Änderung am Array innerhalb der Funktion mit einer Fehlermeldung ahnden:

void fa(const int a[5])
{
    a[2] = 3; // Fehlermeldung des Compilers!
}

Mehrdimensionale Arrays

Bei mehrdimensionalen Arrays kann man nur die erste Dimension so offen halten, wie das bei eindimensionalen Arrays der Fall ist. Die zweite Dimension muss festgelegt werden. Zulässig sind folgende Prototypen:

void fa(int a[5][4]);
void fb(int a[][4]);
void fc(int (*a)[4]);

Die Ähnlichkeit zu den Varianten der Array-Übergabe in der ersten Dimension sind unverkennbar. In der zweiten Dimension muss allerdings eine feste Größe angegeben werden.

Feste Dimension

Die Größe der zweiten Dimension muss angegeben werden, da es ansonsten innerhalb der Funktion nicht möglich ist, die korrekte Speicherstelle zu ermitteln. Beispielsweise ist a[1][1] in unserem Beispiel die sechste Position. Wäre die zweite Dimension aber zehn, dann wäre a[1][1] die zwölfte Stelle. Im Fall eines zweidimensionalen Arrays wird die zweite Dimension auch vom Compiler geprüft. Der Aufruf einer dieser Funktionen mit einem Array, das als b[5][3] definiert wurde, wird vom Compiler entdeckt und verhindert. Die Klammern um *a sind bei fc erforderlich, da der Parameter ansonsten nicht als zweidimensionales Integer-Array interpretiert wird, sondern als ein eindimensionales Array von Zeigern auf Integer.

Referenz-Parameter

In C++ gibt es eine Alternative, wie aus einer Funktion heraus auf Variablen des Aufrufers zugegriffen werden kann. Dazu wird statt einer Zeigervariable eine Referenz als Parameter verwendet. Eine Referenz unterscheidet sich bei der Parameterdeklaration syntaktisch dadurch, dass statt des Sterns ein kaufmännisches Und vorangestellt wird.

Kein Zeiger

Inhaltlich unterscheiden sich Zeigervariablen und Referenzen dadurch, dass bei einer Referenz nicht die Adresse kopiert, sondern direkt auf die Variable verwiesen wird. Die Referenzvariable enthält also keine Speicheradresse, sondern ist quasi ein anderer Name für das Objekt, das als Argument übergeben wird. Innerhalb der Funktion wird auf die übergebene Variable also nicht über einen Zeiger zugegriffen, sondern die Variable kann direkt bearbeitet werden. Konsequenterweise wird beim Aufruf auch nicht die Adresse, sondern die Variable selbst übergeben. Beim Aufruf mit einer Variablen als Parameter ist also eine Referenz von einer Wertübergabe nicht zu unterscheiden. Der Aufruf mit einer Konstanten als Parameter ist nicht zulässig.

[Parameterübergabe per Referenz (refpar.cpp)]

#include <iostream>
using namespace std;

int meineFunktion(int &refParameter)
{
    refParameter = refParameter + 4;
}

int main()
{
    int meineZahl = 5;
    meineFunktion(meineZahl);
    cout << meineZahl << endl; // Ausgabe: 9!
}

Referenz und Zeiger

Der Unterschied zwischen einem Zeiger und einer Referenz als Parameter sind zunächst einmal gering. Auf den ersten Blick scheint es nur syntaktische Gründe zu geben. Und tatsächlich ist es in der Praxis oft möglich, das eine durch das andere zu ersetzen. Der Unterschied liegt darin, dass Sie einem Zeiger jederzeit einen anderen Wert zuweisen können und damit auf eine andere Variable verweisen. Eine Referenz ist dagegen der Stellvertreter für die übergebene Variable und lässt sich nach der Parameterübergabe nicht mehr auf eine andere Zielvariable umbiegen.

const

Bei großen Datenstrukturen hat die Referenz einen Effizienzvorteil gegenüber einer Wertübergabe, da nicht die komplette Struktur kopiert werden muss. Aus diesem Grund wird gern eine Referenzübergabe verwendet, obwohl die Variable gar nicht verändert werden soll. Wenn Sie dem Aufrufer zusichern wollen, dass sein Wert von der aufgerufenen Funktion nicht verändert wird, setzen Sie das Schlüsselwort const ein.

int meineFunktion(const riesentyp &refPar)
{
    ...

Tipp

Aufgrund ihres Charakters als Stellvertreter für andere Variablen können Referenzen weder auf 0 gesetzt noch per Zuweisung auf ein anderes Ziel gesetzt werden. Ihr Ziel erhalten Referenzen also entweder durch Initialisierung oder bei einem Funktionsaufruf. Eine Zuweisung führt immer zur Veränderung der Variable, auf die eine Referenz weist.

Rückgabewert

Eine Referenz kann auch bei einem Rückgabewert verwendet werden. Dabei sollten Sie genau darauf achten, dass keinesfalls eine Referenz auf eine lokale Variable zurückgegeben wird. Nach dem Funktionsende wird diese
nämlich wieder freigegeben. Damit verweist dann die Referenz auf eine ungültige Variable und führt früher oder später zu einer problematischen Situation.

Übung

Beispiel: Stack

Das folgende Beispiel realisiert einen Stapel oder Stack. Eine solche Datenstruktur nimmt Daten auf und gibt die Daten in der umgekehrten Reihenfolge zurück, in der sie abgelegt wurden. Die beiden zentralen Operationen eines Stapels sind push() (engl. schieben), um Daten auf den Stack zu schieben, und pop() (engl. abziehen), um ein Element vom Stack herunter zu holen. Als Daten werden einfache Integer-Variablen verwendet. Es gibt nur einen Stack, auf den man über die globale Variable Anker zugreifen kann.

[Stack als verkettete Liste (stackstruct.cpp)]

#include <iostream>
using namespace std;

struct TListenKnoten
{
    int data;            // simuliert die Daten
    TListenKnoten *next; // Verbindung zum Nachfolger
};

// Globaler Anfangspunkt des Stacks:
TListenKnoten *Anker = 0;

void push(int data)
// Füge ein neues Element hinzu
{
    // Neues Element für die Liste erzeugen:
    TListenKnoten *node = new TListenKnoten;
    node->data = data;  // Besetze die Daten
    node->next = Anker; // Hänge die bisherige Liste an
    Anker = node;       // Setze den Anfangspunkt hierher
}

int pop()
// Entnehme das zuletzt eingefügte Element
{
    int inhalt=0; // Zwischenspeicher für ein Element
    if (Anker)    // ungleich 0! Die Liste ist nicht leer!
    {
        // Sichere Zeiger auf das später zu löschende Element:
        TListenKnoten *old = Anker;
        Anker = Anker->next; // Ziehe nächstes Element nach vorn
        inhalt = old->data;  // Sichere die Daten
        delete old;          // Lösche das Element
    }
    return inhalt; // Liefere den Inhalt zurück
}

int main()
// Testen der Stack-Funktionen
{
    push(2); // Daten in den Stack schieben
    push(5); // Daten in den Stack schieben
    push(18); // Daten in den Stack schieben
    cout << pop() << endl; // muss 18 ausgeben
    cout << pop() << endl; // muss 5 ausgeben
    cout << pop() << endl; // muss 2 ausgeben
}

Sie können natürlich statt einer Integer-Variablen beliebige andere Datenstrukturen verwenden. Falls Sie in Ihrem Programm Stacks für verschiedene Datentypen benötigen, können Sie auch einen Zeiger anstelle der Daten verwenden. Wenn Sie einen Zeiger auf void verwenden, kann die Datenstruktur flexibel mit allen denkbaren Datentypen eingesetzt werden. Schließlich ändert sich die Funktionsweise von push() und pop() bei der Änderung der Daten nicht. Bevor Sie aber zu viel Schweiß in derartige Experimente stecken, sollten Sie sich vorher noch die Templates anschauen. Vielleicht lösen die Ihr Problem wesentlich eleganter.

Übungsaufgabe

Vorbelegte Parameter

C++ ermöglicht es, einzelne Parameter bereits bei der Definition mit Werten vorzubelegen. Beim Aufruf der Funktion können dann beliebig viele der letzten Parameter weggelassen werden. Sie erhalten dann die Werte, die die Funktion vorgegeben hat.

[Vorbelegte Parameter]

void PraeVar(int a, int b, int c=4, char d='A', float e=0.0)
{
    ...
}

int main()
{
    PraeVar(5, 2, 5);
}
Nach dem Aufruf der Funktion PraeVar() sind die Parameter innerhalb der Funktion folgendermaßen belegt:

Die Parameter der Funktion main

ANSI

Nun ist der Zeitpunkt gekommen, einen näheren Blick auf die Hauptfunktion main() zu werfen. Diese Funktion hat nämlich ebenfalls Parameter und einen Rückgabetyp. Allerdings tolerieren es die meisten Compiler, wenn beides weggelassen wird. Der ANSI-Standard schreibt vor, dass die Funktion main() mit dem Rückgabetyp definiert wird.

Rückgabetyp

Der Rückgabewert von main() ist vom Typ Integer, der an das aufrufende Betriebssystem zurückgeliefert wird. Dabei zeigt die Rückgabe von 0 an, dass das Programm ohne Probleme gelaufen ist. Alle anderen Werte werden als Fehlernummern des Programms interpretiert. Was die Nummern im Einzelnen bedeuten, ist nicht festgelegt, sondern kann vom Programmierer frei definiert werden. Die Rückgabewerte können jedenfalls von aufrufenden Programmen oder Skripten ausgewertet werden. Darum sollten Sie für jede Ursache, die zu einem Abbruch Ihres Programms führt eine eigene Fehlernummer festlegen und dies auch dokumentieren.

Aufrufparameter

Die Funktion main() hat auch zwei Aufrufparameter. Aus diesen kann das Programm erfahren, mit welchem Kommando und vor allem mit welchen Argumenten es aufgerufen wurde. Der erste Parameter heißt argc
[1]. Er ist ein Integer und gibt an, mit wie vielen Parametern das Programm aufgerufen wurde. Dabei zählt der Programmname mit, sodass argc immer mindestens 1 ist. Der zweite Parameter heißt argv[2] und ist ein Array von Zeigern auf char. Darin befinden sich der Programmname und die vom Aufrufer angegebenen Programmparameter. Eine vollständige Definition von main() sieht also so aus:[3]

int main (int argc, char* argv[])
{
    ...
}

Die Namen argc und argv sind reine Konventionen. Sie können die Parameter natürlich anders nennen. Da sie aber aber inzwischen unter diesem Namen bekannt sind, führt eine Änderung nicht unbedingt zu mehr Klarheit. Betrachten wir zunächst den zweiten Parameter argv. Er ist ein Array von Zeigern auf C-Strings. Grafisch dargestellt sieht argv also so aus wie in Abbildung (picargv).

An dieser Stelle findet sich im Buch die Abbildung "[Der Parameter argv]" (picargv)

argv ist ein Array, das Zeiger enthält. Jeder der Zeiger zeigt auf einen C-String, also auf ein Array vom Typ char, dessen Ende durch ein Nullbyte gekennzeichnet wird. Der erste Zeiger, also argv[0], zeigt immer auf den Programmnamen, mit dem das Programm gestartet wurde. Entsprechend ist argc immer mindestens eins. Diese Eigenheit wird vor allem unter UNIX gern genutzt, um über den Programmnamen auch die Funktionalität zu steuern. Prominentes Beispiel ist das Packprogramm gzip, das entpackt, wenn es als gunzip aufgerufen wurde. Wird ein Dateiname als Parameter angegeben, findet sich dieser unter argv[1]. In diesem Fall enthält die Variable argc den Wert 2.

Beispiel

Das folgende Programm zeigt mindestens den Namen, unter dem es aufgerufen wurde. Falls weitere Parameter angegeben wurden, werden diese zeilenweise angezeigt.

[Zeigt die Aufrufparameter (main.cpp)]

#include <iostream>
using namespace std;

int main(int argc, char **argv)
{
    for (int i=0; i<argc; i++)
    {
        cout << argv[i] << endl;
    }
    return 0;
}

Variable Anzahl von Parametern

Es gibt Funktionen, wie beispielsweise printf(), die eine variable Anzahl von Parametern zulassen. In der Parameterliste haben solche Funktionen nach dem letzten spezifizierten Parameter drei Punkte. Dies deutet an, dass noch beliebig viele Parameter beliebigen Typs folgen können. Beim Aufruf solcher Funktionen können Sie beliebig viele weitere Parameter an die fest vorgeschriebenen anhängen.

Start und Ende

Auf diese zusätzlichen Parameter können Sie nicht wie auf normale Parameter zugreifen, da Sie ja keinen Variablennamen haben, den Sie verwenden könnten. Mit dem Makro va_start() wird der Zugriff auf die unspezifizierten Parameter eingeleitet. Der erste Parameter ist das Handle vom Typ va_list. An dem Inhalt dieses Handles erkennt va_arg(), wie weit die Variablen abgearbeitet sind. Dieses Handle wird von va_start() vorbelegt. Der zweite Parameter von va_start() ist der letzte spezifizierte Parameter der Schnittstelle. Der Zugriff auf die Parameter wird später durch den Aufruf von va_end() abgeschlossen, dessen einziger Parameter das Handle ist.

Zugriff

Dazwischen wird auf die einzelnen Parameter mit dem Makro va_arg() zugegriffen. Der erste Parameter ist wieder das Handle. Als zweiter Parameter wird der Typ angegeben, den man als Parameter erwartet. Den an die Funktion übergebenen Wert erhalten Sie als Rückgabewert dieses Makros.

[Zugriff auf unspezifizierte Parameter (vararg.cpp)]

#include <iostream>
using namespace std;
#include <stdarg.h>

void meineFunktion(int Anzahl, ... )
{
    va_list params; // Zugriffshandle für Parameter
    int intpar;     // Parameterinhalt
    va_start(params, Anzahl); // Zugriff vorbereiten
    // Durchlaufe alle Parameter (steht in Anzahl)
    for (int i=0; i<Anzahl; i++)
    {
        intpar = va_arg(params, int); // hole den Parameter
        cout << intpar << endl;       // zeige ihn an
    }
    cout << "---" << endl;
    va_end(params); // Zugriff abschließen
}

int main()
{
    // Tests der Funktion mit unterschiedlichen Parametern
    meineFunktion(2, 5, 2);
    meineFunktion(0);
    meineFunktion(4, 5, 2, 0, 3);
}

Anzahl der Parameter

Die Anzahl der Parameter muss der Funktion vom Aufrufer übergeben werden. Das Makro liefert diese Information nicht. Im Beispiel oben wird die Anzahl im ersten Parameter erwartet. Ferner kann meineFunktion() , nur mit ganzzahligen Werten als Parameter umgehen. Bei der Funktion printf() wird die Anzahl und der Typ der Parameter anhand des Format-Strings bestimmt.

Riskant

Wie Sie sehen, ist der Einsatz dieser Technik recht fehleranfällig. Der Compiler kann die Richtigkeit der Parameter nicht überprüfen. Übergibt der Aufrufer eine falsche Anzahl von Parametern, erhält die Funktion nur Unfug, kann den Irrtum aber nicht bemerken. Aus diesem Grund werden variable Parameter auch nur dort eingesetzt, wo sie unumgänglich sind.
[1]
argc steht für argument count. Das bedeutet etwa >>Anzahl der Argumente<<.
[2]
argv steht für argument vector.
[3]
In Einzelfällen werden Sie auch einen dritten Parameter sehen, der ein Zeiger auf die Environment-Variablen darstellt. Dies wird aber vom POSIX-Standard nicht unterstützt. Der Grund ist, dass der Zugriff über die Funktionen getenv() und setenv() sehr viel einfacher ist.


Informatik-Ecke Einstieg in C++ (C) Copyright 2005 Arnold Willemer