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.Der Aufruf von DCC32 geschieht auf folgende Weise :
Option | Startmodul | Bibliotheken |
-Wes | CX.OBJ | CRTS.LIB + SYS4G.LIB |
-Wed | CXD.OBJ | CRTD.LIB + SYS4G.LIB |
-Wdd | DX.OBJ | CRTD.LIB + SYS4G.LIB |
Erweiterung | Behandlung |
.C | C-Compilierung der Quelldatei(en) |
.ASM | Assemblierung der Quelldatei(en) durch einen externen Assembler |
.OBJ | Übergabe an DLINK32 |
.LIB | Übergabe an DLINK32 |
.DEF | Übergabe an 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 :
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 :
3.4.1 #
#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", "" ); */
#define HALLO "Hallo !" #define AUSGABE printf( HALLO ); #define STDIO #include <stdio.h> AUSGABE /* => printf( "Hallo !" ); */ STDIO /* => #include <stdio.h> wird an den Compiler weitergegeben ! */
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 :
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)); */
#define MUL( a,b ) a*b wert = MUL( 2+3, 5+5 );
wert = 2+3*5+5;
Bei der Verwendung von Makros mit Parametern sollten folgende Punkte beachtet werden :
#define NEUVAR(a,b) (a##b)Der Aufruf NEUVAR(test,var) erzeugt so das Symbol (testvar).
#define PRINT( var ) printf( #var "=%d\n", var ) int test = 100; PRINT( test ); /* => printf( "test" "=%d\n",test ); oder printf( "test=%d\n", test ); */
#define WARNUNG printf( "Makrodefinition über \ zwei Zeilen" ) WARNUNG; /* => printf( "Makrodefinition über zwei Zeilen" ); */3.4.3 #undef
#define BLOECKE 32 #define BLOCKGROESSE 512 bufsize = BLOCKE * BLOCKGROESSE; /* bufsize = 32 * 512; */ #ifdef BLOCKGROESSE #undef BLOCKGROESSE #endif #define BLOCKGROESSE 1024 bufsize = BLOECKE * BLOCKGROESSE; /* bufsize = 32 * 1024; */Wird ein bereits bestehender Makrobezeichner erneut definiert, führt dies zu einer Fehlermeldung, wenn die Definitionen nicht identisch sind. Wenn Definitionen in mehreren Header-Dateien geschehen, sollte eine doppelte Definition hiermit vermieden werden :
#ifndef BLOCKGROESSE #define BLOCKGROESSE 512 #endifIn diesem Fall wird BLOCKGROESSE nur definiert, wenn es vorher noch nicht existierte.
Die Bedingungsdirektiven #if, #ifdef und #ifndef funktionieren
wie normale Bedingungsoperatoren in C. Sie werden folgendermaßen
verwendet :
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
#ifdef __STDC__
#if defined(__STDC__)
#ifndef __STDC__
#if !defined(__STDC__)
#if NEUVAL < 0 || NEUVAL > 10 #error NEUVAL muß zwischen 1 und 9 liegen #endif
- #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.
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. |
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 :
auto break case char const continue default do double else enum extern float for goto if int long register return short signed sizeof static struct switch typedef union unsigned void volatile while __cdecl __stdcall3.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 :
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'.
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.
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 |
"Ich bin eine Stringkonstante !"
"Dies ist der Alarmton : \a und dies der Zeilenvorschub : \n"
printf( "Teil1 " "Teil2 " "Teil3" );
Teil1 Teil2 Teil3
printf( "Dieser lange Text \ wurde auf vier Textzeilen verteilt, \ obwohl er in Wirklichkeit einen \ einzeiligen Text darstellt." );
Hierzu ein Beispiel :
enum geraete { fernseher, cdplayer, receiver};
fernseher = 0, cdplayer = 1, receiver = 2
enum geraete { fernseher,cdplayer=2,receiver,rekorder=cdplayer-2 };
fernseher = 0, cdplayer = 2, receiver = 3, rekorder = 0
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
getch(); if (*p) printf( "a" ); m = 500 * ( a + ( 200 / d )); double (*funktion) ( void );
if (m==80) { m++; printf ("%d\n", m); }Die abschließende Klammer ( } ) dient als Endemarkierung für den Anweisungsblock, der daher kein Semikolon folgen darf (eine Ausnahme bilden Strukturdeklarationen und Initialisierungen von Arrays).
char arr[] = "Beispiel"; double vp[4][10]; vp[2][1] = 100.2;
m * 3; /* Audruck wird ausgewertet, aber Ergebnis wird verworfen */ ; /* ergibt einen leeren Ausdruck => leere Anweisung */ C++; /* C wird um 1 erhöht, der alte Wert von C wird verworfen */Semikolons werden außerdem häufig in Schleifen zur absichtlichen Darstellung von leeren Anweisungen benutzt :
while (*s1++ = *s2++ ) ;
void test ( int, char, char *, char [] );Es wird auch als Operator in Komma-Ausdrücken benutzt. Die Kombination beider Komma-Anwendungen ist erlaubt, solange runde Klammern zur Unterscheidung benutzt werden :
/* Aufruf von fprintf mit 3(!) Argumenten : */ fprintf (stdout, fehler, (p1, p2, p3, p4 ));
char *charzeiger; /* = Zeiger auf ein char-Datenfeld */ int *iptr; /* = Zeiger auf ein int-Datenfeld */Zeiger mit mehrfachen Indirektionsebenen werden durch entsprechend viele Sternchen gekennzeichnet :
char **charzeiger; /* = Zeiger auf Array aus char-Zeigern */ int ***iptr; /* = Zeiger auf ein 2-dim. Array aus int-Zeigern */Das Sternchen kann auch als Operator, der einen Zeiger dereferenziert oder als Multiplikations-Operator eingesetzt werden :
int val = *iptr; /* val = Datenfeld, auf das iptr zeigt */ val = val * 4; /* val multipliziert mit 4 */
marker: v=0; ... goto marker;Sprungziele in switch-Anweisungen werden ebenfalls durch den Doppelpunkt gekennzeichnet :
switch (val) { case 100 : printf ("100 !\n"); break; case 200 : printf ("200 !\n"); break; case 300 : printf ("300 !\n"); break; default : printf ("Falscher Wert !\n"); }
int fprintf (FILE *stream, const char *format, ... );Der Funktionsprototyp von fprintf besagt, daß jeder Aufruf mindestens zwei Argumente (stream und format) erfordert. Dahinter können beliebig viele Argumente mit beliebigen Typen folgen.
k = m * 10 + a; double *m = &f;Außerdem dient das Gleichheitszeichen zur Trennung zwischen Variablendeklarationen und Initialisierungslisten :
int arr[5] = { 0, 1, 2, 3, 4 };3.5.7 Speicherklassen
Jede Deklaration benötigt einen Speicherklassenspezifizierer. Folgende Spezifizierer stehen dazu zur Verfügung:
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.
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 :
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 = ..." */
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; }
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.
Hier ein paar Beispiele :
char *text = "Ganz normaler Text"; double **gehaelter;
int (*funk)( void ); char * (*funk2) ( char *, int );
/* Array mit 5 Integerwerten / Index von 0 bis 4 : */ int i_arr[]={0, 1, 2, 3, 4 }; int *iptr; /* Zeiger auf ein Integerobjekt */ iptr = i_arr; /* iptr zeigt auf das erste Element von i_arr */ /* Ausgabe des Wertes, auf den iptr zeigt (= i_arr[0]) : */ printf( "*iptr = %d\n", *iptr ); /* iptr auf nächstes Element in Array setzen : */ iptr++; /* Ausgabe des Wertes, auf den iptr zeigt (= i_arr[1]) : */ printf( "*iptr = %d\n", *iptr ); /* Ausgabe des nachfolgenden Wertes (= i_arr[2]) : */ printf( "iptr[1] = %d\n", iptr[1] );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 :
char *str; int *i; i = (int*)str;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 :
char *str; int *i; i = str; /* Fehler : Unzulässige Typen "int*" und "char*" */3.5.10 Arrays
Ein Array kann auf folgende Art definiert werden :
int array[10];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 :
int array[10] = { 0, 1, 2, 3, 4, 5 };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 :
int array[10][5];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.
/* 8 Elemente à 1 Byte mit abschließendem '\0' : */ char string[] = "Hallo !"; int array[] = { 0, 1, 2, 3, 4 }; /* 5 Elemente à 4 Bytes */Der Bezeichner des Arrays stellt einen Zeiger auf das erste Element des Arrays dar. Folgende Anweisungen sind daher zulässig :
char string[100] = "Hallo !"; strcpy( string, "Neuer Text !" );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 :
extern int irgendwas (double); int main ( int argc, char *argv[] ) { int k; k = irgendwas( 1 ); return 0; }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 :
void *memmove( void *ziel, const void *quelle, size_t anzahl );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.
funktion( void );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.
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 :
struct memnode { unsigned groesse; void *naechster_block; };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 :
struct { unsigned groesse; void *naechster_block; } p, m[10], *f;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 :
typedef struct { unsigned groesse; void *naechster_block; } MEMNODE; MEMNODE p; MEMNODE m[10], *f;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 */ };
struct memnode { unsigned groesse; void *naechster_block; struct memnode *irgenwohin; /* ist erlaubt, da Zeiger => 4 Bytes */ };
Zugriffsoperatoren
Auf Struktur- und Variantenelemente kann über diese beiden Auswahloperatoren zugegriffen werden :
struct irgendwas { int k; char name[32]; double v; } a, *b; b=&a; a.k = 100; b->v = 1.0; strcpy( b->name, "Hallo" ); printf( "%s\n", a.name );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 :
struct irgendwas { char name[32]; int t; double v; float c; } k;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 :
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) :
union irgendwas { double v; int k; char t[6]; float c; } was, *wo, wie[10];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 :
wo = &was; /* 100 in das Integerelement k bringen : */ was.k = 100; /* gibt 100 als Integer aus : */ printf( "%d\n", wo->k ); /* ergibt unvorhersagbares Laufzeitverhalten : */ printf( "%f\n", was.v);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 :
while ( z=getch() != 'A' ) ;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 :
while ( (z=getch() ) != 'A' ) ;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.
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.
if-Anweisung
Die if-Anweisung hat folgende Syntax :
switch-Anweisung
Die switch-Anweisung hat folgende Syntax :
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 ); }
for-Anweisung
Die for-Anweisung besitzt folgendes Format :
for (;;) ;while-Anweisung
Das Format der while-Anweisung sieht so aus :
do...while-Anweisung
Das Format lautet wie folgt :
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.
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 :
return-Anweisung
Besitzt eine Funktion einen Rückgabewert, muß diese Funktion
im Funktionsrumpf mindestens eine return-Anweisung beinhalten. Sie besitzt
folgendes Format :
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 |
strncmp( s1, s2, len );Erst in Assembler werden die Unterschiede deutlich. Der Aufruf der __cdecl-Variante von 'strncmp' sieht in Assembler so aus :
push len push s2 push s1 call _strncmp add esp,12Der Aufruf der __stdcall-Variante ist etwas kürzer :
push len push s2 push s1 call strncmpWie 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:
/* func ist eine __cdecl-Funktion : */ int __cdecl func( ); /* test ist eine __stdcall-Funktion : */ char * __stdcall test( int, char * ); /* test2 ist eine __cdecl-Funktion : */ void __cdecl test2( const char *s, FILE *f ) { printf( s, f ); }Die Deklaration bzw. Definition von Zeigern auf Funktionen sieht so aus :
/* ptr: Zeiger auf _stdcall-Funktion des Typs int ohne Argumente : */ extern int __cdecl (*ptr)( void ); /* ptr2: Array mit Zeigern auf __stdcall-Funktionen des Typs int mit 2 Argumenten : */ __stdcall (*ptr2[10])( char *, int );Wird keine explizite Aufrufkonvention angegeben, so benutzt der Compiler automatisch die __cdecl-Aufrufkonvention, d.h. die Deklarationen
int __cdecl func( );und
int func( );sind somit identisch.