29. Technik: C Calling Convention I

Argumente werden in C by value übergeben – Funktionen arbeiten immer mit Kopien ihrer Argumente, können sie also ändern, ohne dass das den Aufrufenden stört.

In Python sieht es so aus, als sei das auch so, der dahinterstehende Mechanismus ist aber ein ganz anderer.

Das Programm

def foo(bar):
  bar = bar+1

bar = 7
foo(bar)
print bar

gibt natürlich 7 aus, und zwar, weil bar = 7 im globalen Namespace den Namen bar mit dem Objekt 7 verbindet, während der Formalparameter bar in der Funktion foo im lokalen Namespace von foo lebt. Da Python immer zuerst im lokalen Namespace sucht, wird im bar = bar+1 rechts das lokale bar (das nach der Übergabe auch auf 7 verweist) gefragt. Die Zuweisung landet wieder im lokalen Namespace, d.h. das Ergebnis des Ausdrucks auf der rechten Seite, die 8, wird an den Namen bar im lokalen Namespace gebunden. Dieser lokale Namespace wird beim Verlassen der Funktion vergessen, so dass zum Zeitpunkt des print-Statements bar nur noch im globalen Namespace zu finden ist, und dort war und ist es an 7 gebunden. Ähnliches gilt natürlich, für die lokalen Namespaces verschiedener Funktionen. Vgl. auch das global-Statement, das in dieses Verhalten eingreifen kann.

Aber: Wenn man Referenzen auf veränderbare Objekte übergibt, können diese Objekte verändert werden – man operiert in diesem Moment nicht mehr mit dem Namen, sondern mit dem Wert, und der hängt natürlich nicht vom Namespace ab. Werden Funktionen so aufgerufen, dass sie ihre Argumente ändern können, heißt das call by reference. Wir werden sehen, dass so etwas in C ganz vergleichbar gemacht wird – nur sind in C alle Werte prinzipiell veränderbar.

C hat keine Namespaces. Wie also sorgt der Compiler dafür, dass

#include <stdio.h>

int baz(int zot) { int bla;
  bla = zot+9; return bla; }

void foo(int bar) { bar = baz(bar+1); }

int main(void) { int bar=7;
  foo(bar); printf("%d", bar);
}

die 7 ausgibt?

Lösung: Argumente und lokale Variablen werden auf einem Stack gelagert.

Durch direkte Veränderung des Stackpointers kann man gleich einen ganzen Haufen Zeug von “oben” wegnehmen, etwa alle lokalen Variablen einer Funktion auf einmal – so kann ein recht effizientes Vergessen überflüssig gewordener Werte realisiert werden.

Diese Grafik zeigt, wie sich der Stack während der Laufzeit des obigen Programms ändern könnte (im Gegensatz zur Darstellung hier “wachsen” Stacks auf heutigen Maschinen normalerweise nach unten, also zu kleineren Adressen hin). Anfangs (links oben) liegen nur die Rückkehradresse der Funktion main (also die Adresse des Codes, der ausgeführt werden soll, nachdem wir unser main verlassen haben; der Wert 0x80 ist natürlich frei erfunden) sowie der Speicher für die lokale Variable bar auf dem Stack. Beim Aufruf foo(bar) wird zunächst 7, nämlich eine Kopie von bar und danach die Rückkehradresse für die Funktion foo auf den Stack geschoben. Das &main+10 ist wieder frei erfunden; ganz unplausibel ist es aber nicht, dass die CPU 10 Bytes hinter dem Einsprung vom main weiterarbeiten soll, wenn foo fertig ist.

Beim Aufruf von baz(bar) passiert wieder ähnliches, wir sehen den Argument 8, eine Rückkehradresse und diesmal Speicher für die lokale Variable bla. Bei der Rückkehr aus baz muss jetzt nur der Stackpointer (der sagt, was das oberste Element ist) runtergesetzt werden, und alles, was baz (am Stack) gemacht hat, ist vergessen; genauso wird bei der Rückkehr aus foo verfahren, und wenn wir wieder in main sind (rechts oben), ist der Stack wieder wie zu Beginn.

In der Realität ist das heute nicht so ganz einfach, weil Compiler allerlei Tricks zur Optimierung verwenden, zur Erzeugung von geschicktem Code nämlich. Im hier dargestellten System sind Funktionsaufrufe nämlich relativ teuer (bezüglich der Währung Rechenzeit), und so werden Argumente häufig einfach in Registern übergeben. Zumindest bei nichtoptimiertem Code zumal auf Intel-Prozessoren (die relativ wenige benutzbare Register haben) allerdings sieht das auch heute noch ziemlich wie dargestellt aus.

Wie wir schon am Anfang gesehen haben, wird das Ergebnis von Funktionen üblicherweise in einem Register zurückgegeben.

In diesem Bild sind auch die Seiteneffekte etwas besser fassbar: Wenn die Funktion irgendwelchen Speicher außerhalb des Stacks manipuliert (also nicht ausschließlich auf lokale Variablen zugreift), so hat sie Seiteneffekte. In C muss man relativ viel mit Seiteneffekten operieren – was durchaus als ein weiterer Schwachpunkt zu sehen ist, denn Seiteneffekte sind nicht nur theoretisch schwer in den Griff zu bekommen, sondern können Programme auch sehr konfus machen, da sie immer mehr oder weniger die Kapselung der Programmlogik in kleine Bereiche verletzen.

Call by reference

Wenn wir jetzt übergebene Argumente selbst ändern wollen, geht das offenbar nicht ohne weiteres. Pointer schaffen Abhilfe: Statt der Variablen selbst übergeben wir einen Pointer auf sie. Der Pointer kommt auf den Stack und wird vergessen, die Daten selbst jedoch liegen nicht in dem Bereich des Stacks, der nach dem Ende der Funktion freigegeben wird.

Beispiel:

void inc(int *a)
{
  (*a)++;
}
...
int a; inc(&a);

In Python gibt es keinen äquivalenten Mechanismus. Das liegt vor allem daran, dass Python ganz grob immer call by reference macht. Der Aufrufer übergibt Referenzen, die dann an die lokalen Namen gebunden werden. Unveränderliche Werte können aber natürlich nicht geändert werden – eine Funktion inc wie im Beispiel ist in Python nicht zu machen. Veränderbare Werte kann eine Funktion natürlich verändern, und weil der Aufrufer ja auch eine Referenz auf den Wert hält, bekommt er die Änderungen auch mit, etwa so:

def inc(zp):
  zp[0] = zp[0]+1

– aber natürlich will man so wirklich nicht programmieren. Der Haupt-Anwendungsfall für “explizites” (im Gegensatz zum Herumreichen von Referenzen auf allerlei komplizierte Datenstrukturen) call by reference in C ist, mehrere Werte aus einer Funktion zurückzugeben. Python hat dafür Tupel, C nicht (ohne weiteres).

Eine Funktion dieser Art ist scanf, das ja dem Aufrufer zusätzlich zu seinem Rückkehrstatus (nämlich der Zahl der gelesenen Elemente) auch noch die gelesenen Werte mitteilen muss. Daraus erklären sich nun auch die vorher mysteriösen &-Zeichen vor skalaren Variablen. Strings dürfen das & natürlich nicht haben, denn ein String ist ja bereits ein Array von chars, also unter Geschwistern ein char*, also eine Referenz.

Und noch etwas zu Python: Da Pythons Variablenkonzept ganz anders ist als das von C ist es erstaunlich unproblematisch, Python komplett ohne Stack laufen zu lassen – die Variablen liegen ohnehin nicht auf dem Stack, und statt Rückkehradressen samt Kontexten (z.B. den jeweils aktuellen Namespaces) lassen sich Continuations genannte Datenstrukturen verwenden. Wenn man diese Continuations auch Programmen zugänglich macht, kann man ganz erstaunliche Spielchen machen, und ganz grundsätzlich hat ein solches System Vorteile etwa wenn man Programmteile parallel laufen lassen möchte. Mehr zu diesen Fragen findet ihr, wenn ihr das Netz nach stackless python durchsucht.


Markus Demleitner

Copyright Notice