Parameter |
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);
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.
Parametervariablen sind lokale Variablen der Funktion, die beim Funktionsaufruf durch eine Kopie der übergebenen Werte initialisiert werden.
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)
An dieser Stelle erscheint im Buch die Abbildung "Schnittstelle einer Funktion" (grafparas)
[Prototyp]
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.
[Veränderlicher Parameter]
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.
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.
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.
[Parameterübergabe per Referenz (refpar.cpp)]
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.
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.
void trennlinie(int AnzahlStriche);
int main()
{
int breite = 45;
trennlinie(45);
cout << "Programm zur Ermittlung..." << endl;
trennlinie(breite);
}
void trennlinie(int stellen)
{
...
}
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.
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.
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;
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]);
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.
#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.
[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.
[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:
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.
[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;
}
[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);
}
|
Diese Seite basiert auf Inhalten aus dem Buch
Arnold Willemer: Einstieg in C++ Mit freundlicher Genehmigung und Unterstützung des Verlags galileo computing |
| Informatik-Ecke Einstieg in C++ |
(C) Copyright 2005 Arnold Willemer
|