Parameter und Argumente
Zwei Wörter, die austauschbar benutzt werden, aber eine Unterscheidung wert sind:
- Ein Parameter ist die Variable in der Definition der Funktion - das
int ninint square(int n). - Ein Argument ist der Wert, den du an der Aufrufstelle lieferst - die
4insquare(4).
Eine Funktion aufzurufen erzeugt ihre Parameter als frische lokale Variablen und kopiert die Argumente hinein. Diese Kopie ist das ganze Thema dieser Seite.
C übergibt als Wert - immer
Das ist die Regel, und C kennt keine Ausnahme davon: Eine Funktion erhält Kopien ihrer Argumente. Einem Parameter etwas zuzuweisen ändert nur die Kopie.
value ist weiterhin 42. Die Funktion bekam eine Kopie namens x, setzte die Kopie auf null, und die Kopie verschwand, als die Funktion zurückkehrte. Den Parameter in value umzubenennen ändert nichts - der Name verbindet sie nicht; es sind zwei verschiedene Variablen.
Dasselbe gilt für jeden Typ: int, double, char, einen Zeiger, sogar eine ganze Struktur (Feld für Feld kopiert). Wenn du von Python, Java oder JavaScript kommst, beachte, dass diese Sprachen Referenzen auf Objekte als Wert übergeben, sodass das Verändern eines Objekts innerhalb einer Funktion außen sichtbar ist. C hat das nicht - es kopiert das Objekt selbst.
Wertübergabe ist eine Stärke, keine Einschränkung. Eine Funktion kann die Daten des Aufrufers nicht versehentlich beschädigen, was das Durchdenken viel leichter macht.
Das klassische Scheitern: swap
Die Lehrbuchdemonstration ist eine Funktion, die zwei Werte zu tauschen versucht:
Die Logik ist korrekt - sie tauscht a und b wirklich. Aber a und b sind Kopien, die Arbeit wird also in dem Moment weggeworfen, in dem die Funktion zurückkehrt.
Referenzübergabe mit Zeigern nachbilden
Damit eine Funktion die Variable eines Aufrufers ändern kann, gib ihr statt des Werts die Adresse der Variablen. Diese Adresse wird selbst als Wert übergeben - der Zeiger wird kopiert -, aber die Kopie zeigt weiterhin auf das ursprüngliche Objekt, über sie zu schreiben erreicht also den Speicher des Aufrufers.
Drei Stücke Syntax tragen die ganze Idee:
int *ain der Parameterliste deklariertaals „Zeiger auf einint".&xan der Aufrufstelle erzeugt die Adresse vonx.*ainnerhalb der Funktion bedeutet „dasint, auf dasazeigt" - lesbar und zuweisbar.
Das meinen C-Programmierer mit „Referenzübergabe", und Genauigkeit lohnt sich: Es ist immer noch Wertübergabe. Der Zeiger wird kopiert. a selbst etwas zuzuweisen (a = NULL;) würde nur die Kopie ändern; *a etwas zuzuweisen ändert die Variable des Aufrufers. Zeiger behandelt das Modell vollständig.
Der Preis ist, dass eine Aufrufstelle dir nicht mehr sagt, ob eine Variable sich ändern kann - und genau deshalb ist das & Pflicht. swap(x, y) kompiliert nicht; du musst swap(&x, &y) schreiben, und dieses & ist die sichtbare Markierung, dass dieser Aufruf x verändern kann.
Mehrere Ergebnisse zurückgeben
return liefert einen Wert, zusätzliche Ausgaben reisen also über Zeigerparameter. Die übliche Form ist ein Statuscode als Rückgabewert und die eigentlichen Ergebnisse über Zeiger geschrieben:
Beachte, dass q und r ihre bisherigen Werte behalten, wenn der Aufruf fehlschlägt - die Funktion kehrte zurück, bevor sie irgendetwas schrieb. Das ist der Vertrag, den der Aufrufer respektieren muss, und deshalb kommt die Statusprüfung vor der Nutzung der Ausgaben.
Die Alternative ist, eine Struktur mit beiden Feldern zurückzugeben, was Zeiger ganz vermeidet und oft das schönere Design ist, wenn die Werte wirklich zusammengehören.
Arrays sind anders
Arrays folgen der Kopierregel nicht, und das bringt fast jeden ins Stolpern. Wenn du ein Array übergibst, zerfällt es zu einem Zeiger auf sein erstes Element - die Funktion bekommt also eine Adresse, keine Kopie der Daten.
Das Array des Aufrufers hat sich wirklich geändert, und zwar ohne ein & irgendwo - weil data in diesem Zusammenhang bereits eine Adresse ist. Drei Konsequenzen:
int a[] und int *a bedeuten in einer Parameterliste genau dasselbe. Sogar int a[100] tut das; die Größe wird ignoriert. int a[] zu schreiben dokumentiert die Absicht, aber lies es nicht als „ein Array wird kopiert".
sizeof innerhalb der Funktion ist falsch. Das ist der Bug:
In main ist sizeof(data) gleich 20 Bytes und die Länge kommt als 5 heraus. Innerhalb der Funktion ist sizeof(a) die Größe eines Zeigers - 8 auf einem 64-Bit-System -, die „Länge" ist also 2. Die Größe des Arrays ist dort schlicht nicht verfügbar. Übergib die Länge immer als separaten Parameter. Moderne Compiler warnen mit -Wall davor.
Nimm const, wenn du nicht schreiben wirst. const int a[] sagt, dass die Funktion nur liest, was der Compiler durchsetzt und worauf sich ein Leser verlassen kann:
int sum(const int a[], int n); /* verspricht, das Array nicht zu ändern */
Zeichenketten sind ebenfalls Arrays, dasselbe gilt also: void greet(const char *name) ist die Standardsignatur für eine Funktion, die eine Zeichenkette liest, ohne sie zu ändern.
Was ist mit den Kosten?
Eine große Struktur als Wert zu übergeben kopiert bei jedem Aufruf jedes ihrer Bytes. Bei einer Struktur mit einer Handvoll Felder ist das in Ordnung und oft schneller als die Indirektion, die ein Zeiger hinzufügt. Bei einer großen übergib einen Zeiger - und markiere ihn als const, wenn die Funktion nur liest:
struct Config { char name[64]; int flags[32]; double weights[128]; };
void applyConfig(const struct Config *cfg); /* keine 512-Byte-Kopie pro Aufruf */
const struct Config *cfg bedeutet „ein Zeiger auf ein Config, das ich nicht ändern werde", und das verbindet die Effizienz eines Zeigers mit der Sicherheit einer Kopie.
Häufige Fehler
- Erwarten, dass ein schlichter Parameter die Variable des Aufrufers ändert. Er kann es nicht. Übergib einen Zeiger.
- Das
&an der Aufrufstelle vergessen.swap(x, y), woswapZeiger will, ist ein Typfehler - der Compiler fängt ihn ab, aber nur, wenn ein Prototyp sichtbar ist. - Das
*innerhalb der Funktion vergessen.a = b;bei zwei Zeigerparametern tauscht die lokalen Kopien, nicht die Werte. Du willst*a = *b;. sizeofauf einem Array-Parameter benutzen. Immer falsch. Übergib die Länge.- Einen Zeiger auf eine lokale Variable zurückgeben. Die lokale Variable stirbt, wenn die Funktion zurückkehrt, der Aufrufer hält also einen baumelnden Zeiger, was undefiniertes Verhalten ist. Gib als Wert zurück, schreibe in einen vom Aufrufer gelieferten Puffer oder belege mit
malloc. - Einen Zeigerparameter nicht auf
NULLprüfen. Eine Funktion, die alles dereferenziert, was man ihr reicht, stürzt bei einem Nullargument ab.
Häufig gestellte Fragen
Hat C eine Referenzübergabe?
Nein. C hat genau einen Modus: die Wertübergabe. Eine Funktion erhält immer eine Kopie jedes Arguments, einem Parameter etwas zuzuweisen berührt also nie die Variable des Aufrufers. Was wie Referenzübergabe aussieht, ist die Übergabe eines Zeigers als Wert - der Zeiger wird kopiert, zeigt aber weiterhin auf das Objekt des Aufrufers, sodass die Funktion es über den Zeiger ändern kann.
Wie übergibt man in C ein Array an eine Funktion?
Schreibe den Parameter als int a[] oder int *a - sie bedeuten dasselbe - und übergib die Länge als separaten Parameter: int sum(const int a[], int n). Das Array wird nicht kopiert; die Funktion erhält einen Zeiger auf sein erstes Element, Änderungen an a[i] sind also für den Aufrufer sichtbar.
Warum funktioniert sizeof bei einem Array-Parameter in C nicht?
Weil der Parameter in Wahrheit ein Zeiger ist. sizeof(a) innerhalb der Funktion liefert die Größe eines Zeigers (typischerweise 8 Bytes), nicht die Größe des Arrays - sizeof(a)/sizeof(a[0]) ergibt also etwa 2 statt der Elementanzahl. Übergib die Länge immer ausdrücklich.
Wie kann eine C-Funktion mehr als einen Wert zurückgeben?
Übergib Zeiger für die zusätzlichen Ergebnisse und schreibe darüber: int divide(int a, int b, int *quotient, int *remainder) liefert ein Erfolgskennzeichen und füllt beide Ausgaben. Die Alternative ist, eine Struktur zurückzugeben, die die Werte bündelt.