Die Programmiersprache CTutorial / Referenz / LehrgangCopyright © 1999,2000 Helmut Schellong (mit C99 -- Neuer ANSI-Standard; mit C++)Die Audio-Lügen |
||
|
||
Nachfolgend sind auch Textabschnitte für Erklärzwecke
vorhanden, die nicht allgemeingültig sind. Diese beziehen sich auf typische 32Bit-Plattformen mit Intel-iX86-Prozessor, wenn nicht anders angegeben. Beispielsweise sind Byte- und Bit-Anzahlen und Wertbereiche von Datentypen nicht allgemeingültig. Bitweise Zahlendarstellungen erfolgen im 2er-Komplement (z.B. iX86). Siehe auch: Kapitel "Hinweise/Tips" |
^C-Programm, C-Module/Dateien, C-Quellkode, CompilerEin C-Programm besteht aus einer oder mehreren Datei(en) mit der Endung
.c , aus der/denen Eine C-Datei (oder auch C-Modul) wird mit einem beliebigen Text-Editor
erzeugt Der Aufruf-Name eines C-Compilers lautet vom Unix-Ursprung her: cc
Verschiedene Aufruf-Namen: Beispielsweise ein Aufruf eines Borland-Compilers: bcc datei.c erzeugt eine ausführbare Datei: datei.exe Unter Unix: cc -odatei datei.c erzeugt eine ausführbare Datei: datei Obiger Aufruf veranlaßt vier Arbeitsschritte: Programm Datei-Endung C-Präprozessor --> .i cc kann mittels Optionen so gesteuert werden, daß von hinten
gesehen (Linker) cc -O1 -oabc abc1.o abc2.s abc.c -lm Hier wird abc.c optimiert, eine
Exe (abc) erzeugt, abc1.o
wird nur vom Linker verarbeitet, Die Compiler-Optionen -P oder -E, -S, -c bewirken,
daß jeweils nur der Präprozessor (.i), |
^Ein schematisches C-ProgrammDie folgende einzelne Zeile ist bereits ein gültiges C-Programm!: int main(void) { return 0; } Hier wird der Startkode ausgeführt, der nach erfolgreicher Kompilierung
stets automatisch vorhanden ist. C-Programme sehen im Prinzip immer so ähnlich aus wie das nachfolgende Programm: /* xyz.c sc/14.12.99 */ Das obenstehende C-Programm funktioniert zwar, hat aber keine sinnvolle Funktion. C-Programme bestehen also im wesentlichen (fast) immer aus beliebig
vielen Funktionen, Außerhalb von Funktionen dürfen keine Algorithmen programmiert
werden, Vor der ersten Funktion im Quellkode sollte stets eine Liste aus Funktions-Prototypen
stehen, Ein Prototyp auch für 'main' ist durchaus nicht unsinnig, denn
'main' darf auch im sichtbaren Die Header-Dateien <header.h> sind nicht zwingend notwendig. int main(int argc, char **argv, char **envp) argc enthält in aller Regel die Anzahl aller in der Kommandozeile
angegebenen Argumente. for (; argc>0; --argc,++argv) Der Parameter envp ist nicht
ANSI-konform, wird aber im Standard erwähnt und ist fast überall int main(void); Die Schreibweisen *argv[] und
**argv sind in der Argumentliste beide korrekt, int main(int C, unsigned char **A, char **E) argv und envp sind Arrays aus Adressen auf Zeichenketten. |
^C-Zeichensatz, Konstanten, Kommentareabcdefghijklmnopqrstuvwxyz Obenstehend diejenigen Zeichen, die für Namen (und Konstanten)
verwendet werden dürfen. !"#%&'()*+,-./:;<=>?[\]^{¦}~ Diese Zeichen sind Operatorenzeichen und Punktuatoren. Leerzeichen, Tabs, Zeilenvorschübe, Wagenrücklauf, etc. sind
Zwischenraumzeichen, die völlig $@` Dies sind die einzigen (sichtbaren) in C nicht verwendeten ASCII-Zeichen. Konstanten:Wert Typ Zahlenbasis Ziffernmenge 12345 int dezimal:10 0...9 Oktal- und Hexzahlen haben eine Besonderheit,
da deren Zahlenbasen 8,16 Zeichenkonstanten: 'A' '#' 'ä' '\'' '\x41' '\0' L'x'(wchar_t,Unicode) Zeichenkonstanten haben den Typ int
und den Zahlenbereich -128...+127 . Zeichenkettenkonstanten: "Die Programmiersprache \"C\" hat nur wenige Schlüsselwörter." L"Die Programmiersprache \"C++\" hat mehr davon." /*wchar_t*/ Zeichenkettenkonstanten werden von einem Paar
Doppelapostroph eingefaßt: "..." printf("aaaaaaa" "bbb" Die beiden Funktionsaufrufe ergeben die gleiche
Ausgabe, da der Compiler die erste, in Teilstücken printf("aaaaaaa\ /* Der Preprocessor macht dies */ Die folgenden Zeichen-Ersatzdarstellungen können
bei Zeichen- und Zeichenkettenkonstanten \n Zeilenvorschub (LF,NL) Mit \000...\377 oder \x00...\xff
kann der gesamte Zeichensatz angegeben werden. Enumerationen (siehe weiter unten) gehören ebenfalls zu den Konstanten. Kommentare:/* Dies ist ein Kommentar */ // Dies ist ein Zeilenkommentar Alles zwischen /* und */
wird ignoriert, auch Zeilenvorschübe. Zeilenkommentare gelten bis zum Zeilenende, ausschließlich des
Zeilenvorschubs. Achtung!: a= b + c - d //*70*/ 80; |
^C-Schlüsselwörter (Keywords)Typen von Datenobjekten:
const volatile typedef typedef unsigned char byte; byte a, b=2, c, *bp=0; typedef struct _FILE_ FILE *fp; Hier wurde zuletzt der Typ FILE
definiert. (<stdio.h>) typedef int A[10][2]; Es wurde ein Typ 'A' definiert. |
^Elementare DatentypenBitanzahl und Wertbereich sind nachfolgend für typische Intel-Plattformen
gezeigt: Typ Bits Wertbereich/Signifikanz char 8 -128...+127 int 16 DOS: siehe short Die Integer-Typen wurden in Kurzschreibweise angegeben; 'signed' kann auch mit anderen Integer-Typen als char verwendet
werden, was aber redundant wäre. 1er-Komplement-Prozessoren können in 8 Bit nur -127..+127
darstellen, nicht -128..+127. Die Wertebereiche in der obenstehenden Tabelle entsprechen den ANSI-Mindestforderungen, long long ist ganz neuer ANSI-Standard C99. Achtung, es gibt Plattformen, auf denen ein Byte 32 Bit hat ! void void funktion(void) { /*...*/
return; } (void)printf("..."); void *memset_F(void *d0, int i, register unsigned
n) int iarr[64]; |
^OperatorenOperatoren,fallender Rang Zusammenfassung von links/rechts her () [] -> . L unär, monadisch: Operator hat einen Operanden. Die Operatoren * + - & werden
unär als auch binär verwendet!
Operatoren, kurz erklärt:Zu jedem Operator wird nachstehend nur eine kurze Erklärung gegeben, ( ) [ ] -> . * & - + ~ ! ++ -- (typ) sizeof * / % + - << >> < <= > >=
== != & ^ ¦ && ¦ ¦ ?: = += -= *= /= %=
&= ^= ¦= <<= >>= , Bei Variablen-Definitionen können mehrere Variablen-Namen Das Semikolon (;) ist kein Operator,
sondern das Abschlußzeichen einer jeden Anweisung. |
^SpeicherklassenObjekte, die außerhalb von Funktionen definiert werden,
werden statisch im Speicher angelegt. Alle statischen Objekte sind/werden beim Programmstart automatisch und
typgerecht mit 0 initialisiert, static Globale Objekte sind nur im aktuellen C-Modul bekannt: Verkapselung
der Namen/Bezeichner. extern extern long timezone; register register int i, r; auto auto int i; Alle nichtstatischen Objekte werden beim Verlassen der jeweiligen
Funktion zerstört ! Alle Definitionen und Deklarationen von Objekten innerhalb von Funktionskörpern
register- und auto-Objekte werden
bei jedem Funktionsaufruf neu angelegt, Register werden natürlich nicht 'angelegt',
aber deren Inhalt schon. |
^Adressen (Pointer, Zeiger)Objekte sind in C: Jedem Objekt wird bei seiner Definition (Beschreibung+Erzeugung) Speicherplatz
zugewiesen. int i=2, *ip, **ipp; Hier wurden angelegt/definiert: &ipp hat den Typ int*** ; prinzipiell könnte man diese Verschachtelung beliebig weitertreiben. i und *ip und **ipp
haben den Typ int und liefern 2
. ip[0] und ipp[0][0] liefern ebenfalls 2 . Andere Indexwerte als 0 wären allerdings falsch. char *cp= (char*)&i; cp[0] == 2 Falls eine int-Variable aus 4 char-Elementen besteht,
also sizeof(int)==4 ist, Das obige Beispiel mit (Adressen-casting*) geht auch größenmäßig umgekehrt: char A[5]; int *ip; ip= (int*)(A+1); Hier kann jedoch -je nach Prozessor- ein Misalignment-Fehler
passieren! (nicht bei iX86) Eine falsche Zugriffs-Ausrichtung im Speicher kann vorliegen, wenn auf
ein Speicherobjekt Addition, Subtraktion und Differenzbildung bei Adressen:neue_Adresse = Adresse + i ip += 1 ip - 1 i = (int)( ipb - ipa ); Wenn also (auf Quelltextebene) eine Adresse um 1 verändert wird,
zeigt der neue Adressenwert Streng nach ANSI: Adressen-Variablen sollen nur gültige Adressen
enthalten, die auf bestehenden Speicherplatz
extern char **environ; Dieses externe Objekt gibt es auf den meisten Plattformen tatsächlich. In diesem spezifischen Fall wird nicht nur auf ein einzelnes char*-Objekt
gezeigt, sondern auf static char *envarr[]= { "PATH=...............", So könnte das prinzipiell extern programmiert worden sein. environ zeigt
auf die Basisadresse des Arrays: char** environ liefert stets das aktuelle Environment,
Sammlung weiterer Beispiele, die die Zusammenhänge klarmachen sollen:char *cp; (a) event. Misalignment auf Nicht-x86-Prozessoren . |
^Arrays (Vektoren, Felder)Ein Array kann als Kette von lückenlos aufeinander folgenden Objekten
gleichen Typs bezeichnet werden. Arrays können nicht als Ganzes
an Funktionen übergeben und ebensowenig retourniert werden. int A[5]; A[0]=A[1]=A[2]=A[3]=A[4]= 258; /* 258 == 0x00000102 */ 21002100210021002100 Ein 1-dimensionales Array namens A aus 5 int-Elementen. Größe in Bytes: sizeof(A)
== 20
(10220-10200==20). Ein Array-Name allein und ohne Zugriffsoperatoren repräsentiert
(fast immer) die Basisadresse des Arrays: Der Typ des Ausdrucks 'A' ist kontextabhängig: Adresse Zugriff auf Adresse Typ 10200 &A[0] oder A+0 oder A int * printf("%u %d\n", A, A[2]); /* Ausgabe: 10200 258 */ Zugriff auf den Inhalt: A[i] entspricht *(A+i) , A[0] entspricht *A Definition: Zwei-dimensionales Array: int AA[5][2]; 0000000011111111222222223333333344444444 Typ Ausdruck Alternative relativ E./A. int AA[0][0] **AA Element 0 &: Operator & dort erst seit ANSI-C89 möglich. AA[i][j] entspricht (*(AA+i))[j] entspricht *(*(AA+i)+j) entspricht *(AA[i]+j) int (*aap)[2]= AA; AA[i][j] entspricht aap[i][j] int *ip= (int*)AA; AA[i][j] entspricht ip[i*2+j] int *aap[2] ist ein Array aus 2
Elementen vom Typ int* . Siehe oben: int AAA[5][2][3]; AAA[i][j][k] entspricht ip[i*2*3 + j*3 + k] Der Compiler rechnet ip + (i*2*3+j*3+k)*sizeof(int) und erhält so die Zugriffsadresse. AAA[4][1][2]: (4*6+1*3+2)==(29) Folgendes ist in der Praxis nicht selten nützlich: char A[3][256];
Beim Anlegen eines Arrays muß die Nicht-Pointer-Form mit Angabe
aller Dimensionen
Man beachte nachfolgend den Unterschied: static char *A[4]= { "aaaaaa", "bbbbBBBBbbbb", "ccc" }; static char AA[4][13]= { "aaaaaa", "bbbbBBBBbbbb", "ccc" }; sizeof(A ) == 16 A[i] und AA[i] haben beide den Typ
char* . A ist ein Array aus 4 Adressen auf char, Zeichenkettenkonstanten sind eindimensionale Arrays.
Sammlung weiterer Beispiele, die die Zusammenhänge klarmachen sollen:char C[2][3][4]; // 2*3*4= 24 Elemente: 0...23 000000000000111111111111 [2] Zeiger auf ein zweidimensionales Array aus 3x4
char-Elementen, und, char C[2][3][4]; Alle Operationen mit cp und C sind gleichwertig,
solange der Inhalt von cp nicht verändert wird. &cp char (**)[3][4] Adresse von cp &C ist kein Fehler mehr seit ANSI-C89/ISO-C90. Speicher-Allokation mit der Library-Funktion malloc() : char(*bp)[80][2]= (char(*)[80][2]) malloc( Von malloc() zu liefernde Bytes: n= (z_max+1)*80*2*sizeof(char) char C[2][3][4]; Der Speicherplatz eines Objekts kann durch Typ-casting
(s.o.: p,malloc) typmäßig long La[24]; Byteadressen (Assembler-Ebene) in Klammern ()
. char C1[1][10]; // 10 Elemente: 0...9 char CA[8]; * und & heben
sich gegeneinander auf. |
^Strukturen, Unionen, Bitfelder, EnumerationenStrukturen:Strukturen dürfen sämtliche Datenobjekte in beliebiger Mischung
enthalten. Rekursive Strukturen sind zwar nicht möglich, aber Strukturen können
ein Im Unterschied zu Arrays können Strukturen als Ganzes einander
zugewiesen werden, Der Name einer Struktur ist diese Struktur als Ganzes. Strukturen können auf verschiedene Weise deklariert und definiert werden:
struct etikett { /*...*/ }; struct sa_t { int i; char ca[4]; }; struct sb_t SB1,SB2, *pSB= &SB1; SB2= SB1; Namen der Mitglieder eines Strukturtyps sind trotz eventueller Gleichheit
konfliktfrei
Unionen:Unionen gleichen Strukturen bis auf einen Punkt: Die Ausrichtung im Speicher (Alignment) paßt für das Mitglied union ua_t { char cBuf[100*sizeof(long long)]; for (i=0; i<100; ++i) Ucl.ullBuf[i]= 0xA5A5A5A5A5A5A5A5ull; Unionen eignen sich für Konvertierungen, Umsetzungen, Speicherfülloperationen Der Speicherbedarf kann reduziert werden, falls mit verschiedenen Mitgliedstypen (long long: C99)
Bitfelder:Bitfelder können nur innerhalb von Strukturen und Unionen definiert
werden. name:bit_anzahl Bitfelder sollen den Typ unsigned
haben. In aller Regel verarbeiten die Compiler jedoch
auch die schmaleren unsigned-Typen Auf ein definiertes Bitfeld von beispielsweise
12 Bit Breite kann im Zahlenbereich Achtung, die nachfolgenden Bit-Darstellungen der
initialisierten Werte sind typisch struct bit { ushort a:1, b:2, c:3, d:4; } bf = {1,2,4,8}; Für diese Bitfelder initialisieren Compiler in der Regel ein WORD folgendermaßen: 0x0225 = 000000_1000_100_10_1b ----------------------------------------- struct bit { ushort a:1, // 1 Vorstehend initialisiert der Compiler (in der Regel) zwei WORDs (bf) folgendermaßen: 0x0010 0x2205 = 00000000000_10000 00_1000_100_0000_10_1 :4
ist ein Füller, und :0
bewirkt einen Sprung zum nächsten Wort.
Enumerationen:enum { null, eins, zwei, drei, vier }; Die Namen null ... vier sind jetzt mit den konstanten
int-Werten 0 bis 4 verknüpft enum ft { False, True }; enum monate { Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec }; enum { zehn=10, elf, Zehn=10, sechzehn=16, siebzehn }; . |
^InitialisierungenInitialisierungen können als sofortige Zuweisungen bezeichnet
werden, Alle statischen Objekte sind/werden nur einmal beim Programmstart
initialisiert, static int I, D=4; while (lv<240) { int j=1; /*...*/ lv+=j; } Man beachte, daß D=4 nur beim Programmstart
einmalig erfolgt, auch wenn sich die betreffende static char A[5]= { 10, 14, 'z', 22 }; Das fünfte und letzte Array-Element ([4]) wird implizit
mit 0 initialisiert; static char A[ ]= { 10, 14, 'z', 22 }; Dies Array hat nur 4 Elemente, da die Größenangabe
fehlt. static char A[4]= "abc"; Die beiden Array-Definitionen haben das gleiche Resultat. static char A[2][3]= { {3,4,5}, {20,24,28} }; struct { int a[3], b; } AS[]= { {{1,0,3}, 8}, {{2,0,4}, 9} }; Arrays, Strukturen und Unionen müssen mit (mindestens) einem Paar
geschweifter Klammern Bei Unionen kann nur gemäß dem ersten Mitgliedstyp initialisiert werden. |
^FunktionenFunktionen sind Kode-Objekte, während Variablen, Strukturen,
etc. Daten-Objekte sind. Der Name einer Funktion (allein) repräsentiert ihre Adresse, eine
Kode-Adresse. Funktionen können beliebig viele Argumente haben (auch gar keines),
die beim Aufruf Beim Aufruf werden die Argumentwerte kopiert, und diese
Kopien werden an die Funktion Argumentwerte werden mit mindestens int-Breite kopiert. Sobald eine Funktion die Adresse eines Objektes erhalten hat,
kann sie natürlich durch int Fua(int, long); /* Prototypen */ Man beachte, daß zwischen den Parametern i,i1,ip
und der auto-Variablen ab hinsichtlich
Funktions-Adressen:int(*FuaP)(int,long)= Fua; i= (*FuaP)(2, 0L); Anlegen einer Funktionsadressen-Variablen FuaP und
Aufruf von Fua über ihre Adresse. printf("%u %u\n", main, main+1); Na sowas! - Man könnte sogar erste Funktions-Instruktionen überspringen...! =8-O
Funktion mit variabel vielen Argumenten (eingeschränkt portabel):Für vollkommene Portabilität sollten die Makros va_list,
va_start, va_arg, va_end Folgendes Konzept ohne <stdarg.h> ist aber grundsätzlich
flexibler, offener und static int CatS(char *, char *, ...); Die Funktion kopiert beliebig viele Zeichenketten hintereinander in
einen ziel-Puffer. Adresse des zweiten Parameters: a=&S1 Bei dieser Funktion haben alle Parameter (alle: char*)
gleiche Länge.
Funktion mit variabel vielen Argumenten: Voll portable Variante:#include <stdarg.h> static int CatS(char *, ...);
Auf vielen typischen Intel-Plattformen machen
diese Makros va_xxx das gleiche wie die Es soll laut ANSI-Standard der letzte Parameter
vor ,... an va_start() übergeben
werden. <stdarg.h>Typ: va_list void va_start(va_list ap, parmN); va_end(ap) soll einmal aufgerufen
werden: Da va_copy() neu ist ab C99,
wird nachstehend eine Analyse einer typischen typedef char* va_list; va_start(a,S0) sizeof(S0) X arg= va_arg(a,typ) Man erkennt, daß va_start() den Parameter-Zeiger
a mit der auf &S0 folgenden Sie können an dieser Analyse erkennen, was Sie beachten müssen,
falls Sie diese Makros aus Der neue C99-Makro: va_copy(va_list ziel, va_list quelle) : #define va_copy(ziel,a) ((void)(ziel=(a))) Ist irgendwie zum Lachen - was?! Mit va_copy kann ein gültiger Parameterzeiger-Wert
zu einem beliebigen Zeitpunkt f= va_arg(a,int); .
Funktionen rekursiv aufrufen:Die folgende Funktion ruft sich direkt rekursiv selbst auf. Die normalen Startwerte beim Hauptaufruf wären: 1,1
statt 0,8 static void Rek(int i, int m) Die Parameter i,m werden bei jedem Aufruf neu angelegt. Funktionen können sich auch über andere Funktionen hinweg indirekt selbst aufrufen.
Eine Dateiliste rekursiv herstellen:Die folgenden Funktionen bilden die Library-Funktion ftw
nach, die nicht bei allen Systemen # include <dirent.h> . |
^Steuerung des ProgrammablaufesDie nachfolgend gezeigten Anweisungen und Blöcke können beliebig
ineinander if-Anweisung:if ( bedingung ) anweisung ; if ( bedingung ) anweisungs-kommaliste; if ( bedingung ) { anweisung; anweisung; ...; } if ( bedingung ) ; /* Leeranweisung */ if ( bedingung ) falls bedingung!=0 ; if ( ... ) { if (...) ...; anw; } if ( ... ) ...; anw2; else ...; /* else-Fehler! */ if ( ... ) { if (...) ...; } if ( ... ) ...; if (r=0, !a&&(r=1, b>6)&&(r=2, c)) ... Eine leere Bedingung () entspricht (1) - immer TRUE. Bei der Zuordnung eines else-Zweiges zu einem if
wird rückwärts das anw oben hat nichts mit der direkt davor stehenden if-Anweisung
zu tun! i= !a&&(r=1, b>6)&&(r=2, c); /* siehe oben: if (r=0, ... */ i erhält den Wert 1 bei zutreffender
Bedingung, andernfalls 0 .
for-Schleife:Siehe auch if-Anweisung für die verschiedene Gestaltung des Anweisungsteils. for (liste; bedingung; liste_b) anweisung; for (; bedingung; ) anweisung; for (; bedingung; ) { for (a=b+2,++c; b<10&&d; ++b) ... Die anweisung wird nur ausgeführt, wenn die
bedingung!=0 ist. continue springt zu liste_b oder
direkt zur bedingung , falls liste_b
fehlt.
while-Schleife:while ( bedingung ) anweisung; while ( bedingung ) { anweisung; anweisung; ...; } Weiteres: siehe oben, siehe unten.
do-while-Schleife:do anweisung; while ( bedingung ); do { anweisung; anweisung; ...; } while ( bedingung ); anweisung wird garantiert 1-mal ausgeführt -- mindestens 1-mal. Weiteres: siehe oben, siehe unten.
switch-Fallunterscheidung:In Abhängigkeit von einem case-Wert wird zur dazu passenden
case-Marke gesprungen. switch ( case-wert-ausdruck ) { Das break hinter default ist
hier nicht nötig, denn die Verarbeitung würde nach Der 'a'-Zweig fällt nach 'A' durch, weil hinter
'a' kein Sprung erfolgt. Siehe unten: switch-Beispiel
breakspringt aus einer Schleife oder aus einem switch-Block
heraus, continuespringt zur Bedingungsprüfung einer umgebenden Schleife. return; Beendet Funktionen.
goto-Anweisung:goto springt innerhalb von Funktionen vor jede beliebige Anweisung, auch Leeranweisung. goto sprungMARKE; /* ... */ sprungMARKE: anweisung; /* oder Leeranweisung ; */
switch-Beispiel:/* ... */ . |
^Der C-Präprozessorist das erste Programm eines C-Compilers, das eine C-Datei bearbeitet. #define kLAENGE 360 Zeilen, die als erstes Zeichen #
enthalten, sind für den Preprocessor bestimmt. Überall, wo im Quellkode kLAENGE steht, ersetzt
der Preprocessor dies durch 360 , cc -DkLAENGE=360 -DDOS ... Dies kann auch von der Kommandozeile aus definiert werden. # include <stdio.h> Die angegebenen Dateien(Dateiinhalte) werden an den include-Zeilen eingefügt. Nachfolgend eine Menge komplexerer Preprocessor-Zeilen, die viele verschiedene #undef kLAENGE # define D_NBLF (D_NBLD+32) CURSOR(8,12); #s
ist speziell und bewirkt, daß nach Ersetzung von s
zu 8 "8" entsteht. #define cat(a,b) a##b cat(ooo, OOO) Man beachte oben die Vorsichtsmaßnahme:
(s)>(e)
. #if defined(HPUX) && ( NDK > 4 ¦¦ SDK == 11 ) Wie man sieht, ist der Preprocessor ein enormes
Hilfsmittel! #ifdef NAME #if /* Bedingte Kompilierung, verschachtelbar: */ # Vordefinierte Namen:__LINE__ . |
^C-Präprozessor - Extrembeispiel
|
^Komplexe TypenUm Typen in komplexen Fällen korrekt zu erkennen, muß man
vom Namen ausgehen, extern void ( *signal(int, void(*)(int)) )(int); signal void (*sig)(int); sig char *cp[3]; cp ist ein Array aus 3 Elementen des Typs: Adresse auf char: char* char (*cp)[3]; cp ist eine Adresse auf ein Array aus 3 Elementen des Typs: char char *(*cpa[1])[3]; cpa ist ein Array aus 1
Element des Typs: char *(*fuu(int,int))[3][4]; fuu ist
eine Funktion fuu( )
mit zwei int-Args fuu(int,int)
, die einen Zeiger *fuu int main (void) Die drei Zeilen sind gleichbedeutend, da es innerhalb der Klammern (main)
keinen Operator gibt, |
^Sequenzpunkt-RegelnEin Sequenzpunkt schließt alle eventuell noch ausstehenden 'schwebenden' Aktionen ab.
Dieses Thema ist im Standard-Dokument enorm umfangreich, sehr verteilt
und schwer lesbar. Hinweise: Merksatz: Beispiele:arr[x][x++] a= i++ - i + b; i= ++i + 1; i++ = a; funktion(++i, i, a); funktion(a+4, a=b+1, c); y+= y * y; Man sieht, daß 'undefined behaviour' und dergleichen eigentlich
recht leicht vermeidbar ist, |
^Der NULL-PointerDer NULL-Pointer ist ein Adressenwert, der niemals irgendeinem Objekt
zugeordnet wird, Sobald im C-Quelltext der konstante Wert 0 vorkommt und
dies im Zusammenhang mit einem (addr != NULL) Die ersten drei Zeilen sind gleichbedeutend, die letzten drei ebenso. Gemäß ANSI ist NULL folgendermaßen definiert (<stddef.h>, <stdio.h>, ...): #define NULL 0 Achtung, der wirkliche Wert eines NULL-Pointers muß nicht
Null (0x00000000) sein! char *addr= NULL; Deshalb kann die zweite Bedingung FALSE sein, während die
erste garantiert TRUE ist. char *addr= 0xC0000000; Zahlendarstellungen im Quelltext befinden sich auf einer anderen Ebene
als die tatsächliche static int CatS(char *, char *, ...); /* ... */ Falls der Compiler nicht 'sieht', welchen Typ ein Argument hat, soll
ein expliziter Typ-cast Hinweis: |
^Neuer C-Standard C99An einigen anderen Stellen dieses C-Tutorials sind verschiedene C99-Neuheiten kurz genannt. Die tollsten Neuheiten sind VL-Arrays, VM-Typen und 'long long' als
64Bit-Integer-Typ. gcc 2.95.x können offensichtlich
zumindest long long, gcc-VLAs, Compound Literals Die anderen Neuerungen sind 'nur' weitere Optimierungshilfen, Vervollständigungen,
Präzisierungen // inline restrict _Bool _Complex _Imaginary Die Namen bool,complex,imaginary sind keine
Schlüsselwörter, [unsigned] long long Definitionen und Deklarationen ANSI-Header-Dateien: <assert.h> <inttypes.h> <signal.h> <stdlib.h>
Die Initialisierungsmöglichkeiten von Arrays, Strukturen und Struktur-Arrays wurden stark erweitert:Innerhalb einer Initialisierungsliste können zu initialisierende
Elemente mittels einer zusätzlichen Syntax char *A[]= { "abc", "def" }; /* bisher */ typedef struct { int quot, rem; } div_t; struct { int a[3], b; } AS[]= { [0].a={1}, [1].a[0]=2 }; int A[15]= { 1,3,5,7,9,[10]=8,6,4,2,0 }; int A[15]= { 1,3,5,7,9,[3]=8,6,4,2,0 }; union { /*...*/ } U= { .any=8 }; Aus den obenstehenden Möglichkeiten resultiert, daß auch
'normale' Initialisierungen flexibler struct { int a[3], b; } AS[]= { {1}, 2 }; struct { int a[3], b; } AS[]= { {{1,0,0}, 0}, {{2,0,0}, 0} }; AS enthält zwei Strukturen. char A[2][3][4]= { 0,1,2,3,4,5,6,7,8,9,10,11, 12,13 }; Hier wird so initialisiert als ob A[2*3*4] vorläge.
Flexibles Array als letztes Mitglied in Strukturen:struct sfa { int i; char fca[]; } *sp; sp= (struct sfa*)buf; Verhalten als hätte man definiert: sizeof(struct sfa) == sizeof(int /*i*/) Anmerkung:
Zusammengesetzte Literale:Es werden Objekte ohne Namen erzeugt, die sofort zugewiesen werden müssen: C9X-Draft: int *p= (int []){2, 4}; /* global: {konst} */ p zeigt auf ein Array: int[2] . drawline( (struct point){.x=1, .y=1}, .
VLA: Variable-Länge-Array
|
^C++ (ein wenig)C++ ist eine objektorientierte Hybrid-Sprache, die auf C
basiert.
Man kann ohne weiteres aus einem sauber programmierten C-Programm C++ ist aber dennoch eine eigenständige Sprache, mit eigenem ANSI-Standard. Das "Bessere C":
Zusätzliche Schlüsselwörter gegenüber C:new Die letzten Schlüsselworte ab 'namespace' sind später hinzugekommen. Zusätzliche Operatoren bzw. zusätzliche Verwendungen davon::: Auflösung, stellt einen Bezug her Das, was C++ im Kern ausmacht, im Vergleich zu C, sind die sehr stark
erweiterten Möglichkeiten class Kunden { ... }; Kunden K; In C könnte man das -wesentlich uneleganter- folgendermaßen lösen: static int Kunden(const char *, ...); Kunden("%s%s", "+=", "Hans Mustermann,..."); Konkrete C++-Beispiele: . |
^Die ANSI-LibraryDie Programmiersprache C ist sehr 'schlank' und enthält eingebaute
sprachliche Mittel nur dafür, Die Sprache C enthält also beispielsweise keinerlei eingebaute
Funktionen, mit zugehörigen Aber, für all diese Zwecke -und noch mehr- sind Funktionen standardisiert
worden, die von außerhalb, Dieses Konzept ist extrem flexibel. Die Standard-Bibliothek gemäß ANSI/ISO enthält zwar
nicht hunderte von Funktionen,
malloc#include <stdlib.h> void *malloc( size_t size); /* size_t meist unsigned */ Mit malloc kann man sich dynamisch statischen Speicherplatz
vom Betriebssystem besorgen. int(*Buf)[8*1024]; Buf= (int(*)[8*1024]) malloc(sizeof(int[2][8*1024])); if (!Buf) PErr("Speichermangel"), exit(2); Mit genau derjenigen Adresse, die zu einem Speicherbereich zuletzt geliefert
wurde, muß free Mit realloc kann ein bereits bestehender Speicherbereich vergrößert
oder verkleinert werden.
setjmp#include <setjmp.h> static jmp_buf jBuf1; int setjmp(jmp_buf jBuf1); void longjmp(jmp_buf jBuf1, int val); setjmp speichert alle in Frage kommenden Prozessor-Daten in
eine Struktur und liefert 0 bei Erfolg. Ein longjmp-Aufruf kommt also an der zugehörigen setjmp-Stelle
wieder heraus. Alle nichtstatischen dynamischen Objekte zwischen setjmp...longjmp
werden durch
<string.h> (<memory.h>)Die Funktionen aus diesen Header-Dateien ergänzen sehr die Möglichkeiten
und ersetzen Die Funktionen, die mit den drei Zeichen str beginnen
(z.B.: strlen() ), arbeiten mit Die Funktionen, die mit mem beginnen (memcpy(),...),
funktionieren völlig inhaltsunabhängig, Die Menge dieser Funktionen ist sehr groß. Die von früher bekannte <memory.h> ist jetzt in <string.h> enthalten.
fopen#include <stdio.h> FILE *fopen(const char *filename, const char *mode); int fclose(FILE *stream); Mit diesen Funktionen werden Dateien geöffnet und wieder geschlossen. mode: Die drei Dateizeiger stdin,stdout,stderr sind vordefiniert Die Funktions-Familie, die mit FILE* zusammenarbeitet, verwendet
interne Ein-/Ausgabe-Puffer.
fprintf#include <stdio.h> int fprintf(FILE *stream, const char *format, ...); int printf( const char *format, ...); int sprintf(char *s, const char *format, ...); Diese Ausgabe-Funktionen können binär gespeicherte Zahlen
in außerordentlich vielfältiger printf("---%d, %c, %010u\n+++\n", 111, 'A', 789); ---111, A, 0000000789 Die Funktionen arbeiten mit variabel vielen Argumenten (...),
deren jeweiliger Typ Rückgabewert: Anzahl ausgegebener Zeichen; <0 bei
Fehler. format: [[...][arg_ident][...]]... (Zeichenkette) Argument-Identifizierer: %[flags][min_breite][.präzision][längen_mod]konv_spez %% Erzeugt 1 Zeichen % Darstellung Arg-Typ hh h l ll L j z t i:[u]intmax_t s:size_t Flags präzision:
fscanf#include <stdio.h> int fscanf(FILE *stream , const char *format, ...); int scanf( const char *format, ...); int sscanf(const char *s, const char *format, ...); Diese Eingabe-Funktionen sind ein ziemlich genaues Spiegelbild der Ausgabe-Funktionen
fprintf, etc. Alle Eingaben müssen zu den Angaben in der format-Zeichenkette
passen, damit erfolgreich Sobald eine Eingabe nicht (mehr) zum aktuell geltenden Argument-Identifizierer
paßt, Rückgabewert: Anzahl Arg-Zuweisungen (>=0), oder EOF. format: [[...][arg_ident][...]]... (Zeichenkette) Argument-Identifizierer: %[*][max_breite][längen_mod]konv_spez * Keine Zuweisung, kein Arg angeben Ansonsten siehe fprintf. Konversions-Spezifizierer [zeichenklasse]: max_breite: Bei s und [klasse] muß
als zugehöriges Argument eine Pufferadresse angegeben werden, Bei jedem Argument-Identifizierer wird zunächst über eventuell
vorhandene scanf(" abc%d", &i); TAB ENTER abc 7 Die zuletzt angegebene Eingabe paßt zum Format, 7
wird erfolgreich an i zugewiesen
Kurzübersicht der ANSI-Standard-Bibliothek [aus C9X-Draft]Das meiste hiervon ist auch in C89/C90 vorhanden. Diese Liste wirkt auf den ersten Blick überraschend lang.
Diagnostics <assert.h> . |
^Die POSIX-LibraryDie ANSI-Library ist in der POSIX-Library enthalten. Man kann sagen, daß POSIX alle Funktionen zur Verwendung in C-Programmen
zur Verfügung stellt, Beispielsweise Verzeichnisinhalte lesen, Verzeichnisse anlegen und löschen, Praktisch alle Unix-Systeme sind POSIX-Systeme.
open/close#include <fcntl.h> int open(const char *path, int oflags, ...); int close(int fd); /* fd: file descriptor (handle) */ Funktionen zum Öffnen und Schließen von Dateien. Die Handles 0,1,2 sind beim Programmstart bereits geöffnet: fd:0 Standard-Eingabe (stdin) oflags: O_RDONLY O_WRONLY O_RDWR /* Hiervon genau eines */ fdo= open(ofnam, O_RDWR¦O_CREAT¦O_TRUNC¦O_APPEND, 0644); if (fdo<0) PErr("Öffnen fehlgeschlagen"), exit(2); Wenn O_CREAT angegeben ist, muß ein Dateimodus angegeben
werden, damit bei eventuellem O_CREAT: Erzeugt Datei mit Größe 0, falls
diese nicht bereits existiert.
read/write#include <unistd.h> /*<io.h>*/ ssize_t read(int fd, void *buf, size_t nbyte); int write(int fd, const void *buf, unsigned nbyte); Diese Funktionen lesen/schreiben aus/in Dateien und schreiben/lesen
die Bytes in/aus dem Puffer buf
. Unter Unix werden diese Funktionen von fscanf() und
fprintf() benutzt. Der Dateizeiger (s. lseek) wird durch Lesen/Schreiben um die
Anzahl der jeweiligen Bytes Tip:
lseekoff_t lseek(int fd, off_t offset, int whence); Diese Funktion setzt einen Dateipositionszeiger auf eine gewünschte
Position. SEEK_CUR: Bewegt von der aktuellen Position ausgehend um offset
Bytes. filesize= lseek(fd, 0L, SEEK_END); Liefert bei normalen Dateien die Dateigröße.
stat/fstat#include <sys/types.h> int stat(const char *path, struct stat *sp); int fstat(int fd, struct stat *sp); Diese Funktionen liefern einen vollständigen
Informationssatz über Dateien. Nachfolgend die Mitglieder der Struktur: dev_t st_dev; // Filesystem-ID Zeiten in Sekunden GMT. Ein Aufruf: struct stat Stat; Zur Auswertung von st_mode sind Makros in <sys/stat.h> vorhanden. Dateiparameter können geändert werden u.a. mit:
dup/dup2int dup(int fd); int dup2(int fd, int fd2); dup dupliziert einen Handle (file descriptor). dup2 gestattet die Angabe eines gewünschten
Handle-Wertes: fd2 . fd muß natürlich ein gültiger Handle
sein, beispw. von open().
Kurzübersicht über POSIX-, X/Open- und sonstige C-Funktionen (SCO OpenServer 5.0.5)Ausgewählt aus etwa 2000 Funktionen der man-S-Kategorie. Viele Funktionen dieser Liste arbeiten mit zugehörigen Strukturen,
die (jeweils) eine Menge
a64l, l64a - convert between long integer and base-64 ASCII . |
^Modul-KonzepteKleine C-Programme bestehen meist aus einer einzigen C-Datei. Größere Programme bestehen leicht aus 20 und mehr einzelnen
C-Dateien, Ich selbst bevorzuge das folgende Konzept, bei dem ganz einfach die
diversen C-Dateien /* xyz.c 2.11.95 */ # include <stdlib.h> # include "xyz.h" struct kle { /* ... */ }; int main(int, char **); static int vLEN; Damit kann der Compiler genauso einfach aufgerufen werden wie bei kleinen
C-Projekten. Obwohl bei diesem Konzept nach jeder Änderung des Quellkodes dieser
komplett neu Allerdings erreicht man bei 16Bit-DOS-Entwicklung
nicht selten die maximale Größe Bei diesem Konzept reicht es vollkommen aus, ein
Shell-Script im jeweiligen Projekt-Verzeichnis
Allgemein am häufigsten werden wohl extra Projektmanagement-Werkzeuge
verwendet, Hiermit wird jeder C-Modul einzeln kompiliert, woraus jeweils
eine Objekt-Datei *.o entsteht, Nachteile sind, daß in jedem C-Modul alle Informationen extra
bereitgestellt sein müssen. Auf diese Arbeit verzichte ich sehr gerne!
Eine weitere Möglichkeit ist, auf die Projekt-Tools zu verzichten cc -oxyz xyz.c mod/a.c mod/b.c ... Ansonsten gleicht dies dem drüberstehenden Konzept, mit make/IDEs. |
^Hinweise / Tips04.06.2002: Dieses C-Tutorial scheint das beliebteste der Welt zu sein. Jedenfalls ist es auf http://www.google.de mit den Suchworten: c tutorial const volatile -c# -xml in allen drei Suchbereichen an erster Position. http://meta.rrzn.uni-hannover.de zeigt dies Tutorial ebenfalls an erster Stelle. Mit dem Suchwort c99 kommen diese guten Resultate noch eindeutiger.
Stets den Rang von Operatoren beachten! int i[2], *ip=i; Der Inhalt von i[0] wurde inkrementiert, um 1 erhöht. Bei Betrachtung eines jeden Ausdrucks überlegen: Stets an die positiven und ggf. negativen Zahlenwertbereiche der Datentypen
denken! Vorzeichenbehaftete Werte werden vorzeichenerhaltend erweitert, char c='ü'; Stets an die automatischen, impliziten Typ-Umwandlungen des Compilers denken!
Innerhalb von berechnenden (arithmetischen) Ausdrücken werden 'kleinere'
Operanden automatisch Auf int oder unsigned wird ohnehin automatisch erweitert. Bei Zuweisungen wird automatisch erweitert/umgewandelt. Bei Zuweisungen an einen 'kleineren' Typ wird eine Warnmeldung ausgegeben, Bei 2er-Komplement-Prozessoren entspricht das einem einfachen Abschneiden auf die Ziel-Bitbreite. Gefährlich sind Ausdrücke, bei denen das Endresultat garantiert
in den größten vorkommenden i= 30000 + 20000 + 20000 - 30000 - 20000; Die tatsächliche, interne Berechnungsreihenfolge kann ein Compiler
beliebig vornehmen! i= (20000-30000) + 30000 + (20000-20000); So geht's ohne Überlauf. i= a + b + c - d - 128; Der (ältere) Compiler könnte folgendermaßen in den Überlauf addieren, abhängig von Variablenwerten: i= -128; Beispiel aus der Praxis: sec= SPT*(365UL*(j-1970)+(nsj+MA[t[2]]+t[3]-1+(cjsj&&t[2]>=3))) Das meiste wird hier im [u]int-Bereich
(effizienter) berechnet.
ÜberlaufverhaltenEin Überlauf bei vorzeichenbehafteten Werten sollte tunlichst vermieden
werden. unsigned char: 11111111 + 1 == 00000000 11111111 + 100 == 01100011 Wenn also ein Überlauf durch Addition passiert, wird 2^8==256
abgezogen, und zwar von einem Es kann also in den Überlauf addiert werden, und falls anschließend
durch Subtraktion genauso viele Mit signed-Werten geht sowas zwar auch, beispielsweise mit Intel x86, aber -- es ist nicht portabel.
Duales ZahlensystemWert = b(n-1)*2^(n-1) + ... + b2*2^2 + b1*2^1 + b0*2^0 Bei 8 Bit: b7*2^7 + b6*2^6 + b5*2^5 + b4*2^4 + b3*2^3 + b2*2^2 + b1*2^1 + b0*2^0 b7*128 + b6*64 + b5*32 + b4*16 + b3*8 + b2*4 + b1*2 + b0*1 b: BITn: Werte: [0, 1] Zahlensystem, universellWert = s(n-1)*B^(n-1) + ... + s2*B^2 + s1*B^1 + s0*B^0 B: Zahlenbasis
unsigned char Arr[256]= { 'K', 'L', /*...*/ }; Obenstehendes ist ein vollständiger C-Modul, der kompiliert werden kann: cc -c amod.c Es entsteht ein Datei amod.o, eine Objekt-Datei, deren Objekte
in beliebigen extern char Arr[256]; cc -oxyz xyz.c amod.o Falls man amod.o vergißt anzugeben, gibt der Linker eine
Fehlermeldung aus: C99: Padding-Bits und Trap-RepräsentationenLaut C99-Standard können alle Integer-Typen -nur mit Ausnahme
von unsigned char- pppppppsvvvvvvvvvvvvvvvvvvvvvvv Position, Anzahl und Bedeutung sind nicht festgelegt; Irgendwelche unerlaubten Wertekombinationen dieser Padding-Bits nennt
man Trap-Repräsentationen. Es gibt hier eine Ähnlichkeit mit Gleitkomma-Variablen, die ja
ebenfalls nicht jede beliebige Das bedeutet weiterhin, daß beispielsweise Unionen nicht mehr
so vielfältig eingesetzt werden können,
Plattformen mit Prozessoren x86 (Intel, AMD, ...)Diese Prozessoren sind für Compiler-Entwickler und C-Programmierer
Auf Plattformen mit anderen Prozessoren muß man davon ausgehen,
daß einige, viele oder alle der oben Das ist auch der Grund dafür, daß etwa ab 1979 Intel-Prozessoren
bei den Ingenieuren und Programmierern Der ANSI-Standard verfolgt konsequent das Ziel, so wenig wie nur möglich
festzulegen/vorzuschreiben! Beispielsweise sind nur die Zeichen "0123456789" zahlenwertmäßig
um je +1 auseinander, bei allen anderen
Irrtümliche, restriktive Interpretationen des ANSI/ISO-C-StandardsEs werden im Internet auch undefinierte Verhaltensweisen von C-Programmierungen
verbreitet, Nachfolgend sollen die beiden Zuweisungen an i (die rechte
Seite) undefiniert sein, weil Zeiger int A[3][4], *ip, i; i= (&A[0][0])[5]; * * Da bin ich anderer Meinung, weil im Standard (beim Operator ==) steht,
daß die Adresse hinter dem i= ip[0]; Folglich müssen die obenstehenden Zugriffe und zum Schluß der Zeigerinhalt auf [12] gültig sein. . |