ERCC (http://eriedel.info)
Teil 2 (Daten, Funktionen, Operatoren)

Die Sprache C

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 zunehmend 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 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.



Anmerkungen:

1 C stellte eine Weiterentwicklung der Sprache B (Ken Thompson, 1970) dar, das 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.



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


Textanfang
ERCC (http://eriedel.info)  11/2011   © Erhard Riedel Computer Consulting (ERCC)


Link zur ERCC-Hauptseite   Link zur Info-Übersicht