ERCC Info: Die Sprache C

Erhard Riedel

Die Sprache C

Inhalt

  1. Einleitung, Übersetzung, Layout
  2. Daten, Funktionen, Operatoren
  3. Array, String, Zeiger
  4. Konstrukte und Schluss
Teil 1 (Einleitung, Übersetzung, Layout)

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:

  1. Textersatz (und damit die Definition benannter Konstanten)
  2. Einfügung von Dateien
  3. 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.

Teil 2 (Daten, Funktionen, Operatoren)

Datentypen

Obwohl C nur über wenige elementare Datentypen verfügt, hat man aufgrund einiger Modifikatoren und der häufig anzutreffenden Definition von Synonymen anfangs den Eindruck einer verwirrenden Vielfalt. Die folgende Übersicht enthält aber alle einfachen (skalaren) Datentypen in C.

char | character | ein Byte, insb. ein einzelnes Zeichen (genauer: dessen Zahlenwert, z.B. gem. ASCII)
int | integer | eine ganze Zahl (systemabhängig 16-, 32- oder 64-Bit)
short oder short int | short integer | »kurze« Ganzzahl (zumeist 16-Bit)
long oder long int | long integer | »lange« Ganzzahl (32- oder 64-Bit)
float | floating point numbers | einfach genaue Gleitkommazahl (Single)
double |  | doppelt genaue Gleitkommazahl

Die interne Darstellung und damit der Wertebereich ist einer C-Implementation grundsätzlich freigestellt, so dass die »natürlichen« Größen des Zielsystems berücksichtigt werden können. Auch die Relation der Integertypen short, int und long ist nicht allgemein festgelegt. 5 Die obigen Größenangaben treffen also nur auf PC-Systeme zu! Hier werden vorzeichenbehaftete (»signed«) Ganzzahlen im Zweierkomplement- und Gleitkommazahlen im IEEE-Format (32- und 64-Bit) gespeichert und sind damit kompatibel zu den entsprechenden Datentypen von BASIC und anderen Sprachen. 6

Die Auswertung des höchsten Bits (MSB) als Vorzeichen bei den Integertypen (also auch char) kann für jede Integer-Variable explizit durch die Modifizierer signed und unsigned festgelegt werden. Die Voreinstellung ist signed.
C-Implementationen für PC-Systeme stellen mit long double noch den Typ einer erweitert genauen Gleitkommazahl bereit, bei der das 80-Bit-IEEE-Format verwendet wird (kompatibel beispielsweise zu Ext in PowerBASIC).

Neuerer Sprachstandard definiert zusätzlich den Typ long long int bzw. long long als einer Ganzzahl, die (auch auf 32-Bit-Systemen) 64-Bit umfasst.

Wer C-Code betrachtet, wird häufig auf weitere Typbezeichnungen wie WORD, UINT u.v.a. stoßen. Dabei handelt es sich aber nicht um zusätzliche Datentypen, sondern um Synonyme, deren Definition Programmänderungen oder ‑portierungen erleichtern sollen. So würde etwa eine Definition wie

#define WORD unsigned short

und die konsequente Verwendung des Synonyms WORD in der Vergangenheit eine Portierung des Programms von Windows 3.1 nach Windows 95 ff. sicher vereinfacht haben, da sich dieser Typ – anders als int – dabei nicht ändert.
Und umgekehrt könnte durch

#define HANDLE unsigned int

sichergestellt werden, dass eine systembedingte Größenänderung dieses Typs (16- bzw. 32-Bit) auch wirklich überall erfolgt. 7
Auch aus anderen Gründen notwendige Typänderungen (etwa float in double) werden natürlich vereinfacht, wenn sie nur an einer Stelle durchzuführen sind.

Wenn ich nun feststelle, dass an dieser Stelle alle Datentypen von C genannt wurden, wird der BASIC-Programmierer fragen, was denn nun mit Strings (Zeichenketten) ist. Dieses Thema (und den Datentyp Zeiger) behandle ich in einem folgenden Abschnitt.

Aus den elementaren Datentypen lassen sich in C folgende zusammengesetzte Typen erzeugen:

array | Feld (Reihenstruktur) eines Datentypen, in der C-Terminologie meist Vektor genannt
struct | Struktur (Datensatz), Zusammenfassung auch unterschiedlicher Datentypen, in BASIC als (User Defined) Type (UDT) bezeichnet
union | Variante, auch in PowerBASIC vorhanden; Variant in Visual Basic stellt eine transparente, funktionell erweiterte Form dieses Typs dar

Zur Deklaration dieser Datentypen gibt es im Grunde nicht viel zu sagen, da nur gelinde Unterschiede zu den jeweiligen BASIC-Implementationen bestehen. Für Arrays gilt das nicht ganz, und ich komme darauf später noch zu sprechen.

Konstante

Hinsichtlich der Definition von Konstanten in C möchte ich auf eine erschöpfende Darstellung verzichten und nur auf einige Besonderheiten eingehen, die mir erwähnenswert scheinen.

C unterstützt (sogar besonders) Oktalzahlen. Jede Zahl, die mit einer Null beginnt, wird als solche interpretiert, anderenfalls als Dezimalzahl. Das ist aber unproblematisch, wenn man nicht versehentlich – und überflüssigerweise – einer Zahl die Null voranstellt. Zahlen, die mit 0x (oder 0X) beginnen, werden als Hexadezimalzahlen interpretiert.

n = 10;      // dezimal 10
n = 010;     // oktal 10 = dezimal 8
n = 0x10;    // hex 10 = dezimal 16

Die Interpretation bestimmt auch die erlaubten Ziffern (und Buchstaben bei Hex-Zahlen). Grundsätzlich können Integerkonstante den Wertebereich von long und Gleitkommakonstante den von double einnehmen.

C unterscheidet deutlich zwischen Zeichen- und Stringkonstanten. Während die Stringkonstante "Hallo!" vertraut erscheint, ist die Zeichenkonstante 'A' ungewohnt. Der Anfänger muss darauf achten, die unterschiedlichen Anführungszeichen nicht zu verwechseln.

Zeichen- und Stringkonstante können unmittelbar jedes beliebige Zeichen enthalten, denn ein Zeichen, das nicht literal darstellbar ist, kann durch eine Escape-Sequenz definiert werden. Die Escape-Sequenz wird durch einen Backslash (\) eingeleitet, auf den bestimmte Zeichen oder Zahlenwerte folgen. 8 Einige Steuerzeichen sind nützlicherweise als Buchstaben vordefiniert, zum Beispiel:

\n | Zeilenvorschub (new line)
\r | Wagenrücklauf (return)
\t | Tabulator, horizontal

Auch Zeichen, die in diesem Kontext eine besondere Bedeutung haben, können einfach via Escape-Sequenz einbezogen werden:

\\ | ein (einzelner!) Backslash
\" | ein Anführungszeichen
\' | ein einfaches Anführungszeichen (Apostroph)

Außerdem kann jedes Zeichen durch seinen Codewert als Escape-Sequenz definiert werden:

\nnn | ein Zeichen mit dem Codewert nnn – oktal!
\x nnn | ein Zeichen mit dem hexadezimalen Codewert nnn
\0 | ein Null-Byte

Auch hier wird eine gewisse Affinität zu Oktalzahlen deutlich. Jedenfalls stellt C hier sehr praktische Möglichkeiten bereit. Zu beachten ist, dass Dateipfadbezeichnungen im Programmtext ggfs. doppelte Backslashes enthalten müssen!

Zur Klarstellung:

All dies bezieht sich auf Konstante im Quellcode. Die Interpretationen erfolgen durch den Compiler, der übrigens Stringkonstante schon selbst durch ein Null-Byte ergänzt. Daher konnten wir die Stringausgabe in den Beispielen weiter oben einfach durch

printf("n ist größer Null\n");

anweisen. Der Compiler legt die Stringkonstante – einschließlich Zeilenvorschub- und Null-Zeichen – im Datenspeicher ab. (Die Bedeutung dieses Null-Bytes wird klar, wenn wir uns den Strings in C widmen.)

Variablen und Funktionen

Die Namensregeln in C unterscheiden sich nicht sehr von denen in BASIC, allerdings kann die Auswertung (Signifikanz) in der Länge deutlich beschränkt sein. 9 Bedeutsamer ist, dass zwischen Groß- und Kleinbuchstaben unterschieden wird. 10 Somit sind »SUMME«, »summe« und »Summe« drei unterschiedliche Namen!

In C müssen alle Variablen vor ihrer Verwendung deklariert werden. In den obigen Beispielen fehlt also (neben anderem) die Deklaration von n, etwa durch:

int n;

Die Deklaration gleicher Typen kann zusammengefasst werden, was üblicherweise auch ausgenutzt wird:

int a, b, c;

deklariert drei Integer-Variablen.

Die (explizite) Deklaration von Funktionen ist zwar nicht zwingend notwendig, aber empfehlenswert, schon weil sie die Aufdeckung von Unstimmigkeiten unterstützt.

In C werden lokale Variablen (genauer: solche der Speicherklassen auto und register) und Funktionswerte nicht automatisch initialisiert (bzw. definiert)!
Während die folgende BASIC-Funktion durchaus korrekt und zuverlässig ist (als Kommentare: die impliziten Wertzuweisungen)

Function Nosense() As Integer
    Dim n As Integer    ' (meist optional)
    ' n = 0
    Call Unknown(n)
    If n Then Exit Function   ' Nosense = 0 (False)
    Nosense = True
End Function

würde der C-Compiler einen gleichartigen Code sehr wahrscheinlich abweisen. Zu erwarten wären (wenigstens) zwei Warnungen, die sich genau auf unsere falschen Voraussetzungen bezögen: die Verwendung der Variable n bevor ihr Wert definiert ist und das Verlassen der Funktion ohne Definition eines Rückgabewertes. In jedem Fall hätte die Übersetzung einen unzuverlässigen Programmcode zum Ergebnis. 11

Zum Vergleich eine korrekte C-Version:

int Nosense(void)
    {
    int n = 0;
    Unknown(n);
    if (n)
        return FALSE;
    return TRUE;
}

Hierbei wurde übrigens die Gültigkeit der Bezeichner TRUE und FALSE vorausgesetzt.

Wie die C-Funktion zeigt, kann eine Variablen-Deklaration gleich mit einer Initialisierung verbunden werden.
Gemäß der obigen Empfehlung sollte das C-Programm vorab eine Deklaration der Funktionen (sog. Prototypen) enthalten:

int Nosense(void);
void Unknown(int);

Man sieht an diesem Beispiel die schon früher erwähnte Tatsache, dass es in C keine Unterscheidung zwischen Function und Sub gibt, lediglich Funktionen, die entweder einen bestimmten oder keinen (void) Rückgabewert haben.
Die Definition des Rückgabewertes erfolgt mit der Anweisung

return AUSDRUCK;

wodurch die Funktion auch unmittelbar verlassen wird.

Jede Funktion kann in C zudem wie eine Sub-Routine aufgerufen werden, indem man den Funktionswert einfach ignoriert:

Nosense();

Die Klammern müssen auch bei einer parameterlosen Funktion angegeben werden (vgl. Visual Basic). Eine Call-Anweisung existiert nicht.

Funktionsparameter werden in BASIC normalerweise als Referenz (Adresse) übergeben. Die Übergabe als Wert (by value) ist optional. In C ist es gerade umgekehrt, der Programmierer muss also die Referenz-Übergabe selbst veranlassen. Erhält eine Funktion keine Parameter, so wird – auch bei der Definition – anstelle der Parameterliste wiederum das Schlüsselwort void eingesetzt.

Der Gültigkeitsbereich einer Variablen wird im wesentlichen von ihrem Deklarationsort bestimmt. 12 (Weil dabei auch der Speicherort der Variablen festgelegt wird, spricht man in diesem Zusammenhang auch von unterschiedlichen Speicherklassen.)
Einfach gesagt, kann eine Variable global (extern), innerhalb eines Moduls (static) oder nur lokal (auto) gültig sein.

Das Schlüsselwort extern wird nur in Deklarationen verwendet, um die Verbindung zu einer Variable (oder einer Funktion) eines anderen Moduls herzustellen. 13

Eine Variable, die außerhalb von Funktionen deklariert wird, ist statisch und im Rest des Quelltextes gültig (bekannt). Das Schlüsselwort static ist nicht notwendig, kann aber zusätzlich (auch bei Funktionen) angegeben werden, um eine externe Gültigkeit (und eventuelle Namenskonflikte) auszuschließen.

Eine Variable, die innerhalb einer Funktion deklariert wird, ist grundsätzlich nur von lokaler Gültigkeit. 13 Das Schlüsselwort auto ist nicht notwendig und wird auch nie verwendet. Auch eine lokale Variable kann als static deklariert werden. Diese (interne) statische Variable behält ihren Wert auch zwischen Funktionsaufrufen.

C erlaubt die Deklaration von Variablen auch innerhalb eines Blocks. Solche Variablen sind nur innerhalb des Blocks gültig, also gewissermaßen sub-lokal. Gleichnamige Variablen außerhalb des Blocks werden vorübergehend »unsichtbar« und kollidieren daher nicht mit den sub-lokalen Namen.

Globale und statische Variablen werden vom Übersetzer (ggfs.) mit Null initialisiert. Wie bereits gesagt, gilt das nicht für lokale (auto) Variablen, ihr Wert ist also zunächst unbestimmt.

Operatoren

In C gibt es Unterscheidungen zwischen Operatoren, die in BASIC verborgen sind. So wird in BASIC für eine Wertzuweisung wie für einen Vergleich formal derselbe Operator verwendet

n = 20
If n = 20 Then '...

während in C unterschiedliche Operatoren anzuwenden sind!

Entsprechendes gilt für eine Reihe weiterer Operatoren. Zudem gibt es einige, in beiden Sprachen gleich aussehende Operatoren, die ganz unterschiedliche Bedeutungen haben. Die Gefahr der Verwechslung ist also leider nicht gering.

Die folgende Übersicht fasst die wichtigsten Unterschiede zusammen.

Operation/Relation | C |  | BASIC
Zuweisung | = |  | =
Integerdivision | / (wie normale Division) |  | \
Modulo | % |  | Mod
Potenzierung |  |  | ^
Inkrementierung | ++ |  |
Dekrementierung | −− |  |
Gleichheit | == |  | =
Ungleichheit | != |  | <>
NOT: logisch / bitweise | ! | ~ | Not
AND: logisch / bitweise | && | & | And
OR: logisch / bitweise | || | | | Or
XOR: logisch / bitweise |  | ^ | Xor

Auffällig ist vor allem die Unterscheidung zwischen logischen und bitweisen Operatoren in C (bzw. deren Fehlen in BASIC).

C kennt noch einige weitere Operatoren, insb. eine ganze Klasse von Zuweisungsoperatoren, die eine arithmetische oder Bitoperation mit der Zuweisung kombinieren. Die Anweisungen

n = n + 5; a = a * b;

lassen sich damit auch als

n += 5; a *= b;

formulieren, wobei die kürzeren Anweisungen nicht nur bevorzugter C-Stil sind, sondern in der Regel auch effektiver übersetzt werden.

C verfügt ferner über die oben gezeigten Operatoren zur In- bzw. Dekrementierung von Variablen, so dass wir in manchen Fällen gleich drei Formulierungsmöglichkeiten haben (wobei Letzterer der Vorzug zu geben ist):

n = n + 1;
n += 1;
++n;

Ungewöhnlicherweise können die In-/Dekrement-Operatoren sowohl vor (Präfix) als auch nach (Suffix) dem Operanden stehen. Ich erkläre dies anhand der späteren Beispiele.

Sprachunterschiede sind ferner noch bei der Operatoren-Rangfolge zu berücksichtigen. So hat z.B. die logische wie auch bitweise Negation – anders als in Visual Basic und PowerBASIC – Vorrang vor den arithmetischen Operatoren. Dass solche Unterschiede bestehen und bedeutsam sein können, gilt selbstverständlich beim Wechsel auf jede Sprache, die komplexe Ausdrücke erlaubt. (Bei allen nicht standardisierten Sprachen sogar schon für einen Wechsel auf einen anderen Übersetzer derselben Sprache.)

Teil 3 (Array, String, Zeiger)

DIM Array

Während sich viele Sprachelemente von C, so ungewöhnlich die Sprache zunächst erscheinen mag, tatsächlich kaum von BASIC unterscheiden, gibt es bei bestimmten Datentypen einige Besonderheiten.
Eine nicht zu unterschätzende Schwierigkeit und Fehlerquelle besteht im Zusammenhang mit Feldern (Arrays), die in modernem BASIC zumeist sehr viel flexibler definiert (dimensioniert) und indiziert werden können. 14

Es sei ein Integer-Array von 20 Elementen angelegt. In BASIC ist dies durch Anweisungen wie

Dim intarr%(20)
Dim intarr1%(0 To 19)
Dim intarr2%(1 To 20)

aber auch

Dim intarr3%(1981 To 2000)

möglich, in C nur durch

int intarr[20];

womit also lediglich die Elementanzahl festgelegt wird. Die Indices reichen dann von 0 bis 19 (oder allgemein gesagt: immer von 0 bis Elementzahl − 1).

Eine falsche Array-Indizierung ist also recht leicht möglich (meines Erachtens fast schon naheliegend), wenn man die Flexibilität von BASIC gewöhnt ist oder auch einfach vergisst, die Indizierung bei 0 zu beginnen und vor dem Maximum zu beenden:

#define MAXIMUM 20
    // ...
int intarr[MAXIMUM];        // Array mit 20 Elementen
    // ...
intarr[MAXIMUM] = 123;      // schon falsch!!

Vor diesem Fehler dürfte uns der Compiler zwar warnen, weil der Index eine Konstante ist, die nicht mit der Array-Definition übereinstimmt. Zur Laufzeit sind aber »Fehlgriffe« möglich, die bei der Code-Übersetzung nicht erkannt werden.

Berühmt-berüchtigt sind Fehler bei der Definition von Schleifen, wie bspw.

for (n = 0; n <= MAXIMUM; ++n)

wobei der Index n über das Ende des Arrays hinausläuft.

Strings …

Der Unterschied zwischen BASIC und C ist vielleicht nirgendwo größer als bei dem Datentyp String. In BASIC ist ein String tatsächlich ein Objekt, dessen konkrete Verwirklichung – nützlicherweise – verborgen ist. Der fundamentale Datentyp String ist jedoch schlichtweg ein Speicherbereich einer bestimmten Länge. 15 Um diesen für (variablen) Text nutzbar zu machen, wird in C ein Null-Byte verwendet, um das Textende zu kennzeichnen. Die String-Funktionen in C interpretieren das (erste) Null-Byte als String-Ende und können folglich nicht auf beliebige Speicherauszüge angewandt werden. 16

In C gibt es keinen Datentyp String, sondern nur den eines einzelnen Zeichens (char). 17 Ein String ist folglich ein char-Array konstanter Länge:

char string[20];

Dass Strings in C schlichte Zeichenarrays mit der normalen Indizierung sind, wird leicht übersehen. Im folgenden Beispiel soll das letzte Zeichen eines Strings durch ein Ausrufezeichen »!« ersetzt (überschrieben) werden. Die möglichen Realisierungen in BASIC und C (jeweils ohne Deklarationen und String-Initialisierungen):

n% = Len(string$)
Mid$(string$, n%) = "!"
n = strlen(string);
string[−−n] = '!';
/* string[n] = '!';         <- das wäre falsch!! */

In C ist die Position (genauer: der Array-Index) des letzten Zeichens um 1 kleiner als die String-Länge (d.i.: die Elementanzahl). Der Wert von n muss also erst entsprechend dekrementiert werden!
Der Dekrement-Operator −− wird hier als Präfix eingesetzt, weil die Dekrementierung vor der Bewertung des Ausdrucks erfolgen muss. Natürlich wäre stattdessen auch die Indexbezeichnung n − 1 möglich (womit der Wert von n unverändert bliebe).

Wer den komfortablen Umgang mit Strings in BASIC gewöhnt ist, auf den wirkt C in dieser Hinsicht wie eine kalte Dusche. C kennt (mit einer Ausnahme) überhaupt keine Operatoren, die auf einen String als Ganzes angewendet werden können. Die Übertragung eines BASIC-Codes wie

hallo$ = "Hallo, Leute!"
salute$ = hallo$ + " Wie geht's?"
If salute$ = greeting$ Then
    ' ...

kann einen Umsteiger erstmal zur Verzweiflung bringen. Effektive (und portable) Programme sind eben nicht »umsonst« zu haben.

Immerhin stellen die C-Standardbibliotheken eine Vielzahl von Stringfunktionen bereit, so dass die Übertragung dieses Codes nicht wirklich schwierig ist. Es ist hier letztlich die (transparente) dynamische String- bzw. Speicherverwaltung, durch die sich BASIC wesentlich von C unterscheidet. Ihr Fehlen zwingt den C-Programmierer einfach dazu, sich wenigstens ein paar Gedanken über Speicherreservierung und erforderliche Stringlängen zu machen.
Damit führt der (unfreiwillige) Verzicht auf manchen Komfort aber auch zu einem effektiveren Programmierstil. So wird sich der C-Programmierer – vielleicht eher als der BASIC-Programmierer – fragen, ob die verschiedenen Strings und String-Operationen des obigen Beispiels wirklich notwendig sind. 18

Jedenfalls werden wir durch C mit der Tatsache konfrontiert, dass Operationen wie die Zuweisung (=), die Verknüpfung (+ oder &) und der Vergleich (=) von Strings nur scheinbar triviale Vorgänge darstellen. Dabei sind wir bei der Arbeit mit Strings durchaus nicht ohne Unterstützung, wie wir im Zusammenhang mit Stringkonstanten bereits gesehen haben. Auch in den folgenden Anweisungen werden wir durch den C-Compiler unterstützt:

char string[20] = {0};         // Initialisierung mit
                               // 20 Null-Bytes

char string2[] = {"Hallo"};    // erzeugt ein Array mit 6 (!)
                               // Zeichen (inkl. Null-Byte)

char *strptr;                  // Deklaration eines Zeigers,
strptr = "Hallo!";             // der jetzt auf einen konstanten
                               // String (inkl. Null-Byte) zeigt

Das letzte Beispiel erkläre ich gleich näher. Vorab sei gesagt, dass es jedenfalls den einzigen Operator zeigt, der in C auf Strings (und alle anderen Arrays) angewendet werden kann: die Zuweisung der Speicheradresse an eine Zeigervariable. Das ist aber nicht dasselbe, wie die Zuweisung

s$ = "Hallo!"

die uns von BASIC her vertraut ist!

Neben der fälschlichen Operatorenanwendung auf Strings (oder dem Vergessen des »blöden« Nullzeichens) lauern hier also einige Fallen.

… und Zeiger

Nun kommen wir zu einer anderen Besonderheit von C, die dem Neuling nicht weniger Ungemach bereitet: dem Zeiger.
Ein Zeiger (pointer) ist eine Variable, die auf ein Datenobjekt (oder eine Funktion) zeigt, also dessen Speicheradresse enthält. Die Deklaration ist der anderer Variablen fast gleich, allgemein erfolgt sie durch

typ *name;

wobei sich typ auf das referenzierte Datenobjekt, nicht auf den Zeiger selbst bezieht. 19 Der angegebene Datentyp bestimmt also die Verwendung des Zeigers. Der Stern (*) legt fest, dass es sich um einen Zeiger handelt und ist nicht Bestandteil des Variablennamens.

Mit den beiden Anweisungen des vorigen Beispiels

char *strptr;
strptr = "Hallo!";

haben wir einen Zeiger auf Zeichen (char) deklariert und ihn auf die Stringkonstante zeigen lassen. Um diese Adresszuweisung zu verstehen, müssen wir uns vielleicht noch einmal klarmachen, dass C keinen Datentyp String kennt. Eine Stringkonstante ist ein (namenloses) char-Array! Der Verweis auf ein Array – als Ganzes! – ist aber in C gleichbedeutend mit dessen Anfangsadresse, einem Zeigerwert also. Wir könnten also das Beispiel um die Anweisung

strptr = string;

ergänzen und hätten damit die Adresse der Stringkonstanten Hallo! verworfen. Stattdessen zeigt unsere Zeigervariable nun auf das weiter oben definierte Array string, ihr Wert ist also die Adresse von string[0], dem ersten Array-Element.

Durch den Adressoperator & erhalten wir die Adressen für andere (beliebige) Datenobjekte:

char string[20]; int n; double summe;
char *strptr; int *intptr; double *dblptr;
intptr = &n;
dblptr = &summe;
strptr = &string[5];

Durch den Verweisoperator * erhalten wir wieder den Inhalt einer Adresse, salopp gesagt also das, worauf ein Zeiger zeigt:

int a, b, *ip;
a = 5;
ip = &a;         // ip zeigt auf a
b = *ip;         // b = 5, entspricht b = *(&a)
*ip = 0;         // a = 0, ip selbst ist unverändert

Mit diesen Beispielen habe ich nur die allerwichtigsten Aspekte von Zeigern demonstriert. Anstatt weitere nützliche (und verwirrende) Eigenschaften von Zeigern aufzuführen, möchte ich einen einfachen, konkreten Fall vorstellen, bei dem Zeiger benötigt werden.

Erinnern wir uns, dass C-Funktionen ihre Parameter als Werte (by value) erhalten. Es sei ergänzt, dass dies nicht für Arrays zutrifft, hier erhält eine Funktion tatsächlich die Anfangsadresse. (Wie wir oben gesehen haben, entspricht ein Array-Name ohne Indizierung der Adresse des ersten Elements.)
Aber was ist, wenn wir einen anderen Datentyp by reference übergeben wollen, damit die Funktion den Wert ändern kann? Wir definieren den Parameter als Zeiger!

Die folgende Funktion vertauscht die Werte der beiden Parameter. (Das Beispiel ist in ähnlicher Form bei Kernighan/Ritchie [Programmieren in C, Hanser 1983] zu finden.)

void swap(int *a, int *b)
    {
    int tmp;
    tmp = *a;
    *a = *b;
    *b = tmp;
}

Die Funktionsparameter sind vom Typ Zeiger auf int, so dass wir auf die Integerwerte durch den Verweisoperator zugreifen können.

Bei dem Funktionsaufruf verwenden wir den Adressoperator, um die Adressen der Variablen zu übergeben:

int i, n;
i = 11; n = 2;
swap(&i, &n);

Die Funktion gibt selbst keinen Wert zurück (void). In der Praxis benutzt man aber meistens den Funktionswert, um die Wertänderung eines Parameters zu erhalten. Funktionen, denen die Adressen elementarer Datentypen übergeben werden, sind deshalb eher selten.

Anders liegt der Fall natürlich, wenn die Parameter zusammengesetzte Datentypen sind. Die Funktion printf(), die in früheren Beispielen vorgekommen ist, erwartet (neben weiteren, optionalen Parametern) einen char-Zeiger, also die Adresse eines Strings. Deshalb können wir der Funktion ja eine Stringkonstante übergeben. Das gleiche gilt für viele andere Funktionen der C-Standardbibliothek.

Auch der Rückgabewert einer Funktion kann übrigens ein Zeiger sein.

String-Funktionen

Damit können wir noch einmal zu der Arbeit mit Strings zurückkehren. Die Exkursion in das Gebiet der Zeiger erfolgte aus zwei Gründen: Einmal besteht, wie wir gesehen haben, ein enger Zusammenhang zwischen Arrays (also auch Strings) und Zeigern. Zum anderen ist die Verwendung von Strings ohne die (explizite) Verwendung von Zeigern praktisch unmöglich.

Um dies noch etwas deutlicher zu machen, widmen wir uns der Übersetzung des weiter oben schon gezeigten BASIC-Codes (einer kleinen String-Orgie):

hallo$ = "Hallo, Leute!"
salute$ = hallo$ + " Wie geht's?"
If salute$ = greeting$ Then
    ' ...

Wie ich dort betont hatte, müssen wir uns bei der Programmierung in C darüber klar sein, was wir wollen, während wir in BASIC um einiges gedankenloser sein dürfen.
Gehen wir also davon aus, dass wir hallo$ und salute$ noch an anderer Stelle benötigen. (Anderenfalls hätten wir die Zuweisungen wohl vermieden und den Vergleich einfach nur mit einer Stringkonstante vorgenommen.) Dabei soll hallo$ aber konstant sein, während sich der Inhalt von salute$ ändern kann. Deshalb müssen wir die maximale Länge des zusammengesetzten Strings salute$ festlegen. Entscheiden wir uns für den Wert 100, so bedeutet das, dass wir maximal 99 Zeichen plus einem Null-Byte speichern können. Schließlich gehen wir davon aus, dass greeting$ ein gültiger, anderswo (extern) definierter String ist.

Solche Überlegungen müssen wir anstellen, bevor wir den Code nach C übersetzen können! Der kritische Punkt liegt bei den Stringkopien (und ‑verknüpfungen): C reserviert dafür keinen Speicher – es erwartet ihn!

Falls greeting$ modulextern definiert wurde, werden wir die Variable durch die Deklaration

extern char greeting[];

in diesem Modul bekanntmachen müssen.
Im Übrigen sieht die C-Version dann so aus:

char *hallo = "Hallo, Leute!";
char salute[100];
strcpy(salute, hallo);
strcat(salute, " Wie geht's?");
if (strcmp(salute, greeting) == 0)
    // ...

Durch die Stringfunktionen der C-Bibliothek, deren Bezeichnungen zwar knapp, aber doch verständlich sind, können wir auch in C die gewünschten Operationen durchführen. Das Ergebnis ist dem des BASIC-Codes effektiv gleich – oder besser gesagt: scheinbar gleich. Diese Einschränkung muss ich machen, weil »hinter dem Vorhang« eben doch wichtige Unterschiede bestehen. Ein C-»String« ist, um es noch einmal zu sagen, kein dynamisch verwaltetes Datenobjekt, sondern ein Stückchen Speicher, das irgendwo ein Null-Byte als Endemarkierung enthält. Die C-Stringfunktionen bewirken folglich nicht dasselbe wie die Stringoperatoren in BASIC.

Zur Verdeutlichung mag das folgende Programm dienen, das die Header-Datei für die Stringfunktionen einbindet und in der Hauptfunktion eine – fehlerhafte – Stringkopie vornimmt.

#include <string.h>

int main(void)
    {
    char hallo[10];
    char hallo2[] = {"Dieser String ist aber länger!"};
    strcpy(hallo, hallo2);
    return 0;
}

Es wird Ihnen schon klar sein: Die Funktion strcpy() kopiert den Inhalt von hallo2[] (Quelle) nach hallo[] (Ziel). Ihre Parameter sind Zeiger, also schlichtweg Speicheradressen. Durch den fehlerhaften Kopiervorgang wird an der Zieladresse ein zu großer Speicherinhalt überschrieben, was in aller Regel zu Problemen beim weiteren Programmlauf führt.

Teil 4 (Konstrukte und Schluss)

Konstrukte

Im letzten Teil dieser Sprachübersicht geht es um Konstrukte. C verfügt über alle Strukturformen, die wir auch von BASIC her kennen. Neben vielen Übereinstimmungen und Ähnlichkeiten bestehen allerdings auch einige Unterschiede.

Wie ich eingangs gezeigt habe, lassen sich in C mehrere Anweisungen durch geschweifte Klammern zu einem Block zusammenfassen, der syntaktisch einer einzelnen Anweisung entspricht. Da in den C-Konstruktteilen (mit einer Ausnahme) jeweils nur einzelne Anweisungen erlaubt sind, ist die Blockdefinition auch sehr oft notwendig.

Das BASIC-Konstrukt

If n > 0 Then
    m = n
    n = 0
End If

wird in C zu

if (n > 0) {
    m = n;
    n = 0;
}

wohingegen der (eher unschöne) BASIC-Code

If n > 0 Then m = n : n = 0

nicht mit dem (noch unschöneren) C-Code

if (n > 0) m = n; n = 0;

übereinstimmt, weil dem if-Zweig in C hier tatsächlich nur eine Anweisung zugeordnet ist!

Zu sagen, dass BASIC in dieser Hinsicht eindeutiger oder verständlicher ist, würde die Dinge verdrehen. C folgt eben etwas anderen Regeln. Allerdings gibt es besondere Fälle verschachtelter if-Konstrukte, in denen man diese Regeln leicht übersehen und zu einer falschen Ablauflogik gelangen kann:

Grundsätzlich wird der (optionale) else-Zweig immer auf das vorhergehende else-lose if bezogen. Fehlt in einer verschachtelten if-else-Struktur ein else-Teil, so kann diese Auswertung in recht tückischer, weil unauffälliger Weise der Programmiererabsicht zuwiderlaufen.

Das Konstrukt

if (n > 0)
    if (n % 2 == 0)
        printf("positive gerade Zahl\n");
else
    printf("negative Zahl oder Null\n");

ist in Wahrheit mit

if (n > 0)
    if (n % 2 == 0)
        printf("positive gerade Zahl\n");
    else
        printf("negative Zahl oder Null\n");

identisch! Erst durch eine Klammerung

if (n > 0) {
    if (n % 2 == 0)
        printf("positive gerade Zahl\n");
    }
else
    printf("negative Zahl oder Null\n");

wird der else-Teil auf das äußere if bezogen (und das Konstrukt auch semantisch korrekt).

Denken Sie stets daran, dass Einrückungen nur der Optik dienen. Sie sollen zwar die Programmstruktur veranschaulichen, aber sie definieren oder beeinflussen die Struktur nicht.

Man kann daher (nicht nur C-Anfängern) empfehlen, mit Blockdefinitionen ruhig etwas großzügig zu sein. C-Profis mögen lästern, aber geschweifte Klammern machen den Code nicht schlechter und helfen in Zweifelsfällen, die Programmstruktur für uns (und den Compiler!) deutlich zu machen.

Mehrfachauswahlen

C kennt streng genommen kein ElseIf, es ist aber möglich und auch üblich, else und if zusammenzufassen:

if (n > 0)
    printf("positive Zahl\n");
else if (n == 0)
    printf("Null\n");
else
    printf("negative Zahl\n");

In BASIC könnten wir ein gleichwertiges Konstrukt auch mittels Select Case formulieren. Für die Fallunterscheidung in C (switch) trifft dies nicht zu, sie weist demgegenüber einige Unterschiede und Einschränkungen auf.

Zunächst einmal kann das switch-Konstrukt nur verwendet werden, um einen ganzzahligen Ausdruck auf die Gleichheit mit Konstanten hin zu prüfen. Ein »CASE > 0« ist also nicht erlaubt. Die Anwendung auf einzelne Zeichen ist allerdings möglich, da in C ein Zeichen (char) einen numerischen Wert darstellt.

Auch an eine andere Besonderheit muss sich ein BASIC-Programmierer erst gewöhnen:
Das Konstrukt wird mit dem Ende eines zutreffenden case-Zweigs nicht automatisch verlassen! Der Programmierer muss dies vielmehr durch die Anweisung break anordnen. Obwohl diese Eigenart auch nützlich sein kann, führt sie natürlich leicht zu unerwünschten Fallauswertungen und ggfs. zu einer falschen Konstruktlogik.

Das folgende Beispiel zeigt alle wichtigen Eigenschaften und demonstriert daneben, wie sich durch #define »magische Zahlen« vermeiden lassen.

#define ERR_FILE_NOT_FOUND      53
#define ERR_BAD_FILE_NAME       64
#define ERR_PERMISSION_DENIED   70
#define ERR_PATH_FILE_ACCESS    75

// ...

switch(error) {
    case ERR_FILE_NOT_FOUND:
        printf("Datei nicht gefunden.\n");
        break;
    case ERR_PERMISSION_DENIED:
    case ERR_PATH_FILE_ACCESS:
        printf("Dateizugriffsfehler.\n");
        break;
    case ERR_BAD_FILE_NAME:
        printf("Dateiname fehlerhaft.\n");
        break;
    default:
        printf("Sonstiger Fehler.\n");
        break;
}

Man sieht, dass ein case-Zweig ausnahmsweise auch ohne Blockdefinition mehrere Anweisungen enthalten kann. Da bei der Auswertung tatsächlich ein Sprung in den zutreffenden Zweig erfolgt, werden alle folgenden Anweisungen ausgeführt, falls man nicht das Konstrukt durch die break-Anweisung verlässt.

Im Beispiel werden zwei Fälle zusammengefasst. Viele Programmierer verdeutlichen durch einen Kommentar, dass das »Hineinfallen« in den nächsten case-Zweig beabsichtigt ist:

// ...
    case ERR_PERMISSION_DENIED:
        // Zugriffsfehler zusammen behandeln
    case ERR_PATH_FILE_ACCESS:
// ...

Die optionale default-Marke erfüllt denselben Zweck wie Case Else in BASIC. Sie muss jedoch nicht unbedingt an letzter Stelle stehen. Wenn sie das wie im Beispiel (und wie es auch üblich ist) tut, so ist dort ein break eigentlich nicht nötig.

FOR (ohne NEXT, aber mit Extras)

Die for-Schleife in C ist so flexibel, dass sie die Möglichkeiten der For-Next-Schleife in BASIC weit hinter sich lässt und kaum mehr als Zählschleife bezeichnet werden kann, obwohl das natürlich der primäre Verwendungszweck ist.

Die for-Schleife kann durch drei Ausdrücke bestimmt werden, die ihrerseits sehr komplex sein können! Allgemein betrachtet, stellt sich dies so dar:

for (INITIALIZE; CONDITION; STATEMENTS)

dabei bedeutet

INITIALIZE: Anweisungen, die vor Schleifenbeginn ausgeführt werden
CONDITION: Bedingung(en) für das Verweilen in der Schleife
STATEMENTS: Anweisungen, die nach jedem Durchlauf auszuführen sind

Zur Verdeutlichung nehmen wir einmal eine einfache BASIC-Schleife:

For n% = 1 to 20
    MakeWhat(n%)
Next

In C müssen wir vorher die Zählvariable deklarieren:

int n;         // in C Pflicht!

Die Schleife könnte dann so aussehen (1):

for (n = 1; n <= 20; n++)
    MakeWhat(n);

oder so (2):

for (n = 1; n <= 20;) {
    MakeWhat(n);
    n++;
}

oder so (3):

for (n = 1; n <= 20; MakeWhat(n++))
;

Diese Versionen unterscheiden sich im Wesentlichen nur durch ihr Layout. (Die erste Version stellt sicher die gebräuchlichste Form dar.)

Zuerst möchte ich auf zwei besondere Unterschiede zwischen BASIC und C hinweisen:

  1. Die Schleifenbedingung muss in C vollständig formuliert sein, während in BASIC nur ein Grenzwert angegeben wird. Man hat also darauf zu achten, als Bedingung nicht versehentlich n = 20 (bzw. n == 20) anzugeben, wodurch die Schleife überhaupt nicht ausgeführt würde.
  2. In C ist die Änderung der Zählvariable (hier die Inkrementierung von n) nicht implizit und muss angewiesen werden!

Ich habe hierbei übrigens die Suffix-Notation für den Inkrement-Operator ++ verwendet, was nur im dritten Beispiel eine – allerdings wichtige – Rolle spielt. Der Wert von n wird dadurch erst erhöht, nachdem der Ausdruck ausgewertet ist. Somit erhält die Prozedur MakeWhat dort richtigerweise noch den Wert, den n vor der Inkrementierung hat.

Wie bei den anderen Konstruktformen, kann die Schleife syntaktisch nur eine Anweisung (Beispiel 1) oder einen Block (Beispiel 2) enthalten.
Fehlt (wie im dritten Beispiel) jegliche Anweisung, so muss das Konstruktende wenigstens durch ein Semikolon ersichtlich sein. Dieses Semikolon (man spricht auch von einer leeren Anweisung) ist also erforderlich.

Auch die beiden Semikolons zwischen den for-Komponenten sind vorgeschrieben, während die Komponenten selbst fehlen können (siehe zweites Beispiel). Deshalb ist es möglich, durch

for (;;) {
    // ...
}

eine Endlos-Schleife zu formulieren. Die könnte – wie jede Schleife – durch die Anweisung break verlassen werden.

Mit der Anweisung continue kann man unmittelbar den nächsten Schleifendurchlauf veranlassen (entspricht Iterate in PowerBASIC), wobei zuerst die Inkrement-Anweisungen (STATEMENTS-Komponente) ausgeführt werden. Bei der for-Schleife des dritten Beispiels würde also auch noch der Funktionsaufruf von MakeWhat() erfolgen.

Weitere Wiederholungen

Die gewöhnliche bedingte Schleife ist ihrem BASIC-Pendant so ähnlich, dass dazu im Grunde wenig zu sagen ist.

Die allgemeine Syntax ist:

while (Bedingung)
    Anweisung;

Daneben existiert wie in BASIC eine »offene« Form

do
    Anweisung;
while (Bedingung);

bei der die Bedingung erst nach einem Schleifendurchlauf bewertet wird. (Man beachte das abschließende Semikolon!)

Wiederum kann man durch continue den nächsten Durchlauf der (umgebenden) Schleife unmittelbar starten, wobei sofort wieder die Bedingung ausgewertet wird.

Auch diese Schleifen können natürlich durch break vorzeitig verlassen werden. In verschachtelten Schleifen (oder auch switch-Anweisungen) bezieht sich break – erwartungsgemäß – auf das innerste umgebende Konstrukt.

Damit habe ich alle Konstrukte vorgestellt. Der Vollständigkeit halber ist noch zu erwähnen, dass auch C ein goto kennt. Die Syntax ist dieselbe wie in BASIC:

goto alabel
// ...
alabel:
// ...

Über Wohl und Wehe dieser Sprunganweisung ist in der Literatur schon viel gesagt worden. Zweifellos ist goto (auch) in C immer vermeidbar – und in Ausnahmefällen nützlich.

Schlussbemerkungen

Damit ist diese kurze Betrachtung der Sprache C zu Ende. Ich hoffe, diese Übersicht unterstützt interessierte Leser bei den ersten Schritten. Der Vergleich mit BASIC sollte helfen, das Konzept und die Unterschiede von C aus der Sicht einer Hochsprache besser zu verstehen. Vielleicht fühlen sich einige BASIC-Programmierer dadurch auch ermutigt, ein Projekt in C zu beginnen.

Natürlich bedarf es weiterer Literatur, um C besser kennenzulernen und für praktische Aufgaben einzusetzen. Daran besteht auch gewiss kein Mangel. Zudem finden sich im Internet viele (auch kostenlose) Werke über C, von der Einführung bis zu speziellen und anspruchsvollen Darstellungen.
Die folgende Liste soll einige Beispiele geben: 20

  • Der Rheinwerk Verlag (vormals: Galileo Press) bietet unter dem Label <openbook> eine Anzahl IT-Fachbücher, darunter auch ein C-Handbuch, zum Online-Lesen und Download an.

  • Der Programmierer und Autor Steve Summit stellt einige Texte (englisch) über C online, darunter eine Einführung und eine Zusammenstellung (FAQ) von Fragen und Problemen, die im Zusammenhang mit der Sprache C auftauchen.

  • Thomas Steinacher hat eine umfangreiche deutschsprachige Einführung in C verfasst.

Schließlich noch ein paar Hinweise zu kostenlos erhältlichen C-Übersetzern und Programmierwerkzeugen: 20

Die unter Linux obligatorischen (oder über Paketquellen leicht verfügbaren) Compiler und Tools sind vielfach auch für Windows erhältlich. An erster Stelle ist hier natürlich die GNU Compiler Collection GCC zu nennen (http://gcc.gnu.org/install/specific.html#windows).

Microsofts Produkt Visual C++ kann auch C-Code übersetzen und ist als Bestandteil von Visual Studio erhältlich, das in den »Express«-Varianten sowie einer »Community«-Edition auch kostenlos angeboten wird. Visual Studio stellt eine recht komplexe Entwicklungsumgebung dar, die hinsichtlich der Systemvoraussetzungen nicht ganz anspruchslos ist (https://www.visualstudio.com/downloads/).

Das früher kommerzielle Produkt Watcom C/C++ ist in eine Open-Source-Entwicklung übergegangen. Open Watcom ist (auch) für Windows verfügbar und kostenlos (http://www.openwatcom.org). 21

Embarcadero bietet – wie früher schon einmal Borland selbst – kostenlos den Borland C++ 5.5 Compiler an. Dabei handelt es sich allerdings um reine Kommandozeilen-Tools (Compiler, Linker etc.). Der Turbo Debugger ist nicht enthalten, befand sich jedoch im selben Verzeichnis (»TurboDebugger.exe«). Die Download-Adresse wird per E-Mail mitgeteilt (http://edn.embarcadero.com/article/20633).

Auf thefreecountry.com (englisch) finden sich Link-Zusammenstellungen zu vielen kostenlosen Programmierer-Tools und Dokumentationen, darunter auch zu C/C++ Compilern und Interpretern sowie C/C++ Online-Beschreibungen.

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.

5) Für 64-Bit-Systeme existieren zudem verschiedene Datenmodelle, die sich vor allem beim Datentyp long int unterscheiden (32- oder 64-Bit).

6) Diese Begriffe und Datenformate werden in meinem Artikel »Vom Bit zum Wort: Computer und Daten« behandelt.

7) Jedoch verwendet man bei solchen Typdefinitionen meist die Anweisung typedef anstelle von #define.

8) Da es keine Ende-Kennung gibt, ist die Länge der Escape-Sequenz insb. für Ziffernfolgen implementationsabhängig.

9) Nach Kernighan/Ritchie [Programmieren in C, Hanser 1983] etwa auf acht oder weniger Zeichen, was nicht gerade viel ist. Moderne C-Compiler werten in der Regel jedoch eine deutlich höhere Zeichenanzahl aus. In C++ besteht keine Begrenzung.

10) Die Berücksichtigung von Groß- und Kleinschreibung kann meist durch Compiler- bzw. Linker-Einstellung verhindert werden. Nach meinen Erfahrungen ist das aber nicht unproblematisch und deshalb nicht sehr empfehlenswert.

11) Das Hauptproblem ist hier nicht, dass der Code nicht funktioniert, sondern dass er Annahmen über seine Verwendungsweise enthält! Der Code ist okay, wenn die Integer n vollständig durch Unknown(n) definiert und der Funktionswert von Nosense() nicht ausgewertet wird.

12) Um die Dinge nicht unnötig komplizierter zu machen, ist an dieser Stelle nur von Deklaration die Rede, wobei der Unterschied zur Definition (Speicherreservierung) vernachlässigt wird.

13) Die Deklaration als extern ist auch innerhalb einer Funktion für eine funktionsexterne Variable erforderlich, falls deren Definition im Quelltext erst nach der Verwendung erfolgt, die Funktion also vor der Variable definiert wird.

14) Auf die neueren Versionen von Visual Basic (VB.NET) trifft dies leider nicht mehr zu: Hier ist lediglich die Angabe des oberen Indexwertes für ein 0-basiertes Array zulässig bzw. gültig. Die Elementanzahl liegt dann um 1 darüber!

15) Die PC-Systemarchitektur definiert einen fundamentalen Datentyp String allerdings nur in der Weise, dass sie ihn durch spezielle Prozessoranweisungen (Stringbefehle) unterstützt.

16) Zur Verdeutlichung: BASIC »kennt« die Länge eines Strings. (Sie ist im String-Deskriptor enthalten.) Wo es diese Information nicht gibt, wird zwangsläufig eine Endemarkierung benötigt. Die Verwendung des Null-Zeichens (ASCII 0) zu diesem Zweck ist weit verbreiteter Standard. Wer sich mit DOS-Funktionen (Interrupt 21h) befasst, wird aber feststellen, dass die alte Funktion zur Stringausgabe (09h) hierfür das Dollarzeichen verwendet. (Gerüchte besagen, dass dies auf Bill Gates zurückgehen soll.)
Die C-konformen Strings werden häufig mit der Kurzbezeichnung ASCIIZ bedacht, wobei das »Z« für den Abschluss durch Zero (Null als Code-Wert) steht.

17) Wie schon gesagt, ist char ein numerischer Datentyp, der den 1-Byte-Wert eines Zeichens aufnimmt. Für Multi-Byte-Zeichen (insb. gem. Unicode bzw. UCS) ist zusätzlich der Datentyp wchar_t definiert, und die C-Standardbibliothek enthält entsprechend angepasste Stringfunktionen.

18) Es ist aber selbstverständlich so, dass dieser BASIC-Komfort die Konzentration auf die eigentliche Anwendungsentwicklung unterstützt. Dass dies auch in anderen Sprachen wünschenswert sein kann, zeigt etwa die Bereitstellung von Stringobjekten in C++-Klassenbibliotheken.

19) Die interne Repräsentation eines Zeigers ist grundsätzlich von der Systemumgebung abhängig. Auf PC-Systemen – mit unsegmentiertem Speichermodell – stellen Zeiger entweder 32- oder 64-Bit-Adressen dar. Die Unterscheidung zwischen near und far Pointern hat vor allem historische Gründe (früher: 16- oder 32-Bit-Adressen) und ist heute weitgehend bedeutungslos. (Beim segmentierten Speichermodell bezeichnet Far Pointer auch die Kombination aus einem Segmentselector und dem Adresswert, mithin einen 48-Bit-Zeiger.)

20) Stand: Juli 2017. Ergänzungs- oder Korrekturhinweise sind natürlich willkommen!

21) Software und Doku sind dort via FTP erreichbar, Watcom C/C++ für Windows unter der Adresse ftp://ftp.openwatcom.org/install/open-watcom-c-win32-1.9.exe.

http://eriedel.info/info/c-prog/cpl1.html

2011 - 2016 © Erhard Riedel Computer Consulting (ERCC)