C programmieren: Kontrollstrukturen und Funktionen in C
Anbieter zum Thema
Die Syntax der Programmiersprache C ist mächtig, birgt aber auch einige Tücken. Wie soll ein in C geschriebenes Programm ablaufen und wie führe ich gezielt Funktionen in C aus? Dieser Artikel geht näher auf diese syntaktischen Elemente ein.

Auf einige Teile der speziellen Syntax beim Programmieren in C sind wir in einem früheren Beitrag bereits eingegangen: Welche Dateitypen es gibt, wie Deklarationen funktionieren, was für Operatoren genutzt werden können und was unter Ausdrücken zu verstehen ist. Damit lassen sich in Kombination mit den lexikalischen Elementen von C bereits gezielt einige Zeilen funktionierenden Code schreiben.
Aber was bestimmt den Ablauf eines geschriebenen Programms? Und wie ist ein in C programmierter Code gegliedert? Um dies zu verstehen, sollte man sich eingehender mit den in C vorhandenen Kontrollstrukturen und Funktionen befassen.
Kontrollstrukturen in der C-Programmierung
Kontrollstrukturen definieren den Ablauf eines Programms. Die einfachste Kontrollstruktur ist die Sequenz, d.h. Folge. Der Compiler liest dabei den Quelltext von links nach rechts, von oben nach unten, und setzt ihn in Code um, der eine sequentielle Abarbeitung bewirkt. Um dies zu erzeugen, schreibt man also eine Anweisung nach der andern, von links nach rechts, bzw. besser von oben nach unten, hin.
Die nächste Kontrollstruktur ist die Auswahl. C kennt die zwei Auswahl- oder Verzweigungskontrollstrukturen if... else und switch case. Das if-Konstrukt hat folgende allgemeine Form:
if (expression) /* expression muss vom arithmetischen oder Zeigertyp sein */
statement1 /* wenn expression ungleich 0, statement1 ausführen */
statement2 /* sonst statement2 ausführen */
Der else-Teil ist optional. Man beachte, dass es in C kein 'then' und keine Endmarke (endif o.ä.) für diese Konstruktion gibt. Ebenso ist jeweils nur ein statement erlaubt; braucht man mehrere, so muss man zum {block statement} greifen. Falls man mehrere geschachtelte if-Strukturen verwendet, ordnet der Compiler das else immer dem jeweilig direkt vorausgehenden if zu, so dass man durch Verwendung von Blockklammern {} für die korrekte Gliederung sorgen muss. Die visuelle Gestaltung des Quelltexts ist nur eine Lesehilfe und hat für die Syntax der Sprache C keine Bedeutung.
Das zweite Auswahlkonstrukt, switch case, hat viele Formen. Am häufigsten gebraucht wird die folgende allgemeine Form:
switch (integral expression) {
case constintexpr1 : /* Der : ist die Syntaxkennung für eine Marke. */
statement1
statement2
break; /* hier wird der switch in diesem Fall verlassen. */
case constintexpr2 :
statement3
statement4 /* break fehlt: Es geht weiter zum nächsten Fall! */
default:
statement5
}
Die Ausdrücke in den case-Marken müssen konstante integrale Ausdrücke sein. Mehrere Marken sind erlaubt. Für den kontrollierenden Ausdruck findet Integer-Erweiterung statt und die case-Konstanten werden in den so erweiterten Typ umgewandelt. Danach dürfen keine zwei Konstanten den gleichen Wert haben. Die default-Marke darf pro switch nur einmal vorhanden sein; sie deckt alles ab, was von den anderen Marken nicht erfasst wird und darf an beliebiger Stelle erscheinen.
Das Problem des switch-Konstrukts ist die break-Anweisung: fehlt sie, geht die Abarbeitung über die nächste Marke hinweg einfach weiter (sog. fall through). Dies kann man natürlich geschickt ausnutzen, ein fehlendes – vergessenes – break hat jedoch oft schon zu den seltsamsten Überraschungen geführt. Es ist daher zu empfehlen, einen beabsichtigten Fall von fall through durch einen entsprechenden Kommentar besonders kenntlich zu machen, allein schon um selbst den Überblick wahren zu können.
Die nächste wichtige Kontrollstruktur ist die Wiederholung, auch Schleife genannt. Hier hält C drei verschiedene Konstrukte bereit:
while (expression) /*solange expression ungleich 0 */
statement /* statement ausführen */
expression muss vom arithmetischen oder Zeigertyp sein und wird bewertet. Falls nicht 0, wird statement ausgeführt; dies wird solange wiederholt, bis expression zu 0 bewertet wird. Dies ist eine sogenannte kopfgesteuerte Schleife.
Soll das while-Konstrukt mehrere Anweisungen kontrollieren, greift man üblicherweise zur Blockanweisung.
do
 : statement /* statement ausführen */
while (expression); /* solange bis expression zu 0 bewertet wird */
statement wird ausgeführt, dann wird expression bewertet. expression muss wie oben vom arithmetischen oder Zeigertyp sein. Falls nicht 0, wird dies solange wiederholt, bis expression zu 0 bewertet wird. Man beachte das syntaktisch notwendige Semikolon am Schluss des Konstrukts. Dies ist eine sogenannte fußgesteuerte Schleife. Für mehrere zu kontrollierende Anweisungen gilt das gleiche wie oben.
for (expression1; expression2; expression3)
statement
Jeder der drei Ausdrücke in der Klammer des for-Konstrukts darf auch fehlen, die beiden Semikola sind jedoch syntaktisch notwendig. Zu Beginn wird einmalig expression1 bewertet, ihr Typ unterliegt keiner Einschränkung. Sind mehrere Ausdrücke erforderlich, ist dies der Platz für den Einsatz des Sequenzoperators (,). Hier erfolgt daher meist die Initialisierung der Schleife. Als nächstes wird expression2 bewertet, sie muss vom arithmetischen oder Zeigertyp sein. Ist der Wert ungleich 0, so wird statement ausgeführt. Alsdann wird expression3 bewertet, ihr Typ unterliegt keiner Einschränkung. Hier erfolgt meist die Reinitialisierung der Schleife. Dann wird wieder expression2 bewertet. Der Zyklus wird solange wiederholt, bis die Bewertung von expression2 0 ergibt. Fehlt expression2, wird dieser Fall als ungleich 0 bewertet.
Die for-Schleife ist gut lesbar und übersichtlich, da Initialisierung, Test und Reinitialisierung dicht beieinander und sofort im Blickfeld sind, bevor man überhaupt mit der Betrachtung des Schleifenkörpers beginnt. Sie ist daher sehr beliebt.
So genannte Endlosschleifen programmiert man in C folgendermaßen:
for (;;) statement
oder
while (1) statement
Den letzten Teil der Kontrollstrukturen bilden die sogenannten Sprunganweisungen: goto label; springt zu einer Marke in der umgebenden Funktion. Diese Anweisung findet in der strukturierten Programmierung keine Verwendung und wird auch im Systembereich nur selten gebraucht. Sie ist jedoch nützlich in der (nicht für menschliche Leser bestimmten) maschinellen Codegenerierung.
break; darf nur in switch oder in Wiederholungsanweisungen stehen und bricht aus der es umgebenden Anweisung aus. continue; darf nur in Wiederholungsanweisungen stehen und setzt die es umgebende Anweisung am Punkte der Wiederholung fort. return expressionopt ; kehrt aus einer umgebenden Funktion mit der optionalen expression als Rückgabewert zurück.
Funktionen in C
Funktionen sind das Hauptgliederungsmittel eines Programms. Jedes gültige C-Programm muss eine bestimmte Funktion enthalten, nämlich die Funktion main().
Funktionen in C erfüllen die Aufgaben, die in anderen Programmiersprachen function, procedure oder subroutine genannt werden. Sie dienen dazu, die Aufgaben des Programms in kleinere, übersichtliche Einheiten mit klaren und wohldefinierten Schnittstellen zu unterteilen. Funktionsdeklarationen haben die allgemeine Form:
Typ Funktionsname(Parameterliste);
Wird Typ nicht angegeben, so wird int angenommen, man sollte dies aber unbedingt vermeiden. Ist die Parameterliste leer, kann die Funktion eine unspezifizierte Anzahl (auch Null) Parameter unspezifizierten Typs nehmen. Besteht die Parameterliste nur aus dem Schlüsselwort void, nimmt die Funktion keine Parameter. Andernfalls enthält die Parameterliste einen oder mehrere Typnamen, optional gefolgt von Variablennamen, als durch Komma separierte Liste.
Als letzter (von mindestens zwei) Parametern ist als Besonderheit auch die Ellipse (…) erlaubt. Dies bedeutet dann eine variable Anzahl sonst unspezifizierter Parameter.
Die Variablennamen haben für den Compiler keine Bedeutung, können aber dem Programmierer als Hinweis auf die beabsichtigte Verwendung dienen, im Sinne einer besseren Dokumentation. Zum Beispiel sind diese beiden Deklarationen,
oder
auch Funktionsprototypen genannt, für den Compiler identisch. Die Typangaben der Parameterliste, ihre Anzahl und Reihenfolge – auch Signatur (signature) genannt – dienen dem Compiler zur Fehlerdiagnose beim Aufruf, d.h. der Benutzung der Funktion. Deshalb sollten Funktionsdeklarationen – die Prototypen – der Benutzung der Funktionen – dem Funktionsaufruf (function call) – immer vorausgehen.
Funktionsdefinitionen haben die allgemeine Form:
Typ Funktionsname(Parameterliste)
{
Deklarationen und Definitionen
Anweisungen
}
Funktionen können nur außerhalb von Blöcken definiert werden. Eine Funktionsdefinition ist immer auch gleichzeitig eine Deklaration. Der Hauptblock einer Funktion, auch Funktionskörper genannt, ist der einzige Ort, wo Code (im Sinne von ausführbaren Prozessorbefehlen) erzeugt werden kann.
Typ und Signatur einer Funktion müssen mit etwaigen vorausgegangenen Prototypen übereinstimmen, sonst gibt es Fehlermeldungen vom Compiler. Beim Funktionsaufruf (function call) schreibt man lediglich den Namen der Funktion, gefolgt von den in Klammern gesetzten Argumenten, oft auch aktuelle Parameter genannt, als durch Komma separierte Liste. Die Argumente nehmen den Platz der formalen Parameter ein und werden, da dies ein Zuweisungskontext ist, im Typ angeglichen. Die Reihenfolge der Bewertung dieser Argumentzuweisung ist dabei nicht festgelegt – es ist nur sichergestellt, dass alle Argumente bewertet sind, bevor der eigentliche Aufruf, d.h. der Sprung zum Code des Funktionskörpers erfolgt.
Falls die Funktion ein Ergebnis liefert, den sog. Funktionswert, kann man dieses zuweisen oder weiter verarbeiten, muss es aber nicht (wenn man z.B. nur an dem Seiteneffekt interessiert ist). Ein Beispiel dazu:
len = strlen(„hello, world\n“); /* Funktionswert zuweisen */
printf(„hello, world\n“); /* kein Interesse am Funktionswert */
Ein Funktionsaufruf stellt einen Ausdruck dar und darf überall stehen, wo ein Aus-druck des Funktionstyps stehen kann. Eine void-Funktion hat definitionsgemäß keinen Funktionswert und ihr Aufruf darf daher nur in einem für diesen Fall zulässigen Zusammenhang erscheinen (z.B. nicht in Zuweisungen, Tests etc.).
Die Ausführung des Funktionskörpers endet mit einer return-Anweisung mit einem entsprechenden Ausdruck, dies ist wieder als Zuweisungskontext zu betrachten, und es wird in den Typ der Funktion konvertiert. Eine void-Funktion endet mit einer ausdruckslosen return-Anweisung oder implizit an der endenden Klammer des Funktionsblocks.
Die Funktion main()
Die Funktion main() spielt eine besondere Rolle in der C-Programmierung. Ihre Form ist vom System vordefiniert, sie wird im Programm nicht aufgerufen, denn sie stellt das Programm selbst dar. Die Funktion main() wird vom Start-Up-Code, der vom Linker dazu gebunden wird, aufgerufen, d.h. das Programm beginnt mit der Ausführung der ersten Anweisung im Funktionskörper von main(). Die Funktion hat zwei mögliche Formen:
Form 1:
int main(void)
{ Körper von main() }
oder Form 2
int main(int argc, char *argv[])
{ Körper von main() }
Die erste Form verwendet man, wenn das Programm keine Parameter nimmt, die zweite, wenn Parameter auf der Kommandozeile übergeben werden, die dann im Programm ausgewertet werden sollen.
Im zweiten Fall – argc (argument count) und argv (argument vector) sind hierbei lediglich traditionelle Namen – bedeutet der erste Parameter die Anzahl der Argumente, inklusive des Programmnamens selbst, und ist daher immer mindestens 1. Der zweite Parameter (hier also argv) ist ein Zeiger auf ein Array von Zeigern (pointer) auf nullterminierte C-Strings, die die Kommandozeilenparameter darstellen. Dieses Array ist selbst auch nullterminiert, also ist argv[argc]==0, der sogenannte Null-Pointer. Der erste Parameter, argv[0], zeigt traditionell auf den Namen, unter dem das Programm aufgerufen wurde. Falls dieser nicht zur Verfügung steht, zeigt argv[0] auf den Leerstring, d.h. argv[0][0] ist ‚\0‘. Die in argc und argv gespeicherten Werte, sowie der Inhalt der damit designierten Zeichenketten können vom Programm gelesen und dürfen, wenn gewünscht, auch verändert werden.
Vor Beginn der Ausführung von main() sorgt das System dafür, das alle statischenObjekte ihre im Programm vorgesehenen Werte enthalten. Ferner werden zur Interaktion mit der Umgebung drei Dateien geöffnet:
- stdin standard input Standardeingabestrom (meist Tastatur)
- stdout standard output Standardausgabestrom (meist Bildschirm)
- stderr standard error Standardfehlerstrom (meist Bildschirm)
Diese Standardkanäle haben den Datentyp FILE* und sind definiert im Header stdio.h. In beiden möglichen Formen ist main() als int-Funktion spezifiziert. Der Wert ist die Rückgabe an das aufrufende System und bedeutet den Exit-Status des Programms, der dann z.B. Erfolg oder Misserfolg ausdrücken oder anderweitig vom aufrufenden System ausgewertet werden kann.
Das Programm, bzw. main(), endet mit der Ausführung einer return-Anweisung mit einem entsprechenden Ausdruck. Dies ist ein Zuweisungskontext und wird in den Typ von main(), das heißt nach int konvertiert. main() endet auch, wenn irgendwo im Programm die Funktion exit(), definiert in stdlib.h, mit einem entsprechenden Wert aufgerufen wird. Das kann auch innerhalb einer ganz anderen Funktion geschehen! Dieser Wert gilt dann als Rückgabewert von main().
Wenn main() mit einer ausdruckslosen return-Anweisung oder an der schließenden Klammer seines Funktionsblocks endet, ist der Rückgabewert unbestimmt. Bei der Beendigung von main() werden erst alle mit atexit() [ebenfalls definiert in stdlib.h] registrierten Funktionen in umgekehrter Reihenfolge ihrer Registrierung aufgerufen. Sodann werden alle geöffneten Dateien geschlossen, alle mit tmpfile() (definiert in stdio.h) erzeugten temporären Dateien entfernt und schließlich die Kontrolle an den Aufrufer zurückgegeben.
Das Programm kann auch durch ein – von ihm selbst mit der Funktion raise() (definiert in signal.h), durch einen Laufzeitfehler (z.B. unbeabsichtigte Division durch Null / divide by Zero, illegale Speicherreferenz durch fehlerhaften Zeiger, etc.) oder durch den Aufruf der Funktion abort() (definiert in stdlib.h) terminieren. Was dann im Einzelnen geschieht, wie und ob geöffnete Dateien geschlossen werden, ob temporäre Dateien entfernt werden und was dann der Rückgabestatus des Programms ist, ist implementationsabhängig.
In den letzten Abschnitten war immer wieder die Rede von Arrays oder sogenannten Pointern bzw. Zeigern. Was genau es damit auf sich hat, werden wir im nächsten Artikel zu unserer Reihe "C programmieren" näher erläutern.
:quality(80)/p7i.vogel.de/wcms/df/19/df1941dd67cc2f8cbaf1a90ca1d2691a/72909102.jpeg)
C programmieren: lexikalische Grundlagen
:quality(80)/p7i.vogel.de/wcms/e6/77/e6770de81252f37333fe8544d2c006d9/72919325.jpeg)
C programmieren: Datentypen, Deklarationen, Operatoren und Ausdrücke
Hinweise: Dieser Beitrag, mit Ausnahme der ersten beiden und des letzten Absatzes, ist Copyright © Bernd Rosenlechner 2007-2010. Dieser Text kann frei kopiert und weitergegeben werden unter der Lizenz Creative Commons – Namensnennung – Weitergabe unter gleichen Bedingungen (CC – BY – SA) Deutschland 2.0.
Dieser Beitrag findet sich zudem im Handbuch „Embedded Systems Engineering“ im Kapitel "Einführung in die Sprache C". Das Handbuch ist auch als kostenlose PDF-Version in voller Länge auf ELEKTRONIKPRAXIS.de verfügbar.
* Prof. Dr. Christian Siemers lehrt an der Technische Universität Clausthal und arbeitet dort am Institut für Elektrische Informationstechnik (IEI).
Artikelfiles und Artikellinks
(ID:45383808)