The Real Adok's Way to C
Folge 1
Begleit-Dateien:
+++ Vorwort +++
Ich schrieb diesen Kurs in den Jahren 1997/1998 für die von mir herausgegebene
elektronische Zeitschrift "Hugi". Er erschien in Fortsetzungen. Den ersten
Teil schrieb ich bereits, als ich das C-Programmieren erst selbst erlernte.
Inzwischen ist viel Zeit vergangen, meine Kenntnisse haben sich gefestigt.
Deshalb liegt dieser erste Teil in komplett neuer Fassung vor. An den anderen
Teilen habe ich nur geringfügige Änderungen vorgenommen.
Ursprünglich richtete sich dieser Kurs an Leute, die die damals
weitverbreitete Programmiersprache QBasic bereits beherrschten. So konnte ich
die besonderen Eigenschaften von C erklären, ohne grundlegende Begriffe wie
Datentypen zu erläutern. In der neuen Fassung des ersten Teils gehe ich auch
auf diese Grundbegriffe ein.
+++ Einleitung +++
Die Programmiersprache C wurde in den 70er Jahren von Kerningham und Ritchie
entwickelt. Sie gehört zu den höheren Programmiersprachen, d.h. man schreibt
Programme auf einer höheren Abstraktionsebene, die dem menschlichen Denken
näher steht als dem "Denken" der Maschine. Es ist aber dennoch möglich,
systemnah zu programmieren, z.B. mit Zeigern, Ansprechen einzelner Adressen im
Arbeitsspeicher usw. Deshalb wird C auch als "mittlere Sprache" bezeichnet.
Wegen der Kombination aus mächtigen Konstrukten und maschinennahen Befehlen
erlangte C in professionellen Programmierkreisen rasch eine große Verbreitung.
Viele Betriebssysteme, wie UNIX, Windows und OS/2, und die meisten
Anwendungsprogramme für diese Systeme wurden in C geschrieben.
C bildet außerdem die Basis für die objektorientierte Programmiersprache C++.
C++ ist die heute am meisten eingesetzte Programmiersprache für professionelle
Anwendungen. Auch Java baut auf C auf, auch wenn in Java nicht alle
C-Konstrukte möglich sind.
+++ Compiler +++
Es gibt sehr viele verschiedene C-Compiler für Windows, UNIX und eine Vielzahl
anderer Plattformen. Einige sind kommerziell, andere über das Internet (zum
Teil sogar im Sourcecode) kostenlos erhältlich.
Bei der Wahl eines C-Compilers sollte man darauf achten, daß er zum
ANSI-C-Standard kompatibel ist. Nur dann kann man sich sicher sein, daß alle
C-Konstrukte von ihm richtig übersetzt werden können.
Da C++ bis auf wenige Ausnahmen eine Obermenge von C ist, d.h. fast alles, was
man in C schreiben kann, auch mit einem C++-Compiler übersetzt werden kann,
kann man natürlich als C-Programmierer auch mit einem C++-Compiler arbeiten.
Einige empfehlenswerte C- bzw. C++-Compiler sind:
- Watcom C++ (von Symantec):
Gibt es in Universitätsbuchhandlungen als vergünstigte Studentenversion. Die
Studentenversion ist voll einsatzfähig, die Einschränkungen sind lediglich
rechtlicher Natur (z.B. darf man mit einer Studentenversion erzeugte
Programme nicht kommerziell vertreiben). Für Fortgeschrittene ist im
Internet eine Open-Source-Version auf http://www.openwatcom.org/ kostenlos
erhältlich.
- Borland C++ (von Inprise):
Eine ältere, aber vollständige Version des Compilers für DOS und Win32 ist
auf http://www.borland.com/ sowie auf Cover-CDs verschiedener
Computerzeitschriften erhältlich.
- Visual C++ (von Microsoft):
Kommerziell. Der Windows-Compiler gilt als sehr effizient.
- GNU C++:
V.a. für UNIX-Systeme; es existieren aber auch Portierungen für andere
Plattformen (z.B. für DOS: DJGPP). Im Internet kostenlos erhältlich. Das
Programm steht unter der "GNU Public Licence" (GPL). Detail auf
http://www.gnu.org/.
Weiters möchte ich folgenden Compiler erwähnen:
- Quick C (von Microsoft):
Befindet sich auf der "Turbo Toolbox" CD. Diesen Compiler verwendete ich
anfangs, als ich C erlernte. Er ist der Vorgänger von Visual C++ und
arbeitet unter MS-DOS. Quick C hat einen guten Programm-Editor mit
Online-Hilfe, ähnlich der Programmieroberfläche von QBasic und dem
MS-DOS-Texteditor (edit.com). Außerdem unterstützt er einige zusätzliche,
systemnahe Funktionen, die das C-Programmieren für Leute, die bisher QBasic
benutzten, erleichtert. Allerdings hat der Quick-C-Compiler einen
gravierenden Nachteil: Er ist fehlerhaft. Ich möchte ihn deshalb nicht
empfehlen.
+++ Los geht's: Etwas Theorie +++
C-Programme bestehen im Wesentlichen aus Deklarationen von Funktionen und
evtl. Datentypen sowie Definitionen von Variablen und Konstanten.
Funktionen sind Algorithmen, die bestimmte Eingabe-Daten (Input) verarbeiten
und in der Regel auch ein Resultat ausgeben (Output). Funktionen, die kein
Resultat ausgeben, nennt man in anderen Programmiersprachen Prozeduren. C
macht allerdings keinen Unterschied zwischen Prozeduren und Funktionen;
Prozeduren müssen in C als void-Funktionen (Näheres wird später erklärt)
deklariert werden.
Die Hauptfunktion, die unmittelbar nach dem Starten eines C-Programms
aufgerufen wird, heißt main oder - in Windows-Programmen - WinMain.
Variablen sind in imperativen Programmiersprachen wie C Behälter, die
verschiedene Werte (Daten) speichern können. Die Art der Werte, die eine
Variable speichern kann, nennt man Datentyp. Auf die verschiedenen Datentypen
werde ich in Teil 2 genauer eingehen.
Konstanten sind unveränderliche Werte.
Eine Funktion zu deklarieren, bedeutet, festzulegen, welche Datentypen ihr
Input (auch Parameter genannt) und ihr Output haben.
Eine Funktion zu definieren, bedeutet, ihren Algorithmus festzulegen.
Eine Variable zu definieren, bedeutet, ihr einen Datentyp und damit
Speicherplatz zuzuweisen.
Die Deklarationen von Funktionen können in C in der Regel entfallen, wenn man
die Funktionen in der richtigen Reihenfolge definiert. Was ich damit meine,
werde ich später an einem praktischen Beispiel erklären.
+++ Der grundlegende Aufbau einer Funktion +++
Am Beispiel der Hauptfunktion main möchte ich euch zeigen, wie man eine
Funktion in C definiert.
Zuerst schreibt man den Funktionskopf:
void main(void)
Das erste void gibt hier an, daß main keinen Output zurückgibt (und damit
eigentlich eine Prozedur ist). Das zweite void gibt an, daß man main auch
keine Parameter (keinen Input) zuweisen kann.
Als nächstes kommt die offene geschwungene Klammer. Sie leitet den
Funktionsrumpf ein. Danach folgen, wenn benötigt, Deklarationen und evtl.
Definitionen von lokalen Variablen. Zuletzt kommt der Algorithmus. Dieser
besteht aus Anweisungen, d.h. Zuweisungen, Rechenoperationen, speziellen
Konstrukten wie z.B. Schleifen sowie Aufrufen anderer Funktionen. Der
Funktionsrumpf wird schließlich mit einer geschwungenen "Klammer zu"
abgeschlossen.
So etwa könnte die Hauptfunktion main eines ganz einfachen Programmes
aussehen:
void main(void)
{
int i; // Variablendefinition
i = 200; // Zuweisung
printf("Hallo Welt!\n"); // Funktionsaufruf
printf("Die Zahl i betraegt %d.",i); // Funktionsaufruf
getch(); // Funktionsaufruf
}
Diese Hauptfunktion allein ist jedoch kein lauffähiges Programm. Es fehlt noch
die Deklaration der Funktion printf, die von main aufgerufen wird.
+++ Grundlagen des Präprozessors +++
printf und getch sind vordefinierte Funktionen; sie gehören zu der
"Standardbibliothek" jedes C-Compilers. printf dient der Ausgabe von Daten auf
dem Bildschirm oder auf anderen Ausgabegeräten, getch dient der Eingabe eines
Zeichens von der Tastatur. Die Deklaration von printf befindet sich in der
"Headerdatei" stdio.h, die von getch in der Headerdatei conio.h; auch diese
sind Bestandteil eines jeden C-Compilers.
Um die Deklarationen von printf und getch in ein Programm einzubinden,
schreibt man am Anfang dieses Programmes daher:
#include <stdio.h>
#include <conio.h>
#include ist eine "Präprozessorfunktion". Der Präprozessor war ursprünglich
ein externes Programm, das die Aufgabe hatte, einen Sourcecode im "Rohzustand"
in eine für den C-Compiler lesbare Fassung zu bringen, die der C-Compiler in
eine ausführbare Form übersetzen konnte. Wenn man einen C-Sourcecode
übersetzen wollte, mußte man also zuerst ihn vom Präprozessor und dann das
Produkt vom Compiler verarbeiten lassen.
Heutzutage ist der Präprozessor in den Compiler integriert, d.h. bei jedem
Aufruf des Compilers wird der Sourcecode zuerst "prä-prozessiert" und dann in
eine ausführbare Form übersetzt.
Die Präprozessorbefehle bilden eine "Makrosprache", die mit dem eigentlichen
Programmieren in C nichts zu tun hat, aber dem Programmierer das Leben
erleichtert, weil er sich so einiges an Schreibarbeit ersparen kann. Gäbe es
den Präprozessor nicht, hätten wir die Dateien stdio.h und conio.h öffnen, die
Deklarationen von printf und getch suchen und in unser Programm einfügen
müssen.
printf wird in stdio.h von Borland C++ 5.5 übrigens folgendermaßen deklariert:
int _RTLENTRY _EXPFUNC printf(const char * __format, ...);
Ich möchte auf die Bedeutung dieser Deklaration allerdings nicht näher
eingehen, weil das im Moment zu weit führen würde.
+++ Unser erstes Programm! +++
Vervollständigen wir also unser erstes Programm und lassen wir es von einem
C-Compiler übersetzen. Hier ist der vollständige Sourcecode:
// Das Hallo-Welt-Programm
#include <stdio.h>
#include <conio.h>
void main(void)
{
int i; // Variablendefinition
i = 200; // Zuweisung
printf("Hallo Welt!\n"); // Funktionsaufruf
printf("Die Zahl i betraegt %d.",i); // Funktionsaufruf
getch();
}
Gebt dieses Programm in einem Texteditor ein, speichert es ab (z.B. als
first.c) und compiliert es; wie Letzteres geht, hängt von dem von euch
benutzten Compiler ab, also seht im Compilerhandbuch oder in der Online-Hilfe
nach.
Vorausgesetzt, ihr habt alles richtig abgetippt, hat der Compiler jetzt aus
diesem Sourcecode ein ausführbares Programm erzeugt. Wenn ihr den Sourcecode
first.c genannt und für DOS oder Windows compiliert habt, heißt dieses
ausführbare Programm vermutlich first.exe. Startet nun dieses Programm. Auf
dem Bildschirm erscheint jetzt Folgendes:
Hallo Welt!
Die Zahl i betraegt 200.
Das Programm wartet auf einen Tastendruck. Drückt eine beliebige Taste, und es
wird beendet.
Gehen wir den Sourcecode Zeile für Zeile durch, um ihn im Detail zu verstehen:
Zeile 1: // Das Hallo-Welt-Programm
Dies ist ein Kommentar; er dient lediglich dem Programmierer als
Orientierungshilfe. Vom Compiler wird er ignoriert.
Strenggenommen handelt es sich um einen C++-Kommentar, ist also
nicht im ANSI-C-Standard enthalten. Er wird aber von den meisten
heutigen C-Compilern auch übersetzt.
Die Zeichenfolge // leitet den C++-Kommentar ein. Sie signalisiert,
daß der Compiler den Rest der aktuellen Programmzeile ignorieren
soll.
Ein echter C-Kommentar wird dagegen mit der Zeichenfolge /*
eingeleitet und mit der Zeichenfolge */ abgeschlossen. Alles
zwischen diesen beiden Zeichenfolgen wird vom Compiler ignoriert.
Dadurch kann ein C-Kommentar auch über mehrere Programmzeilen gehen.
Statt
// Das Hallo-Welt-Programm
hätten wir auch schreiben können:
/* Das Hallo-Welt-Programm */
oder:
/*
Das Hallo-Welt-Programm
*/
Zeile 2:
Leerzeile. Sie dient nur der Übersichtlichkeit des Programms. Ich
werde also künftig nicht mehr auf die Leerzeilen eingehen.
Zeile 3: #include <stdio.h>
Hier ist #include. Das ist der Präprozessor-Befehl, mit dem man
Headerdateien in eigene Programme einbinden kann. Als Parameter muß
der Name der Headerdatei eingegeben werden. Dabei gibt es zwei
Möglichkeiten: Die eine Möglichkeit ist, den Namen der Headerdatei
zwischen Kleiner-/Größerzeichen zu schreiben (wie im Beispiel). Dann
sucht der Compiler automatisch in bestimmten Verzeichnissen, die in
der Regel in einer Konfigurationsdatei festgelegt sind oder über die
Kommandozeile angegeben wurden, nach der Headerdatei.
Die andere Möglichkeit ist, den Namen der Headerdatei zwischen zwei
Anführungszeichen zu setzen. Dann läßt sich auch ein Pfad
mitangeben. Allerdings sucht der Compiler in diesem Fall nicht
automatisch in den voreingestellten Verzeichnissen!
Die Headerdatei, die in dieser Programmzeile eingebunden wird, heißt
stdio.h. Sie ist die am meisten benutzte Headerdatei und ebnet den
Weg für die Verwendung der Standardbefehle für Eingabe und Ausgabe.
Auch conio.h (Zeile 4) enthält Eingabe-/Ausgabe-Befehle. Andere
wichtige Headerdateien sind z.B. assert.h, graph.h, process.h und
dos.h.
Damit es keine Mißverständnisse gibt: Die Headerdateien beinhalten
nicht die Funktionen selbst, sondern nur Funktions-Deklarationen und
teilweise auch Konstanten-Definitionen. Die Funktionen selbst
befinden sich in der Standard-Bibliothek des Compilers.
Zeile 6: void main(void)
Ja, hier ist sie wieder: Die Funktion main! An diesem Exemplar
dieser häufig anzutreffenden Gattung ist deutlich zu erkennen, wie
main aufgebaut wird (nett ausgedrückt, stimmt's?). Zuerst die
Angaben, daß es keinen Rückgabewert und auch keine Parameter gibt.
In Zeile 7 dann die offene geschwungene Klammer, und in Zeile 13 die
geschlossene geschwungene Klammer. Dazwischen (also in den Zeile 8
bis 12) steht der Algorithmus.
Zeile 8: int i; // Variablendefinition
Hier wird die lokale (d.h. nur für diese Funktion erreichbare)
Variable i definiert. int steht für den Datentyp Integer (ganze
Zahl). Im nächsten Teil werde ich auf die einzelnen Datentypen näher
eingehen.
Wie man sieht, werden Variablendefinitionen stets mit einem
Semikolon (";") abgeschlossen. Dies gilt auch für alle anderen
Anweisungen, also Zuweisungen, Funktionsaufrufe und andere
Konstrukte.
Zeile 9: i = 200; // Variablendefinition
Der Variablen i wird der Wert 200 zugewiesen. Anders gesagt: Dem
Behälter i wird befohlen, den Wert 200 zu speichern.
Zeile 10: printf("Hallo Welt!\n"); // Funktionsaufruf
Hier ist unsere erste Funktion, printf! Wie bereits gesagt, ist sie
in stdio.h deklariert. Deshalb mußten wir diese Headerdatei in das
Programm einbinden.
Zwischen den runden Klammern steht der Parameter (der Input also).
Man merke sich: Funktionsparameter müssen stets zwischen runden
Klammern geschrieben werden.
In diesem speziellen Fall ist der Parameter zusätzlich in
Anführungszeichen "verpackt", weil es sich um eine Zeichenkette
(englisch "String") handelt.
Wozu dient printf? In dieser einfachsten Form gibt es den als
Parameter angegebenen Text auf das Standardausgabegerät - also auf
den Bildschirm - aus. Zu beachten ist hierbei, daß printf nicht
automatisch einen Zeilenwechsel ausgibt. Schriebe man z.B.:
printf("Zeile 1");
printf("Zeile 2");
so würde man folgende Ausgabe erhalten:
Zeile 1Zeile 2
Für einen Zeilenwechsel muß man eine "Escape-Sequenz" in die
Zeichenkette einbauen: \n. \n wird also nicht als Text ausgegeben,
sondern stellt den Befehl dar, die Zeile zu wechseln.
Schreiben wir also:
printf("Zeile 1\n");
printf("Zeile 2");
oder auch nur:
printf("Zeile 1\nZeile 2");
so lautet die Ausgabe:
Zeile 1
Zeile 2
Es gibt noch mehr solcher Escape-Sequenzen. Hier die wichtigsten:
\\ gibt Backslash (\) auf dem Bildschirm aus
\a läßt einen Piepser ("Beep") ertönen
\b Rückschritt (entspricht einem Druck auf die Backspace-Taste)
\" gibt ein Anführungszeichen aus
\' gibt ein Hochkomma aus
\f Seitenvorschub
Zeile 11: printf("Die Zahl i betraegt %d.",i); // Funktionsaufruf
Diese Zeile führt zur Ausgabe:
Die Zahl i betraegt 200.
Man merkt, daß %d ein Platzhalter für den Wert der Zahl i ist. Wie
man mit solchen Platzhaltern arbeitet, wird in Teil 3 genau erklärt.
Zeile 12: getch();
Eigentlich müßte man schreiben:
c = getch();
(wobei c eine Variable des Typs char wäre). getch wartet nämlich,
bis der Benutzer ein beliebiges Zeichen eingibt, und gibt dann den
ASCII-Code dieses Zeichens zurück. c = getch(); würde also dazu
führen, daß im Behälter c der ASCII-Code dieses Zeichens gespeichert
wird.
Allerdings interessiert uns der ASCII-Code in diesem Programm gar
nicht. Wir wollen nur, daß das Programm auf einen Tastendruck wartet
und erst dann beendet wird. Deshalb genügt es, getch(); zu
schreiben. Der ASCII-Code der Eingabe wird damit verworfen.
+++ Stärken von C +++
Tatsächlich liegt eine der Stärken von C meiner Meinung nach darin, daß man
Funktionen - wie im Fall von getch(); - auch prozedural aufrufen kann.
Ein weiterer Vorteil von C liegt darin, daß man in einer einzigen Anweisung
vieles unterbringen kann, wofür man in anderen Sprachen mehrere Anweisungen
benutzen müßte. Ein praktisches Beispiel (aus einer Implementierung des
Quick-Sort-Algorithmus):
swap(&dataspace[counter_min++], &dataspace[counter_max--]);
Damit wird zuerst die Funktion swap mit den Adressen der Variablen
dataspace[counter_min] und dataspace[counter_max] aufgerufen. Anschließend
wird counter_min um 1 erhöht ("inkrementiert") und counter_max um 1
verkleinert ("dekrementiert"). In anderen Programmiersprachen hätte man dafür
drei Anweisungen benötigt:
swap(&dataspace[counter_min], &dataspace[counter_max]);
counter_min = counter_min + 1;
counter_max = counter_max - 1;
Allerdings sollte man diese Fähigkeit von C auch nicht übertreiben.
Anweisungen wie etwa
while(printf("Hallo!\n"),a=b++=++c,a!=c);
sind einfach nur unübersichtlich und fehleranfällig. Da schreibt man doch
lieber:
do
{
printf("Hallo!\n");
c++;
b = c;
a = b;
b++;
} while (a!=c);
Dann hat C noch weitere Vorteile. Zum Beispiel werden kleinere ausführbare
Dateien als in manchen anderen Programmiersprachen erzeugt, weil durch die
Technik der Headerdateien nur die Teile der Laufzeitbibliothek eingebunden
werden, die auch wirklich benötigt werden.
Auch die Technik des Präprozessors hat so manche Vorteile. Einen haben wir ja
bereits kennengelernt: Man erspart sich, Deklarationen von Funktionen der
Standardbibliothek oder anderer Bibliotheken zu suchen. Mehr über den
Präprozessor findet ihr in Teil 3.
+++ Besonderheiten in der Syntax von C +++
Auf folgende Eigenschaften von C müßt ihr aufpassen, insbesondere wenn ihr
eine laxere Programmiersprache (wie QBasic) gewöhnt seid:
- C unterscheidet zwischen Groß- und Kleinschreibung. Schlüsselwörter werden
für gewöhnlich klein geschrieben. Wird ein Schlüsselwort, z.B. int, groß
geschrieben, ist es für den C-Compiler ein anderes Schlüsselwort!
- Alle Variablen müssen definiert werden.
- Am Ende jeder Anweisung - Ausnahme: Blöcke - steht ein Semikolon (;).
- Wenn man eine Funktion aufruft, aber keine Parameter übergibt, müssen die
Parameterklammern trotzdem geschrieben werden!
- Arrays (siehe Teil 2) haben eckige Klammern.
- Wenn man einen Array mit mehreren Dimensionen hat, schreibt man nicht etwa
a(3,1,4), sondern a[3][1][4]!
Also dann: Fleißig das bisher Gelernte wiederholen und mit printf & Co.
experimentieren!