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.