Die Sprache C
Einleitung
Wer einen ersten Blick auf C-Programme wirft, wird sich vielleicht erschrocken und entmutigt abwenden. In der Tat ist die Sprache sozusagen »wortkarg«, und der von vielen C-Programmierern bevorzugte Stil ist es erst recht. C gilt daher vielen als kompliziert und kryptisch. In Wahrheit handelt es sich aber um eine einfache (und ziemlich systemnahe) Sprache. Dabei ist C nicht nur sehr leistungsf�hig und effektiv, es ist auch ohne Frage eine der wichtigsten Sprachen, in manchen Bereichen sogar die wichtigste. Schon aus diesem Grund m�chte man jedem Programmierer raten, C zu lernen – und sei es als »Zweitsprache«.
C wurde von Dennis Ritchie f�r ein UNIX-System entwickelt und (1972) implementiert. 1 Kurz danach wurde das in Assembler programmierte Betriebssystem selbst in C umgeschrieben, und C etablierte sich schlie�lich als die UNIX-Sprache. Die enge Verbindung von UNIX und C ist somit historisch begr�ndet, sie beruht nicht auf irgendwelchen speziellen Spracheigenschaften. C wurde folglich auch auf viele andere Systeme portiert und erlangte durch den »Boom« der Mikrocomputer ernorme zus�tzliche Bedeutung.
Zu diesem Erfolg haben – neben den erw�hnten
Spracheigenschaften – zwei weitere Umst�nde beigetragen:
C ist in sehr hohem Ma� standardisiert. Es gibt also keine
verschiedenen »C-Dialekte«. Lediglich einige
maschinennahe Details (insb. Datenformate) k�nnen von der
konkreten Sprachimplementation abh�ngen.
C wurde von Bjarne Stroustrup zu der objektorientierten
Sprache C++ erweitert. (Auch diese Erweiterungen sind
standardisiert.) Gerade hinsichtlich der zeitgem��en
graphischen Benutzungsoberfl�chen, aber auch bei komplexen
Anwendungen �berhaupt, bietet die objektorientierte
Programmierung (OOP) viele Vorteile, weshalb C hier weitgehend durch C++ ersetzt wird. 2
Tats�chlich gibt es auch kaum noch reine C-Compiler. Die aktuellen �bersetzer verarbeiten sowohl C++ (.cpp) als auch C (.c).
In den folgenden Kapiteln werde ich die Sprache C vorstellen – und versuchen, den Blickwinkel eines BASIC-Programmierers dabei besonders zu ber�cksichtigen. 3 Eine umfassende Einf�hrung in die Sprache ist, das sei betont, damit nicht beabsichtigt. Wer sich n�her mit C besch�ftigen m�chte, wird weitere Erkl�rungen ben�tigen. Einige Hinweise hierzu befinden sich am Schluss.
Der penible Compiler
Bevor wir uns nun mit C selbst befassen, m�chte ich kurz auf einige Aspekte der Programm�bersetzung eingehen, die f�r den Neuling gleicherma�en �berraschend wie frustrierend sein k�nnen.
Es ist keine Seltenheit, dass die �bersetzung eines C-Programms gleich mit Dutzenden von Hinweisen quittiert und abgebrochen wird. Der BASIC-Programmierer reagiert oft geschockt, vermutet er doch, dass er C �berhaupt nicht verstanden und ein extrem fehlerhaftes Programm erstellt hat. Die scheinbare »Kritikwut« des Compilers hat aber (zumeist) andere Gr�nde.
Zun�chst einmal unterscheiden C-Compiler zwischen Fehlern (errors) und Warnungen (warnings). Eine Fehlermeldung bedeutet – wie in BASIC –, dass der Compiler etwas nicht »versteht« und folglich nicht �bersetzen kann. Mit einer Warnung weist der Compiler hingegen auf eine m�gliche oder vermeintliche Unstimmigkeit im Programm hin. Der Compiler hat sozusagen den Verdacht, dass der erzeugte Objektcode vielleicht nicht das ist, was der Programmierer eigentlich will.
Warnungen haben im Grunde etwas damit zu tun, dass man in C »fast alles machen kann«. In Wahrheit ist der
C-Compiler also gar nicht so »penibel«, sondern sehr tolerant, und erlaubt Dinge, die sich als raffinierte Tricks oder grober Unfug erweisen k�nnen. Der Programmierer darf etwa einer 16-Bit-Integer den Wert einer 32-Bit-Integer zuweisen, wobei ein Datenteil verloren geht – der Compiler warnt lediglich davor. (Ein BASIC-Compiler hingegen wird sich vermutlich mit Hinweis auf einen Overflow einfach weigern.)
Viele Warnungen beziehen sich auf Verst��e gegen Sprachstandards (etwa ANSI oder portables C), einige auf wirkungslosen oder nicht erreichbaren Code und nicht benutzte Variablen oder Funktionsparameter.
Warnungen lassen sich abschalten (und nat�rlich ignorieren). Da aber zumindest ein Gro�teil der Warnungen
�beraus n�tzlich ist – und das besonders f�r den Anf�nger –, ist davon eher abzuraten.
Gerade der BASIC-Programmierer wird auch davon �berrascht,
dass Fehler (und erst recht Warnungen) den C-Compiler nicht von
der weiteren �bersetzung abhalten. Der Vorgang wird vielmehr
erst abgebrochen, nachdem eine bestimmte Anzahl von Fehlern
und/oder Warnungen erreicht worden ist. Es gen�gt also ein
tats�chlicher Fehler, um eine Vielzahl von Meldungen zu
erhalten. Besonders bei einigen Syntaxfehlern, etwa dem Vergessen
einer Klammer oder eines Semikolons, »findet« der
Compiler meist etliche Folgefehler, die eigentlich nur
das Resultat der fortgesetzten �bersetzungsversuche sind.
Diese Verfahrensweise des C-Compilers ist dennoch gar nicht so
verkehrt. Wer hat sich nicht schon �ber die
»Kurzsichtigkeit« des BASIC-Compilers ge�rgert, der nach dem
ersten Fehler gleich abbricht und den zweiten erst nach Korrektur und
erneutem Aufruf meldet?!
�bersetzungsvorspiel
Die erste Instanz bei der �bersetzung eines C-Programms ist nicht der eigentliche Compiler, sondern der Pr�prozessor (preprocessor), der in den Compiler integriert oder auch ein eigenst�ndiges (vom Compiler aufgerufenes) Programm sein
kann.
Der Pr�prozessor wird durch bestimmte Anweisungen gesteuert, die alle mit dem Doppelkreuz (#) beginnen und in etwa mit den Metastatements oder Compiler-Direktiven von PowerBASIC und Visual Basic vergleichbar sind, in den M�glichkeiten aber deutlich dar�ber hinausgehen. Auf eine ausf�hrliche Darstellung sei hier allerdings zu Gunsten einer vereinfachten �bersicht verzichtet.
Die wichtigsten Funktionen des Pr�prozessors lassen sich in drei Kategorien unterteilen:
- Textersatz (und damit die Definition benannter Konstanten)
- Einf�gung von Dateien
- Bedingte Compilierung
Der Pr�prozessor ver�ndert dabei den Quellcode im Arbeitsspeicher, also die Ausfertigung, die danach an den Compiler weitergereicht wird. Die Quelltextdateien bleiben selbstverst�ndlich unver�ndert.
Aufgrund der Anweisung
#define TRUE 1
ersetzt der Pr�prozessor im gesamten nachfolgenden Quellcode (au�er in Zeichenketten) den Text »TRUE« durch die Eins. Anders gesagt, darf »TRUE« dann �berall verwendet werden, wo auch die Konstante »1« zul�ssig w�re. Und mit
#define HELP_FILE "app_help.htm"
l�sst sich entsprechend ein konstanter Dateiname festlegen. Solche Namensdefinitionen – und die Vermeidung magischer Zahlen – sind der haupts�chliche Verwendungszweck dieser Pr�prozessor-Anweisung. Die Gro�schreibung der definierten Namen ist �blich, aber nicht zwingend.
Die Zeile
#include "modul-2.c"
ersetzt der Pr�prozessor durch den Inhalt der angegebenen Datei. Die Datei wird somit vor der �bersetzung durch den Compiler in den Quelltext eingef�gt. Meistens verwendet man diese Anweisung f�r die so genannten Header-Dateien, auf die ich gleich zur�ckkomme.
Schlie�lich kann die �bersetzung bzw. die Zusammensetzung des Quellcodes von einer Bedingung (einem konstanten
Wahrheitswert oder einer Definition) abh�ngig gemacht werden. Die bedingten Anweisungen k�nnen sich an den Pr�prozessor
ebenso wie an den Compiler richten.
Die entsprechenden Pr�prozessor-Anweisungen stimmen mit dem IF-Konstrukt der Sprache weitgehend �berein:
#if WIN32
// Anweisungen f�r den Fall,
// dass WIN32 <> 0 (also true) ist
#else
// Anweisungen f�r WIN32 = 0
#endif
Die Konstante WIN32 sollte zuvor definiert worden
sein. (Allerdings k�nnen Definitionen auch durch Compiler-Option
oder Aufrufparameter vorgenommen werden.)
Ob eine Definition erfolgt ist, kann gleichfalls Gegenstand der
Bedingung sein. Die Anweisungen
#ifdef MAXVALUE // nicht �ndern #else #define MAXVALUE 32 #endif
oder k�rzer
#ifndef MAXVALUE #define MAXVALUE 32 #endif
holen eine Definition nur f�r den Fall nach, dass diese noch nicht (oder nicht mehr) g�ltig ist.
Hauptfunktion und Header
Jedes C-Programm muss eine Hauptfunktion (main function) enthalten, deren Name grunds�tzlich festgelegt ist. Diese Funktion hei�t main (bei Windows-Programmen auch WinMain bzw. DllMain oder LibMain).
Der Quelltext (HALLO.C) f�r ein ganz schlichtes Programm, das nur einen kurzen Gru� auf dem Bildschirm anzeigt, k�nnte so aussehen:
#include <stdio.h> int main() { printf("Hallo, Leute!\n"); return 0; }
Wir wollen an dieser Stelle von den Details absehen (sie werden sp�ter noch erkl�rt) und uns auf das Wesentliche konzentrieren.
Die Funktion main enth�lt – eingeschlossen in geschweifte Klammern – nur zwei Anweisungen, wovon die zweite (return) hier ohne Bedeutung ist. Mit der ersten Anweisung wird die Funktion printf aufgerufen, die wir grob mit der Print-Funktion von BASIC vergleichen k�nnen. Wie diese ist sie eine externe Funktion zur Ausgabe der (nachfolgenden) Parameter. Anders als in BASIC, m�ssen wir den C-�bersetzer jedoch gewisserma�en auf den Gebrauch der Funktion vorbereiten.
Dies geschieht durch die erste Programmzeile. Sie enth�lt eine Anweisung f�r den Pr�prozessor, den Inhalt der Datei
stdio.h einzuf�gen. Bei dieser Datei, der Name stellt ein K�rzel f�r standard input/output dar, handelt es sich um eine Header-Datei (mitunter auch vollst�ndig �bersetzt als »Kopfdatei« bezeichnet), die zu der Entwicklungsumgebung (dem C-�bersetzer)
geh�rt. Header-Dateien folgen in der Regel dem Namensschema »*.h«, es handelt sich aber um gew�hnliche Textdateien, die vor allem Definitionen und Funktionsdeklarationen enthalten.
Die Datei stdio.h enth�lt unter anderem die Deklaration der printf-Funktion, nur aus diesem Grund ist sie in das Beispielprogramm einzuf�gen.
Zur Einf�gung �bersetzereigener Header-Dateien werden die Namen innerhalb spitzer Klammern angegeben, damit sie der �bersetzer in seinem Standardverzeichnis (meist einem Unterverzeichnis namens »include«) sucht. Programmspezifische Header-Dateien werden hingegen zumeist im Programmverzeichnis gespeichert und dann mit einer Anweisung wie
#include "hallo.h"
eingef�gt.
Header-Dateien enthalten, das sei einmal klargestellt, nichts, was nicht auch in der Quellcode-Datei stehen k�nnte. Man benutzt sie vor allem, um solche Definitionen und Deklarationen zusammenzufassen, die von mehreren Quellcode-Dateien (Modulen oder �hnlichen Programmen) verwendet werden.
Layout
In diesem Abschnitt wollen wir uns nun den Details der Sprachdefinition widmen. Einiges ist zweifellos gew�hnungsbed�rftig, aber nicht wirklich kompliziert. Freilich, wer in einer C-Dokumentation oder einem Lehrbuch die formale Definition (die »Grammatik«) beschrieben sieht, wird eher entmutigt. Ich verzichte hier deshalb auf eine solche formale Beschreibung und behandle nur (und eher praxisbezogen) die wichtigen »Grammatikregeln« von C, wobei einige Aspekte auch etwas vereinfacht dargestellt werden.
In C gibt es relativ wenig formale (Layout‑) Vorschriften. Zum Beispiel k�nnte sich eine einzelne Anweisung ohne weiteres �ber mehrere Zeilen erstrecken. Das Zeilenende hat n�mlich (in den meisten F�llen) keine andere Bedeutung als ein Leerzeichen.
Als eine Konsequenz muss jede Anweisung, Definition oder Deklaration mit einem Semikolon (;) abgeschlossen werden. 4 F�r BASIC-Programmierer ist das nat�rlich sehr ungewohnt, und der h�ufigste (Anf�nger‑) Fehler besteht denn auch darin, irgendein »bl�des« Semikolon zu vergessen – oder eins zuviel zu setzen:
if (n > 0) printf("n ist gr��er Null\n"); /* "automatisches" end if! */
Wie man hier sieht, sind Konstrukteinleitungen davon ausgenommen!
Der Aufruf der Funktion printf in der zweiten Zeile dient uns hier und im Folgenden nur als beispielhafte, verdeutlichende Anweisung.
Die dritte Zeile ist ein Kommentar, der durch die beiden Zeichenkombinationen /* und */ definiert wird. C ist hierin flexibler als andere Sprachen, denn der Kommentar kann viele Textzeilen umfassen, aber ebenso auch mitten in einer Code-Zeile vorkommen! Eine weitere Form, bei der durch zwei Schr�gstriche // der Zeilenrest als Kommentar definiert wird (und die also z.B. dem Kommentar in BASIC entspricht), existiert in C++, wird aber von den meisten Compilern auch in C-Code akzeptiert.
Das Beispiel zeigt gleich eine weitere Besonderheit, die ich durch den Kommentar verdeutlicht habe: C kennt kein End If (und auch kein Next, Loop oder End Function). Stattdessen werden zusammengeh�rige Anweisungssequenzen (»Bl�cke«) durch geschweifte Klammern definiert, wie wir dies zuvor schon bei der Funktion main gesehen haben. Enth�lt ein Konstrukt nur eine einzige Anweisung, kann diese Blockdefinition entfallen.
if (n > 0) { n = 0; printf("n war gr��er Null\n"); }
Hier war sie notwendig, sonst h�tte die zweite Anweisung (Aufruf der printf-Funktion) nichts mehr mit dem if-Konstrukt zu tun gehabt. Die Einr�ckungen dienen nur der Optik!
Als weiteres Beispiel f�r den gestalterischen Spielraum sei dasselbe Konstrukt in einer etwas abweichenden, h�ufig anzutreffenden Form gezeigt:
if (n > 0) { n = 0; printf("n war gr��er Null\n"); }
Eigentlich sollte die Klammerung von Bl�cken unproblematisch sein. Im Vergleich zu End If etc. sind die geschweiften Klammern allerdings unauff�lliger und k�nnen leichter �bersehen oder vergessen werden.
Die obigen Beispiele geben schlie�lich noch eine Besonderheit her: die erforderliche Klammerung von Ausdr�cken als Bedingungen. Selbst wenn wir die Bedingung extrem vereinfachen k�nnen, weil uns nur ein von Null verschiedenes n interessiert, m�ssen wir die Klammern verwenden:
if (n) printf("n ist ungleich Null\n");
ist gleichbedeutend mit dem BASIC-Konstrukt
If n Then Print "n ist ungleich Null" End If
weil auch in C ein von Null verschiedener Wert als true
(wahr) interpretiert wird. Das Resultat einer logischen Bewertung
ist in C allerdings 1, nicht −1 wie in BASIC.
Ohne die Klammern um n erhalten wir in C aber einen
�bersetzungsfehler.
Problematischer sind nat�rlich Sprachunterschiede, die zu einem semantischen Fehler f�hren, den der Compiler nicht erkennen kann.
Anmerkungen:
1) C stellte eine Weiterentwicklung der Sprache B (Ken Thompson, 1970) dar, die wiederum auf BCPL (Martin Richards) basierte. Diese Vorl�ufer wurden danach bedeutungslos.
2) Auch die objektorientierte Programmiersprache C# ist zu einem gewissen Teil von C abgeleitet. Zudem existieren weitere Sprachen (bspw. PHP, Perl, Java) die �hnlichkeiten mit C aufweisen. Daher hilft die Vertrautheit mit der C-Syntax auch beim Verst�ndnis etlicher anderer Sprachen.
3) Die Sprachdefinition orientiert sich hierbei an den fr�heren Varianten von Microsoft-BASIC und kompatiblen Produkten. Deshalb sind auch einige konkrete Bezugnahmen auf PowerBASIC und »klassisches« Visual Basic enthalten.
4) Wie bereits gezeigt, betrifft dies nicht die Pr�prozessor-Anweisungen.