3. DCC32 - Der C-Compiler

DCC32 ist ein C-Compiler, der 32-Bit-Zielcode für die x86-Plattform erstellt und zwei verschiedene Ausgaben erzeugen kann :

Zur Steuerung der Arbeitsweise erwartet DCC32 bestimmte Optionen, die im folgenden Abschnitt beschrieben werden.

 
3.1 Optionen

Der Aufruf von DCC32 geschieht auf folgende Weise :

Dabei gilt :
3.2 Aufruf von DLINK32

Nach Übersetzung aller Quelldateien führt DCC32 das Linken der Applikation durch, sofern dies nicht durch die Optionen -c bzw. -S außer Kraft gesetzt wurde. Der Aufruf des Linkers (DLINK32) richtet sich dabei nach den Optionen -l und -W sowie nach den Dateien aus der Kommandozeile. Hier werden einige Aufrufe von DCC32 mit der jeweils resultierenden DLINK32-Kommandozeile gezeigt :

dcc32 -lm -lc m1.c m2.c m3.c
=> dlink32 -m -c m1.obj m2.obj m3.obj cx.obj,m1.exe,m1.map,crts.lib sys4g.lib

dcc32 m1.asm m2.obj m3.obj m4.c l1.lib
=> dlink32 m1.obj m2.obj m3.obj m4.obj cx.obj,m1.exe,m1.map,l1.lib crts.lib sys4g.lib

dcc32 -Wdd m1.obj m5.lib m2.c m.def m3.lib m4.asm
=> dlink32 m1.obj m2.obj m4.obj dx.obj,m1.dll,m1.map,m5.lib m3.lib crtd.lib sys4g.lib,m.def

Die einzelnen Elemente der Kommandozeile für DLINK32 werden sequentiell aus der Kommandozeile von DCC32 entnommen, und zwar in dieser Reihenfolge :

  1. -l - Optionen
  2. Quelldateien und Objektmodule aus DCC32-Kommandozeile + Startmodul durch Option -W
  3. Name des ersten Moduls + ".EXE"/".DLL" bzw. Name durch Option -e
  4. Name des ersten Moduls + ".MAP" bzw. Name durch Option -e + ".MAP"
  5. Bibliotheken aus DCC32-Kommandozeile + Bibliotheken durch Option -W
  6. Definitionsdatei aus DCC32-Kommandozeile

3.3 Konfigurationsdateien

Häufig benutzte Optionen können in einer Konfigurationsdatei namens DCC32.CFG zusammengefaßt werden. DCC32 sucht automatisch nach einer solchen Datei im aktuellen Verzeichnis und danach im Verzeichnis, in dem sich die ausführbare Datei DCC32.EXE befindet (z.B. im Verzeichnis C:\DICERTE\BIN). Die Datei DCC32.CFG ist eine reine ASCII-Textdatei, wobei die Optionen mit Leerzeichen (auch Zeilenvorschüben) voneinander getrennt sein müssen. Im Unterverzeichnis 'BIN' wird bei der Installation automatisch eine solche Konfigurationsdatei angelegt.

Werden Optionen in einer Konfigurationsdatei und in der Kommandozeile angegeben, haben die Optionen aus der Kommandozeile grundsätzlich Vorrang und heben gegebenenfalls Optionen aus der Konfigurationsdatei wieder auf.


3.4 Der Präprozessor

Der Präprozessor von DCC32 enthält einen Makroprozessor, der den Quelltext analysiert, bevor der eigentliche Compiler mit der Arbeit beginnt. Mit dem Präprozessor können Sie die folgenden Aufgaben lösen :

Der Präprozessor sucht im Quelltext nach Zeilen, die mit dem Zeichen '#' beginnen. Durch dieses Zeichen werden Präprozessor-Direktiven gekennzeichnet, die vom Präprozessor bearbeitet werden (dem '#' können beliebig viele Whitespace-Zeichen vorstehen oder folgen). Nachstehend werden alle Präprozessor-Direktiven beschrieben :

3.4.1 #

Steht das Zeichen # allein in einer Zeile, zeigt dies eine Null-Direktive an, die vom Präprozessor ignoriert wird.
3.4.2 #define Diese Direktive leitet die Definiton eines Makros ein. Makros stellen einen Mechanismus dar, mit dem mehrere Symbole durch eine Menge formaler, funktionsähnlicher Parameter ersetzt werden können. Die allgemeine Syntax lautet so:

#define Makrobezeichner [ Symbolsequenz ]

Jedes Auftreten von Makrobezeichner im Quelltext wird von nun an durch die oben angeführte Symbolsequenz ersetzt. Diese Ersetzung wird auch Makroerweiterung genannt. Die Symbolsequenz wird auch Makrorumpf genannt. Wird der Makrobezeichner als Bestandteil einer Zeichenkette, Zeichenkonstanten oder eines Kommentars benutzt, wird er nicht erweitert. Als Beispiel hier ein kleiner Auszug aus einem Quelltext :
#define HALLO "Hallo !"
#define LEERSTRING ""
#define dummy

printf( HALLO );   
/* => printf( "Hallo !" ); */

printf( "dummy" ); 
/* dummy ist eine Zeichenkette, also keine Erweiterung */

printf( "%s", LEERSTRING ); 
/* => printf( "%s", "" ); */
Eine fehlende Symbolsequenz wie im Falle von 'dummy' führt zur Entfernung des Makrobezeichners aus dem Quelltext. Nach jeder Erweiterung eines Makros wird die gesamte Zeile erneut nach Makros durchsucht, was die Benutzung von geschachtelten Makros ermöglicht, d.h. der Makrorumpf kann weitere Makrobezeichner enthalten. Die Benutzung von Direktiven im Makrorumpf führt zwar auch zu einer Erweiterung, doch der Präprozessor erkennt Direktiven nicht innerhalb eines Makrorumpfs. Dies führt dazu, daß die Anweisung unverändert an den Compiler weitergegeben wird. Da dieser diese Anweisung nicht erkennt, gibt er sofort in der Zeile einen Fehler aus (Ausnahmen sind die Direktiven #line und #pragma). Hierzu ein Beispiel :
#define HALLO "Hallo !"
#define AUSGABE printf( HALLO );
#define STDIO #include <stdio.h>

AUSGABE 
/* => printf( "Hallo !" ); */

STDIO 
/* => #include <stdio.h> wird an den Compiler weitergegeben ! */
Alle Zeichen in der Symbolsequenz, einschließlich Semikolons, erscheinen in der Makroerweiterung. Die Symbolsequenz endet mit dem Erreichen des Zeilenvorschub-Zeichens.
Makros mit Parametern besitzen folgende Syntax :

#define Makrobezeichner ( [ Argumentliste ] ) Symbolsequenz

Die Argumentliste ist eine Folge von Bezeichnern, die durch Kommata voneinander getrennt werden und Ähnlichkeit mit der Argumentliste einer C-Funktion haben. Jedes Argument in dieser Liste spielt die Rolle eines formalen Arguments. Auf diese Art definierte Makros werden auf folgende Weise im Quelltext aufgerufen :

Makrobezeichner [Whitespaces] ( [Argumente] )

Die Syntax ist rein äußerlich identisch mit der des Aufrufs einer C-Funktion. Die Anzahl der Argumente muß mit der Anzahl der Parameter in der #define-Direktive übereinstimmen, ansonsten wird eine Fehlermeldung generiert. Ein solcher Makroaufruf erwirkt eine doppelte Ersetzung. Zuerst werden der Makrobezeichner und die Argumente durch die Symbolsequenz ersetzt. Danach werden alle formalen Argumente innerhalb der Symbolsequenz durch ihre tatsächlichen Argumente aus dem Aufruf ersetzt. Hierzu ein Beispiel :

#define MUL(a,b) ((a)*(b))
wert = MUL( 5, 10 ); /* wert = ((5)*(10)); */
Der Grund für das offensichtliche Überangebot an Klammern wird durch diesen Aufruf sicher deutlich :
#define MUL( a,b ) a*b
wert = MUL( 2+3, 5+5 );
Ohne die Klammern würde diese Erweiterung erzeugt :
wert = 2+3*5+5;
Das Ergebnis ist nun nicht mehr 50 (wie ursprünglich wohl beabsichtigt), sondern 22, da die Multiplikation eine höhere Priorität hat als die Addition. Durch eine Klammerung von Argumenten kann dieses Problem vermieden werden.

Bei der Verwendung von Makros mit Parametern sollten folgende Punkte beachtet werden :

3.4.3 #undef 3.4.4 #include Diese Direktive ermöglicht das Einfügen von Programmtext aus einer anderen Quelldatei (meist sogenannte Header-Dateien). Es gibt zwei Versionen dieser Direktive :

#include <Headername>
#include "Headername"

Der Headername muß jeweils ein gültiger Dateiname mit Dateierweiterung (meist ".h") sein. Eine Pfadangabe ist optional. Der Präprozessor ersetzt die #include-Direktive aus dem Quelltext durch den gesamten Inhalt der angegebenen Headerdatei. Die beiden Versionen <Headername> und "Headername" unterscheiden sich in der Art und Weise, wie der Präprozessor nach den entsprechenden Header-Dateien sucht. Diese Unterschiede wurden bei der Compiler-Option -I erläutert.
3.4.5 #if / #ifdef / #ifndef DCC32 unterstützt die bedingte Compilierung, indem bestimmte Zeilen durch den Präprozessor durch Leerzeilen ersetzt werden, um so deren Compilierung zu verhindern. Auch die Zeilen, die mit einem # beginnen, werden unterdrückt, sofern es sich nicht um die Direktiven #if..., #elif, #else und #endif handelt. Alle Direktiven für die bedingte Compilierung müssen in der Quelldatei abgeschlossen werden, in der sie begonnen wurden.

Die Bedingungsdirektiven #if, #ifdef und #ifndef funktionieren wie normale Bedingungsoperatoren in C. Sie werden folgendermaßen verwendet :

#if Ausdruck-1
Sektion-1 ]
#elif Ausdruck-2 Zeilenvorschub [ Sektion-2 ] ]
...
#elif Audruck-n Zeilenvorschub [ Sektion-n ] ]
#else Zeilenvorschub [ Ende-Sektion ] ]
#endif

Wenn Ausdruck-1 nicht Null (also wahr) ist, werden die Quelltextzeilen (möglicherweise leer) in Sektion-1 vom Präprozessor bearbeitet und an den Compiler weitergegeben. Dabei kann es sich sowohl um Präprozessor-Direktiven wie auch um Quellanweisungen handeln. Der nach Sektion-1 folgende Quelltext wird bis zur zugehörigen #endif-Direktive übersprungen. Ergibt Ausdruck-1 jedoch den Wert Null (also falsch), wird Sektion-1 komplett ignoriert. Der Präprozessor wird die Ausführung dann an der nächsten #elif-Direktive (falls vorhanden) wieder aufnehmen, wobei nun Audruck-2 ausgewertet wird. Ist Ausdruck-2 wahr, wird Sektion-2 bearbeitet und an den Compiler weitergegeben und der Quelltext bis zur zugehörigen #endif-Direktive ignoriert. Ergibt auch Ausdruck-2 den Wert Null (falsch), dann geht die Kontrolle an das nächste #elif weiter usw. bis entweder #else oder #endif erreicht wird. Das optionale #else wird für den Fall verwendet, wenn alle vorherigen Ausdrücke den Wert Null (falsch) ergeben haben. Die Direktive #endif beendet den gesamten Block, der mit #if begonnen wurde.

Die abgearbeitete Sektion kann weitere Bedingungsblöcke in beliebiger Tiefe enthalten.

Der defined-Operator bietet eine flexible Alternative für die Überprüfung, ob Kombinationen von Bezeichnern definiert sind oder nicht. Dieser Operator ist nur innerhalb von #if- und #elif-Ausdrücken zulässig. Der Vorteil ist, daß man defined in komplexen Ausdrücken verwenden kann :

#if defined(__STDC__) || defined(__DCC32__)
...
#endif
Mit Hilfe der Bedingungsdirektiven #ifdef und #ifndef können Sie testen, ob ein bestimmter Bezeichner definiert ist oder nicht :
#ifdef __STDC__
ist identisch mit :
#if defined(__STDC__)
Oder :
#ifndef __STDC__
ist identisch mit
#if !defined(__STDC__)
3.4.6 #line Die #line-Direktive dient dazu, Zeilennummern in ein Programm für Querverweise oder zur Fehlerüberprüfung einzubauen. Besteht ein Quelltext aus mehreren Teilen, die aus unterschiedlichen Dateien stammen, ist es sinnvoll, diese Teile mit den Zeilennummern und Dateinamen ihrer Ursprungsdateien zu kennzeichnen. Die Syntax für diese Direktive lautet wie folgt :

#line Integer-Konstante [ "Dateiname" ]

Dadurch wird angezeigt, daß die folgende Quellenanweisung ihren Ursprung in der Zeile Integer-Konstante aus der Datei Dateiname hat. Alle folgenden Quellzeilen werden nun von Integer-Konstante ausgehend durchnumeriert.Die #line-Direktive wird vor allem von Zusatzprogrammen benutzt, die C-Programmtexte erzeugen, und selten von (normalsterblichen) C-Programmierern. Aus diesem Grund wird hier bewußt kein Beispiel für die Anwendung der #line-Direktive aufgeführt.
3.4.7 #error Die #error-Direktive wird für gewöhnlich in einer Präprozessor-Bedingung eingesetzt, um ein unerwünschtes Ergebnis zum Zeitpunkt der Compilierung zu dokumentieren. Die Syntax für die #error-Direktive ist folgende :

#error Meldung

Beim Antreffen dieser Direktive gibt der Präprozessor die Meldung als Warnung aus. Hier ein Beispiel :
#if NEUVAL < 0 || NEUVAL > 10
#error NEUVAL muß zwischen 1 und 9 liegen
#endif
3.4.8. #pragma #pragma Direktiven-Name

Mit #pragma kann DCC32 eigene Direktiven definieren, ohne dabei mit anderen Compilern in Konflikt zu geraten. Wird Direktiven-Name nicht erkannt, wird die gesamte #pragma-Direktive ignoriert. DCC32 erkennt zur Zeit folgende Direktiven :

- #pragma symused sym [sym ...]
Diese Direktive unterbindet die Compiler-Warnung "<sym> ist überflüssig", indem die Benutzung der angegebenen Symbole simuliert wird. Bei unbekannten Symbolen wird die Direktive ignoriert. Als Symbole können die Namen aller deklarierten Funktionen und Variablen (auch Funktionsparameter) benutzt werden, aber keine Präprozessor-Makros.

- #pragma warn N
Durch '#pragma warn' kann die Ausgabe von Warnmeldungen durch den Compiler für bestimmte Programmabschnitte ein- oder ausgeschaltet werden. Bei N=1 werden alle Warnungen angezeigt, bei N=0 unterdrückt. Diese Direktive hat Vorrang vor der Kommandozeilen-Option -w.

3.4.9.Vordefinierte Makros : DCC32 enthält eine Reihe vordefinierter Makros, die in der folgenden Tabelle aufgelistet werden :

Makro Bedeutung
__DATE__ Liefert Datum und Uhrzeit, an dem der Präprozessor die Bearbeitung der Quelldatei begonnen hat.
__DCC32__ Dieses Makro ist immer definiert und zeigt die Compilierung durch DCC32 an.
__LINE__ Dieses Makro liefert die Nummer der zu diesem Zeitpunkt abgearbeiteten Quellzeile.
__FILE__ Liefert den Namen der augenblicklich abgearbeiteten Quelldatei..
__STDC__ Dieses Makro ist immer definiert und zeigt an, daß die Compilierung nach ANSI-Norm erfolgt.
__DPMI32__ Dieses Makro ist immer definiert und zeigt an, daß das Compilat unter einem 32Bit-DPMI-Host ablaufen kann.
__FLAT__ Dieses Makro ist immer definiert und zeigt an, daß das Compilat im 32Bit-FLAT-Speichermodell ablaufen wird.
3.5 Der Compiler

Nachdem der Präprozessor seine Arbeit beendet hat, beginnt der Compiler mit der Übersetzung der Quelldateien. Da der Compiler ANSI-kompatibel ist und wir davon ausgehen, daß Sie C bereits beherrschen, erfolgt in diesem Abschnitt nur eine Aufstellung der wesentlichen Eigenschaften des Compilers.

3.5.1 Schlüsselwörter

Hier ist zunächst eine alphabetisch-geordnete Aufstellung aller Schlüsselwörter von DCC32 :

3.5.2 Bezeichner

Bezeichner sind beliebige Namen von beliebiger Länge für Funktionen, Variablen, Datentypen etc. Bezeichner können die Zeichen 'a'-'z', 'A'-'Z' , '_' sowie die Ziffern '0'-'9' enthalten, aber mit einer Einschränkung : das erste Zeichen muß ein Buchstabe (groß oder klein) oder der Unterstrich sein. Die maximale Länge eines Bezeichners beträgt 127 Zeichen, wobei jeder längere Bezeichner automatisch auf diese Länge gekürzt wird.


3.5.3 Groß- und Kleinschreibung

DCC32 unterscheidet generell zwischen Groß- und Kleinschreibung, so daß Buffer, BUFFER und BuffeR drei unterschiedliche Bezeichner sind. Für globale Bezeichner aus anderen Modulen gelten dieselben Regeln in Bezug auf Namensgebung wie für normale Bezeichner.

3.5.4 Konstanten

Konstanten sind Symbole, die für feste numerische oder Zeichenwerte stehen. DCC32 kennt diese vier Typen von Konstanten :

Integerkonstanten können dezimale, hexadezimale oder oktale Zahlen sein. Sind keine Suffixe vorhanden, wird der Datentyp einer Konstanten von ihrem Wert abgeleitet.

Dezimalkonstanten
Dezimalkonstanten reichen von 0 bis 4294967295. Konstanten außerhalb dieses Bereiches erzeugen eine Warnmeldung und werden entsprechend abgeschnitten. Dezimalkonstanten dürfen nicht mit einer 0 beginnen, ansonsten werden sie als Oktalkonstanten interpretiert.

int a=100;  /* dezimal 100 */
int b=0100; /* Achtung : dezimal 64 = oktal 100 */
int c=0;    /* dezimal 0 = oktal 0 */

Hexadezimalkonstanten
Alle Konstanten, die mit 0x oder 0X beginnen, werden als Hexadezimalkonstanten interpretiert. Der Wertebereich geht von 0 bis 0xFFFFFFFF. Größere Werte erzeugen eine Warnmeldung und werden entsprechend abgeschnitten.

int a = 0x100;   /* hexadezimal 100 */
int b = 0X10000; /* hexadezimal 10000 */

Oktalkonstanten
Alle Konstanten, die mit einer Null(= '0', aber nicht '0x'/'0X' !) beginnen, werden als Oktalkonstanten interpretiert. Der Wertebereich reicht von 0 bis 037777777777. Konstanten außerhalb dieses Bereiches erzeugen eine Warnmeldung und werden entsprechend abgeschnitten.

int a = 0500; /* oktal 500 */
int b = 001;  /* oktal 1 */

Suffixe
Beinhaltet eine der oben aufgeführten Konstanten den Suffix 'l' oder 'L', wird die Konstante als long dargestellt. Durch den Suffix 'u' oder 'U' wird eine Kostante als unsigned (vorzeichenlos) dargestellt. In der folgenden Tabelle ist aufgelistet, welche Konstanten welchem Typ entsprechen, wobei davon ausgegangen wird, daß die Konstanten ohne Suffix benutzt wurden :

Typ Bereich
int 0 bis 2147483647
0x0 bis 0x7FFFFFFF
00 bis 017777777777
unsigned int 2147483648 bis 4294967295
0x80000000 bis 0xFFFFFFFF
020000000000 bis 037777777777

Da DCC32 ein reiner 32Bit-Compiler ist, besteht prinzipiell kein Unterschied zwischen einem int- und einem long-Datentyp. Der Suffix 'l' bzw. 'L' ist somit unbedeutend. Das gleiche gilt für unsigned int und unsigned long, wo die Suffix-Kombinationen 'UL' bzw. 'ul' gleichbedeutend sind mit 'U' bzw. 'u'.

Eine Gleitkommakonstante besteht aus fünf Bestandteilen :
     
  1. Vorkommastellen
  2. Dezimalpunkt
  3. Nachkommastellen
  4. e oder E und ein vorzeichenbehafteter, ganzzahliger Exponent (optional)
  5. f oder F, bzw. l oder L als Suffix

Von diesen Bestandteilen können folgende weggelassen werden :


Hier ein paar Beispiele :

Konstante Wert
1012. 1012
18E5 18 x 105 = 1800000
10. 10 x 100
.1 1 x 10-1 = 0.1
.012e3 0.012 x 103 = 12
12.1712 12.1712
17e-5 17.0 x 10-5 = 0.00017

Gleitkommakonstanten ohne Suffix sind vom Typ double. Durch den Suffix 'f' oder 'F' kann einer Gleitkommakonstanten der Typ float und durch 'l' oder 'L' der Typ long double zugewiesen werden.

Eine Zeichenkonstante besteht aus einem in Apostrophs eingeschlossenen Zeichen, wie etwa 'T' oder 'n'. In C ist eine Zeichenkonstante immer vom Datentyp int.

char-Typen
Konstanten, die aus einem einzigen Zeichen bestehen (z.B. 'A', 'b' oder '\0'), werden als int-Datentyp dargestellt. In diesem Fall gilt das niederwertigste Byte als vorzeichenbehaftet, was bedeutet, daß die drei höherwertigen Bytes den Wert -1 (0xFF) enthalten, wenn der Wert des Zeichens größer ist als 127. Ist der Wert des Zeichens kleiner oder gleich 127, enthalten die drei höherwertigen Bytes den Wert 0 (0x00). Dies kommt dadurch, daß der Datentyp char identisch ist mit signed char, wodurch die Werte aller Zeichen grundsätzlich vorzeichenbehaftet dargestellt werden. Bei der Übertragung eines Zeichens in ein int-Datenfeld muß daher auch das Vorzeichen auf das int-Datenfeld übertragen werden (diesen Vorgang nennt man Vorzeichenerweiterung, bzw. "sign-extension").

Escape-Sequenzen
Mit dem Backslash (\) werden Escape-Sequenzen begonnen, die zur Repräsentation nicht darstellbarer Zeichen dienen :

Sequenz Wert Bedeutung
\a 0x07 Alarmton
\b 0x08 Backspace
\f 0x0C Seitenvorschub (form feed)
\n 0x0A Zeilenvorschub (line feed)
\r 0x0D Wagenrücklauf (carriage return)
\t 0x09 horizontaler Tabulator
\v 0x0B vertikaler Tabulator
\\ 0x5C Backslash
\' 0x27 Apostroph
\" 0x22 Anführungszeichen
\? 0x3F Fragezeichen
\O ... O = String mit max. 3 Oktalziffern
\xH ... H = String mit Hexziffern
\XH ... H = String mit Hexziffern

Stringkonstanten bzw. String-Literale sind eine spezielle Konstantenkategorie für feste Zeichenketten. Eine Stringkonstante ist vom Typ char[] und gehört zur Speicherklasse static. Diese Konstanten setzen sich aus einer Reihe beliebig vieler Zeichen in Anführungszeichen zusammen :
"Ich bin eine Stringkonstante !"
Innerhalb der Zeichenkette sind auch beliebig viele Escape-Sequenzen erlaubt :
"Dies ist der Alarmton : \a und dies der Zeilenvorschub : \n"
Stringkonstanten werden intern als die angegebene Zeichenkette plus abschließendem Null-Zeichen ('\0') abgespeichert. Ein leerer String wird als ein '\0'-Zeichen abgelegt. Stringkonstanten, die nur durch Whitespaces voneinander getrennt sind, werden automatisch verkettet :
printf( "Teil1 " "Teil2 " "Teil3" );
erzeugt die Ausgabe :
Teil1 Teil2 Teil3
Durch Verwendung des Backslash (\) können Stringkonstanten auf mehrere Zeilen verteilt werden :
printf( "Dieser lange Text \
wurde auf vier Textzeilen verteilt, \
obwohl er in Wirklichkeit einen \
einzeiligen Text darstellt." );
Aufzählungskonstanten sind Bezeichner, die in enum-Deklarationen definiert werden und int-Datentypen darstellen. Sie können daher in allen Ausdrücken verwendet werden, in denen Integerkonstanten erlaubt sind. Derselbe Bezeichner darf im Gültigkeitsbereich der enum-Deklaration nur einmal vorkommen.

Hierzu ein Beispiel :

enum geraete { fernseher, cdplayer, receiver};
fernseher, cdplayer und receiver sind Aufzählungskonstanten vom Typ geraete, die jeder Variablen vom Typ geraete oder int zugewiesen werden können. Die Werte der Aufzählungskonstanten lauten so :
fernseher = 0, cdplayer = 1, receiver = 2
In dem Beispiel
enum geraete { fernseher,cdplayer=2,receiver,rekorder=cdplayer-2 };
sind die Konstanten folgendermaßen gesetzt :
fernseher = 0, cdplayer = 2, receiver = 3, rekorder = 0
Daraus ergibt sich bereits, daß die Konstanten nicht eindeutige Werte haben müssen.
3.5.5 Interne Darstellung

Es folgt eine Aufstellung aller Datentypen mit Platzbedarf und Wertebereichen :

Datentyp Größe (Bits) Wertebereich
char 8 -128 bis 127
unsigned char 8 0 bis 255
short int 16 -32768 bis 32767
unsigned short int 16 0 bis 65535
int 32 -2147483648 bis 2147483647
unsigned int 32 0 bis 4294967295
long 32 -2147483648 bis 2147483647
unsigned long 32 0 bis 4294967295
float 32 3.4 x 10-38 bis 3.x 1038
double 64 1.7 x 10-308 bis 1.7 x 10308
long double 80 3.4 x 10-4932 bis 1.1 x 104932

3.5.6 Interpunktionszeichen

while (*s1++ = *s2++ )
  ;
3.5.7 Speicherklassen

Jede Deklaration benötigt einen Speicherklassenspezifizierer. Folgende Spezifizierer stehen dazu zur Verfügung:

auto wird nur bei lokalem Gültigkeitsbereich einer Variablen verwendet und impliziert damit lokale Lebensdauer. Da dies standardmäßig bei allen lokalen Variablen der Fall ist, wird dieser Spezifizierer nur äußerst selten benutzt. Durch diese Speicherklasse wird dem Compiler mitgeteilt, daß der Speicherplatz und Initialisierungswert einer Variablen bzw. der Rumpf einer Funktion in einem anderen Quelltextmodul definiert ist. Funktionen und Variablen, die extern deklariert sind, sind in allen Quelltextmodulen eines Programms sichtbar, solange diese nicht explizit als static definiert wurden. Dieser Speicherklassenspezifizierer weist den Compiler an, die definierte Variable (wenn möglich) in einem CPU-Register zu speichern, um die Zugriffsgeschwindigkeit auf diese zu erhöhen und das Programm zu verkleinern. Der Compiler entscheidet letztendlich eigenständig, ob er die Variable in einem Register halten kann oder nicht, d.h. er kann die register-Angabe auch ignorieren. Variablen und Funktionen mit dem Gültigkeitsbereich Datei und der Speicherklasse static haben statische Dauer, d.h. ihre Lebensdauer ist auf dieses Quelltextmodul begrenzt. Bei lokalen Variablen einer Funktion kann hierdurch erreicht werden, daß diese ihre Inhalte zwischen den Funktionsaufrufen behalten (solche lokalen, statischen Variablen werden beim Programmstart grundsätzlich mit Null initialisiert, wenn keine explizite Initialisierung vorgenommen wurde). Durch das Schlüsselwort typedef werden neue Datentypen definiert, z.B.
typedef unsigned char UCHAR;
UCHAR c; /* ist identisch mit 'unsigned char c;' */

UCHAR ist nun ein Synonym für unsigned char und kann an allen Stellen benutzt werden, wo auch unsigned char erlaubt ist. Durch typedef werden keine Datenobjekte definiert.

3.5.8 Variablenmodifizierer

Neben den Schlüsselwörtern zur Festlegung der Speicherklasse können in einer Deklaration Modifizierer eingesetzt werden, die die zum Bezeichner gehörende Speicherbelegung beeinflussen :

Der Modifizierer const verhindert Zuweisungen an ein Objekt sowie Nebeneffekte wie Inkrementierung und Dekrementierung. Ein const-Zeiger unterliegt ebenfalls diesen Regeln und kann daher auch nicht verändert werden (eine Veränderung des Objekts, auf das er zeigt, ist aber möglich).
const val = 1; /* => const int val = 1; */
const double pi = 3.14159265359;
/* Zeiger s ist nicht veränderbar : */
char * const s = "Ich bin veränderbarer Text";
/* String ist nicht veränderbar : */
const char *t = "Ich bin ein unveränderbarer Text";
/* String und Zeiger u sind nicht veränderbar : */
const char * const u = "u und ich sind unveränderbar";
Folgende Operationen sind ungültig :
val = 3;            /* Meldung : Zuweisung an "const"-Bezeichner "val" */
s = "Wie geht's ?"; /* Meldung : Zuweisung an "const"-Bezeichner "s" */
t[0] = 'R'; 	    /* Meldung : Zuweisung an "const"-Bereich */
u++;        	    /* Meldung : Zuweisung an "const"-Bezeichner "u" */
u[2]='U';   	    /* Meldung : Zuweisung an "const"-Bereich */
Folgende Operationen sind hingegen erlaubt :
s[0]='R'; /* s wird hierdurch nicht verändert */
strcpy( s, "Neuer Text" ); /* hier wird s auch nicht verändert */
t = "Hallo !"; /* der ursprüngliche String bleibt unverändert */
Ein Zeiger auf einen const-Wert darf keinem auf einen nicht-const-Wert weisenden Zeiger zugewiesen werden, da hierdurch eine Zuweisung an den const-Wert über den nicht-const-Zeiger möglich wäre :
char *v = s; /* nicht erlaubt, wegen "char * const s = ..." */
Der Modifizierer volatile bestimmt, daß das Objekt geändert werden kann, und zwar nicht nur durch das Programm, sondern auch von außen, z.B. durch eine Interrupt-Funktion. Dies warnt den Compiler davor, Annahmen über den Wert dieses Objekts anzustellen, was dazu führt, daß ein solches Objekt niemals als Registervariable behandelt wird :
volatile int counter;

void timerhandler( void )
{
  counter++;
  OriginalHandler( 8 );
}

int main( int argc, char *argv[] )
{
  RegisterHandler( 8, timerhandler );

  for (;;) {
    printf( "%d\n", counter );
  }

  return 0;
}
Das obige Programmfragment installiert einen neuen Handler für Interrupt 8 (Timerinterrupt), der automatisch 18,2 mal pro Sekunde aufgerufen wird. Dadurch wird (im Hintergrund) automatisch die Variable counter bei jedem Aufruf um eins erhöht. Das normale Programm gibt währenddessen in einer Endlosschleife ständig den Wert der Zählervariable aus. Durch den Spezifizierer volatile wird der Inhalt der Variablen counter bei jedem Schleifendurchlauf erneut aus dem Speicher gelesen und ausgegeben. Ohne volatile-Zusatz kann es passieren, daß der Compiler den Wert von counter einmal vor der Schleife in ein CPU-Register lädt und immer diesen Wert ausgibt, da der Compiler weiß, daß der Wert innerhalb der Schleife nicht verändert wird. Dadurch würde immer der gleiche Wert aus dem CPU-Register ausgegeben werden, während sich "im Hintergrund" der wahre Inhalt von counter im Speicher ständig ändert.
3.5.9 Zeiger

Es gibt zwei Kategorien von Zeigern : Zeiger auf Objekte und Zeiger auf Funktionen. Beide Typen sind spezielle Objekte, die Speicheradressen enthalten. Da jede Adresse 32 Bits breit ist, benötigt jeder Zeiger immer 4 Bytes Speicher. Generell kann gesagt werden, daß Zeiger auf Funktionen für den Zugriff auf Funktionen und für die Übergabe von Funktionen als Argumente an andere Funktionen verwendet werden. Berechnungen mit Zeigern auf Funktionen sind generell unzulässig. Zeiger auf Objekte hingegen können inkrementiert und dekrementiert werden, wenn Arrays oder Datenstrukturen im Speicher abgesucht werden. Obwohl Zeiger im Prinzip Zahlen beinhalten, die praktisch dieselben Eigenschaften haben wie vorzeichenlose Integervariablen, gelten einige besondere Regeln im Umgang mit Zeigern.

Ein Zeiger auf ein Objekt eines beliebigen Typs enthält die Speicheradresse dieses Typs. Da Zeiger selbst auch Objekte sind, können auch Zeiger auf Zeiger weisen. Arrays, Strukturen und Varianten (Unions) sind weitere Objekte, auf die ein Zeiger weisen kann.

Hier ein paar Beispiele :

char *text = "Ganz normaler Text";
double **gehaelter;
Einen Zeiger auf eine Funktion kann man sich als Adresse (Offset) im Codesegment vorstellen, an der der Code einer Funktion gespeichert ist. Ein Zeiger auf eine Funktion hat den Typ "Zeiger auf eine Funktion, die den Typ typ zurückliefert" :

typ (*funktion) ( Argumentliste );

Hier ein paar Beispiele :
int (*funk)( void );
char * (*funk2) ( char *, int );
Im ersten Fall ist funk ein Zeiger auf eine Funktion, die keine Argumente erwartet und einen Wert vom Typ int zurückliefert. funk2 ist ein Zeiger auf eine Funktion, die zwei Argumente vom Typ char * und int erwartet und einen Zeiger auf ein char-Objekt zurückliefert.
Ein Zeiger muß bei der Deklaration generell mit einem Datentyp verbunden werden, auch wenn er auf einen Wert vom Typ void zeigt. Diesen void-Zeiger kann man später auf ein Objekt beliebigen Typs zeigen lassen. Ein NULL-Zeigerwert ist eine Adresse, die sich von jedem gültigen Zeiger in einem Programm unterscheidet. Die Zuweisung des Integerwerts Null (0) weist einer Zeigervariablen den Zeigerwert NULL zu. Aus Gründen der Lesbarkeit kann das Präprozessor-Makro NULL benutzt werden, das in jeder Standard-Headerdatei definiert wird. Alle Zeiger können erfolgreich auf Gleichheit oder Ungleichheit zu NULL geprüft werden. Zuweisungen zwischen Zeigern, die auf unterschiedliche Typen zeigen, können Warnmeldungen oder Fehler während der Compilierung hervorrufen.
Bei Berechnungen mit Zeigern wird davon ausgegangen, daß ein Zeiger auf ein Array von Objekten zeigt. Die Erhöhung eines Zeigerwertes um 1 beispielsweise läßt den Zeiger auf das nachfolgende Element im Array zeigen (die tatsächliche Adresse wird um die Speichergröße des Typs erhöht, auf den der Zeiger weist). Hier ein Beispiel : Bei der Inkrementierung oder Dekrementierung eines Zeigers wird dessen Zeigerwert immer um x*sizeof(typ) erhöht bzw. verringert, wobei x für den Wert steht, um den der Zeiger erhöht oder verringert werden soll. Zeigertypen können mit dem Mechanismus der Typkonvertierung in andere Zeigertypen umgewandelt werden : In diesem Beispiel wird der Zeigerwert von str explizit in ein int * umgewandelt und der Zeigervariablen i zugewiesen. Ohne den int*-Zusatz würde der Compiler einen Fehler melden : 3.5.10 Arrays

Ein Array kann auf folgende Art definiert werden :

Dies erzeugt ein Integer-Array mit 10 Elementen, die von 0 bis 9 durchnumeriert sind. In diesem Fall belegt das Array exakt 40 Bytes, da pro Element (=Integer) 4 Bytes benötigt werden. Ein Array kann bereits bei der Definition mit Werten initialisiert werden : Die ersten 6 Elemente werden mit den Werten 0, 1, 2, 3, 4 und 5 gefüllt. Bei der Initialisierung müssen nicht alle Elemente mit Werten belegt werden. Mehrdimensionale Arrays werden wie folgt definiert : Dieses 2-dimensionale Array entspricht einer Matrix mit 10 Zeilen und 5 Spalten. Da wieder jedes Element 4 Bytes benötigt, belegt das gesamte Array insgesamt 200 Bytes.
In C besteht ein Array aus einem kontinuierlichen Speicherbereich, der genau so groß ist, daß alle Elemente darin Platz finden. Wenn in einem Array-Deklarator ein Ausdruck vorhanden ist, muß er eine positive Integerkonstante ergeben. Der Wert ist die Anzahl der Elemente. Jedes Element in einem Array ist numeriert und zwar von 0 bis zur Anzahl der Elemente-1. Mehrdimensionale Arrays werden durch Deklaration von Arrays des Typs Array aufgebaut. Es ist aber auch möglich, die Anzahl der Elemente vom Compiler berechnen zu lassen, wie in diesem Beispiel : Der Bezeichner des Arrays stellt einen Zeiger auf das erste Element des Arrays dar. Folgende Anweisungen sind daher zulässig : Da der Compiler eigentlich ein char * als erstes Argument der Funktion strcpy erwartet, müßte er einen Fehler melden. Da er dies nicht tut, kann man daraus folgern, daß die Typen char [] und char * identisch sind. Daß dies in der Praxis tatsächlich der Fall ist, zeigt die einwandfreie Ausführung des obigen Codefragments.
 

3.5.11. Funktionen

Funktionen sind die zentralen Bestandteile eines C-Programms. Während andere Sprachen (z.B. Pascal) eine Unterscheidung zwischen Prozeduren (keine Rückgabewerte) und Funktionen (Rückgabewerte) machen, übernehmen Funktionen in C beide Aufgaben.

Jedes C-Programm muß zumindest aus der main-Funktion bestehen, die beim Programmstart durch das Startmodul aufgerufen wird und den Einstiegspunkt für die Ausführung eines C-Programms darstellt. Vor der Benutzung einer Funktion muß diese durch einen Prototyp im voraus deklariert werden, damit der Compiler später die Gültigkeit der Aufrufe überprüfen und gegebenenfalls automatische Typkonvertierungen vornehmen kann. Funktionen können beliebig oft in Form eines Prototyps deklariert werden, solange die Prototypen identisch sind. Eine Deklaration sieht folgendermaßen aus :

wobei typ der optionale Rückgabewert der Funktion funk mit der Voreinstellung int ist. Bei einer Deklaration in dieser Form ist der Compiler nicht in der Lage, die Argumente bei späteren Aufrufen zu prüfen, da in diesem Prototyp keine angegeben wurden. Durch diesen Prototyp wird die Typüberprüfung bei Aufrufen dieser Funktion deshalb nicht durchgeführt. Der erweiterte Prototyp, der die Argumentprüfung erlaubt, sieht so aus :
Deklaratoren legen den Typ für die von der Funktion erwarteten Argumente fest. Der Compiler verwendet diese Informationen, um einen Funktionsaufruf auf seine Gültigkeit hin zu überprüfen. Der Compiler ist nun auch in der Lage, bestimmte Typkonvertierungen vorzunehmen, die beim ersten Prototyp nicht möglich wären, was dadurch zu einem Fehlverhalten und/oder schwer auffindbaren Programmfehlern führen kann. Hier ein Beispiel : Da der Compiler hier über den korrekten Prototypen der Funktion irgendwas verfügt, kann er die Integerkonstante '1' im Aufruf der Funktion eigenständig in einen double-Typ umwandeln, bevor die Funktion aufgerufen wird. Ohne den Prototypen würde der Integerwert 1 an die Funktion gegeben werden, was zu einem Programmfehler während der Ausführung bis hin zum Absturz führen kann. DCC32 gibt automatisch eine Warnmeldung aus, wenn eine Funktion aufgerufen wird, für die noch kein Prototyp vorliegt. In einem solchen Fall verwendet DCC32 einen Standardprototypen mit Rückgabetyp int und aufgehobener Typüberprüfung.

Für Dokumentationszwecke ist es auch möglich, in einem Funktionsprototypen die Namen für die Argumente zu nennen. Ein Beispiel ist die Funktion memmove, die drei Argumente erwartet :

Durch diese Deklaration wird die Benutzung der Funktion schnell klar. Für den Compiler sind nur die Typen interessant, daher ignoriert er die Bezeichner in der Deklaratorenliste.
Eine Funktion, die keine Argumente benötigt, wird mit void als Deklaratorliste deklariert : Diese Funktion liefert einen Integer-Rückgabewert und benötigt keine Argumente. Wird bei einem Aufruf dieser Funktion doch ein Argument übergeben, erzeugt dies einen Compilier-Fehler.
Treten bei einem Funktionsaufruf Differenzen in den Datentypen zwischen den Argumenten im Aufruf und den Deklarationen im Funktionsprototyp auf, versucht DCC32 automatisch die Argumente in die Typen zu konvertieren, wie sie im Prototyp angegeben wurden. Um dies zu umgehen, kann man die Argumente beim Aufruf auch explizit selbst durch einen Cast konvertieren. Enthält der Prototyp eine Ellipse (...), dann werden nur die Argumente bis zur Ellipse konvertiert. Wurde kein Prototyp angegeben, findet grundsätzlich keine Typkonvertierung und -prüfung statt, d.h. alle Argumente werden ihrem Typ entsprechend an die Funktion übergeben.
Stimmt ein Prototyp nicht mit der Funktionsdefinition überein, findet der Compiler diesen Fehler nur dann, wenn sich Prototyp und Definition im gleichen Quelltextmodul befinden. Ansonsten können auch in diesem Fall zur Laufzeit schwer auffindbare Programmfehler entstehen. Stehen die Prototypen in einer Headerdatei, sollte diese beim Compilieren von Funktionen mit eingeladen werden, um eventuelle Unterschiede in der Deklaration und Definition einer Funktion vom Compiler aufspüren zu lassen.
 

3.5.12 Strukturen

Eine Struktur ist ein Typ, der für eine vom Benutzer definierte Ansammlung von benannten Elementen (oder Komponenten) besteht. Diese Elemente können jeden standardmäßigen und selbstdefinierten Typ in beliebiger Reihenfolge aufnehmen. Außerdem kann in einer Struktur ein Bitfeld verwendet werden, was sonst nicht zulässig ist. Strukturen werden mit dem Schlüsselwort struct definiert :

memnode ist in diesem Beispiel der sogenannte "Tag"-Name der Struktur. Läßt man ihn weg, erhält man eine unbenannte Struktur. Diese unbenannten Strukturen kann man zur sofortigen Definition von Variablen dieses Strukturtyps benutzen, indem die Bezeichner für diesen Typ direkt hinter der Definition aufgezählt werden : p ist hier eine Variable, m ein Array mit 10 Elementen und f ein Zeiger dieses Strukturtyps. Beim Deklarieren einer Struktur kann beim typedef ebenfalls der Tag-Name weggelassen werden : Normalerweise wird zur Deklaration einer Struktur entweder ein Tag-Name oder ein typedef ohne Tag-Name, aber mit Strukturname verwendet. Beide Kombinationen sind möglich. Die Element-Deklarationsliste innerhalb der Strukturklammern deklariert die Typen und Namen der Strukturelemente. Ein Strukturelement kann jeden beliebigen Typ annehmen, bis auf eine Ausnahme : der Typ eines Elements darf nicht den Typ der Struktur haben, die gerade deklariert wird :
struct memnode {
  unsigned groesse;
  void *naechster_block;
  struct memnode irgendwas; /* Fehler : Undefinierte Feldgröße */
};
Ein Strukturelement darf jedoch ein Zeiger auf ein Objekt vom Typ der Struktur sein :
struct memnode {
  unsigned groesse;
  void *naechster_block;
  struct memnode *irgenwohin; /* ist erlaubt, da Zeiger => 4 Bytes */
};
Der Grund liegt darin, daß der Compiler im ersten Fall die Deklaration der Struktur noch nicht abgeschlossen hat und somit auch die Größe der Struktur nicht weiß, die er für das 3.Element vom Typ "struct memnode" benutzen soll. Im zweiten Fall jedoch ist das 3.Element ein Zeiger, der unabhängig vom zugrundeliegenden Typ immer 4 Bytes groß ist und dessen Länge damit bekannt ist.

Zugriffsoperatoren

Auf Struktur- und Variantenelemente kann über diese beiden Auswahloperatoren zugegriffen werden :

Der Operator . ist der direkte und -> der indirekte Zugriff auf Strukturelemente. Hier ein Beispiel : Der Operator -> dient zum Zugriff auf Strukturelemente über einen Zeiger, während der Operator . zum Zugriff auf Strukturelemente benutzt wird, die direkt über ein Objekt definiert wurden.

Speichernutzung

Speicher wird einer Struktur elementweise von links nach rechts zugewiesen :

Das Objekt k belegt genug Speicher für einen 32-Zeichen-String, ein 4-Byte-Integer, ein 8-Byte-Double und ein 4-Byte-Float (also 48 Bytes). Es findet per Voreinstellung keine Ausrichtung der Strukturkomponenten auf Word- oder Dword-Grenzen statt, d.h. die einzelnen Komponenten der Struktur liegen nahtlos hintereinander im Speicher (es werden keine Füllbytes zwischen die einzelnen Komponenten gelegt, damit diese z.B. an Wortgrenzen liegen / siehe Compiler-Option -a).

Bitfelder

Es können "signed int" und "unsigned int" Integerelemente als Bitfelder in einer Struktur mit einer Größe von 1 bis 32 definiert werden. Die Größe und optionalen Bezeichner des Bitfelds werden wie folgt angegeben :

Als Datentypen sind "signed int" und "unsigned int" erlaubt. Läßt man den Bezeichner des Bitfeldes weg, wird das Feld in der Größe definiert, ist danach aber nicht verwendbar. Damit können z.B. bestimmte Hardware-Register abgebildet werden, bei denen einige Bits ungenutzt bleiben.

Integer-Bitfelder werden immer in 2er-Komplementform angelegt, wobei das linke Bit das höchstwertige Bit darstellt. Bei vorzeichenbehafteten Datentypen dient dieses Bit als Vorzeichen.
 

3.5.13 Varianten (Unions)

Die Variante ist ein Typ, die in vielen Punkten einer Struktur ähnelt. Der Hauptunterschied ist der, daß in einer Variante immer nur eines der deklarierten Elemente aktiv ist. Die Größe einer Variante wird durch ihr größtes Element bestimmt (und nicht von der Summe aller Elemente wie bei einer Struktur) :

Der Bezeichner was kann dazu benutzt werden, einen Double-, Integer, Float-Wert oder 6 Zeichen zu speichern, aber nicht alle gleichzeitig wie bei einer Struktur. Die Größe dieser Variante beträgt 8 Bytes, was durch das double-Element v ausgelöst wurde, welches das größte Elemente in dieser Variante darstellt. Auf die Elemente einer Variante wird genauso zugegriffen wie auf die Elemente einer Struktur (also mit . oder ->). In einer Variante sind ebenfalls Bitfelder als Elemente erlaubt, die aber den gleichen Regeln in Varianten unterliegen, wie alle anderen Elemente : es ist immer nur eines aktiv.

Hier ein Beispiel, das die obige Variante benutzt :

Das zweite printf ist zwar erlaubt, allerdings entspricht das Bitmuster in der Variante zu dem Zeitpunkt dem Integerwert 100. Durch was.v wird nun versucht, dieses Bitmuster als double-Wert zu interpretieren, was ein falsches Ergebnis liefern wird, da double-Werte intern auf eine andere Weise codiert werden.

3.5.14. Ausdrücke und Operatoren

Die Auswertung von Ausdrücken richtet sich nach bestimmten Konvertierungsregeln, der Abarbeitungsreihenfolge, der Richtung der Operatoren, dem Vorhandensein von Klammern und den Datentypen der Operatoren.

In der folgenden Tabelle ist zunächst die Reihenfolge der C-Operatoren aufgelistet. Ganz oben steht der Operator mit der höchsten und am Ende der mit der niedrigsten Prioritätsstufe :

Stufe Operator Funktion Abarbeitungsrichtung
16 ->, . Auswahloperatoren links nach rechts
16 [ ] Array-Index links nach rechts
16 ( ) Funktionsaufruf links nach rechts
16 ( ) Typkonstruktion links nach rechts
15 ++, -- Inkrement, Dekrement rechts nach links
15 ~ bitweises NOT rechts nach links
15 ! logisches NOT rechts nach links
15 +, - unäres Plus / Minus rechts nach links
15 *, & Dereferenzierung / Adressoperator rechts nach links
15 ( ) Typvorgabe (Cast) rechts nach links
14 ->*, .* Auswahloperatoren für Zeiger links nach rechts
13 *,/,% multiplikative Operatoren links nach rechts
12 +, - Addition und Subtraktion links nach rechts
11 <<, >> Bitverschiebung links nach rechts
10 <, <=,=>, > relationale Operatoren links nach rechts
9 ==, != Gleichheit, Ungleichheit links nach rechts
8 & bitweises AND links nach rechts
7 ^ bitweises XOR links nach rechts
6 | bitweises OR links nach rechts
5 && logisches AND links nach rechts
4 || logisches OR links nach rechts
3 ? : arithmetischer if-Operator links nach rechts
2 =, *=, /=, &=, +=, -=, <<=, >>=, &=, |=,= Zuweisungsoperatoren rechts nach links
1 , Komma-Operator links nach rechts

Hier ein Beispiel für eine auf den ersten Blick korrekte, auf den zweiten jedoch falsche Anweisung :

Die Absicht war es, mit getch() eine Taste von der Tastatur zu lesen, diesen Wert z zuzuweisen und danach auf ungleich 'A' zu prüfen. Es passiert jedoch folgendes : es wird eine Taste von der Tastatur gelesen, diese wird auf ungleich 'A' geprüft und das Ergebnis dieses Vergleichs wird z zugewiesen. Dieses Verhalten kommt dadurch, daß != (Ungleichheit) eine höhere Priorität hat wie = (Zuweisung). Dies kann man durch Klammerung vermeiden : Das Verhalten der in der obigen Tabelle aufgeführten Operatoren (einschließlich sizeof) entspricht der ANSI-Norm, weswegen wir uns hier eine genaue Beschreibung der einzelnen Operatoren sparen (dies kann in jedem C-Buch nachgelesen werden).

3.5.15 Anweisungen

Anweisungen steuern den Ablauf während der Ausführung eines Programms. Enthält das Programm keine Sprung- und Auswahlanweisungen, dann werden die Anweisungen sequentiell bis zur letzten Anweisungen durchgearbeitet.

Es gibt zwei verschiedene Möglichkeiten eine Anweisung mit einem Label (Sprungziel) zu versehen :

Label-Bezeichner : Anweisung
Der Label-Bezeichner dient als Sprungziel für eine unbedigte goto-Anweisung. Der Gültigkeitsbereich eines Label-Bezeichners erstreckt sich über eine ganze Funktion.

case Konstantenausdruck : Anweisung
Sprungziele in case- und default-Anweisungen werden nur in Verbindung mit der switch-Anweisung verwendet.

Auswahlanweisungen bestimmen den Programmablauf durch die Auswertung bedingter Ausdrücke. Es gibt zwei Auswahlanweisungen : if und switch.

if-Anweisung

Die if-Anweisung hat folgende Syntax :
 

if ( Bedingter-Ausdruck ) Anweisung-1
else Anweisung-2 ]

Der bedingte Ausdruck wird ausgewertet. Ist das Ergebnis ungleich Null (also wahr), wird Anweisung-1 ausgeführt, ansonsten (falls vorhanden) Anweisung-2. Ist kein else-Zweig vorhanden und der Ausdruck falsch, wird Anweisung-1 ignoriert. Anweisung-1 und Anweisung-2 können beide wieder eine if-Anweisung sein, wodurch die Verschachtelung von if-Anweisungen möglich ist. In diesem Fall bezieht sich jedes else auf die letzte if-Anweisung, falls dies nicht durch Anweisungsblöcke ( { } ) vom Programmierer anders festgelegt wird.

switch-Anweisung

Die switch-Anweisung hat folgende Syntax :

switch (Ausdruckcase-Anweisungen

Mit Hilfe der switch-Anweisung kann die Steuerung an eine von mehreren Anweisungen weitergegeben werden, die mit einem case-Label versehen sind. Zu welchem case-Label gesprungen wird, hängt vom Wert des Ausdrucks ab. Der Ausdruck muß ein Integer-Typ sein. Die case-Anweisungen können mit einem oder mehreren case-Labels versehen werden. Hierzu ein Beispiel :

int c;
c=getch();
switch ( c ) {
  case 'a' : printf( "\'a\' wurde gedrückt !\n" ); break;
  case 'b' : printf( "\'b\' wurde gedrückt !\n" ); break;
  case 'c' :
  case 'd' : printf( "\'c\' oder \'d\' wurde gedrückt !\n" ); break;
  default : printf( "\'%c\' wurde gedrückt !\n", c );
}
Im case-Block darf höchstens ein default-Label vorkommen. Dieses übernimmt eine vergleichbare Aufgabe wie das else in einer if-Anweisung, d.h. es fängt alle Werte ab, die nicht durch die vorhergehenden case-Label erfaßt wurden. Gibt es eine Übereinstimmung zwischen dem Inhalt der Variable c und einem der konstanten Ausdrücke, werden die case-Anweisungen hinter dem betreffenden case-Label ausgeführt. Schlagen sämtliche Vergleiche fehl und gibt es auch kein default-Label, dann wird keine der case-Anweisungen ausgeführt (die Steuerung wird an die nachfolgende Anweisung weitergegeben). Die break-Anweisung dient zum Verlassen des switch-Blocks am Ende einer Gruppe von Anweisungen zu einem bestimmten case-Label.
Wiederholungsanweisungen ermöglichen die Erzeugung von Schleifen. Dazu gibt es drei Anweisungen :

for-Anweisung

Die for-Anweisung besitzt folgendes Format :

for ( [Initialisierungsausdruck]; [Testausdruck]; [Inkrementierungsausdruck] ) Anweisung

Die Syntax erlaubt für jeden der drei (jeweils optionalen) Ausdrücke jeden Grad an Komplexität.
    1. Der Initialisierungsausdruck wird (wenn vorhanden) ausgeführt. Dadurch werden meist ein oder mehrere Schleifenzähler initialisiert.
    2. Nun wird der Testausdruck ausgewertet. Ergibt er den Wert Null (falsch), wird die for-Anweisung beendet und die darauf folgende Anweisung ausgeführt.
    3. Ergibt er jedoch einen Wert ungleich Null (wahr), wird die Anweisung ausgeführt, was auch der Fall ist, wenn der Testausdruck nicht vorhanden ist.
    4. Jetzt folgt die Auswertung des Inkrementierungsausdrucks, durch den meist die Schleifenzähler verändert werden. Danach geht die Bearbeitung wieder zu Schritt 2 zurück.

    Diese Anwendung der for-Anweisung erzeugt eine Endlosschleife :
    for (;;) ;
    while-Anweisung

    Das Format der while-Anweisung sieht so aus :

      while (BedigungsausdruckAnweisung

    Die Anweisung wird solange ausgeführt, wie der Bedingungsausdruck einen Wert ungleich Null (wahr) ergibt. Der Bedingungsausdruck wird dabei vor jeder Ausführung der Anweisung ausgewertet. Ergibt dieser den Wert wahr, wird die Anweisung ausgeführt, ansonsten wird zur darauf folgenden Anweisung verzweigt. Gibt es keine Sprungbefehle, muß die Anweisung auf den Wert von Bedingungsausdruck Einfluß nehmen, oder der Bedingungsausdruck muß sich selbst beeinflussen, ansonsten erhält man eine Endlosschleife.

    do...while-Anweisung

    Das Format lautet wie folgt :

      do Anweisung while (Bedingungsausdruck);

    Die Anweisung wird solange ausgeführt, wie der Bedingungsausdruck einen Wert ungleich Null (wahr) ergibt. Der Unterschied zur while-Anweisung besteht darin, daß der Bedingungsausdruck nach der Ausführung der Anweisung ausgewertet wird. Die Anweisung wird also mindestens einmal ausgeführt. Ansonsten gelten die gleichen Regeln wie bei der while-Anweisung.

Durch eine Sprunganweisung wird die Programmausführung unbedingt an eine andere Anweisung übertragen. Es gibt diese vier Sprunganweisungen :

break-Anweisung

Eine break-Anweisung kann nur innerhalb von Wiederholungsanweisungen und innerhalb einer switch-Anweisung angewendet werden. Sie beendet die Ausführung dieser Anweisungen und gibt die Kontrolle an die jeweils folgende Anweisung weiter. Bei Verschachtelungen beendet ein break immer die innerste einschließende Schleifen- oder switch-Anweisung.

continue-Anweisung

Eine continue-Anweisung kann nur in Schleifen-Anweisungen benutzt werden. In while- und do-while-Schleifen überträgt sie die Steuerung an die Auswertung der Testbedingung und in for-Schleifen an die Auswertung des Inkrementierungsausdrucks. In verschachtelten Schleifen gehört eine continue-Anweisung zur innersten einschließenden Schleife.

goto-Anweisung

Das Format der goto-Anweisung sieht wie folgt aus :

goto Label;

Die goto-Anweisung überträgt die Kontrolle an die mit Label gekennzeichnete Anweisung innerhalb einer Funktion.

return-Anweisung

Besitzt eine Funktion einen Rückgabewert, muß diese Funktion im Funktionsrumpf mindestens eine return-Anweisung beinhalten. Sie besitzt folgendes Format :

return [ Ausdruck ];

Das Ergebnis der Auswertung von Ausdruck muß dem Typ des Rückgabewerts der Funktion entsprechen. Trifft das Programm auf eine return-Anweisung, wird die Ausführung der aktuellen Funktion beendet und das Ergebnis der Auswertung von Ausdruck an die aufrufende Funktion zurückgegeben. Besitzt eine Funktion keinen Rückgabewert (=void), darf kein Ausdruck bei der return-Anweisung angegeben werden, ansonsten erzeugt der Compiler eine Fehlermeldung ( => "Ungültiger Rückgabewert" ). Bei void-Funktionen kann die abschließende return-Anweisung auch weggelassen werden.
3.5.16 Aufrufkonventionen

Aufrufkonventionen steuern die Übergabe von Argumenten an Funktionen, die Benennung von Funktionen und die Art, wie Argumente wieder entfernt werden. Zur Zeit unterstützt DCC32 zwei Aufrufkonventionen : __cdecl und __stdcall. Die Unterschiede zwischen beiden Konventionen sind in dieser Tabelle aufgelistet :

Aufrufkonvention  Argumentübergabe  Funktionsbenennung  Argumententfernung
__cdecl von rechts nach links Unterstrich vor Funtionsname  durch Aufrufer
__stdcall von rechts nach links keine Veränderung durch Aufgerufenen

Die Unterschiede werden deutlicher, wenn man sich den Aufruf einer Funktion mit beiden Konventionen ansieht. Als Beispiel soll die Funktion strncmp in beiden Varianten vorliegen und in beiden Varianten aufgerufen werden. In C sieht der Aufruf in beiden Fällen identisch aus : Erst in Assembler werden die Unterschiede deutlich. Der Aufruf der __cdecl-Variante von 'strncmp' sieht in Assembler so aus : Der Aufruf der __stdcall-Variante ist etwas kürzer : Wie in der Tabelle oben beschrieben, werden in beiden Varianten die Argumente von rechts nach links übergeben, d.h. erst len, dann s2 und danach s1. Der erste Unterschied äußert sich erst bei der Benennung der Funktion : _strncmp bei __cdecl und strncmp bei __stdcall. Der nächste Unterschied ist die Entfernung der Argumente vom Stack. Bei der __cdecl-Variante muß der Aufrufer eigenständig die Argumente entfernen (in diesem Fall durch 'add esp,12'), während die __stdcall-Variante dies automatisch erledigt. Durch diesen letzten Unterschied ist klar, daß die Aufrufe von __stdcall-Funktionen generell etwas schneller sind, da der Compiler nicht jedesmal die Befehlsfolge 'add esp,xxx' erzeugt, wodurch auch der Code etwas kleiner wird.

Bei der Deklaration bzw. Definition von Funktionen wird die verwendete Aufrufkonvention zwischen Typ und Funktionsnamen angegeben, z.B. so:

Die Deklaration bzw. Definition von Zeigern auf Funktionen sieht so aus : Wird keine explizite Aufrufkonvention angegeben, so benutzt der Compiler automatisch die __cdecl-Aufrufkonvention, d.h. die Deklarationen und sind somit identisch.