Im ersten Artikel zum Thema Programmierung ging es um die allgemeine Entwicklungsumgebung, sprich: was wird überhaupt benötigt, um zu programmieren. Heute wollen wir auch schon direkt mit Programmieren anfangen und dabei eines der zentralen Konzepte der meisten Programmiersprachen besprechen: die Funktionen.
Ein Hinweis vorab: ich werde Programmcode ausnahmslos in Englisch schreiben, ganz einfach, weil das in weiten Teilen der Informatik (und insbesondere der Forschung) weit verbreitet ist und sich meiner Meinung nach aus Kommunikationsgründen auch so gehört (unter anderem auch, da die meisten Programmiersprachen ohnehin englische Begriffe verwenden).
Fangen wir mit ein wenig technischem Hintergrund an. Funktionen bilden den zentralen Kern der meisten Programmiersprachen, da über sie der Programmfluss gesteuert wird. In einem früheren Artikel hatte ich schon einmal beschrieben, wie Rechner ungefähr arbeiten. Grob gesagt funktioniert das folgendermaßen: die auszuführenden Anweisungen stehen, gespeichert als Binärzahlen (der Rechner weiß, wie er die Zahlen interpretieren muss), im Programmspeicher des Rechners, und zwar in sogenannten Speicherzellen; zusammengehörige Anweisungen stehen dabei in der Regel auch hintereinander. Jede Speicherzelle hat eine bestimmte Adresse (eine einfache Zahl), über die sie identifiziert werden kann. Zusätzlich speichert der Rechner im Program Counter (PC) die Adresse der nächsten auszuführenden Anweisung.
Über die Modifikation des PCs kann also gesteuert werden, welche Anweisung als nächstes ausgeführt werden soll – man spricht von der Steuerung des Programmflusses. Eine Funktion ist allgemein gesagt eine Folge zusammengehöriger Anweisungen, die hintereinander im Programmspeicher ab einer bestimmten Adresse stehen. Ein Funktionsaufruf ist – technisch gesehen – das setzen des PCs auf die Startadresse der aufgerufenen Funktion mit anschließender Ausführung der zur Funktion gehörenden Anweisungen und danach erfolgendem Rücksprung an die Stelle vor dem Aufruf.
Klingt kompliziert? Schauen wir uns das einmal an einem konkreten Beispiel an. Rufen wir uns zuerst noch einmal den Code aus dem letzten Artikel ins Gedächtnis:
#include <cstdio> int main() { printf( "Hello World!\n" ); return 0; }
Die erste Zeile ignorieren wir erst einmal. Interessant wird es hier:
In dieser Zeile sehen wir die Definition einer Funktion, die in diesem Fall den Namen main
trägt (zu dem Namen gleich noch etwas). Das ominöse int
ist der Rückgabewert der Funktion; er besagt in diesem Fall, dass die Funktion eine ganze Zahl “berechnet” (im weitesten Sinne; int
ist die Abkürzung für “integer”, englisch für ganze Zahl) und als Ergebnis zurückliefert – ganz so, wie man es aus der Mathematik kennt, wo etwa die mathematische Funktion f(x) = x²
das Quadrat eines gegebenen Parameters (zum Thema Parameter kommen wir in einem der folgenden Artikel) berechnet. Die leeren Klammern hinter main
besagen, dass die Funktion keine Argumente erwartet (dazu gleich noch etwas). Insgesamt nennt man eine solche Definition Funktionskopf oder Funktionssignatur.
Auf die Signatur folgt für gewöhnlich der Funktionsrumpf; dieser fasst in geschweiften Klammern die Anweisungen beziehungsweise Statements zusammen, die beim Aufruf der entsprechenden Funktion ausgeführt werden sollen. In unserem Fall sind das die folgenden beiden Zeilen:
printf( "Hello World!\n" ); return 0;
Die erste Anweisung ist einer der bereits erwähnten Funktionsaufrufe. Hier wird die Funktion printf
aufgerufen, die dafür sorgt, dass Text auf dem Bildschirm ausgegeben wird; die Funktion erwartet (mindestens) ein Argument, also einen übergebenen Wert, der den auszugebenden Text beschreibt, in unserem Fall "Hello World!\n"
. "Hello World!\n"
ist ein sogenannter String (oder deutsch: eine Zeichenkette), also eine beliebige Folge von Zeichen.
Die zweite Anweisung ist eine sogenannte return
-Anweisung, mit welcher gesagt wird, welchen Ergebniswert eine Funktion zurückgeben soll; in unserem Fall ist das einfach der Wert 0.
Um das Konzept des Funktionsaufrufes und der Funktionsdefinition besser zu verstehen, wollen wir unseren Code etwas ändern. Anstatt einer einzelnen Anweisung printf( "Hello World!\n" );
wollen wir das Ganze in eine separate Funktionen auslagern. Wir schreiben also den folgenden Code:
#include <cstdio> void greet() { printf( "Hello " ); printf( "World!\n" ); } int main() { greet(); return 0; }
Was sehen wir hier? Nun, zuerst einmal eine neue Funktion greet
mit dem Rückgabetyp void
; dieser besagt, dass unsere Funktion einfach keinen Wert zurückgibt – muss sie auch nicht, sie soll uns nur grüßen. Der Funktionsrumpf besteht aus 2 Aufrufen der printf
-Funktion, einfach mit aufgeteiltem String. Zusätzlich wurde in der main
-Funktion der Aufruf von printf
durch den Aufruf der neuen greet
-Funktion ersetzt.
So weit, so einfach (hoffe ich). Aufmerksamen Lesern dürften nun aber ein paar Sachen aufgefallen sein, die ich bisher verschwiegen habe.
Als erstes wäre da die Frage, woher die Funktion printf
überhuapt herkommt, und was diese ominöse erste Zeile bewirkt:
#include <cstdio>
Beides hängt zusammen; printf
ist eine Funktion der sogenannten Standardbibliothek von C++. Die Standardbibliothek einer Programmiersprache ist eine Sammlung von unter anderem verschiedenen Funktionen für alle möglichen allgemeinen Zwecke; printf
entstammt nun genau aus dieser. Deklariert1 ist diese Funktion in der Datei cstdio
(zusammen mit vielen anderen Funktionen), welche mit der Zeile #include <cstdio>
in die aktuelle Datei eingebunden wird (der Compiler weiß selber, wo er diese Datei suchen muss, deswegen klappt das). Damit wird dem Compiler die Funktion printf
also bekannt gemacht und wir können sie problemlos aufrufen.
1 Über den Unterschied zwischen Deklaration und Definition schreibe ich ein andermal.
Eine weitere auftauchende Frage könnte sein, was das \n
am Ende des Hello World!
-Stringes zu bedeuten hat. \n
ist eine sogenannte Escape-Sequenz; das sind kurze Zeichenketten, die besondere Symbole darstellen. \n
ist dabei das Zeichen für den Zeilenumbruch, da wir am Ende unseres Strings einen solchen haben wollen, ihn aber nicht einfach in den String schreiben können (das verbietet C++). Der folgende Code ist also ungültig:
void greet() { printf( "Hello World! " ); }
Escape-Sequenzen beginnen immer mit einem Backslash, gefolgt von (meist) einem oder mehreren Buchstaben, Symbolen oder Ziffern, welche die genaue Escape-Sequenz beschreiben. Das bringt natürlich sofort die Frage mit sich, wie man denn dann einen normalen Backslash innerhalb eines Strings darstellt – das geschieht wiederum über eine Escape-Sequenz, und zwar per Doppelbackslash: \\
Eine letzte wichtige Frage ist noch offen: woher weiß das Programm, dass es gerade bei der main
-Funktion anfangen soll, und warum liefert die eigentlich eine ganze Zahl zurück? Beides ist Konvention; in der Standardeinstellung verlangen alle Compiler in einem zu kompilierenden Programm eine Funktion mit dem Namen main
, welche als sogenannter Einsprungpunkt der Anwendung genutzt wird, sprich, welche automatisch bei Programmstart als erstes aufgerufen wird. Dass die main
-Funktion eine ganze Zahl als Rückgabewert erwartet, ist ebenfalls Konvention. Früher wurde damit häufig angegeben, ob eine Anwendung erfolgreich beendet wurde oder ob ein Fehler auftrat – die zurückgegebene Zahl gab einen Hinweis auf den Fehler. Moderne Compiler akzeptieren aber auch eine main-Funktion ohne Rückgabewert, sprich mit der Signatur void main()
und dann ohne die Zeile return 0;
.
Das war der erste Artikel zum Thema Funktionen. Fragen wie immer in die Kommentare oder (bei speziellen Fragen) gern auch per Mail. Im nächsten Artikel wird es dann (vermutlich) um das zweite wichtige Konzept von Programmiersprachen gehen, nämlich die Variablen. Seid gespannt!
Kommentare (28)