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!