C-Programmierung: Grundlagen

Historisches

Bearbeiten

1964 begannen das Massachusetts Institute of Technology (MIT), General Electrics, Bell Laboratories und AT&T ein neues Betriebssystem mit der Bezeichnung Multics (Multiplexed Information and Computing Service) zu entwickeln. Multics sollte ganz neue Fähigkeiten wie beispielsweise Timesharing und die Verwendung von virtuellem Speicher besitzen. 1969 kamen die Bell Labs allerdings zu dem Schluss, dass das System zu teuer und die Entwicklungszeit zu lang wäre und stiegen aus dem Projekt aus.

Eine Gruppe unter der Leitung von Ken Thompson suchte nach einer Alternative. Zunächst entschied man sich dazu, das neue Betriebssystem auf einem PDP-7 von DEC (Digital Equipment Corporation) zu entwickeln. Multics wurde in PL/1 implementiert, was die Gruppe allerdings nicht als geeignet empfand, und deshalb das System in Assembler entwickelte.

Assembler hat jedoch einige Nachteile: Die damit erstellten Programme sind z. B. nur auf einer Rechnerarchitektur lauffähig, die Entwicklung und vor allem die Wartung (also das Beheben von Programmfehlern und das Hinzufügen von neuen Funktionen) sind sehr aufwendig.

Deshalb suchte man für das System eine neue Sprache zur Systemprogrammierung. Zunächst entschied man sich für Fortran, entwickelte dann aber doch eine eigene Sprache mit dem Namen B, die stark beeinflusst von BCPL (Basic Combined Programming Language) war. Aus der Sprache B entstand dann die Sprache C. Die Sprache C unterschied sich von ihrer Vorgängersprache hauptsächlich darin, dass sie typisiert war. Später wurde auch der Kernel von Unix in C umgeschrieben. Auch heute noch sind die meisten Betriebssystemkerne, wie Windows oder Linux, in C geschrieben.

1978 schufen Dennis Ritchie und Brian Kernighan mit dem Buch The C Programming Language zunächst einen Quasi-Standard (auch als K&R-Standard bezeichnet). 1988 ist C erstmals durch das ANSI–Komitee standardisiert worden (als ANSI-C oder C-89 bezeichnet). Beim Standardisierungsprozess wurden viele Elemente der ursprünglichen Definition von K&R übernommen, aber auch einige neue Elemente hinzugefügt. Insbesondere Neuerungen der objektorientierten Sprache C++, die auf C aufbaut, flossen in den Standard ein.

Der Standard wurde 1999 überarbeitet und ergänzt (C99-Standard). Im Gegensatz zum C89-Standard, den praktisch alle verfügbaren Compiler beherrschen, setzt sich der C99-Standard nur langsam durch. Es gibt momentan noch kaum einen Compiler, der den neuen Standard vollständig unterstützt. Die meisten Neuerungen des C99-Standards sind im GNU-C-Compiler implementiert. Microsoft und Borland, die zu den wichtigsten Compilerherstellern zählen, unterstützen den neuen Standard allerdings bisher nicht, und es ist fraglich ob sie dies in Zukunft tun werden.

Was war / ist das Besondere an C

Bearbeiten

Die Entwickler der Programmiersprache legten größten Wert auf eine einfache Sprache mit maximaler Flexibilität und leichter Portierbarkeit auf andere Rechner. Dies wurde durch die Aufspaltung in den eigentlichen Sprachkern und die Programmbibliotheken (engl.: libraries) erreicht.

Daher müssen, je nach Bedarf, weitere Programmbibliotheken zusätzlich eingebunden werden. Diese kann man natürlich auch selbst erstellen um z. B. große Teile des eigenen Quellcodes thematisch zusammenzufassen, wodurch die Wiederverwendung des Programmcodes erleichtert wird.

Wegen der Nähe der Sprache C zur Hardware, einer vormals wichtigen Eigenschaft, um Unix leichter portierbar zu machen, ist C von Programmierern häufig auch als ein "Hochsprachen-Assembler" bezeichnet worden.

C selbst bietet in seiner Standardbibliothek nur rudimentäre Funktionen an. Die Standardbibliothek bietet hauptsächlich Funktionen für die Ein-/Ausgabe, Dateihandling, Zeichenkettenverarbeitung, Mathematik, Speicherreservierung und einiges mehr. Sämtliche Funktionen sind auf allen C-Compilern verfügbar. Jeder Compilerhersteller kann aber weitere Programmbibliotheken hinzufügen. Programme, die diese benutzen, sind dann allerdings nicht mehr portabel.

Der Compiler

Bearbeiten

Bevor ein Programm ausgeführt werden kann, muss es von einem Programm – dem Compiler – in Maschinensprache übersetzt werden. Dieser Vorgang wird als Kompilieren, oder schlicht als Übersetzen, bezeichnet. Die Maschinensprache besteht aus Befehlen (Folge von Binärzahlen), die vom Prozessor direkt verarbeitet werden können.

Neben dem Compiler werden für das Übersetzen des Quelltextes die folgenden Programme benötigt:

  • Präprozessor
  • Linker

Umgangssprachlich wird oft nicht nur der Compiler selbst als Compiler bezeichnet, sondern die Gesamtheit dieser Programme. Oft übernimmt tatsächlich nur ein Programm diese Aufgaben oder delegiert sie an die entsprechenden Spezialprogramme.

Vor der eigentlichen Übersetzung des Quelltextes wird dieser vom Präprozessor verarbeitet, dessen Resultat anschließend dem Compiler übergeben wird. Der Präprozessor ist im wesentlichen ein einfacher Textersetzer welcher Makroanweisungen auswertet und ersetzt (diese beginnen mit #), und es auch durch Schalter erlaubt, nur bestimmte Teile des Quelltextes zu kompilieren.

Anschließend wird das Programm durch den Compiler in Maschinensprache übersetzt. Eine Objektdatei wird als Vorstufe eines ausführbaren Programms erzeugt. Einige Compiler - wie beispielsweise der GCC - rufen vor der Erstellung der Objektdatei zusätzlich noch einen externen Assembler auf. (Im Falle des GCC wird man davon aber nichts mitbekommen, da dies im Hintergrund geschieht.)

Der Linker (im deutschen Sprachraum auch häufig als Binder bezeichnet) verbindet schließlich noch die einzelnen Programmmodule miteinander. Als Ergebnis erhält man die ausführbare Datei. Unter Windows erkennt man diese an der Datei-Endung .exe.

Viele Compiler sind Bestandteil integrierter Entwicklungsumgebungen (IDEs, vom Englischen Integrated Design Environment oder Integrated Development Environment), die neben dem Compiler unter anderem über einen integrierten Editor verfügen. Wenn Sie ein Textverarbeitungsprogramm anstelle eines Editors verwenden, müssen Sie allerdings darauf achten, dass Sie den Quellcode im Textformat ohne Steuerzeichen abspeichern. Es empfiehlt sich, die Dateiendung .c zu verwenden, auch wenn dies bei den meisten Compilern nicht zwingend vorausgesetzt wird.

Wie Sie das Programm mit ihrem Compiler übersetzen, können Sie in der Referenz nachlesen.

Hallo Welt

Bearbeiten

Inzwischen ist es in der Literatur zur Programmierung schon fast Tradition, ein Hello World als einführendes Beispiel zu präsentieren. Es macht nichts anderes, als Hallo Welt! auf dem Bildschirm auszugeben, ist aber ein gutes Beispiel für die Syntax (Grammatik) der Sprache:

/* Das Hallo-Welt-Programm */

#include <stdio.h>

int main()
{
  printf("Hallo Welt!\n");

  return 0;
}

Dieses einfache Programm dient aber auch dazu, Sie mit der Compilerumgebung vertraut zu machen. Sie lernen

  • Editieren einer Quelltextdatei
  • Abspeichern des Quelltextes
  • Aufrufen des Compilers und gegebenenfalls des Linkers
  • Starten des compilierten Programms

Darüber hinaus kann man bei einem neu installierten Compiler überprüfen, ob die Installation korrekt war, und auch alle notwendigen Bibliotheken am richtigen Platz sind.

  • In der ersten Zeile ist ein Kommentar zwischen den Zeichen /* und */ eingeschlossen. Alles, was sich zwischen diesen Zeichen befindet, wird vom Compiler nicht beachtet. Kommentare können sich über mehrere Zeilen erstrecken, dürfen aber nicht geschachtelt werden (obwohl einige Compiler dies zulassen).
  • In der nächsten Zeile befindet sich die Präprozessor-Anweisung #include. Der Präprozessor bearbeitet den Quellcode noch vor der Compilierung. An der Stelle der Include-Anweisung fügt er die (Header-)Datei stdio.h ein. Sie enthält wichtige Definitionen und Deklarationen für die Ein- und Ausgabeanweisungen.
  • Das eigentliche Programm beginnt mit der Hauptfunktion main. Die Funktion main muss sich in jedem C-Programm befinden. Das Beispielprogramm besteht nur aus einer Funktion, Programme können aber in C auch aus mehreren Funktionen bestehen. In den runden Klammern können Parameter übergeben werden (später werden Sie noch mehr über Funktionen erfahren).
    Die Funktion main() ist der Einstiegspunkt des C-Programms. main() wird immer sofort nach dem Programmstart aufgerufen.
  • Die geschweiften Klammern kennzeichnen Beginn und Ende eines Blocks. Man nennt sie deshalb Blockklammern. Die Blockklammern dienen zur Untergliederung des Programms. Sie müssen auch immer um den Rumpf (Anweisungsteil) einer Funktion gesetzt werden, selbst wenn er leer ist.
  • Zur Ausgabe von Texten wird die Funktion printf verwendet. Sie ist kein Bestandteil der Sprache C, sondern der Standard-C-Bibliothek stdio.h, aus der sie beim Linken in das Programm eingebunden wird.
  • Der auszugebende Text steht nach printf in Klammern. Die " zeigen an, dass es sich um reinen Text, und nicht um z. B. Programmieranweisungen handelt.
  • In den Klammern steht auch noch ein \n. Das bedeutet einen Zeilenumbruch. Wann immer sie dieses Zeichen innerhalb einer Ausgabeanweisung schreiben, wird der Cursor beim Ausführen des Programms in eine neue Zeile springen.
  • Über die Anweisung return wird ein Wert zurückgegeben. In diesem Fall geben wir einen Wert an das Betriebssystem zurück. Der Wert 0 teilt dem Betriebssystem mit, dass das Programm fehlerfrei ausgeführt worden ist.

C hat noch eine weitere Besonderheit: Klein- und Großbuchstaben werden (meistens) unterschieden. Man bezeichnet eine solche Sprache auch als case sensitive. Die Anweisung printf darf also nicht als Printf geschrieben werden.

Hinweis: Wenn Sie von diesem Programm noch nicht viel verstehen, ist dies nicht weiter schlimm. Alle (wirklich alle) Elemente dieses Programms werden im Verlauf dieses Buches nochmals besprochen werden.

Ein zweites Beispiel: Rechnen in C

Bearbeiten

Wir wollen nun ein zweites Programm entwickeln, das einige einfache Berechnungen durchführt, und an dem wir einige Grundbegriffe lernen werden, auf die wir in diesem Buch immer wieder stoßen werden:

#include <stdio.h>

int main()
{
   printf("3 + 2 * 8 = %i\n", 3 + 2 * 8);
   printf("(3 + 2) * 8 = %i\n", (3 + 2) * 8);
   return 0;
}

Zunächst aber zur Erklärung des Programms: In Zeile 5 berechnet das Programm den Ausdruck 3 + 2 * 8. Da C die Punkt-vor-Strich-Regel beachtet, ist die Ausgabe 19. Natürlich ist es auch möglich, mit Klammern zu rechnen, was in Zeile 6 geschieht. Das Ergebnis ist diesmal 40.

Das Programm besteht nun neben Funktionsaufrufen und der Präprozessoranweisung #include auch aus Operatoren und Operanden: Als Operator bezeichnet man Symbole, mit denen eine bestimmte Aktion durchgeführt wird, wie etwa das Addieren zweier Zahlen. Die Objekte, die mit den Operatoren verknüpft werden, bezeichnet man als Operanden. Bei der Berechnung von (3 + 2) * 8 sind + , * und ( ) die Operatoren und 3, 2 und 8 sind die Operanden. (%i ist eine Formatierungsanweisung die sagt, wie das Ergebnis als Zahl angezeigt werden soll, und ist nicht der nachfolgend erklärte Modulo-Operator.)

Keine Operatoren hingegen sind {, }, ", ;, < und >. (Wobei < und > nur bei Verwendung in einem #include keine Operatoren sind. Außerhalb einer #include -Anweisung werden sie als Vergleichsoperatoren verwendet.) Mit den öffnenden und schließenden Klammern wird ein Block eingeführt und wieder geschlossen, innerhalb der Anführungszeichen befindet sich eine Zeichenkette, mit dem Semikolon wird eine Anweisung abgeschlossen, und in den spitzen Klammern wird die Headerdatei angegeben.

Für die Grundrechenarten benutzt C die folgenden Operatoren:

Rechenart Operator
Addition +
Subtraktion -
Multiplikation *
Division /
Modulo %

Für weitere Rechenoperationen, wie beispielsweise Wurzel oder Winkelfunktionen, stellt C keine Funktionen zur Verfügung - sie werden aus Bibliotheken (Libraries) hinzugebunden. Diese werden wir aber erst später behandeln. Wichtig für Umsteiger: In C gibt es zwar den Operator ^, dieser stellt jedoch nicht den Potenzierungsoperator dar, sondern den bitweisen XOR-Operator! Für die Potenzierung muss deshalb ebenfalls auf eine Funktion der Standardbibliothek zurückgegriffen werden.

Häufig genutzt in der Programmierung wird auch der Modulo-Operator (%). Er ermittelt den Rest einer Division. Beispiel:

printf("Der Rest von 5 durch 3 ist: %i\n", 5 % 3);

Wie zu erwarten war, wird das Ergebnis 2 ausgegeben.

Wenn ein Operand durch 0 geteilt wird oder der Rest einer Division durch 0 ermittelt werden soll, so ist das Verhalten undefiniert. Das heißt, der ANSI-Standard legt das Verhalten nicht fest.

Ist das Verhalten nicht festgelegt, unterscheidet der Standard zwischen implementierungsabhängigem, unspezifiziertem und undefiniertem Verhalten:

  • Implementierungsabhängiges Verhalten (engl. implementation defined behavior) bedeutet, dass das Ergebnis sich von Compiler zu Compiler unterscheidet. Allerdings ist das Verhalten nicht dem Zufall überlassen, sondern muss vom Compilerhersteller festgelegt und auch dokumentiert werden.
  • Auch bei einem unspezifizierten Verhalten (engl. unspecified behavior) muss sich der Compilerhersteller für ein bestimmtes Verhalten entscheiden, im Unterschied zum implementierungsabhängigen Verhalten muss dieses aber nicht dokumentiert werden.
  • Ist das Verhalten undefiniert (engl. undefined behaviour), bedeutet dies, dass sich nicht voraussagen lässt, welches Resultat eintritt. Das Programm kann bspw. die Division durch 0 ignorieren und ein nicht definiertes Resultat liefern, aber es ist genauso gut möglich, dass das Programm oder sogar der Rechner abstürzt oder Daten gelöscht werden.

Soll das Programm portabel sein, so muss man sich keine Gedanken darüber machen, unter welche Kategorie ein bestimmtes Verhalten fällt. Der C-Standard zwingt allerdings niemanden dazu, portable Programme zu schreiben, und es ist genauso möglich, Programme zu entwickeln, die nur auf einer Implementierung laufen. Undefiniertes Verhalten ist in jedem der Fälle zu vermeiden, es ist dabei nicht garantiert, dass derselbe Compiler im selben Programm bei jedem Programmaufruf dasselbe Verhalten zeigt.

Kommentare

Bearbeiten

Bei Programmen empfiehlt es sich, vor allem wenn sie eine gewisse Größe erreichen, diese zu kommentieren. Selbst wenn Sie das Programm übersichtlich gliedern, wird es für eine zweite Person schwierig werden, zu verstehen, welche Logik hinter Ihrem Programm steckt. Vor dem gleichen Problem stehen Sie aber auch, wenn Sie sich nach ein paar Wochen oder gar Monaten in Ihr eigenes Programm wieder einarbeiten müssen.

Fast alle Programmiersprachen besitzen deshalb eine Möglichkeit, Kommentare in den Programmtext einzufügen. Diese Kommentare bleiben vom Compiler unberücksichtigt.

Kommentare in C

Bearbeiten

In C werden Kommentare in /* und */ eingeschlossen. Ein Kommentar darf sich über mehrere Zeilen erstrecken. Eine Schachtelung von Kommentaren ist nicht erlaubt.

In neuen C-Compilern, die den C99-Standard beherrschen, aber auch als Erweiterung in vielen C90-Compilern, sind auch einzeilige Kommentare, beginnend mit // zugelassen. Er wird mit dem Ende der Zeile abgeschlossen. Dieser Kommentartyp wurde mit C++ eingeführt und ist deshalb in der Regel auch auf allen Compilern verfügbar, die sowohl C als auch C++ beherrschen.

Beispiel für Kommentare:

/* Dieser Kommentar
   erstreckt sich
   über mehrere
   Zeilen. */

#include <stdio.h>  // Dieser Kommentar endet am Zeilenende.

int main()
{
  printf("Beispiel für Kommentar:\n");
  //printf("Dieser Text wird niemals ausgegeben.\n");
  
  return 0;
}

Hinweis: Tipps zum sinnvollen Einsatz von Kommentaren finden Sie im Kapitel Programmierstil. Um die Lesbarkeit zu verbessern, wird in diesem Wikibook häufig auf die Kommentierung verzichtet.