Einiges zu ANSI-C

P. Baeumle, b.i.b. Bergisch Gladbach

This document was created from RTF source by rtftohtml version 2.7.5.
© 1996 Peter Baeumle, b.i.b. Bergisch Gladbach. Alle Rechte vorbehalten.



Inhaltsverzeichnis

1 Vorwort                                                                        

2 Einleitung                                                                     
2.1 Geschichte und Einordnung der Programmiersprache C                           
2.2 Ein erstes Beispielprogramm                                                  
2.3 Reservierte Worte (Schlüsselwörter)                                          
2.3.1 Befehlsschlüsselwörter                                                     
2.3.2 Schlüsselwörter für Speicherklassen                                        
2.3.3 Schlüsselwörter für Datentypen                                             
2.3.4 Weitere Schlüsselwörter                                                    
2.4 Grundlegender Programmaufbau                                                 
2.5 Kurzer Vergleich zwischen Pascal und C                                       

3 Einfache Datentypen                                                            
3.1 Zeichen (char)                                                               
3.2 Numerische Datentypen                                                        
3.3 Konstanten (Literale)                                                        
3.4 Aufzählungstypen (enum)                                                    
3.5 Selbstdefinierte Datentypen (typedef)                                        
3.6 Der Datentyp void                                                            
3.7 Typenkonversion (casting)                                                    

4 Operatoren und Funktionen                                                      
4.1 Operatoren                                                                   
4.1.1 Arithmetische Operatoren                                                   
4.1.2 Inkrementoperatoren                                                        
4.1.3 Datentyp-Operatoren                                                        
4.1.4 Logische und Vergleichsoperatoren                                          
4.1.5 Bit-Manipulationen                                                         
4.1.6 Zuweisungsoperatoren                                                       
4.1.7 Der Sequenzoperator                                                        
4.1.8 Der Bedingungsoperator                                                     
4.1.9 Übersichtstabelle: Prioritäten und Reihenfolge von Bewertungen             
4.2 Der Preprocessor                                                             
4.2.1 Include-Direktive                                                          
4.2.2 Define-Direktive und Makros                                                
4.2.3 Trigraphen                                                                 
4.2.4 Bedingte Compilation                                                       
4.3 Funktionen                                                                   
4.3.1 Formaler Aufbau einer Funktion                                             
4.3.2 Parameter und Rückgabewerte                                                
4.3.3 Bibliotheken und Headerfiles (Übersicht)                                   

5 Terminal-Ein-/Ausgabe und Zeichenketten                                        
5.1 Zeichenweise Ein- und Ausgabe                                                
5.2 Zeichenketten (Strings)                                                      
5.3 Ein-/Ausgabe-Formatierung                                                    
5.3.1 Die Funktion printf()                                                      
5.3.2 Die Funktion scanf()                                                       
                                                                                 
6 Kontrollstrukturen                                                             
6.1 Einzelanweisungen und Blöcke                                                
6.2 Logische Ausdrücke und Verzweigung (if, if-else)                             
6.3 Iterationen (while, for, do-while)                                           
6.3.1 Die while-Schleife                                                         
6.3.2 Die for-Schleife                                                           
6.3.3 Die do-while-Schleife                                                      
6.4 Mehrfachverzweigung (switch) und Marken                                      
6.5 Abbruchmöglichkeiten (continue, break, return, exit)                         
6.6 Sprünge (goto)                                                               

7 Modularität, Gültigkeitsbereiche und Speicherklassen                           
7.1 Modularität und Gültigkeitsbereiche                                          
7.2 Speicherklassen (auto, static, register, extern)                             
7.3 Attribute für Datentypen: const und volatile                                 
7.3.1 Das Schlüsselwort const                                                   
7.3.2 Das Schlüsselwort volatile                                                

8 Höhere Datentypen                                                              
8.1 Eindimensionale Arrays                                                       
8.2 Mehrdimensionale Arrays                                                      
8.3 Strukturen (struct)                                                          
8.4 Variante Strukturen (union)                                                  
8.5 Bit-Felder                                                                   
8.6 Pointer                                                                      
8.7 Pointer auf Pointer und Arrays von Pointern                                  
8.8 Kommandozeilenargumente und Umgebungsinformation                             
8.8.1 Kommandozeilenargumente: argc und argv                                     
8.8.2 Umgebungsinformation: envp                                                 
8.9 Rekursion und Rekursive Strukturen: Listen und Bäume                         
8.9.1 Rekursion                                                                  
8.9.2 Lineare Listen                                                             
8.9.3 Bäume                                                                      

9 Dateiverarbeitung                                                              
9.1 Vordefinierte Dateien stdin, stdout und stderr                               
9.2 Sequentieller Dateizugriff und I/O-Funktionen                                
9.2.1 Öffnen der Datei                                                           
9.2.2 Verarbeiten der Datensätze                                                 
9.2.3 Schließen der Datei                                                        
9.2.4 Beispiele zur sequentiellen Dateiarbeit                                    
9.3 Wahlfreier Zugriff (random access)                                           

10 Ergänzungen                                                                   
10.1 make und Makefiles                                                          
10.2 Debugger                                                                    
10.3 Profiler                                                                    
10.4 Cross-Reference-Listen mit cxref                                            

11 Literaturhinweise


Vorwort

Die nachfolgende Ausarbeitung zur Programmiersprache C ist als Begleitung des Unterrichts gedacht; daher erhebt es keinen Anspruch auf Vollständigkeit hinsichtlich des Umfangs der standardisierten Programmiersprache C. Es wird sich bereits an frühen Programmbeispielen zeigen, daß sehr schnell die verschiedensten Aspekte (dynamische Speicherverwaltung, Pointer, Preprocessor-Konzept und Bibliotheken, Ausgabeformatierung mit der printf()-Routine) auftauchen.

Wesentliche Teile dieses Scripts sind angelehnt an das im Literaturverzeichnis aufgeführte, grundlegende Standardwerk von Kernighan und Ritchie sowie an Unterrichtsmaterialien und Ausarbeitungen meiner Kolleginnen und Kollegen.

Für das Verständnis werden solide Kenntnisse der Programmierung in Pascal vorausgesetzt; an zahlreichen Stellen werden Sachverhalte auch durch einen Vergleich mit Pascal zu erläutern versucht. In einem gesondert Abschnitt werden insbesondere exemplarische Teile von Pascal und C einander gegenübergestellt.

In den Ergänzungen wird auf einige der üblichen Zusatzprogramme zur Softwareentwicklung (Makefiles, Debugger, Profiler, Cross-Reference-Lister) eingegangen; aus Platzgründen konnte auf zahlreiche andere nicht eingegangen werden: z.B. den Syntaxprüfer lint (siehe hierzu die Online-Hilfe unter UNIX), die zum Bereich des Compilerbaus gehörigen Tools lex und yacc oder auch die verschiedenen Versionskontrollsysteme (wie etwa das SCCS unter UNIX).

Noch ein Hinweis für mittellose DOS-Anwender: wer auch gerne unter DOS C erproben möchte, jedoch kein Geld für einen der kommerziellen C-Compiler hat oder ausgeben möchte, kann im PC-Bereich die älteren Versionen der Borland-C- und -C++-Compiler sehr preisgünstig erwerben.


Einleitung

Geschichte und Einordnung der Programmiersprache C

C ist eine all purpose language, eine Programmiersprache für sehr viele verschiedene Anwendungsbereiche. Bereits in einer Statistik über mehrere Tausend Stellenangebote in Deutschland im Jahre 1990 hatte C die allseits sehr beliebte Programmiersprache COBOL überrundet, der Trend geht seitdem ungebrochen stark in Richtung C sowie der von Bjarne Stroustrup hervorgebrachten objektorientierten Erweiterung, C++.

C ist sehr eng mit dem Betriebssystem UNIX verbunden, für das die Sprache sogar eigens entwickelt worden ist. Der Unterricht wird demzufolge naturgemäß auf der Grundlage des Betriebssystems UNIX stattfinden.

Wichtige Ideen für C entstammen den beiden Programmiersprachen BCPL, die Martin Richards, und B, die Ken Thompson 1970 für das erste UNIX-System auf einer DEC-Anlage entwickelt hat. C ist eine typisierte Sprache, wobei die Datentypen jedoch bei weitem nicht so streng überwacht werden wie bei Pascal. Weiterhin finden sich die klassischen Datenstrukturen wie Felder, Zeiger, Strukturen (records) und Variante Strukturen (unions); C verfügt über die aus Pascal bekannten Kontrollstrukturen: Blockung (mit geschweiften Klammern { und }), Alternativen (if, if-else, switch) und Schleifen (while, for, do-while). Daneben gibt es Möglichkeiten des vorzeitigen Verlassens einer Kontrollstruktur (z.B. mit break). Funktionen können Ergebniswerte beliebiger Datentypen besitzen und auch rekursiv aufgerufen werden. (Viel Spaß.) Im Gegensatz zu Pascal können in C Funktionen jedoch nicht verschachtelt definiert werden. Variablen können lokal in einem Block, in einer Funktion, in einer Datei oder in einem gesamten Programm, das aus mehreren Quelltextdateien besteht, verfügbar sein. Lokale Variablen sind defaultmäßig automatic, was bedeutet, daß sie bei Betreten des Gültigkeitsbereiches neu angelegt und bei Verlassen desselben wieder zerstört werden.

C verwendet einen Preprocessor, der Makros im Quelltext ersetzt, andere Quelldateien (via #include) einfügt und bedingte Compilation erlaubt. Auch durch dieses Konzept kann C sehr leicht auf sich ändernde Verhältnisse, z.B. andere Systeme, angepaßt werden.

Der eigentliche Kern der Programmiersprache C ist sehr klein. Er umfaßt zum Beispiel noch nicht einmal Routinen für Terminal-Ein- und -Ausgabe! Die Leistungsfähigkeit erhält C durch die sogenannten Bibliotheken (Libraries), die jeweils konkret eingebunden werden müssen.

C ist einerseits sehr maschinennah, beispielsweise stehen Operationen zur bitweisen Verarbeitung zur Verfügung und über Zeiger sind Speicheradressen direkt ansprechbar. Andererseits ist C sehr portabel, wenn man sich nur an die allgemeinen Richtlinien von ANSI C hält. (1989 wurde der Standard vom amerikanischen Normungsinstitut ANSI verabschiedet.)

Durch das liberale Umgehen mit z.B. Datentypen und die vielfältigen Möglichkeiten, sehr kompakten (sprich: schwer lesbaren) Code zu schreiben, wird C oft nachgesagt, daß es extrem fehleranfällig sei, die Programme schwerer als babylonische Keilschriften zu entziffern seien und Hackermentalitäten begünstigt würden. Kernighan und Ritchie betonen daher, daß C auf der grundlegenden Annahme basiert, daß Programmierer wissen, was sie tun, und daß sie ihre Absichten eben nur explizit kundtun müßten.

Ein erstes Beispielprogramm

Das minimale, leere C-Programm sieht aus wie folgt.

main()

{

}

Trotzdem erkennt man schon einiges:

Das Hauptprogramm ist (nur) eine Funktion mit dem festgelegten Namen main.

Hinter dem Funktionsnamen folgt die formale Parameterliste, hier ist sie leer, daher stehen die runden Klammern einfach so einsam umher.

Der Anweisungsteil der Funktion (des Hauptprogramms) ist ebenfalls leer; zusammengehalten werden die Anweisungen durch das in erster Näherung dem Pascal-begin-end entsprechenden geschweiften Klammerpaar { und }.

Etwas interessanter wird dann schon das zweite Beispiel: das Standardprogramm "Hello, world!".

/* hello.c */

#include <stdio.h>

void main(void)

{

printf("Hello, world!\n");

} /* end main */

Nun ist schon mehr zu erkennen:

In der ersten Zeile steht ein Kommentar. Kommentare, die über eine beliebige Strecke im Quelltext gehen können, beginnen mit den Zeichen "/*" und enden mit "*/".

Die nächste Direktive (#include) geht an den Preprocessor: er wird hier aufgefordert, die gesamten Deklarationen der sogenannten Headerdatei einzubinden. (Dateinamen von Headerfiles enden üblicherweise auf .h.)

Vor dem Funktionsnamen main ist nun das Wort void zu lesen. Dieser seltsame Datentypname steht nur dafür, daß diese Funktion nichts zurückliefern soll. Im Prinzip wurde damit einem Pascal-Programmierer gesagt, daß main hier wie eine Prozedur verwendet wird.

Innerhalb der formalen Parameterliste steht nun nicht mehr nichts, sondern das Wörtchen void um anzudeuten, daß da nichts stehen soll. So macht das Spaß.
Auch wenn das paradox anmutet: so signalisiert ein Programmierer, daß die Funktion (hier: main) tatsächlich keine Parameter erhalten soll.

Der Funktionsname printf ist das write von C. Wie oben bereits gesagt wurde, ist die Funktion printf nicht im eigentlichen C enthalten, sondern eine Bibliotheksfunktion. Über die obige Direktive #include <stdio.h> wurde gesagt, daß die Standard-Ein- und -Ausgabe-Routinen (standard input output) mit eingebunden werden sollen.

Die Funktion printf hat hier einen Parameter: eine Zeichenkette, die in doppelten Hochkommata notiert wird. Darin steht zum einen der Klartext "Hello, world!", zum anderen folgt danach jedoch noch etwas Besonderes: "\n" ist eines von mehreren zur Verfügung stehenden Ersatzsymbolen, "\n" steht speziell für einen Zeilenvorschub.

Das Semikolon hinter dem Aufruf von printf muß übrigens sein, anders als bei entsprechenden Situationen in Pascal. Das Semikolon in C schließt (u.a.) eine Anweisung ab! Nachstehend noch der Vergleich zum funktionsäquivalenten Pascal-Programm.
{ hello.p bzw. hello.pas }
program HelloWorld(output);
begin
writeln('Hello, world!')
end.

Reservierte Worte (Schlüsselwörter)

Die Programmiersprache ANSI C kennt eine Reihe reservierter Worte (Schlüsselwörter), die in den nächsten Abschnitten sortiert aufgelistet werden sollen.

Befehlsschlüsselwörter

Die folgenden Schlüsselwörter stehen für Befehle der Sprache C.

break case continue default do else (entry)

for (goto) if return sizeof switch while



Anmerkungen:

Das Schlüsselwort "entry" ist ein Überbleibsel aus den Pioniertagen von C; es ist in ANSI C nicht inhaltlich implementiert.

Das Schlüsselwort "goto" existiert leider auch noch; mit ihm werden die im Bereich der strukturierten Programmierung als unmoralisch gebrandmarkten Sprünge gekennzeichnet.

Das Schlüsselwort "sizeof" steht streng genommen nicht für einen Befehl, sondern für einen Operator. Dieser liefert die Größe (in Bytes) einer Variablen oder eines Datentyps zurück.

Schlüsselwörter für Speicherklassen

Auf Speicherklassen wird in einem späteren Kapitel eingegangen. Hier nur bereits übersichtsartig die dazugehördenen Schlüsselwörter.

auto kennzeichnet eine Variable, deren Allokation beim Eintritt in den Gültigkeitsbereich und Deallokation beim Austritt daraus erfolgt;

extern sind globale Variablen, die für die gesamte Programmlaufzeit allokiert werden;

register entspricht auto, nach Möglichkeit wird jedoch statt eines Speicherplatzes ein Register belegt; bei modernen Compilern ist dieses Schlüsselwort relativ obsolet, da bei der Codegenerierung in der Regel sowieso optimiert wird.

static kennzeichnet Variablen, deren Allokation für die gesamte Programmlaufzeit erfolgt, deren Gültigkeit jedoch block- oder modullokal festgelegt ist.

Schlüsselwörter für Datentypen

Für verschiedene vordefinierte Datentypen stellt C reservierte Worte bereit.

char: Zeichen (1 Byte)

double: Gleitkommazahl, doppelte Genauigkeit

enum: Kennzeichnung für Aufzählungstyp

float: Gleitkommazahl

int: Ganzzahl

long (oder long int): Ganzzahldatentyp

unsigned: Kennzeichnung zur Interpretation eines Datentyps ("ohne Vorzeichen"), z.B. "unsigned int" oder "unsigned char"

short: Ganzzahl

signed Kennzeichnung zur Interpretation eines Datentyps als vorzeichenbehaftet, z.B. signed int (=int) oder signed char

struct: Struktur (=Record)

typedef: Eigendefinition von Datentypen

union: Variante Struktur (=varianter Record)

void: "leerer" Datentyp

Weitere Schlüsselwörter

Daneben gibt es noch zwei weitere Schlüsselwörter:

const: kennzeichnet einen Datentyp für einen nicht veränderbaren Speicherplatz.

volatile: Typattribut für eine Variable, die durch externe Einflüsse (von außerhalb des Programmes) verändert werden kann, beispielsweise die Systemuhr.

Grundlegender Programmaufbau

Ein C-Programm besteht generell aus den folgenden Komponenten:

Den Preprocessor-#include-Anweisungen, mit denen externe Dateien (z.B. die bereits erwähnten Headerfiles) in den Quelltext einbezogen und Schnittstellen zu den Bibliotheksroutinen der C Library geschaffen werden.

Den Preprocessor-#define-Anweisungen, mit denen symbolische Konstante und Makros definiert werden können.
(Beispiel: #define MAXIMUM 100 )

Datentypen und Variablen, die vor dem Hauptprogramm deklariert werden; hier werden globale bzw. externe Variablen und globale Datentypen festgelegt, die allen Teilen des C-Programms zugänglich sein können.

Das Hauptprogramm main (genau genommen: die Funktion main()): hier findet der Programmeinstieg statt. main() darf und muß in jedem C-Programm genau einmal vorkommen; häufig steht es als erste Funktion in dem Quelltext, sofern das Programm nicht sowieso mehrere Quelltextdateien umfaßt, bei denen dann main() oft eine eigene Datei (z.B. main.c) spendiert bekommt.

Es folgen dann ggf. weitere Funktionen.

Dabei ist es im Prinzip gleichgültig, ob main() die erste oder die letzte Funktion oder irgendeine zwischen den anderen Funktionen ist; es ist lediglich guter Stil, main() entweder stets am Anfang oder stets am Ende eines Ein-Datei-Programmes zu plazieren.

Kurzer Vergleich zwischen Pascal und C

Nachfolgend sollen übersichtsartig (für einen ersten Eindruck und zum späteren Nachschlagen) einige Dinge von Pascal und C gegenüber gestellt werden. (Diese Aufstellung wurde dem Buch von Müldner und Steele entlehnt.) Auf die einzelnen Details wird später in diesem Script bzw. im Unterricht näher eingegangen.

          ANSI-C                                              Pascal             
/* ... */                    Kommentare             { ... }                      
short, int, long char        Datentypen             integer char real            
float, double                                                                    
#define PI 3.14 const        Konstanten             const PI=3.14;               
float PI=3.14;                                                                   
int i;                       Variablen              var i: integer;              
void main(void) {  }         Hauptprogramm          program                         
                                                    xy(input,output);
begin end. + - * / + - Grundrechenarten + - * / + - Vorzeichen / % Ganzzahldivision div, mod scanf("%d",&i); Eingabe Ausgabe read(i) write(i) printf("%d",i); < <= > >= == != Vergleichsoperatoren < <= > >= = <> && || ! logische Operatoren and, or, not { ... } Verbundanweisung begin ... end (Block) if (x<1) y=2; else if-Konstrukt if x<1 then y:=2 else y=3; (Alternative) y:=3; while (x<100) x += 1; kopfgesteuerte while x < 100 do x := x Schleife + 1 do { ... } while fußgesteuerte repeat ... until x>=100 (x<100); Schleife for (i=1; i<=100; i++) for-Schleife for i:=1 to 100 do ... ... (entsprechen sich nicht ganz) switch(i) { case 1: Mehrfachauswahl case i of 1: printf("eins"); break; write('eins'); 2: case 2: write('zwei'); printf("zwei"); break; otherwise { oder else default: ... } } ... end char a[100]; char b[6][11]; Felder var a: array[0..99] of char; var b: array[0..5,0..10] of char; struct { float x, y; } Strukturen var r: record x, r; y: real; end; typedef enum { ROT, Aufzählungstyp type farbe= GRUEN, BLAU } farbe; (rot,gruen,blau);

Einfache Datentypen

C kennt im wesentlichen die von Pascal gewohnten einfachen Datentypen - mit Ausnahme von boolean. Eine Variable wird deklariert in der syntaktischen Form

typname variablenname;

Der Operator sizeof dient zur Feststellung, wieviele Bytes ein Datentyp oder eine Variable dieses Datentyps benötigen. Beispiele hierzu finden Sie nachfolgend.

Zeichen (char)

Eine Variable vom Datentyp char benötigt 8 Bits (1 Byte) Speicherplatz und kann jeweils ein Zeichen aus dem der Maschine zugrundeliegenden Code (bei uns i.d.R. ASCII[2]) aufnehmen.

Beispiel

void main(void)

{ /* Die Variable zeichen wird */

char zeichen; /* vom Datentyp char */

zeichen = 'A'; /* deklariert und erhält per */

/* Zuweisung den Wert 'A'. */

}

Die Variable zeichen kann jeweils eines der 256 Zeichen des (in der Regel ASCII-)Codes aufnehmen. C unterscheidet zwei Varianten: signed bedeutet, daß der Datentyp char wie der numerische Bereich -128..127 behandelt wird, das ASCII-Zeichen 255 wird also (numerisch) als -1 interpretiert; demgegenüber bedeutet unsigned, daß char wie der Bereich 0..255 verwendet wird, das ASCII-Zeichen 255 wird also auch numerisch als 255 interpretiert. Welche Interpretation ein konkreter Compiler vornimmt, ist maschinenabhängig, aber auch vom Programmierer oder von der Programmiererin einstellbar. Auf dem HP9000-System von Hewlett-Packard ist char beispielsweise als signed voreingestellt. Aus diesem Grund wird in C häufig, z.B. bei der Bibliotheksfunktion getch(), der (stets vorzeichenbehaftete) Datentyp int (statt char) für ein Zeichen verwendet! Diese Betrachtung mag einem treuen Pascalianer seltsam vorkommen; wir werden aber sehr schnell sehen, daß C es anscheinend nicht so genau nimmt mit der Unterscheidung von char und numerischen (ganzzahligen) Datentypen: in Wahrheit ist char jedoch nichts anderes als ein numerischer Datentyp, dessen Ausprägungen lediglich bedarfsweise als Repräsentanten des dem Rechner zugrundeliegenden Codes (z.B. ASCII) interpretiert werden!

Will man von der jeweiligen Voreinstellung abweichen, kann man eine Variable explizit als signed char oder unsigned char vereinbaren.

Numerische Datentypen

Der Datentyp int (integer) vertritt den (vorzeichenbehafteten) Ganzzahlbereich, entspricht also auch der Angabe signed int. Der Speicherplatzbedarf ist von ANSI nicht vorgeschrieben, also maschinenabhängig; bei HP-UX 9.0 beträgt dieser 4 Bytes, bei PC-Compilern in der Regel 2 Bytes. In der unten auszugsweise abgedruckten Headerdatei limits.h (hier in der Version von Hewlett Packards UNIX-Derivat HP-UX) werden symbolische Konstanten bereitgestellt, z.B. INT_MAX, dem größten Wert aus dem Bereich des Datentyps int. (Dies entspricht dem MAXINT von Pascal.)

/* /usr/include/limits.h .. stark gekürzt .. */

#define CHAR_BIT 8 /* Number of bits in a char */

#define CHAR_MAX 127 /* Max integer value of a char */

#define CHAR_MIN (-128)/* Min integer value of a char */

#define INT_MAX 2147483647 /* Max decimal value of an int */

#define INT_MIN (-2147483648)/* Min decimal value of an int */

Mit unsigned int kann erwartungsgemäß angegeben werden, daß der Speicherplatz ohne Vorzeichenbit interpretiert wird, der Wertebereich also bei 0 (statt bei INT_MIN) beginnt. Statt der Deklaration unsigned int i; genügt im übrigen bereits unsigned i;.

short und long (int) sind weitere Ganzzahldatentypen, jeweils defaultmäßig als signed interpretiert. Die Größen für diese Datentypen sind wiederum maschinenabhängig, bei HP-UX 9.0 wie bei den gängigen PC-Compilern belegt ein short-Speicherplatz 2 Bytes und einer vom Typ long (int) 4 Bytes. Analog zu unsigned int kann auch hier über das vorangestellte Wort unsigned eine entsprechende andere Interpretation erzwungen werden.

Beispiel:

void main(void)

{

unsigned short us;

short ss; /* = signed short */

unsigned long ul;

long sl; /* = signed long */

}

Die ganzzahligen Datentypen und der Datentyp char (sowie die später erläuterten Aufzählungstypen) werden (entsprechend wie in Pascal) auch als ordinale oder, etwas irreführend vielleicht, als integer-Datentypen bezeichnet.

Mit float, double und long double stehen drei verschiedene Gleitkommatypen zur Verfügung. Wieder sind die Bereiche maschinenabhängig. Zu den Gleitkommatypen stehen in der Datei float.h (sh. unten) entsprechende vordefinierte Konstanten.

/* /usr/include/float.h .. stark gekürzt .. */

#define FLT_MAX 3.40282347E+38

#define FLT_MAX_10_EXP 38

#define DBL_MAX 1.7976931348623157E+308

#define DBL_MAX_10_EXP 308

Konstanten (Literale)

Eine ganzzahlige Konstante wie 123 hat den Typ int; eine long-Konstante wird (zur Betonung) mit der Endung l oder L geschrieben: 123L ist damit vom Typ long[3]. Ist eine ganzzahlige Konstante zu groß für int, so wird sie jedoch implizit als long interpretiert. Für vorzeichenlose (unsigned) Konstanten kann das Suffix U (oder u) verwendet werden: 123U ist eine Konstante vom Typ unsigned int. Entsprechend ist 123UL ein Konstante vom Typ unsigned long.

Ganzzahlkonstanten können auch oktal und hexadezimal angegeben werden. (Freude kommt auf!) Beginnt eine ganzzahlige Konstante mit einer 0, so wird sie als Oktalzahl interpretiert: 007 ist dezimal 7, 010 ist dezimal 8. Beginnt sie dagegen mit 0x oder 0X, so wird sie hexadezimal interpretiert: 0x1f oder 0x1F stehen für den dezimalen Wert 1*16+15*1=31.

Gleitpunktkonstanten enthalten einen Dezimalpunkt (123.45) und/oder einen Exponenten (12E-2 steht für 0.12, 1.234E2 steht für 123.4). Ohne ein spezielles Suffix sind diese vom Typ double, mit dem Suffix F (oder f) float, mit L vom Typ long double!

Eine Zeichenkonstante (z.B. 'A') wird stets als ganzzahlig angesehen. So ist etwa im ASCII-Code 'A' der Wert 65, '0' ist 48. Häufig werden Zeichenkonstanten zum Vergleich mit anderen Zeichen herangezogen.

Gewisse Sonderzeichen können auch in sogenannten Ersatzdarstellungen angegeben werden: der Zeilenvorschub (LineFeed) oder der Wagenrücklauf (Carriage Return) können als '\r' bzw. '\n' (auch als NewLine gelesen) geschrieben werden. Daneben kann in der Form '\0???' bzw. '\x??' ein Zeichen oktal oder hexadezimal angegeben werden[4].

Beispiel: '\007' und '\x7' stehen gleichermaßen für das ASCII-Zeichen Nr. 7 (Klingelzeichen).

Hier die Ersatzdarstellungen im Überblick:
\a Klingelzeichen (Bell)
\b Backspace
\f Seitenvorschub (FormFeed)
\n Zeilenvorschub (LineFeed)
\r Wagenrücklauf (Carriage Return)
\t Tabulatorzeichen
\v Vertikaler Tabulator
\\ steht für den Backslash \
\? Fragezeichen
\' Anführungszeichen
\" doppeltes Anführungszeichen

Warnung: Bitte unterscheiden Sie künftig zwischen 'x' und "x"! Das einfache Hochkomma steht in C für ein einzelnes Zeichen, das doppelte Hochkomma wird jedoch für Zeichenketten (Strings) stehen!

Aufzählungstypen (enum)

C kennt wie Pascal Aufzählungstypen (enumerated types), auch wenn diese in C seltener eingesetzt werden. Mit der Deklaration

enum boolean { FALSE, TRUE };

wird ein Datentyp (enum) boolean vereinbart; implizit sind damit die (letztlich numerischen) Konstanten FALSE (=0) und TRUE (=1) vereinbart worden[5]. Allerdings hat C aufgrund seiner äußerst liberalen Lebenseinstellung keine Probleme damit, einer in diesem Sinne deklarierten boolean-Variablen auch Werte wie 2, 17 oder 'A' zuzuweisen!

Zwei Beispiele solcher enum-Deklarationen:

enum wochentage { MO=1, DI, MI, DO, FR, SA, SO };

Das geht auch: MO wird damit auf 1 statt auf 0 festgelegt; alle anderen Werte folgen: DI=2 usw.

enum wochentage tag; /* Deklaration einer Variablen*/

tag=DI; /* und Wertzuweisung */

enum faecher { BIOLOGIE, CHEMIE, MATHEMATIK, PHYSIK } fach;

/* ... */

for (fach=BIOLOGIE; fach<=PHYSIK; fach++)

/* tueirgendwas */

Selbstdefinierte Datentypen (typedef)

Wiederum wie bei Pascal können auch in C eigene Typ(nam)en geschaffen werden. Die Syntax hierfür ist einfach: typedef <alter-Typ> <neuer-Typname>; Mit der Deklaration

typedef int INTEGER;

wird ein Datentyp namens INTEGER geschaffen, der synonym mit int verwendet wird. Die Verwendung von typedef wird später sinnvoller werden bei höheren und strukturierten Datentypen.

Der Datentyp void

Einen etwas eigenwilligen Datentypen kennt (ANSI) C unter dem Namen void. Hiermit kann explizit gesagt werden, daß eine Funktion keinen Rückgabewert besitzt, oder daß eine Parameterliste leer ist. Das haben wir bereits im Eingangsbeispiel hello.c gesehen.

Eine Variable kann (natürlich) nicht vom Datentyp void sein: der Compiler meldet dann "unknown size for ...", denn der Datentyp void hat keine Größe!

Typenkonversion (casting)

Hat ein mehrwertiger Operator mit Werten oder Variablen von verschiedenen Typen zu tun, so wird (häufig) eine implizite oder explizite Typkonversion (sogenanntes Casting) durchgeführt.

Hierzu ein kleines konkretes Beispielprogramm.

/* casting.c */

void main(void)

{

char c;

int i;

float x;

i=65;

c=i; /* funktioniert, c=65='A' */

c=i+32; /* geht auch, c=97='a' */

i=7/9*c; /* geht ebenfalls, ist aber 0, */

/* denn 7/9 ist (als int!) 0 !!! */

x=3.45;

i=x; /* geht, i wird auf 3 gesetzt */

x=i; /* geht natürlich auch */

}

Neben den impliziten Typumwandlungen gibt es auch explizite, sogenannte Casts oder Castings[6]: mit der Syntax ( <typname> ) expression wird der entsprechende Ausdruck auf den angegebenen Datentyp projiziert.

Ein Beispiel:

float x;

x= 7/9; /* damit wird x auf 0 gesetzt! */

x= (float)7/9; /* damit wird x auf 0,777778 gesetzt! */

Operatoren und Funktionen

In diesem Kapitel geht es um eines der Herzstücke von C: mit den Operatoren und den Funktionen wird die Umsetzung der verschiedenen Algorithmen in die Programmiersprache C erst möglich.

Operatoren

Im folgenden sollen kurz sämtliche Operatoren[7] von C vorgestellt werden. Einige davon werden naturgemäß erst zu einem späteren Zeitpunkt verständlich werden.

Arithmetische Operatoren

C besitzt für die vier Grundrechenarten die entsprechenden zweistelligen (binären) arithmetischen Operatoren + (Addition), - (Subtraktion), * (Multiplikation) und / (Division), die jeweils für alle numerischen Datentypen existieren.

Der Ergebnistyp[8] einer solchen Operation richtet sich dabei nach den Operanden; so ist 7/9 (als int) 0, 7.0/9 jedoch der erwartete Wert 0,777778!

Daneben gibt es den Modulo-Operator %, der den Rest bei der Ganzzahldivision in C darstellt (13%5=3) und dem mod aus Pascal entspricht. Der entsprechende div-Operator ist der /, denn 13/5=2.

Inkrementoperatoren

In C können die häufig gebrauchten Formulierungen, die in Pascal noch i:=i+1 lauteten, kürzer gefaßt werden mit den sogenannten Inkrementoperatoren[9]: i++ oder ++i. Anstelle der C-Anweisungen i=i-1 kann geschrieben werden i-- oder --i (Inkrementoperatoren). Die Operatoren ++ und -- (jeweils als Präfix- oder Suffixoperator) inkrementieren bzw. dekrementieren die betreffende Variable jeweils um 1.

Datentyp-Operatoren

Wie bereits erwähnt: sizeof ist ein Operator, mit dem die Speicherplatzanforderungen einer Variablen oder eines Datentyps abgefragt werden können. Daneben ist der cast-Operator noch zu erwähnen, der eine explizite Typumwandlung erzwingt.

Beispiel:

unsigned long i;

int memory;

memory=sizeof i; /* Speicherbedarf von i */

memory=sizeof(unsigned long);/* Speicherbedarf von uns.long */

i = (unsigned long) memory; /* i wird der gecastete Wert */

/* von memory zugewiesen */

Logische und Vergleichsoperatoren

In C gibt es keinen Datentyp boolean wie in Pascal. Für C ist jeder numerische Wert ungleich 0 gleichwertig mit TRUE (wahr), nur die 0 wird als FALSE (falsch) interpretiert. Dementsprechend gibt es auch logische Operatoren, die als Ergebnisse die Werte 0 oder "ungleich 0", meistens konkret den Wert 1, zurückliefern. So ist && der logische UND-Operator, || der logische ODER-Operator und ! kennzeichnet die (logische) Negation.

Achtung: Verwechseln Sie bitte die logischen nicht mit den bitweisen Operatoren, die im Abschnitt Bit-Manipulationen vorgestellt werden!

Die üblichen sechs Vergleichsoperatoren besitzt C ebenfalls: mit < wird auf "kleiner als" geprüft, mit > auf "größer als", mit <= bzw. >= auf "kleiner oder gleich" bzw. "größer oder gleich", mit == wird die Gleichheit geprüft und mit != die Ungleichheit.

Achtung: C ist immer noch sehr liberal! Wird versehentlich a=b statt a==b geschrieben, so stört das den C-Compiler nicht; statt der logischen Abfrage a==b "ist a gleich b?" wird jedoch bei "a=b" der Wert von b der Variablen a zugewiesen und dieser Wert dient als Rückgabewert und somit als Beurteilung, ob TRUE oder FALSE vorliegt; nur wenn b den Wert 0 hat, ist "a=b" FALSE, sonst stets TRUE! Dies ist eine sehr häufige Fehlerquelle!!!

Bit-Manipulationen

Speziell für die ordinalen Datentypen (char, short, int, long in beiden Varianten signed oder unsigned) existieren sechs Operatoren für sogenannte Bit-Manipulationen.

Operator   Wertigkeit                 Bezeichnung / Erläuterung                   
    &      binär                      bitweise Und-Verknüpfung                    
    |      binär                      bitweise Oder-Verknüpfung                   
    ^      binär                      exclusive Oder-Verknüpfung (XOR)            
   <<      binär                      Bit-Verschiebung nach links (shift left)    
   >>      binär                      Bit-Verschiebung nach rechts (shift right)  
    ~      unär                       bitweises Komplement                        

Ein kleines Beispiel hierzu:

unsigned char a,b,c; /* Bitnummer: 7654 3210 */

a=0x11; /* = 17 Bitmuster: 0001 0001 */

b=0x0F; /* = 15 0000 1111 */

c=a & b; /* c wird gesetzt auf: 0000 0001 */

c=a | b; /* c wird gesetzt auf: 0001 1111 */

c=a ^ b; /* c wird gesetzt auf: 0001 1110 */

c=a << 1; /* c wird gesetzt auf: 0010 0010 */

c=b >> 2; /* c wird gesetzt auf: 0000 0011 */

c=~a; /* c wird gesetzt auf: 1110 1110 */

Zuweisungsoperatoren

C kennt eine Reihe von Zuweisungsoperatoren. Während Pascal nur den Operator := für die direkte Zuweisung kennt, gibt es bei C die folgenden. Für die Beispiele seien die Deklarationen int a,b,c; zugrundegelegt.

a = b + c; /* gewöhnliche Zuweisung */

a += b; /* steht für a = a + b; */

a -= b; /* steht für a = a - b; */

a *= b; /* steht für a = a * b; */

a /= b; /* steht für a = a / b; */

a %= 5; /* a = a % 5; */

a &= b; /* a = a & b; */

a |= b; /* a = a | b; */

a ^= b; /* a = a ^ b; */

a <<= 2; /* a = a << 2; */

b >>= a; /* b = b >> a; */

Der Sequenzoperator

Mit dem Sequenzoperator , können mehrere Anweisungen zu einer einzigen zusammengefaßt werden. Dieser wird später z.B. innerhalb der for-Schleife gelegentlich verwendet.

Beispiel:

int i=0,j=1,k=2;

/* Eine "horizontale" Sequenz von drei Anweisungen */

i=1, j*=i, k+=i;

Der Bedingungsoperator

In dem Kapitel Kontrollstrukturen werden die if- und anderen Verzweigungskonstrukte von C behandelt. Im Reigen der Operatoren findet sich ein einziger dreiwertiger (ternärer) Operator, der Fragezeichenoperator oder Bedingungsoperator.

An die Stelle eines beliebigen Ausdruckes kann auch ein Ausdruck der Form <bedingung> ? <ausdruck1> : <ausdruck2> treten. Trifft die <bedingung> zu, d.h. ist <bedingung> != 0, so wird <ausdruck1> genommen, andernfalls <ausdruck2>.

Beispiel:

a = (b > c ? b : c);

Hier wird a der Wert von b zugewiesen, falls b > c ist; andernfalls wird a der Wert von c zugewiesen.

Übersichtstabelle: Prioritäten und Reihenfolge von Bewertungen

Nachstehend werden die Prioritäten und die Bewertungsreihenfolgen, die sogenannten Assoziativitäten, der Operatoren in ANSI-C aufgelistet. Die Prioritäten sind von oben nach unten abnehmend aufgeführt; die Operatoren innerhalb einer Zeile werden gemäß ihrer Assoziativität verarbeitet. Der * unter Priorität 14 ist die Pointerreferenzierung, der unter 13 ist der Rechenoperator Multiplikation, die Zeichen + und - unter 14 sind die unären Vorzeichenoperatoren, das &-Zeichen unter Priorität 14 ist der Adreßoperator, das &-Zeichen unter 8 ist das bitweise Und.

Priorität  Operator                                   Assoziativität       
   15       ( )   [ ]   ->   .                        von links nach       
                                                      rechts               
   14      ! ~ ++ -- + - (TYP) * & sizeof             von rechts nach      
                                                      links                
   13      * / %  (Rechenoperationen)                 von links nach       
                                                      rechts               
   12      + - (binär)                                von links nach       
                                                      rechts               
   11      <<  >>                                     von links nach       
                                                      rechts               
   10      < <= > >=                                  von links nach       
                                                      rechts               
    9      == !=                                      von links nach       
                                                      rechts               
    8      &                                          von links nach       
                                                      rechts               
    7      ^                                          von links nach       
                                                      rechts               
    6      |                                          von links nach       
                                                      rechts               
    5      &&                                         von links nach       
                                                      rechts               
    4      ||                                         von links nach       
                                                      rechts               
    3       ?:                                        von rechts nach      
                                                      links                
    2      =  +=  -=  /=  *=  %=  >>=  <<=  &=  |=    von rechts nach      
           ^=                                         links                
    1      , (Sequenz-Operator)                       von links nach       
                                                      rechts               



Der Preprocessor

Wie bereits erwähnt, fällt die erste Arbeit bei der C-Programmentwicklung, die der Compiler[10] zu erledigen hat, an den C-Preprocessor. Er ist im wesentlichen für Textersetzungen und die Compilersteuerung[11] zuständig (sh. Seite 25).

Include-Direktive

Zum einen werden von diesem die #include-Zeilen ausgewertet, die angesprochenen Dateien (Include-Files) zur Compilationszeit eingebunden. Hierbei handelt es sich in der Regel um Headerfiles, d.h. um Quelltextdateien, in denen (nur) Deklarationen stehen. Solche Dateien tragen die Endung .h; es können aber prinzipiell auch andere Quelltextteile ausgelagert und included werden.

Hinweis: Wird die Datei in spitzen Klammern angegeben (#include <stdio.h>), so wird im festgelegten Pfad (bei UNIX ist das in der Regel /usr/include) nach der Datei (stdio.h) gesucht; wird die Datei in doppelten Hochkommata angegeben (#include "myprog.h"), so wird nur im aktuellen Verzeichnis (bzw. in dem eventuell angegebenen relativen oder absoluten Pfad) gesucht.

Define-Direktive und Makros

Weiterhin leistet der Preprocessor Textersatzfunktionen. Eine solche Definition hat die Form

#define <name> <ersatztext>

und sorgt dafür, daß überall, wo im ursprünglichen Quelltext <name> vorkam, in der erweiterten Quelltextfassung <ersatztext> steht. Dies gilt jedoch nicht innerhalb von Zeichenketten! <name> kann dabei einer der üblichen Namen sein, per Konvention schreibt man diesen in Großbuchstaben; der <ersatztext> darf irgendeine Zeichenkette sein, die sogar nötigenfalls über mehrere Zeilen gehen kann: in diesem Fall muß auf der vorherigen Zeile mit einem Backslash \ abgeschlossen und in der ersten Spalte der Folgezeile fortgesetzt werden.

Beispiel:

#include <stdio.h>

#include "myprog.h"

#define MAXIMUM 120

#define MINIMUM 100

#define ANZAHL (MAXIMUM-MINIMUM) /* funktioniert auch! */

Darüber hinaus können aber auch über den Preprocessor Makros mit Parametern definiert werden.

Beispiel:

#define SQUARE(x) ((x)*(x))

Hiermit wird vereinbart, daß SQUARE(x) ein Makro ist, bei dem x dynamisch ersetzt wird. Eine Anweisung der Form

y = SQUARE(3);

wird vom Preprocessor somit expandiert zu

y = ((3)*(3));

Die Klammerung im Textersatz in der Definition von SQUARE ist übrigens nicht akademisch! Wird die Anweisung

y = SQUARE(x1+x2);

vom Preprocessor gelesen, so wird daraus bei obiger Definition korrekt die Zeile

y = ((x1+x2)*(x1+x2));

Betrachten wir dagegen folgende Definition:

#define SQUARE(x) x*x

Die Anweisung

y = SQUARE(x1+x2)+x3;

wird damit (nur auf den ersten Blick überraschend) ersetzt zu

y = x1+x2*x1+x2+x3;

Im Gegensatz zu Funktionen ist es den Makros übrigens egal, welche Datentypen da auf sie niederprasseln: der Preprocessor macht schließlich nur eine einfache Textersetzung und keine semantische Typüberprüfung!

Trigraphen

Beim Programmieren arbeiten wir, bewußt oder unbewußt, stets mit mehreren Zeichensätzen gleichzeitig. Zum einen ist der ganze Code (bei uns in der Regel der 8-Bit-ASCII-Zeichensatz) des Betriebssystems und mehr oder weniger der Tastatur verfügbar, zum zweiten ist da der Zeichensatz, den die jeweilige Programmiersprache versteht.

Da nicht auf allen (vor allem älteren) Tastaturen jedes benötigte Zeichen für C zu finden ist, gibt es die sogenannten Dreizeichenfolgen (Trigraphen, trigraph sequences). Hierbei handelt es sich um Ersatzzeichenfolgen für ein bestimmtes Zeichen, wie sie in der nachstehenden Tabelle aufgeführt sind. So ist a??(1??) ein gültiger, wenn auch schwer lesbarer Ersatz für a[1].

Soll etwas, z.B. eine Sequenz von mehreren Zeichen, nicht interpretiert werden, so kann stets mit dem Fluchtzeichen (Quotierungszeichen) \ gearbeitet werden: die Anweisung

printf("Was ist das?\?!");

führt nach der Phase der Textersetzung durch den Preprocessor zur Anweisung

printf("Was ist das??!");

und damit zur Ausgabe

Was ist das??!

auf dem Bildschirm.

    Dreizeichenfolge      ...ersetzt das      
       (Trigraph)         Zeichen             
          ??=             #                   
          ??(             [                   
          ??)             ]                   
          ??/             \                   
          ??'             ^                   
          ??<             {                   
          ??>             }                   
          ??!             |                   
          ??-             ~                   

Bedingte Compilation

Schließlich sei noch auf eine weitere, in der Praxis sehr wichtige Anwendung von #define hingewiesen: die bedingte Compilation. Unter Bedingter Compilation versteht man die Möglichkeit, ein Quelltextstück nur unter einer gewissen Voraussetzung überhaupt compilieren zu lassen. Diese Voraussetzung ist das Definiertsein einer symbolischen Konstanten oder die Gleichheit mit einem bestimmten Wert. Folgendes Beispiel soll dies verdeutlichen; dabei werden gleichzeitig die Preprocessor-Direktiven #if, #ifdef, #ifndef, #else, #elif und #endif vorgestellt.

Beispiel:

#define TESTPHASE 1 /* während der Programmentwicklung */

/* wird TESTPHASE definiert als 1 */

#if TESTPHASE == 1

# define PROGRAMMVERSION "Kolibri 1.0 [Testversion]"

#elif TESTPHASE == 2

# define PROGRAMMVERSION "Kolibri 1.0 [Alpha-Release]"

#else

# define PROGRAMMVERSION "Kolibri 1.0 [Final-Release]"

#endif

/* ....... */

printf("%s\n",PROGRAMMVERSION);

#ifdef TESTPHASE /* ist TESTPHASE definiert worden? */

printf("Wir befinden uns in der Testphase des Programms.\n");

#endif

/* ....... */

#ifndef TESTPHASE /* wenn nicht definiert, dann... */

printf("Wir befinden uns in der Abschlußphase...\n");

#endif

/* ....... */

Funktionen

Eine Funktion kennen Sie bereits: main(), das Hautprogramm von C. In gleicher Weise können beliebig viele Funktionen[12] für ein C-Programm deklariert und definiert werden.

Wir haben aber gelegentlich auch schon eine weitere Funktion verwendet: printf(), eine Bibliotheksfunktion (vgl. den Abschnitt zu Bibliotheken und Headerfiles, Seite 29), die in <stdio.h> deklariert wird.

Formaler Aufbau einer Funktion

Der formale Aufbau einer Funktion ist recht einfach:

<ergebnistyp> <funktionsname> ( <parameterliste> )

{

<deklarationen>

<anweisungen>

}

Hierbei ist <ergebnistyp> irgendein vordefinierter oder selbstdefinierter Datentyp, die <parameterliste> eine –  eventuell leere  – komma-getrennte Aufzählung von Übergabeparametern.

Die Aufrufbarkeit und Gültigkeit wird auf Seite 43 im Kapitel zur Modularität eingehender besprochen. An dieser Stelle sei lediglich ausgeführt, daß eine Funktion eine jede andere (und sich selbst - Rekursion!) aufrufen kann, die dem Compiler zu diesem Zeitpunkt bereits bekanntgemacht worden ist. Durch das sogenannte Prototyping, i.e. das Voranstellen der Deklarationen der Funktionen vor die eigentlichen Implementationen (Definitionen) wird in der Praxis erreicht, daß jede Funktion jede andere aufrufen kann.

Ein ganz wichtiger, für Pascal-Programmierer ernüchternder Punkt: ANSI C kennt nur Wertübergaben, call by value! Eine Referenzübergabe (call by reference) in dem Sinne gibt es nicht! Wir werden weiter unten sehen, wie das Leben in C trotzdem weitergehen kann[13].

Sehen wir uns einige einfache Beispiele an.

Beispiel:

#include <stdio.h>

/* Prototypen: Bekanntmachen aller Funktionen, damit u.a. */

main() diese aufrufen kann. */

float KreisFlaeche(float);

void main(void)

{

float radius;

printf("\nBitte einen Radius eingeben: ");

scanf("%f",&radius); /* Einlesen eines float-Wertes */

printf("Radius: %f Kreisfläche: %f",

radius,KreisFlaeche(radius));

} /* end main */

float KreisFlaeche(float r)

{

#define PI (3.1415926) /* #define kann überall im Source stehen */

return PI*r*r;

} /* end KreisFlaeche */

Bei diesem kleinen Beispielprogramm ist einiges neu.

Zunächst einmal sehen wir (entsprechend kommentiert) einen sogenannten Prototypen der Funktion KreisFlaeche(). Hier wird dem Compiler mitgeteilt, daß es eine solche Funktion geben und welchen Rückgabewert/typ sowie welche Parameterliste sie haben wird. Der Prototyp wird mit Semikolon abgeschlossen.

Im Hauptprogramm main() ist der Aufruf von scanf() neu; dies ist das Gegenstück zu read(ln) bei Pascal. Wie printf() ist auch scanf() in <stdio.h> vereinbart; auf einige der zahlreichen Möglichkeiten von scanf() soll an anderer Stelle (sh. Seite 35) eingegangen werden. Hier nur die "lokale" Erläuterung: scanf() erwartet als ersten Parameter einen sogenannten Formatstring. In unserem Beispiel wird mit "%f" nur gesagt, daß ein float-Wert eingelesen werden soll. Als zweiter Parameter wird mit "&radius" gesagt, daß scanf() die Adresse der Variablen radius verwenden soll, um dort hinein den von Tastatur eingelesenen Wert abzuspeichern. Dies ist der angekündigte "Trick", wie trotz des call by value die Variable radius doch in der Funktion verändert werden kann: es wird einfach die Speicheradresse von radius ("by value") übergeben, damit kann de facto dann doch der Speicherplatz radius des Hauptprogramms angesprochen und verändert werden.

Neu ist bei printf() eine entsprechende Erweiterung: auch hier ist nun der erste Parameter ("Radius: %f Kreisfläche: %f") ein sogenannter Formatstring, der zweite und dritte Parameter sind die float-Variable radius und der Funktionsaufruf KreisFlaeche(radius), der einen float-Wert zurückliefert. "%f" steht also auch hier wieder für die sachgemäße Interpretation der beiden Werte durch printf().

Nach main() folgt nun (erstmals) eine weitere Funktion, hier KreisFlaeche(). Der formale Aufbau ist mit dem von main() vollkommen vergleichbar. Als Parameter wird ein float namens r vereinbart, als Rückgabetyp wird ebenfalls float benannt. Im Block der Funktion, d.h. zwischen den geschweiften Klammern, wird zunächst über den Preprocessor PI auf den allseits bekannten Wert 3.1415926 gesetzt, dann wird mit dem Schlüsselwort return der Rückgabewert festgelegt. An dieser Stelle wird übrigens die Funktion auch schon wieder verlassen, selbst wenn anschließend noch weitere Anweisungen folgen sollten!

Parameter und Rückgabewerte

Die Parameter bei einer C-Funktion können von beliebigen Datentypen sein. Es gibt auch die Möglichkeit, auf die hier allerdings nicht näher eingegangen werden soll, Parameterlisten offen zu gestalten, d.h. nicht schon bei der Deklaration der Funktion festzulegen, wieviele Parameter die Funktion beim konkreten Aufruf haben soll.

Auf einen historischen Aspekt soll an dieser Stelle eingegangen werden: man unterscheidet heutzutage zwischen dem üblichen ANSI C, das auch hier in wesentlichen Teilen besprochen wird, und dem klassischen Kernighan-Ritchie-C (K&R-C), dem old fashioned style. Die unterschiedliche Definition von Funktionen ist eine der wesentlichen Änderungen von Kernighan-Ritchie-C zu ANSI C. Im K&R-C würde die obige Funktion KreisFlaeche() wie folgt deklariert und definiert werden.

float Kreisflaeche(radius)

float radius;

{

#define PI (3.1415926)

return PI*r*r;

} /* end of KreisFlaeche K&R-Style */

Bibliotheken und Headerfiles (Übersicht)

Wie erwähnt umfaßt der eigentliche Kern von C noch nicht einmal Routinen zur Terminal-Ein- und Ausgabe! (Hierzu speziell mehr im nachfolgenden Kapitel auf Seite 30.) Für fast alle weitergehenden Aufgabenstellungen muß C daher auf die Standardbibliothek (standard library) zugreifen.

Darunter versteht man eine Menge von Deklarationen und Funktionen, die als Bibliothek (bei UNIX: Dateien mit den Endungen[14] .a (archive) oder .sl (shared library)) in Form von Object code mit eingebunden wird; die zugehörigen Deklarationen und Prototypen finden sich in den bereits mehrfach erwähnten Headerfiles, die mit der #include-Direktive durch den Preprocessor eingebunden werden.

In der folgenden Übersicht sind die Standard-Headerfiles gemäß ANSI aufgeführt, die bei einem Standard-UNIX-System üblicherweise in dem Verzeichnis /usr/include zu finden sind.

assert.h Programmdiagnose

ctype.h Zeichenklassen

errno.h beinhaltet Fehlernummern

float.h enthält Fließkomma-Grenzwerte

limits.h enthält Ganzzahl-Grenzwerte

locale.h Lokalisierung (Anpassung an spezielles System)

math.h Mathematische Deklarationen und Routinen

setjmp.h Globale Sprünge

signal.h Signalverarbeitung

stdarg.h Arbeiten mit variablen Argumentlisten

stddef.h Deklaration allgemeiner Werte (z.B. von NULL)

stdio.h Standard Ein-/Ausgabe (standard i/o)

stdlib.h Hilfsfunktionen

string.h Zeichenkettenverarbeitung

time.h Datum und Uhrzeit

Terminal-Ein-/Ausgabe und Zeichenketten

In diesem Kapitel soll auszugsweise auf die Routinen der Standardbibliothek zur Ein- und Ausgabe eingegangen werden. Da ANSI diese Routinen in ihrer Wirkungs- und Anwendungsweise vorgeschrieben hat, können Programme, die sich nur dieser Funktionen bedienen, leicht von einem System auf ein anderes portiert werden.

In der Headerdatei stdio.h finden sich u.a. die folgenden Prototypen. Hier stehen auch die Deklarationen für die Standarddateien stdin, stdout und stderr (Fehlerausgabekanal).

/** ... auszugsweise ... **/

extern int printf(const char *,...);

extern int scanf(const char *,...);

extern int sprintf(char *, const char *,...);

extern int sscanf(const char *, const char *,...);

extern int getchar(void);

extern char *gets(char *);

extern int putchar(int);

extern int puts(const char *);

Zeichenweise Ein- und Ausgabe

Der einfachste Mechanismus besteht darin, ein einzelnes Zeichen ein- bzw. auszugeben. Zur Eingabe eines Zeichens von Tastatur dient die Funktion getchar(). Prototyp:

int getchar(void);

getchar() liefert bei jedem Aufruf das nächste Zeichen im Eingabestrom (in der Regel stdin) oder den (ebenfalls in stdio.h definierten) Wert EOF (end of file) zur Kennzeichnung, daß der Eingabestrom geschlossen worden ist[15].

Beispiel: Das nachfolgende Programm readchar.c liest ein Zeichen von Tastatur ein und gibt es zusammen mit seiner Nummer im ASCII-Code wieder aus. Beachten Sie jedoch: die Eingabe ist gepuffert, d.h. es muß erst [Return] gedrückt werden, bevor die Zuweisung an zeichen und die Ausgabe via printf() geschehen können[16]!

#include <stdio.h>

void main(void)

{

int zeichen; /* Beachten Sie: zeichen ist int! */

zeichen=getchar();

printf("Das Zeichen ist %c [ASCII-Nr.: %d]!\n",zeichen,zeichen);

} /* end main */

Neben dem bereits erwähnten printf() dient die Funktion putchar() zur Ausgabe eines einzelnen Zeichens auf den Ausgabestrom, in der Regel die Datei stdout. Prototyp:

int putchar(int);

Die Funktion putchar() gibt das übergebene Zeichen auf den Ausgabestrom aus; gleichzeitig liefert sie das ausgegebene Zeichen oder EOF zurück - EOF dann, wenn ein Fehler aufgetreten ist.

Hinweis: Bei den meisten Compilern sind getchar() und putchar() keine Funktionen, sondern als Makros realisiert. Auf diesen zunächst nicht so relevanten Unterschied soll an dieser Stelle nicht weiter eingegangen werden.

Beispiel: Das zugegebenermaßen nicht besondes aufregende nachstehende Programm liest ein Zeichen über getchar() von Tastatur ein und gibt es mittels putchar() wieder aus.

#include <stdio.h>

void main(void)

{

int c = getchar();

putchar(c);

} /* end main */

In der Datei ctype.h stehen u.a. die folgenden Prototypen. Die isirgendwas()-Routinen prüfen, ob ein gewisser Sachverhalt vorliegt, isalpha() prüft beispielsweise, ob das übergebene Zeichen ein (amerikanischer) Buchstabe ist, isdigit() ob es ein Ziffernzeichen ist, islower() ob es ein Kleinbuchstabe ist usw. Die Funktionen tolower() und toupper() wandeln (amerikanische) Buchstaben um in Klein- bzw. Großschreibung[17].

/** ... auszugsweise ... **/

extern int isalnum(int);

extern int isalpha(int);

extern int isdigit(int);

extern int islower(int);

extern int isprint(int);

extern int ispunct(int);

extern int isspace(int);

extern int isupper(int);

extern int tolower(int);

extern int toupper(int);

Zeichenketten (Strings)

Für weitergehende Ein- und Ausgabe (z.B. mittels scanf() und printf(), was beispielhaft bereits weiter vorne vorgestellt worden ist) sind Zeichenketten (Strings) erforderlich.

Zeichenketten können entweder statisch (wie packed array of char bei Pascal) oder dynamisch mit Pointern (wie der Datentyp String bei vielen Pascal-Dialekten) vereinbart werden. Sowohl auf die Array-, als auch die Pointer-Variante wird später noch ausführlicher eingangen werden. Im Moment sollen uns die grundlegenden Deklarationen zur Anwendung in Zusammenhang mit scanf() und printf() genügen.

#include <stdio.h>

#include <string.h>

void main(void)

{

char Statisch[20];

char *Dynamisch = "Hello, world!";

/** ... **/

strcpy(Statisch,Dynamisch);

printf("%s",Statisch);

printf("%s",Dynamisch);

putchar(Statisch[0]);

/** ... **/

} /* end main */

Im obigen Beispielprogramm(auszug) strings.c wird ein statisches Array von 20 char-Speicherplätzen angelegt, die (für Pascal-Programmierer gewöhnungsbedürftig) mit 0 bis 19 adressiert werden können.

Wichtig: In C werden Zeichenketten mit einer terminierenden '\0' (ASCII-Zeichen Nr. 0) abgespeichert, die natürlich auch 1 Byte benötigt; daher "paßt" in die oben deklarierte Variable Statisch nur eine (sichtbare) Zeichenkette von maximal 19 Zeichen, wenn man nicht blaue Wunder erleben will!

Darunter wird die Variable Dynamisch durch "char *" vereinbart als Zeiger auf char (Pointer auf char). Dadurch erhält diese Variable zunächst nur den Speicherplatz, den ein Zeiger benötigt! Hier wird jedoch sofort –  das geht in C generell  – ein konstanter Text zugewiesen, so daß Dynamisch sofort über 14 Bytes verfügen kann. Mit der Funktion strcpy(), die in dem Headerfile string.h mit dem Prototypen char *strcpy(char *, const char *) vereinbart wurde, wird der Inhalt des zweiten Parameters in die Zeichenkette, die im ersten Parameter übergeben wird, kopiert. Der konkrete Funktionsaufruf strcpy(Statisch,Dynamisch); kopiert also den Inhalt von Dynamisch in die Variable Statisch - inklusive der terminierenden '\0'.

Unabhängig von der Deklarationsform kann bei beiden Variablen in gewohnter Weise auf die einzelnen Zeichen zugegriffen werden: Dynamisch[0] ist ein char, das hier das 'H' beinhaltet, Statisch[13] hat nach der Kopieraktion das Zeichen '\0' (ASCII-0) bekommen.

+----------------------------------------+

Skizze: [[brokenbar]]H[[brokenbar]]e[[brokenbar]]l[[brokenbar]]l[[brokenbar]]o[[brokenba]],[[brokenbar]] [[brokenbar]]w[[brokenbar]]o[[brokenbar]]r[[brokenbar]]l[[brokenbar]]d[[brokenba]]![[brokenbar]]\0[[brokenbar]]?[[brokenbar]]?[[brokenbar]]?[[brokenbar]]?[[brokenbar]]?[[brokenbar]]?[[brokenbar]]

+----------------------------------------+

0 1 2 3 4 5 6 7 8 9 10 13 19

Ein-/Ausgabe-Formatierung

Die wesentlichen Standardroutinen in C zur Ein- und Ausgabe von Zeichen(ketten) und numerischen Werten sind printf() und scanf().

Die Funktion printf()

Die Ausgaberoutine printf() ist in stdio.h deklariert mit dem Prototyp

(extern) int printf(const char *,...);

die drei Punkte deuten eine variabel lange Parameterliste an. Der erste Parameter bei printf() ist der sogenannte Formatstring: das ist das Muster, wie die Ausgabe aussehen soll. printf() schreibt auf den Standardausgabestrom (stdout, in der Regel der Bildschirm).

Beispiele:

printf("Konstanter Text"); /* Formatstring=konstanter Text */

printf("Zahl: %d",i); /* %d bewirkt, daß i als int */

/* aufbereitet ausgegeben wird */

printf("%d %c %x",i,j,k); /* i wird als int, j als char, */

/* k hexadezimal aufbereitet... */

Der Formatstring enthält also zwei verschiedene Arten von Objekten: gewöhnliche Zeichen (wie das Wort "Zahl" im Beispiel b)), die direkt in die Ausgabe geschrieben werden, und Formatierungen, die jeweils die entsprechende Umwandlung und Aufbereitung des nächsten Arguments von printf() bewirken. Jede solche Umwandlungsangabe beginnt mit einem Prozentzeichen % und endet mit einem Umwandlungszeichen. Dazwischen kann, in dieser Reihenfolge, angegeben werden:

ein Minuszeichen: damit wird das Argument linksbündig ausgegeben;

eine positive, ganze Zahl, die eine minimale Feldbreite bestimmt; benötigt das Argument mehr Stellen als angegeben, so werden ihm diese auch gegeben, benötigt es weniger, so wird mit Blanks aufgefüllt;

ein Punkt, der die Feldbreite von der Genauigkeit (precision) trennt;

eine positive, ganze Zahl, die die maximale Anzahl von Zeichen festlegt, die von einer Zeichenkette ausgegeben werden sollen, oder die Anzahl Ziffern, die nach dem Dezimalpunkt bei einer Gleitkommazahl ausgegeben werden, oder die minimale Anzahl von Ziffern, die bei einem ganzzahligen Wert ausgegeben werden sollen;

der Buchstabe h oder H, wenn short ausgegeben werden soll, oder der Buchstabe l oder L, wenn das Argument long ist.

Hinweis: Wenn das Zeichen nach % keines der obigen Zeichen und kein Umwandlungszeichen ist, dann ist die Wirkung von printf() undefiniert!

Nachstehend eine kurze (nicht vollständige) Übersicht über wichtige Formatierungszeichen.

Symbol   ...steht für...                     
   d     dezimale Ganzzahl, int              
   x     hexadezimale Ganzzahl, int          
   u     vorzeichenlose Ganzzahl, unsigned   
         int                                 
  hd     kurze Ganzzahl, short int           
  ld     dezimale Ganzzahl, long int         
   f     Gleitkommazahl, float               
  lf     Gleitkommazahl, double              
   c     einzelnes Zeichen, char             

Nachstehend ein kleines Beispiel zur Illustration: Zunächst das Programm, dann das Ablauflisting.

#include <stdio.h>

void main(void)

{

char *txt="Eine kleine Textzeile";

int i=123456;

printf("\n:%s:",txt);

printf("\n:%15s:",txt);

printf("\n:%-10s:",txt);

printf("\n:%15.10s:",txt);

printf("\n:%-15.10s:",txt);

printf("\n:%-10.5s:",txt);

printf("\n:%.10s:",txt);

printf("\n");

printf("\n:%20d:",i);

printf("\n:%-10d:",i);

printf("\n:%5.3d:",i);

printf("\n");

} /* end main */

Und das zugehörige Ablauflisting:

:Eine kleine Textzeile:

:Eine kleine Textzeile:

:Eine kleine Textzeile:

: Eine klein:

:Eine klein :

:Eine :

:Eine klein:

: 123456:

:123456 :

:123456:

Die Funktion scanf()

Die Funktion scanf() ist die zu printf() analoge Einleseroutine, die ebenfalls in stdio.h deklariert ist mit dem Prototypen

int scanf(const char *,...);

scanf() liest Zeichen aus dem Standardeingabestrom (stdin), wobei die Verarbeitungsweise wieder über einen Formatstring kontrolliert wird. scanf() hört auf, wenn die Format-Zeichenkette vollständig abgearbeitet ist, oder aber wenn ein Eingabefeld nicht zur Umwandlungsangabe paßt. Als Funktionsresultat wird die Anzahl erfolgreich erkannter und zugewiesener Eingabefelder zurückgeliefert. Am Eingabeende wird EOF zurückgeliefert. Der nächste Aufruf von scanf() beginnt dann seine Arbeit unmittelbar nach dem zuletzt umgewandelten Zeichen.

Übersicht: Elementare scanf()-Formatstrings (Auszug)

Zeichen  Argument    Eingabe                                                  
 d, i    int*        dezimal, ganzzahlig, int                                 
   u     int*        dezimal ohne Vorzeichen, unsigned                        
   c     char*       ein einzelnes Zeichen                                    
   s     char*       eine Zeichenkette (ohne Hochkommata), das Zeichen '\0'   
                     wird von C automatisch hinzugefügt. Wichtig: Es wird     
                     allerdings nur bis zum ersten Whitespace (Leerzeichen,   
                     Tabulator etc.) eingelesen!                              
e, f, g  float*      Gleitkommazahlen, float                                  

Einige Beispiele für scanf()-Aufrufe:

int i;

float f;

char txt[80];

char c;

/* ... */

scanf("%d",&i); /* Wichtig: Adreßoperator & vor i !!! */

scanf("%f",&f); /* Denn C kennt nur call by value !!! */

scanf("%s",txt); /* Arrays sind intern Adressen, daher */

/* muß hier kein Adreßoperator genom- */

scanf("%c",&c); /* men werden! */

scanf("%c",&(txt[0])); /* Adresse von txt[0] */

scanf("%u",&i);

Warnung: Noch einmal ausdrücklich: einer der häufigsten Anfängerfehler ist die Formulierung

scanf("%d",i);

Hiermit wird nicht auf den Speicherplatz i eingelesen, sondern dorthin in den Hauptspeicher, wohin der momentane Wert von i (wenn überhaupt) gerade zeigt. Unter UNIX wird dieser Fehler normalerweise bemerkt, auf dem PC kann er beliebige Nebenwirkungen haben...

Aus Platzgründen soll es bei diesem kurzen Einblick bleiben; neben den hier vorgestellten beiden Grundroutinen sind in stdio.h natürlich noch eine ganze Reihe weiterer Routinen zum (formatierten) Einlesen aus Dateien oder Speicherbereichen vorhanden (fprintf() und fscanf(), sprintf() und sscanf()). Speziell fprintf() wird dann benötigt, wenn Ausgaben auf den Fehlerkanal stderr geleitet werden sollen, wie die folgende Skizze illustriert.

/* error1.c */

include <stdio.h

void main(void)

{

printf("Diese Ausgabe geht auf stdout...\n");

fprintf(stderr,"Diese Ausgabe geht auf stderr...\n");

} /* end main */

Ablauflisting:

1. Bei Aufruf "error1" (stdout und stderr gehen auf den Bildschirm)

Diese Ausgabe geht auf stdout...

Diese Ausgabe geht auf stderr...

2. Bei Aufruf "error1 > anyfile"

Diese Ausgabe geht auf stderr...

(Die erste Textzeile ist in der Datei anyfile zu finden!)

Kontrollstrukturen

Im folgenden soll kurz auf die Kontrollstrukturen in ANSI C eingegangen werden. Dabei wird auf Ihren soliden Pascal-Kenntnissen[18] aufgesetzt, die Beispiele aus Platzgründen werden also bewußt sehr kurz gehalten. Ebenso wird davon ausgegangen, daß Sie die Spielregeln der strukturierten Programmierung bereits kennengelernt haben.

Einzelanweisungen und Blöcke

Jeder Ausdruck in C wird zu einer Anweisung, wenn ihm ein Semikolon folgt! Die folgenden Zeilen beschreiben also in diesem Sinne einzelne C-Anweisungen.

x=y=z=0;

i++;

printf("\f\v\tLook here, are we \bnormal?\n");

Mit geschweiften Klammern { und } wird ein Block festgelegt. Ein solcher Block ist syntaktisch eine Anweisung und darf infolgedessen überall dort stehen, wo der Syntax gemäß eine Anweisung plaziert werden darf.

Innerhalb eines Blockes können (zu Beginn) (lokale) Variablen deklariert werden! (Vgl. hierzu das nachfolgende Kapitel zur Modularität.) Nach der schließenden geschweiften Klammer steht hierbei kein Semikolon.

Beispiel:

void main(void)

{ /* Hier beginnt der Block */*

int i; /* i ist gültig nur innerhalb dieses Blockes */

/* ... */

} /* Hier endet der Block und damit die Gültigkeit von i */

Logische Ausdrücke und Verzweigung (if, if-else)

Mit der if-Anweisung bzw. if-else-Anweisung werden ein- oder zweifache Alternativen formuliert. Die Syntax unterscheidet sich etwas von der aus Pascal bekannten:

if ( <expression> ) <statement1> else <statement2>

Natürlich ist der else-Zweig optional. Die runden Klammern bei dem logischen Ausdruck sind erforderlich.

Beispiel:

if ( x != 0 )

z /= x;

else

z = -1;

Zu beachten ist hierbei, daß "logisch" bei C stets numerisch (ganzzahlig) bedeutet! Das heißt, der im if-Konstrukt auftretende Ausdruck kann generell jeden beliebigen numerischen Wert annehmen: wie bereits erwähnt wird 0 als FALSE, jeder andere Wert als TRUE interpretiert.

Obiges Beispiel läßt sich also umschreiben zu:

if (x)

z /= x;

else

z = -1;

Warnung: Ein häufiger Fehler (vor allem bei Menschen aus der Pascal-Welt) ist die Formulierung

if (x=0) ...;

in C wird hier der Variablen x der Wert 0 zugewiesen, das Ergebnis des Ausdruckes ist damit 0 (=FALSE), und ggf. wird der else-Zweig abgearbeitet! Korrekt muß es also

if (x==0) ...;

lauten!

Iterationen (while, for, do-while)

C bietet die drei gewohnten Schleifen an: die kopfgesteuerte while-Schleife, die fußgesteuerte do-while-Schleife und eine Schleife mit dem Schlüsselwort for, die weit mehr als die aus Pascal bekannte Zählschleife beinhaltet.

Die while-Schleife

Die kopfgesteuerte while-Schleife hat die Syntax

while ( <expression> ) <statement>

und wird solange abgearbeitet, wie <expression> einen Wert ungleich 0 besitzt.

Beispiel:

while ( i < MAX )

{

sum += i++; /* i wird nach der Summation inkrementiert! */

}

/* Die geschweifte Block-Klammerung wäre hier nicht zwingend erforderlich. */

Die for-Schleife

Die for-Schleife in C ist ein sehr mächtiges Konstrukt, das durchaus nicht nur für die klassischen Zählschleifen verwendet wird.

Die Syntax hat die Form

for ( <expression1> ; <expression2> ; <expression3> ) <statement>

Mit Ausnahme der später zu besprechenden continue-Anweisung ist die for-Schleife äquivalent zu

<expression1>;

while (<expression2>)

{

<statement>

<expression3>;

}

Das bedeutet: <expression1> ist eine Anweisung, die vor dem eigentlichen Schleifenbeginn ausgeführt wird. <expression2> ist die logische Bedingung, die die weitere Schleifenverarbeitung regelt: solange <expression2> erfüllt (d.h. ungleich 0) ist, wird <statement> ausgeführt. Schließlich ist <expression3> eine Anweisung, die zum Abschluß eines jeden Schleifendurchgangs ausgeführt wird, die sogenannte Reinitialisierung.

Beispiel: Die nachstehende for-Anweisung summiert alle ganzen Zahlen von 0 bis 9 in der Variablen sum auf; dabei wird sum kompakterweise auch noch im Kopf der for-Schleife initialisiert.

for (sum=i=0; i<10; i++)

{

sum += i;

}

Beispiel: Soll bei for nichts initialisiert werden, so kann ein for-Konstrukt auch so aussehen:

for ( ; i<10; i++ )

{

sum += i;

}

Beispiel: Gelegentlich werden Endlosschleifen benötigt, z.B. wenn innerhalb einer Funktion mit return schon vorzeitig aus einer Schleife herausgesprungen wird. Das kann dann so aussehen:

for (;;)

{

/* ... */

if (x==0)

return -1;

/* ... */

} /* end for */

Die do-while-Schleife

In der Praxis etwas seltener wird die fußgesteuerte do-while-Schleife eingesetzt. Die Syntax ist

do { <statements> } while ( <expression> );

dabei werden die <statements> solange abgearbeitet, wie <expression> einen Wert ungleich 0 besitzt, also TRUE ist. Beachten Sie bitte, daß dies die negierte Formulierung zum repeat-until-Konstrukt in Pascal ist!

Beispiel:

do

{

sum += i;

i++;

} while ( i < MAX );

Dieses Beispiel würde in Pascal so aussehen:

repeat

sum := sum + i;

i := i + 1

until i> = MAX;

Mehrfachverzweigung (switch) und Marken

C kennt auch die Mehrfachverzweigung - das case von Pascal ist hier die switch-Anweisung. Die allgemeine Syntax hat die Form

switch ( <expression> )

{

case <const-expr> : <statements>

case <const-expr> : <statements>

default : <statements>

}

Hierbei wird <expression> mit den einzelnen konstanten (und paarweise verschiedenen) Ausdrücken hinter den Schlüsselwörtern case verglichen; ist irgendwo Gleichheit festzustellen, so wird dort eingesprungen, und die dahinter stehenden Anweisungen werden ausgeführt. Dabei werden dann sämtliche weiteren Anweisungen innerhalb des switch-Konstruktes abgearbeitet, also auch die, die hinter einer späteren switch-Marke stehen!

Um dies zu verhindern, kann mit break (vgl. den nächsten Abschnitt) der Ausstieg aus dem Konstrukt befohlen werden, wie im entsprechenden Beispiel dort gezeigt werden wird.

Trifft keine der case-Marken zu, so wird bei dem Schlüsselwort default eingesprungen; diese Marke darf jedoch auch fehlen, in diesem Fall findet bei switch dann keine Verarbeitung statt[19].

Abbruchmöglichkeiten
(continue, break, return, exit)

Es ist manchmal sinnvoll, einen strukturierten Ablauf vorzeitig abzubrechen. Dies kann um den Preis umfangreicheren und schwerer zu lesenden Codes stets durch die Einführung logischer Flags geschehen, die den weiteren Ablauf z.B. eines Teiles der Anweisungen einer Schleife regeln. C stellt aber einige Möglichkeiten bereit, gezielt einen solchen vorzeitigen Ausstieg (und nur diesen) vorzunehmen.

Die Anweisung

continue;

bewirkt innerhalb einer Schleife, daß der restliche Schleifenkörper übersprungen und es mit ggf. dem nächsten Schleifendurchlauf weitergeht. Bei verschachtelten Schleifen bezieht sich continue naheliegenderweise auf die innerste Ebene.

Beispiel:

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

{

/* ... */

if (func(i)<0)

continue; /* negative Werte werden übersprungen */

} /* end for */

Die Anweisung

break;

ist einfach: stößt das Programm innerhalb einer Schleife oder switch-Anweisung auf ein break, so wird die entsprechende Struktur (vorzeitig) verlassen.

Beispiel:

switch ( i )

{

case 1 : printf("\nEins\n");

break;

case 2 : printf("\nZwei\n"); /* ohne break; wird im Falle von i==2 */

/* in der nächsten Zeile weitergemacht */

case 3 : printf("\nDrei\n");

break;

default: printf("\nKeine der Zahlen...\n");

break; /* nicht erforderlich, aber guter Stil */

} /* end switch /

Die return-Anweisung wurde bereits in Zusammenhang mit Funktionen vorgestellt: stößt die Abarbeitung auf ein return (mit oder ohne einen darauffolgenden Rückgabewert), so wird die betreffende Funktion beendet. Geschieht dies innerhalb von main(), so endet das gesamte Programm.

Mit

return 5;

oder

return(5);

wird der Wert 5 dabei zurückgeliefert.

C stellt einen weiteren Mechanismus (sozusagen als Notausstieg) bereit: mit der Standardfunktion exit() wird das (gesamte) Programm beendet, gleichgültig, aus welcher Funktion heraus exit() aufgerufen wird. Der Prototyp in stdlib.h ist

void exit(int status);

Als status kann dabei ein Wert an die aufrufende Instanz (Betriebssystem) zurückgeliefert werden. Vordefiniert sind die beiden symbolischen Konstanten EXIT_SUCCESS und EXIT_FAILURE, die für einen inhaltlich erfolgreichen bzw. fehlerbehafteten Programmausstieg stehen.

In einem korrekten Zweig sollte also mit

exit(EXIT_SUCCESS);

deutlich gemacht werden, daß das Programm ohne Fehler abgearbeitet worden ist; dementsprechend ist

exit(EXIT_FAILURE);

das Signal dafür, daß im Programm ein (nicht behebbarer) Fehler aufgetreten ist.

Sprünge (goto)

Neben den bisher vorgestellten Ablaufstrukturen gibt es auch in C ein goto, das aber seit der Erfindung der strukturierten Programmierung nicht mehr verwendet wird.

Modularität, Gültigkeitsbereiche und Speicherklassen

C ist eine Programmiersprache, die modulares Arbeiten unterstützt. Als Modul soll im folgenden der Quelltext einer einzelnen Datei, eines einzelnen Sourcefiles, angesehen werden. Das Programm seinerseits kann aus 1 bis endlich vielen Modulen bestehen, jedes Modul aus 0 bis endlich vielen[20] Funktionen.

Beispiel: Ist der gesamte Quelltext eines Programms verteilt auf die Dateien main.c, sub1.c und sub2.c, so besitzt dieses Programm drei Module. Unter UNIX können diese nun mit dem Compileraufruf

cc main.c sub1.c sub2.c -o myprog

compiliert und zu einem ausführbaren Programmfile namens myprog zusammengebunden werden.

Modularität und Gültigkeitsbereiche

C kennt zwar keine Verschachtelung von Funktionen (wie Pascal); da aber in jedem Block (nicht nur in dem äußersten) Variablen deklariert werden können, ist hier zumindest in dieser Hinsicht eine entsprechende Blockstruktur wie bei Pascal zu finden. Wird in einem inneren Block eine Variable i deklariert, so überdeckt diese für die Dauer dieses Blockes eine eventuell in einem äußeren Block oder auf Grundebene (außerhalb aller Blöcke) deklarierte Variable (oder Konstante oder Funktion) i.

Ein C-Programm besteht aus einer Reihe von externen (globalen) Objekten, das können Variablen oder Funktionen sein. (main() ist auf jeden Fall ein solches externes Objekt.) Dabei wird extern als Kontrast zu intern verwendet und bezeichnet alle Objekte, die außerhalb einer Funktion vereinbart werden. Die Variablendeklarationen innerhalb einer Funktion führen dementsprechend zu internen Variablen. Auch die Variablen(namen) in den Parameterlisten sind in diesem Sinne interne Größen. Funktionen sind stets extern.

Per Default haben externe Objekte die Eigenschaft, daß alle Verweise auf sie mit gleichem Namen auch das gleiche Objekt bezeichnen, sogar aus Funktionen heraus, die separat compiliert worden sind. Dies nennt man externe Bindung[21].

Gültig ist eine interne Variable stets nur in der Einheit, in der sie deklariert wurde; eine externe Variable ist im gesamten Programm gültig – mit der Einschränkung des Überdecktwerdens durch gleichnamige lokale Variablen.

Funktionen, die in C immer auf der globalen Ebene stehen müssen, sind von sich aus extern, d.h. aus jedem Modul kann auf sie zugegriffen werden, Prototyping vorausgesetzt. Dies kann durch explizites Hinzufügen des Schlüsselwortes extern vor den Rückgabetyp betont werden.

Beispiel:

extern float power(float,int);

Dagegen kann die Gültigkeit und Aufrufbarkeit einer Funktion auf das betreffende Modul beschränkt werden, indem vor den Rückgabetyp das Schlüsselwort static gesetzt wird.

Beispiel:

static float power(float,int);

Nun ist power() nur noch von Funktionen desselben Moduls aufrufbar!

Speicherklassen (auto, static, register, extern)

Es gibt grundsätzlich zwei Speicherklassen in C: automatisch (auto) und statisch (static). Zusammen mit dem Kontext der Deklaration eines Objektes (z.B. einer Variablen) bestimmen verschiedene Schlüsselwörter die zu verwendende Speicherklasse.

Automatische Objekte existieren (nur) lokal in einem Block und werden bei Verlassen des Blockes zerstört. Deklarationen innerhalb eines Blockes kreieren automatische Objekte, wenn keine Speicherklasse explizit angegeben wird. Mit dem Schlüsselwort register deklarierte Objekte sind automatisch, werden jedoch nach Möglichkeit in Hardware-Registern verwaltet.

Statische Objekte können lokal in einem Block, in einer Funktion oder auch außerhalb von allen Blöcken deklariert werden; sie behalten ihre Speicherplätze und Werte aber in jedem Fall bei Verlassen von und beim Wiedereintritt in Blöcke und Funktionen bei! In einem Block (und in einer Funktion) werden Objekte mit dem Schlüsselwort static als statisch deklariert. Außerhalb von allen Blöcken sind Objekte stets statisch. Mit static können sie lokal für ein Modul (Quelltextfile) vereinbart werden, dadurch erhalten sie eine sogenannte interne Bindung (internal linkage); für ein gesamtes Programm werden sie global bekannt, wenn keine Speicherklasse angegeben wird oder aber durch Verwendung des Schlüsselwortes extern, dadurch erhalten sie externe Bindung (external linkage).

Übersicht: Speicherklassen, Gültigkeitsbereiche und Lebensdauer

           Klasse             Gültigkeit          Lebensdauer    Automatische   
                                                                 Initialisieru  
                                                                 ng?            
auto                          Block               Block          nein           
register                      Block               Block          nein           
extern                        Programm            Programmlauf   ja             
static (blockintern)          Block               Programmlauf   ja             
static (außerhalb aller       Quelldatei (Modul)  Programmlauf   ja             
Blöcke)                                                                         

Attribute für Datentypen: const und volatile

ANSI C kennt zwei Attribute für Datentypen: const und volatile. Diese Attribute können mit jeder Typangabe gekoppelt auftreten.

Das Schlüsselwort const

Ein Objekt mit dem Attribut const (für konstant) muß initialisiert werden und kann anschließend nicht mehr verändert werden, darf also insbesondere nicht neue Werte zugewiesen bekommen. Der Compiler hat die Möglichkeit, const-Objekte in anderen Speicherbereichen zu verwalten als normale Variablen.

Beispiel:

const double PI=3.1415926;

Das Schlüsselwort volatile

Mit dem Attribut volatile wird dem Compiler mitgeteilt, daß das entsprechende Objekt durch externe Einflüsse geändert werden kann, z.B. durch die Systemuhr. Der Compiler benötigt eine solche Angabe, damit er nicht anhand des Programmcodes davon ausgeht, daß sich ein Objekt nicht ändert und eventuell durch eine ansonsten sinnvolle Optimierung das Programm verfälscht.

Höhere Datentypen

C kennt mit Ausnahme von Mengen (sets) die strukturierten und dynamischen Datentypen wie Pascal auch: Arrays (Felder, Vektoren), Strukturen (feste und variante Records), Zeiger (Pointer) und Files (Dateien), die hier jedoch in einem eigenständigen Kapitel behandelt werden. Und wenn man Mengen doch benötigt, dann helfen vielleicht die Bit-Felder weiter, die in diesem Kapitel in einem eigenen Abschnitt eingeführt werden.

Eindimensionale Arrays

Eine über einen ganzzahligen Index ansprechbare endliche Sequenz von Speicherplätzen desselben Typs heißt auch in C Array oder Feld. Deklaration und Verwendung werden im folgenden Beispiel arrays1.c demonstriert.

Beispiel:

/* arrays1.c */

#include <stdio.h>

void main(void)

{

int i, a[10], j ;

char s[10]="Hello!";

for (i=0; i<10; i++)

a[i]=i*i;

printf("\n&i=%d",&i); /* Adresse von i ausgeben */

printf("\n&j=%d",&j); /* Adresse von j ausgeben */

printf("\na=%d",a); /* Was ist a, das Array? */

printf("\n&a=%d",&a); /* Adresse von a */

printf("\n&a[0]=%d",&(a[0])); /* Adresse von a[0] */

printf("\n&a[1]=%d",&(a[1])); /* Adresse von a[1] */

printf("\na[0]=%d",a[0]); /* Wert in a[0] */

printf("\na[1]=%d\n",a[1]); /* Wert in a[1] */

for (i=0; i<10; i++)

printf("s[%d]=%d ",i,s[i]); /* Was steht in s? */

putchar('\n');

} /* end main */

Das Ablauflisting zu arrays1.c:

&i=2063807476

&j=2063807432

a=2063807436

&a=2063807436

&a[0]=2063807436

&a[1]=2063807440

a[0]=0

a[1]=1

s[0]=72 s[1]=101 s[2]=108 s[3]=108 s[4]=111 s[5]=33 s[6]=0 s[7]=0

s[8]=0 s[9]=0

Zu beachten ist hierbei:

Der Name des Arrays (z.B. a in arrays1.c) steht bereits für die Adresse des Feldes; d.h. z.B. bei scanf() muß kein Adreßoperator & mehr angegeben werden!

Der Indexbereich eines Arrays beginnt stets bei 0. Das oben deklarierte Array int a[10]; besitzt somit zwar zehn Komponenten, aber a[10] gibt es nicht!

Bereits in Abschnitt 4.2. wurden Zeichenketten vorgestellt. Auch dies sind Arrays. So deklariert und definiert in dem obigen Beispielprogramm arrays1.c die Zeile

char s[10]="Hello!";

eine Zeichenkette s mit zehn Speicherplätzen, wovon jedoch auch das Stringendezeichen ASCII-0 einen Platz belegt! s[0] ist hier 'H' usw.

Noch einmal sei daran erinnert, daß in der Headerdatei string.h eine ganze Reihe von Funktionen für Strings deklariert sind. Sehen Sie sich unter UNIX doch einmal die Datei /usr/include/string.h bzw. bei einem PC-Compiler die Datei string.h im entsprechenden INCLUDE-Verzeichnis an!

Und noch etwas: auch wenn die Deklaration char *s2; etwas anderes als ein Array, nämlich einen Zeiger, beschreibt, so kann s2 dennoch syntaktisch genauso wie das obige Array s verwendet werden: hier kann also ebenfalls mit s2[0] auf das erste Element von s2 zugegriffen werden, sofern ein solches existiert!

Mehrdimensionale Arrays

Selbstverständlich können Arraystrukturen auch geschachtelt werden. Ein zweidimensionales Array von int-Werten kann z.B. deklariert werden durch

int Matrix[10][20];

Hiermit steht eine Datenstruktur mit 10*20 int-Speicherplätzen zur Verfügung; anschaulich kann auf die Elemente in den 10 Zeilen und 20 Spalten zugegriffen werden wie folgt.

Matrix[0][0]=1; /* das allererste Element */

Matrix[9][19]=200; /* das allerletzte Element */

Matrix[9][0]=181; /* das erste Element der letzten Zeile */

Bemerkung: Wie zuvor ist auch hier (natürlich) der Name Matrix bereits die Adresse des Feldes!

Strukturen (struct)

Was Pascal die Records sind, heißt bei C struct (Struktur oder Verbund). Diese werden z.B. für die Arbeit mit Datenbanken benötigt; nachstehend sehen wir uns ein kleines Beispiel mit einem struct an. Spätestens hier wird im übrigen die Verwendung von typedef sehr sinnvoll!

/* structs1.c */

#include <stdio.h>

#define STRLEN 128

struct Personal

{

char Nachname[STRLEN];

char Vorname[STRLEN];

int PersonalNr;

float Gehalt;

} personal1;

typedef struct Einsatz

{

char Name[STRLEN];

char Fach[STRLEN];

int Stunden;

} EINSATZ;

void main(void)

{

struct Personal personal2;

EINSATZ einsatz;

strcpy(personal1.Nachname,"Müller");

strcpy(personal1.Vorname,"Alfons");

personal1.PersonalNr=111;

personal1.Gehalt=999.99;

personal2=personal1;

} /* end main */

In diesem Beispiel wird ein Datentyp "struct Personal" vereinbart mit den Komponenten Nachname, Vorname, PersonalNr und Gehalt der entsprechend genannten Datentypen. Damit ist noch kein Speicherplatz allokiert worden. Dies geschieht erst durch das Anfügen des Bezeichners "personal1" hinter die Strukturdefinition! personal1 ist hier also eine (externe) global gültige Variable mit den genannten Komponenten. Der Zugriff, wie weiter unten in dem Beispielprogramm structs1.c zu sehen, geschieht (wie bei Pascal) durch den Punktoperator: personal1.Nachname greift auf die Komponente Nachname der Variablen personal1 zu.

Wie im Hauptprogramm zu sehen ist, muß allerdings bei jeder neuen Deklaration einer Variablen von diesem Strukturtyp auch das Wort struct mitgeschrieben werden, was zumindest unbequem ist. Die Typendefinition EINSATZ im obigen Beispiel zeigt, wie es angenehmer geht: innerhalb der typedef-Klausel wird die Struktur Einsatz deklariert und dieser dann der Synonymname EINSATZ (in Großbuchstaben) gegeben. Eine Variable einsatz kann dann wie oben im Hauptprogramm einfach durch

EINSATZ einsatz;

deklariert werden.

Natürlich können Strukturen (wie alle anderen höheren Datentypen) auch geschachtelt werden. Legen wir die Deklarationen aus dem obigen Beispiel structs1.c zugrunde, so kann mit

struct STRUKTUR

{

struct Personal p;

EINSATZ e;

} struktur;

eine Variable struktur vom Typ struct STRUKTUR vereinbart werden; korrekt sind dann die Zugriffe struktur.p.PersonalNr oder struktur.e.Stunden.

Variante Strukturen (union)

Während beim normalen struct alle Komponenten im Speicher hintereinander liegen, gestattet der Datentyp union das umzusetzen, was in Pascal variante Records sind: mehrere Komponenten liegen übereinander, so daß ein- und derselbe Platz für verschiedenartige Daten genutzt werden kann. Die Syntax ist ansonsten wie bei den (festen) structs.

Auch hier am besten ein kleines Beispiel.

/* unions1.c */

#include <stdio.h>

#include <string.h>

union U2

{

unsigned char s[10];

unsigned int i;

} u2;

void main(void)

{

printf("sizeof(struct U2): %d\n",sizeof(union U2)); /* ==10 */

printf("Sizeof(struct u2): %d\n",sizeof(u2));

u2.i=123;

strcpy(u2.s,"A");

/* Nun ist (auf dem PC!) u2.i==65 (ASCII-Code) */

} /* end main */

Verwendet werden solche varianten Strukturen (unions) z.B. dann, wenn in einem Datenbestand sich ausschließende verschiedenartige Ausprägungen auftreten können und man die jeweils auf gleichviel Speicherplatz unterbringen möchte.

Beispiel: Im PC-Bereich werden unions beispielsweise eingesetzt für die Arbeit mit den Registern, die einmal byteweise, ein anderes Mal in Worteinheiten angesprochen werden müssen[22]. Der Turbo C Compiler von Borland deklariert[23] deshalb die folgenden Strukturen und die Union REGS:

struct WORDREGS /* wortweise adressierte Register */

{

unsigned int ax, bx, cx, dx, si, di, cflag, flags;

};

struct BYTEREGS /* byteweise adressierte Halbregister */

{

unsigned char al, ah, bl, bh, cl, ch, dl, dh;

};

union REGS /* die variable Verbindung dieser beiden */

{

struct WORDREGS x;

struct BYTEREGS h;

};

Wird nun eine Variable

union REGS reg;

deklariert, so kann mit

reg.x.ax = 0xFF00;

/* höherwertiges Byte auf 0xFF setzen, niedrigerwertiges Byte auf 0x00 */

dafür gesorgt werden, daß reg.h.ah den Wert 0xFF, reg.h.al den Wert 0x00 erhält.

Bit-Felder

Ein Spezialfall einer Struktur sind die sogenannten Bit-Felder (bit fields)24. Hier wird in einer Strukturvereinbarung durch einen Doppelpunkt getrennt jeweils vorgeschrieben, wieviele Bits eine Komponente umfassen soll. Dieser Ansatz eignet sich insbesondere gut zur Verwaltung von Flags, logischen Merkern.

Wichtig: Fast alle Aspekte von Bit-Feldern sind implementierungsabhängig; es gibt keine Arrays von Bit-Feldern, und Bit-Felder haben keine Adressen, so daß der Adreßoperator auf sie nicht angewendet werden kann.

Auch hier wieder ein Beispiel. (Mit den beiden #define-Direktiven werden die symbolischen Konstanten TRUE und FALSE auf 1 und 0 gesetzt, wobei allerdings dem konkreten Rechner überlassen bleibt, in welcher internen Darstellung und Speicherbreite 1 und 0 ermittelt werden, daher die etwas eigenwillige Deklaration (1==1), die eben immer TRUE ist!)

/* bitfields.c */

#include <stdio.h>

#define TRUE (1==1)

#define FALSE (0==1)

void main(void)

{

struct Bitfields

{

unsigned int optionA : 1;

unsigned int optionB : 1;

unsigned int optionC : 1;

} bf;

printf("\nstruct Bitfields benötigt %d Bytes.\n",

sizeof(struct Bitfields));

bf.optionA=TRUE;

bf.optionB=TRUE;

bf.optionC=FALSE;

printf("Option A ist%s aktiv.\n",(bf.optionA ? "" : " nicht"));

printf("Option B ist%s aktiv.\n",(bf.optionB ? "" : " nicht"));

printf("Option C ist%s aktiv.\n",(bf.optionC ? "" : " nicht"));

bf.optionA = ~bf.optionA; /* Erinnern Sie sich noch an ~? */

printf("\nNach \"bf.optionA = ~bf.optionA;:\n");

printf("Option A ist%s aktiv.\n",(bf.optionA ? "" : " nicht"));

printf("Option B ist%s aktiv.\n",(bf.optionB ? "" : " nicht"));

printf("Option C ist%s aktiv.\n",(bf.optionC ? "" : " nicht"));

} /* end main */

Ablauflisting:

struct Bitfields benötigt 4 Bytes.

Option A ist aktiv.

Option B ist aktiv.

Option C ist nicht aktiv.

Nach "bf.optionA = ~bf.optionA;":

Option A ist nicht aktiv.

Option B ist aktiv.

Option C ist nicht aktiv.

Es wird lokal in main() ein Bitfeld bzw. eine Struktur Bitfields deklariert, die über drei 1-Bit-Komponenten (optionA, optionB, optionC) verfügt. Wie das Programm zeigt, wird rechnerabhängig für die Bitfeld-Variable bf die nächsthöhere ganzzahlige Grundeinheit genommen, hier 4 Bytes auf einer UNIX-Anlage. Auf einem PC werden in dieser Situation üblicherweise zwei Bytes verwendet.

Pointer

Ein Pointer (Zeiger) ist ein Datentyp, bei dem Speicheradressen verwaltet werden. Eine Variable von einem Pointertyp kann also jeweils eine konkrete Speicheradresse (z.B. von einer anderen statischen Variablen) beinhalten. Pointer sind insoweit dynamisch, als sie mit ihrer Deklaration zunächst keinen weiteren Speicherplatz zugewiesen bekommen als den für die eigentliche Adresse.

Die allgemeine Syntax der Pointerdeklaration hat die Grundform:

<datentyp> * <bezeichner>;

Und wieder ist es wie bei Pascal: die Pointervariable selbst gehorcht den üblichen Gültigkeitsregeln wie alle anderen Variablen auch; der Speicherplatz, auf den sie aktuell zeigt, kann sich jedoch irgendwo im Arbeitsspeicher befinden!

Beispiele:

int *pi; /* pi ist ein Zeiger auf einen int-Speicherplatz */

char *s; /* s ist ein Zeiger auf einen char-Speicherplatz, */

/* damit jedoch bereits als Zeichenkette nutzbar! */

float *pf; /* pf ist entsprechend ein Pointer auf float */

Sehen wir uns ein erstes kleines Beispielprogramm an, in dem auch von der sogenannten Pointerarithmetik Gebrauch gemacht wird: zu einem Pointer dürfen wir int-Werte addieren (und von ihm in den definierten Grenzen auch abziehen), auf eigenes Risiko, denn wir müssen dabei selbst aufpassen, daß wir in erlaubtem Speicherbereich bleiben.[25]

/* pointer1.c */

#include <stdio.h>

void main(void)

{

int dummy=99, i;

char *s, c;

int *pi, a[3]={ 100,110,120 } ; /*Initialisierung eines Arrays*/

printf("1.Teil:\n");

c='A';

s=&c; /* & ist der Adreßoperator */

printf("s=%s s=%d &c=%d\n",s,s,&c);

printf("2.Teil:\n");

pi=a;

printf("pi=%d *pi=%d *pi+1=%d *(pi+1)=%d\n",

pi,*pi,*pi+1,*(pi+1));

pi++;

printf("pi=%d *pi=%d *pi+1=%d *(pi+1)=%d\n",

pi,*pi,*pi+1,*(pi+1));

printf("3.Teil:\n");

i=100;

pi=&i;

printf("pi=%d *pi=%d *pi+1=%d *(pi+1)=%d\n",

pi,*pi,*pi+1,*(pi+1));

} /* end main */

Ablauflisting:

1.Teil:

s=A$ƃ s=3678 &c=3678

2.Teil:

pi=3670 *pi=100 *pi+1=101 *(pi+1)=110

pi=3672 *pi=110 *pi+1=111 *(pi+1)=120

3.Teil:

pi=3682 *pi=100 *pi+1=101 *(pi+1)=99

Zu dem Programm im einzelnen: Zunächst werden zwei int-Variablen deklariert (dummy und i), wobei dummy bereits auf 99 initialisiert wird. Dann werden ein Zeiger auf char (s) und ein char (c), ein Zeiger auf int (pi) und ein int-Array mit drei Komponenten (a) vereinbart. Das Array erhält bereits bei der Deklaration seine Startwerte: a[0]=100, a[1]=110 und a[2]=120.

Im 1.Teil wird dann c auf 'A' und der Zeiger s auf die Adresse von c (&c) gesetzt. Im Ablauflisting sehen wir denn auch, daß s[0] nun den Wert 'A' besitzt, die Adressen s und &c sind auch tatsächlich gleich (hier: 3678). Allerdings sehen wir bei der Textausgabe von s, daß nach dem 'A' noch aller möglicher Schrott (hier beispielhaft: $ƃ) folgt: das liegt daran, daß Zeichenketten soweit reichen, bis '\0' gefunden wird!

Im 2.Teil wird der Zeiger pi auf a, d.h. die Adresse des Arrays gesetzt. (Wir erinnern uns: Arrays sind bereits die Adressen!) Im darauffolgenden printf werden der Reihe nach ausgegeben: der Inhalt des Pointers pi (3670), der Inhalt des Speicherplatzes (3670), auf den pi zeigt (*pi, hier: 100), dann dieser Wert plus 1 (101) und zuletzt der Inhalt des nächsten Speicherplatzes, hier 110, denn pi zeigte ja auf das erste Element des Arrays a, und 110 ist der zweite Array-Eintrag; pi+1 ist die oben erwähnte Pointerarithmetik. Anschließend wird mit pi++; pi einen Speicherplatz weitergesetzt, das ist nicht 1 Byte, sondern 1*sizeof(int)! Die printf()-Ausgabe der entsprechenden Werte erklärt sich fast selbst.

Im 3.Teil schließlich erhält i den Wert 100, pi wird auf die Adresse von i (hier ist das 3682) gesetzt. Und als Überraschung und Warnung gleichzeitig: mit (pi+1) stoßen wir diesmal auf einen Speicherplatz, der mit i natürlich nichts mehr zu tun hat: dort steht der Wert *(pi+1)=99, der zuvor auf die dummy-Variable zu Kontrollzwecken zugewiesen wurde. Sie sehen: C läßt uns eine ganze Menge Freiheit!

Pointer auf Pointer und Arrays von Pointern

Mit Pointern kann man viel machen. Man kann sie zum Beispiel auch iterieren (Zeiger auf Zeiger). Ebenso können Arrays von Pointern und Pointer auf Arrays deklariert werden und vieles mehr. Im folgenden sollen beispielhaft einige Aspekte diskutiert werden, die für die weitere Praxis der C-Programmierung relevant sein können.

Beispiel: Betrachten wir die folgenden Deklarationen.

char **bildschirm;

char (*bs2)[80];

char *bs3[80];

char *(bs4[80]);

char bs5[25][80];

Hier ist bildschirm ein Zeiger auf Zeiger von char; solch eine Variable könnte z.B. dazu benötigt werden, einen Bildschirminhalt zu verwalten, wobei die Bildschirmgröße zur Compilezeit nicht zwingend feststehen muß[26].

bs2 ist ein Pointer auf ein Array von 80 Zeichen; bs3 und bs4 sind identische Arrays von jeweils 80 Zeigern auf char; bs5 schließlich ist ein harmloses zweidimensionales Array von Zeichen.

Das nachfolgende Beispiel illustriert den nicht ganz trivialen Umgang mit Pointern auf Pointer am Beispiel einer Variablen bildschirm, die in der Praxis dazu dienen kann, den jeweiligen Bildschirminhalt zu verwalten.

Mit der Deklaration char** bildschirm; gibt es indes erst einen einzigen Speicherplatz! Dieser ist so groß, wie auf dem jeweiligen System Pointer(variablen), Adressen eben, sind. Um wie hier fünfundzwanzig Zeilen verwalten zu können, muß zunächst für diese fünfundzwanzig (Zeilen-)Pointer Speicherplatz allokiert werden, was mit der Bibliotheksroutine malloc() (memory allocate, Prototyp in stdlib.h) geschehen kann. Bei malloc() ist zum einen anzugeben, wieviel Speicherplatz benötigt wird, in der Regel geschieht dies mit der Verwendung von sizeof(), und zum anderen, für was für einen Typ der Pointer dienen soll. In unserem Fall benötigt bildschirm ZEILEN-mal die Größe eines Pointers auf char (das ist sizeof(char*)), und bildschirm selbst ist ein Zeiger auf Zeiger auf char. (Das steht ja auch in der Deklaration von bildschirm.) Dies sieht dann konkret so aus:

bildschirm=(char **)malloc(sizeof(char*)*ZEILEN);

Jetzt stehen ZEILEN-viele Pointer auf char zur Verfügung. Nun kann für jede Zeile Speicherplatz für einen solchen Pointer auf char allokiert werden, der SPALTEN-viele Zeichen aufnehmen muß; für i von 0 bis ZEILEN-1 ist also folgendes notwendig:

bildschirm[i]=(char *)malloc(sizeof(char)*SPALTEN);

Die so bereitgestellten Speicherplätze stehen solange im Programmlauf zur Verfügung, bis sie (oder Teile davon) mit free() wieder freigegeben werden. free() ist ebenfalls eine in stdlib.h deklarierte Bibliotheksfunktion.

/* pointer2.c */

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#define SPALTEN 81 /* Diese Werte könnten auch dynamisch erst */

#define ZEILEN 25 /* festgelegt werden als Variablen! */

void main(void)

{

char **bildschirm;

int i,j;

/* Speicherplatz für die ZEILEN viele Zeilenpointer wird geholt*/

bildschirm=(char **)malloc(sizeof(char*)*ZEILEN);

if (bildschirm==NULL) /* malloc() scheiterte! */

{

fprintf(stderr,"\nFehler bei malloc()!\n");

exit(EXIT_FAILURE); /* einfachste Fehlerbehandlung! */

}

for (i=0; i<ZEILEN; i++)

{

/* Speicherplatz für jede Zeile wird geholt */

bildschirm[i]=(char *)malloc(sizeof(char)*SPALTEN);

if (bildschirm[i]==NULL) /* malloc() scheiterte! */

{

fprintf(stderr,"\nFehler bei malloc()!\n");

exit(EXIT_FAILURE);

}

/* Sicherheitshalber eine sehr umsichtige Initialisierung */

for (j=0;j<SPALTEN; j++)

bildschirm[i][j]='?';

/* Kopieren eines Mustertextes in die i-te Zeile */

strcpy(bildschirm[i],"Mustertext");

}

/* Kontrollausgabe der Zeilen */

for (i=0; i<ZEILEN; i++)

{

printf("\n%d->:%s:",i,bildschirm[i]);

/*Alternative dazu: printf("\n%d->:%s:",i,*(bildschirm+i));*/

}

/* Und hier wird der gesamte Bildschirm zeichenweise ausgegeben*/

printf("\n");

for (i=0; i<ZEILEN; i++)

{

for (j=0; j<SPALTEN-1; j++)

{

putchar(bildschirm[i][j]);

}

putchar('\n');

}

/* Der Speicherplatz wird wieder freigegeben: zuerst der

für alle Zeilen und danach der für die Zeilenpointer */

for (i=0; i<ZEILEN; i++)

free(bildschirm[i]);

free(bildschirm);

} /* end main */

/* end of file pointer2.c */

Kommandozeilenargumente und Umgebungsinformation

ANSI C berücksichtigt die Tatsache, daß in der Praxis zum einen sehr häufig beim Aufruf Argumente direkt an das auszuführende Programm mitgegeben werden müssen, zum zweiten, daß Informationen aus dem Umgebungsbereich (environment) verarbeitet werden müssen. Auf die Möglichkeiten, die C hier bietet, soll in diesem Abschnitt eingegangen werden.

Kommandozeilenargumente: argc und argv

Bisher war die Deklaration der (Hauptprogramm-)Funktion main() stets

void main(void

- das heißt: an main() wurde bisher nichts übergeben. Hier sieht C über die Deklaration

void main(int argc, char **argv)

oder, synonym dazu,

void main(int argc, char *argv[])

die Möglichkeit vor, eine Anzahl von Parametern (als Anzahl von Zeichenketten) beim Programmaufruf mitzugeben. Der Name argc steht dabei für den argument counter, den Zähler, argv (argument vector) ist das Array (oder der Zeiger) auf die argc vielen Zeichenketten, der sogenannte argument vector.

Wird das Programm argument beispielsweise aufgerufen in der Form

argument 1 2 3

so sind argc==4 (!), argv[0]=="argument\0", argv[1]=="1\0", argv[2]=="2\0" und – nun erraten Sie es bereits – argv[3]=="3\0".

Ein minimales Beispiel hierzu finden Sie im nächsten Unterabschnitt.

Umgebungsinformation: envp

Die Informationen über den Umgebungsbereich können gegebenenfalls über einen dritten Parameter bei main() abgerufen werden. Mit der Deklaration

void main(int argc, char **argv, char **envp)

oder, synonym dazu,

void main(int argc, char *argv[], char *envp[])

kann mit der Variablen envp (environment pointer) ein Zeiger auf den Umgebungsbereich angelegt werden. Dies funktioniert in entsprechender Weise bei den beiden Betriebssystemen UNIX und DOS.

Die Verwendung von envp (und der Parameter argc und argv) soll das nachstehende kleine Beispiel illustrieren.

/*

envp.c

Kurzdemonstration: Kommandozeilenargumente inkl. Umgebungsbereich */

#include <stdio.h>

void main(int argc, char **argv, char **envp)

{

int i, nr=1;

for (i=0; i<argc; i++)

printf("Argument[%d]=[%s]\n",i,argv[i]);

printf("\nUmgebungsbereich:\n");

while (*envp)

printf("%2d: \"%s\"\n",nr++,*(envp++));

} /* end main */

Nachstehend noch die Ablauflistings von envp bzw. ENVP.EXE – einmal unter UNIX, einmal unter DOS.

Bildschirmprotokoll von envp.c unter UNIX:

Aufruf des Programms:

envp argument1 argument2

Ausgabe des Programms:

Argument[0]=[envp]

Argument[1]=[argument1]

Argument[2]=[argument2]

Umgebungsbereich:

1: "_=./envp"

2: "LANG=n-computer"

3: "NLSPATH=/usr/lib/nls/n-computer/%N.cat:/usr/lib/nls/C/%N.cat"

4: "PATH=/bin:/usr/bin:/usr/contrib/bin:/usr/local/bin:."

5: "COLUMNS=80"

6: "SHELL_LEVEL=1"

7: "EDITOR=/usr/contrib/bin/me"

8: "CCOPTS=-Aa"

9: "HISTFILE=/users/guest/.sh_history"

10: "LOGNAME=guest"

11: "MAIL=/usr/mail/guest"

12: "NETID=pcr103.bg.bib.de"

13: "SHELL=/bin/ksh"

14: "HISTORY=100"

15: "TMOUT=1200"

16: "HOME=/users/guest"

17: "FCEDIT=vi"

18: "TERM=vt220-ncsa"

19: "PWD=/tmp"

20: "TZ=MEZ-1MESZ"

Bildschirmprotokoll von envp.c unter DOS:

Aufruf des Programms:

envp argument1 argument2

Ausgabe des Programms:

Argument[0]=[D:\C\STUFF\ENVP.EXE]

Argument[1]=[argument1]

Argument[2]=[argument2]

Umgebungsbereich:

1: "COMSPEC=c:\4dos\4dos.com"

2: "TEMP=c:\tmp"

3: "OLDPATH=d:\bat;c:\bat;c:\dos;c:\sw\bib;c:\util;c:\novell\netware4;x:\util"p> 4: "NETNAME=PCR103"

5: "TMP=c:\tmp"

6: "LOGNAME=BAEUMLE"

7: "PLANPATH=H:\VERW\INFO\PLAN"

8: "WINPMT=[Windows aktiv $t$h$h$h] $p$g$s"

9: "PATH=c:\4dos;c:\dos;c:\sw\bib;c:\sw\util;c:\novell;c:\bat"

Rekursion und Rekursive Strukturen:
Listen und Bäume

Wie in jeder anständigen Programmiersprache[27] ist Rekursion auch in C möglich, gleichermaßen direkte Rekursion wie indirekte. In den folgenden Abschnitten sollen mit zunehmendem Schwierigkeitsgrad drei Beispiele für Rekursion in C vorgestellt werden.

Rekursion

Das Grundprinzip der direkten Rekursion in C: eine Funktion f() ruft sich selbst auf, sofern nicht eine Abbruchbedingung (Rekursionsboden) erfüllt ist. Die indirekte Rekursion unterscheidet sich davon dadurch, daß zwei oder mehr Funktionen beteiligt sind, die sich wechselseitig aufrufen.

Zur einfachen, direkten Rekursion sei das beliebte Beispiel der Berechnung der Fakultät angeführt: zu einer Zahl n soll n!, das Produkt aller natürlichen Zahlen von 1 bis n, berechnet werden. – Nachfolgend ein Programmlisting hierzu.

/* fakultaet.c */

#include <stdio.h>

long fakultaet(long);

void main(void)

{

long n=5;

printf("\nDie Fakultät von %ld ist %ld.\n",n,fakultaet(n));

} /* end main */

long fakultaet(long n)

{

if (n>1)

return n*fakultaet(n-1);

return 1;

} /* end fakultaet */

Lineare Listen

Eine lineare Liste ist eine (dynamische) Datenstruktur, die, über Zeiger verknüpft, beliebig (endlich) viele Werte (eines gewissen Typs) z.B. geordnet verwalten kann.

Hier klicken für ein Bildchen.

Wird in eine solche, aufsteigend geordnete, lineare Liste (von int-Werten beispielsweise) ein neuer Wert aufgenommen, so muß dafür ein neuer Speicherplatz mit malloc() allokiert und der entsprechende Eintrag an die passende Stelle der bisherigen Liste eingefügt werden; ist der neue Wert der größte, so muß der neue Speicherplatz einfach an das Ende der Liste angehängt werden. Ist die Liste (noch) leer, so bildet dieser Wert zusammen mit dem entsprechenden Zeiger die komplette lineare Liste.

Das nachstehende Programm linliste.c zeigt beispielhaft, wie auf der Kommandozeile eine Reihe von int-Werten mitgegeben werden, die zugehörige sortierte lineare Liste aufgebaut und dann wieder ausgegeben wird.

Anmerkungen zu dem Programm:

Die Datenstruktur struct LineareListe bzw. LISTE beinhaltet der Einfachheit halber nur die zwei Komponenten element (int) und next (LISTE*), einen Zeiger auf das nächste Listenelement. Dieser hat den Wert NULL[28], wenn kein Nachfolger in der Liste existiert, dies also der letzte Eintrag in der Liste ist.

Wie für gute Programme üblich: wird linliste ohne die korrekte Anzahl Parameter, hier also ohne Parameter, aufgerufen, so wird eine kurze Erläuterung ausgegeben, wie das Programm korrekt aufgerufen werden muß.

Das schöne Konstrukt while(*(++argv)) testet, ob *argv!=NULL ist, d.h. ob noch weitere Parameter im Argumentenvektor argv vorhanden sind; ++argv bewirkt, daß dieser Zeiger bei jedem Schleifendurchlauf weitergesetzt wird. Hier muß der Präfixoperator ++ genommen werden, damit nicht argv[0] (der Programmname selbst) mit verarbeitet wird.

atoi() ist eine Konvertierungsfunktion der Standardbibliothek: atoi steht für ascii-to-int und wandelt eine Zeichenkette um in einen int-Wert. (Dazu verwandte Funktion sind atof(), atod() usw.)

/* linliste.c */

#include <stdio.h>

#include <stdlib.h>

/* Struktur und deren Typ deklarieren */

typedef struct LineareListe

{

int element;

struct LineareListe *next;

} LISTE;

/* Prototypen */

void Einfuegen(LISTE **,int);

void Ausgeben(LISTE *);

/* Hauptprogramm */

int main(int argc, char **argv)

{

LISTE *liste;

liste=NULL;

/* Falls keine Parameter angegeben: kurze Erläuterung geben */

if (argc==1)

{

fprintf(stderr,"\nAufruf: linliste intwert "

"{ intwert ... }\n");

return(EXIT_FAILURE);

} /* end argc==1, d.h. keine Parameter */

/* Die Werte werden in die lineare Liste eingefügt */

while (*(++argv))

Einfuegen(&liste,atoi(*argv));

/* Die lineare Liste wird zur Kontrolle ausgegeben */

Ausgeben(liste);

/* An das Betriebssystem zurückgeben: alles ok! */

return(EXIT_SUCCESS);

} /* end main */

/* Einfuegen von wert an passender Stelle (aufsteigend sortiert) */

void Einfuegen(LISTE **pliste,int wert)

{

if (*pliste==NULL)

{

*pliste = (LISTE*)malloc(sizeof(LISTE));

if (*pliste == NULL)

{

fprintf(stderr,"\nmalloc()-Aufruf schlug fehl!\n");

exit(EXIT_FAILURE);

} /* end if malloc() schlug fehl */

(*pliste)->element=wert;

(*pliste)->next=NULL;

} /* end if *pliste==NULL */

else if (wert < (*pliste)->element) /* hier einfügen */

{

LISTE *ptr2;

int tmp;

ptr2=(LISTE*)malloc(sizeof(LISTE));

if (ptr2 == NULL)

{

fprintf(stderr,"\nmalloc()-Aufruf schlug fehl!\n");

exit(EXIT_FAILURE);

} /* end if malloc() schlug fehl */

/* Auf dem neuen Platz wird der alte Listeneintrag */

/* gespeichert - und dort wird der neue Wert eingetragen */

tmp=(*pliste)->element;

(*pliste)->element=wert;

ptr2->element=tmp;

/* Nun wird der neue Wert an der aktuellen Position einge-*/

/* schoben, der Rest der Liste nach hinten gehängt. */

ptr2->next=(*pliste)->next;

(*pliste)->next=ptr2;

} /* Position zum Einfügen gefunden */

else

{

Einfuegen(&((*pliste)->next),wert);

} /* rekursiver Zweig - weiter hinten anhängen oder einfügen*/

} /* end Einfuegen */

/* Einfache Ausgabe der linearen Liste */

void Ausgeben(LISTE *liste)

{

if (liste==NULL)

printf(" (Ende der Liste)\n");

else

{

printf("%d ",liste->element);

Ausgeben(liste->next);

}

} /* end Ausgeben */

/* Ende der Datei linliste.c */

Aufruf des Programms:

linliste 3 7 1 6 4 2 5 8 9 10

Ausgabe des Programms:

1 2 3 4 5 6 7 8 9 10 (Ende der Liste)

Bäume

Lineare Listen sind ein Spezialfall von Bäumen. Ein Baum ist eine dynamische Datenstruktur mit einzelnen Elementen (Knoten) und einer Ordnung mit Vorgänger und Nachfolger, so daß jeder Knoten (außer dem ersten, der sogenannten Wurzel) genau einen (direkten) Vorgänger und endliche viele Nachfolger besitzt. Ist die Anzahl der Nachfolger auf 2 beschränkt, so spricht man von einem binären Baum.

Hier klicken für ein Bildchen.

Das nachstehende –  nicht mehr ganz so  – kleine Programm baum1.c illustriert exemplarisch die Deklaration von und den Umgang mit binären Bäumen:
Die Funktion InsertTree() trägt einen neuen Wert (zwischen 1 und 99) so in den Baum ein, daß dieser gemäß der Inorder-Sortierung aufgebaut wird, das bedeutet, daß links unterhalb eines Knotens nur kleinere, rechts unterhalb eines jeden Knotens nur größere Werte abgespeichert werden. Vgl. hierzu das Ablauflisting zum folgenden Beispielprogramm.
poloniex api command W poloniex api command h poloniex api command a poloniex api command t poloniex api command poloniex api command F poloniex api command e poloniex api command a poloniex api command t poloniex api command u poloniex api command r poloniex api command e poloniex api command s poloniex api command poloniex api command W poloniex api command i poloniex api command l poloniex api command l poloniex api command poloniex api command T poloniex api command r poloniex api command a poloniex api command d poloniex api command e poloniex api command r poloniex api command s poloniex api command poloniex api command a poloniex api command n poloniex api command d poloniex api command poloniex api command D poloniex api command e poloniex api command v poloniex api command e poloniex api command l poloniex api command o poloniex api command p poloniex api command e poloniex api command r poloniex api command s poloniex api command poloniex api command A poloniex api command p poloniex api command p poloniex api command r poloniex api command e poloniex api command c poloniex api command i poloniex api command a poloniex api command t poloniex api command e poloniex api command ? poloniex api command poloniex api command Die Funktion DisplayTree() gibt den jeweiligen Baum mit einfacher Liniengraphik dargestellt auf den Bildschirm aus.
Die Funktion Deallocate() schließlich gibt den für den Baum per malloc() reservierten Speicherplatz wieder frei.
Sämtliche Funktionen sind der Natur der Sache angemessen rekursiv formuliert, denn der Datentyp TREE ist ebenfalls eine rekursive Struktur!

/*

baum1.c

Demonstration: Rekursion innerhalb eines binären Baumes.

*/

#include <stdio.h>

/*** Symbolische Konstanten ***/

#define ZEILEN 6

#define SPALTEN 80

/*** Typendeklaration für TREE und AUSGABE ***/

typedef struct BinaryTree

{

int root;

struct BinaryTree *left, *right;

} TREE;

typedef char AUSGABE[ZEILEN][SPALTEN];

/*** Prototypen ***/

void Deallocate(TREE*);

void InsertTree(TREE**,int);

void DisplayTree(TREE *,AUSGABE,int,int,int);

void main(void)

{

TREE *t;

int wert, i, j;

AUSGABE buffer;

/* Initialisierungen: Bildschirm-Buffer und TREE-Pointer t */

t=NULL;

for (i=0; i<ZEILEN; i++)

{

for (j=0; j<SPALTEN-1; j++)

buffer[i][j]=' ';

buffer[i][SPALTEN-1]='\0';

} /* end for i */

/* Ein-/Ausgabeschleife */

do {

printf("\nAktueller Inhalt des Baumes: ");

if (t) {

DisplayTree(t,buffer,SPALTEN,0,SPALTEN/2);

for (i=0; i<ZEILEN; i++)

printf("\n%s",buffer[i]);

} else {

printf(" (leer) \n\n");

} /* end if (t) */

printf("\nWelche ganze Zahl im Bereich 1..99 soll "

"aufgenommen werden?\n[0=Ende] >");

scanf("%d",&wert);

if (wert<=0 || wert>=100)

break;

InsertTree(&t,wert);

} while (wert);

Deallocate(t);

} /* end main */

void Deallocate(TREE * tree)

{

if (tree==NULL) /* Ist der Baum leer? Dann Abbruch... */

return;

if (tree->left!=NULL) /* Ansonsten erst den linken, dann */

Deallocate(tree->left);

if (tree->right!=NULL)/* den rechten Teilbaum löschen, */

Deallocate(tree->right);

free(tree); /* dann den aktuellen Knoten. */

} /* end Deallocate */

void InsertTree(TREE **pt, int wert)

{

if (*pt==NULL)

{

*pt=(TREE*)malloc(sizeof(TREE));

(*pt)->left=(*pt)->right=NULL;

(*pt)->root=wert;

}

else

{ /* es werden nur neue Werte eingetragen! */

if (wert > (*pt)->root) /* rechts anhängen */

InsertTree(&((*pt)->right),wert);

else if (wert < (*pt)->root) /* links anhängen */

InsertTree(&((*pt)->left),wert);

} /* end if *pt */

} /* end InsertTree */

void DisplayTree(TREE * t,AUSGABE buffer,

int orient, int zeile, int spalte)

/* Angedeutete graphische Ausgabe des Baumes; die vorliegende

* Variante verarbeitet nur maximal zweistellige positive

* int-Einträge und geht bei einem 80-Spalten-Bildschirm

* höchstens bis zu einer Tiefe von 6 mit einer akzeptablen

* Darstellung.

*/

{

/* linken Teilbaum ausgeben */

if (t->left!=NULL && zeile<ZEILEN-1)

{

int i;

DisplayTree(t->left,buffer,spalte, zeile+1,

spalte - abs(orient-spalte)/2 );

/* Eine Prise Liniengraphik ... */

for (i=spalte-abs(orient-spalte)/2; i<spalte; i++)

buffer[zeile][i]='-';

buffer[zeile][spalte-abs(orient-spalte)/2]='+';

}

/* rechten Teilbaum ausgeben */

if (t->right!=NULL && zeile<ZEILEN-1)

{

int i;

DisplayTree(t->right,buffer,spalte, zeile+1,

spalte + abs(orient-spalte)/2 );

/* Eine Prise Liniengraphik ... */

for (i=spalte+abs(orient-spalte)/2; i>spalte; i--)

buffer[zeile][i]='-';

buffer[zeile][spalte+abs(orient-spalte)/2]='+';

}

/* diesen Knoten ausgeben: Ausgabe zuletzt, damit

* die Liniengraphik ggf. überschrieben wird!

*/

if (t->root > 9)

buffer[zeile][spalte-1] = (t->root)/10 + '0';

buffer[zeile][spalte] = (t->root)%10 + '0';

} /* end DisplayTree */

Ablauflisting:

Aktueller Inhalt des Baumes: (leer)

/** Listing hier gekürzt **/

Welche ganze Zahl im Bereich 1..99 soll aufgenommen werden?

[0=Ende] >66

Aktueller Inhalt des Baumes:

+------------------45-------------------+

+--------23---------+ +--------75---------+

12 25 66 78---+

99

Welche ganze Zahl im Bereich 1..99 soll aufgenommen werden?

[0=Ende] >77

Aktueller Inhalt des Baumes:

+------------------45-------------------+

+--------23---------+ +--------75---------+

12 25 66 +---78---+

77 99

Welche ganze Zahl im Bereich 1..99 soll aufgenommen werden?

[0=Ende] >0

Dateiverarbeitung

In ANSI C werden Dateien (files) über einen Dateizeiger (File-Pointer) vom Typ FILE verwaltet, der in der Headerdatei stdio.h deklariert ist[29].

Vordefinierte Dateien stdin, stdout und stderr

Auch ohne in C explizit auch nur eine Datei deklariert zu haben, sind drei Dateien (bzw. File-Pointer darauf) bereits geöffnet: der Standardeingabekanal stdin (Default: Tastatur), die Standardausgabe stdout (Default: Bildschirm) und der Fehlerkanal stderr (Default: Bildschirm). Selbstverständlich können beim Aufruf eines Programms diese Standardkanäle umgelenkt werden. Unter dem Betriebssystem UNIX[30] kann das Programm a.out aufgerufen werden z.B. in der Form

a.out > outfile < infile 2> errorfile

Damit ist dann stdin ein Pointer auf die Datei infile, stdout einer auf outfile und stderr ein Pointer auf die Datei errorfile.

Sequentieller Dateizugriff und I/O-Funktionen

Die sequentielle Dateiverarbeitung in C läuft nach dem Grobmuster ab: Öffnen der Datei mit fopen(), Verarbeiten der Daten(sätze) mit verschiedenen Bibliotheksroutinen, Schließen der Datei mit fclose(). Dabei kann in zwei Modi gearbeitet werden: als Textdatei (bei den meisten Compilern der Default) oder als Binärdatei. Der Unterschied ist der, daß bei Binärdateien keinerlei Konvertierungen beim Lesen aus oder Schreiben in Dateien stattfinden, während bei Textdateien betriebssystemspezifische Umwandlungen erfolgen können. So ist unter MS-DOS das Zeilenende als "\r\n" (Carriage Return + Line Feed) definiert, während es in C (und unter UNIX) nur "\n" (Line Feed) ist. Hier wird beim Arbeiten im Textmodus automatisch eine Umwandlung vorgenommen.

Die Prototypen der hier vorgestellten Funktion befinden sich im stdio.h-Headerfile.

Ablaufschema:

1.Schritt: Öffnen der Datei mit fopen()

2.Schritt: Verarbeiten der Datensätze, das heißt:

a) zeichenweise Ein-/Ausgabe (fgetc(), fputc())

b) zeilenweise Ein-/Ausgabe (fgets(), fputs()) (Textfiles)

c) formatierte Ein-/Ausgabe (fscanf(), fprintf())

d) blockweise Ein-/Ausgabe (fread(), fwrite())

3.Schritt: Schließen der Datei mit fclose() [mittelbar bei exit()]

Wir wollen dies im folgenden schrittweise ausformulieren.

Öffnen der Datei

Prototyp:

FILE * fopen(const char * filename, const char * mode);

Aktion: fopen() versucht die Datei filename zu öffnen in dem unter mode beschriebenen Modus.

Dabei kann mode folgendes sein:

"r" für lesenden Zugriff auf eine existente Datei,

"w" für schreibenden Zugriff (destruktives Schreiben),

"a" für anhängenden Schreibzugriff auf eine bel. Datei;

Dahinter kann (optional) mit einem "t" oder "b" explizit gesagt werden, ob im Text- oder Binärmodus gearbeitet werden soll. Dahinter wiederum kann mit einem "+" angegeben werden, daß lesend und schreibend zugegriffen werden soll!

"w+" bedeutet, daß lesend und schreibend auf eine eventuell schon existente Datei zugegriffen werden soll, wobei allerdings die Dateilänge zuerst auf 0 gesetzt wird, d.h. es handelt sich wiederum um ein destruktives Schreiben.

"a+" schließlich öffnet die Datei so, daß an das Ende der evtl. bereits existenten Datei geschrieben wird.

Rückgabe: fopen() liefert einen FILE-Pointer zurück oder NULL, falls der Zugriff scheitert.

Beispiel:

if ((fp=fopen("beispiel","wb"))==NULL)

{

fprintf(stderr,"Fehler beim Öffnen der Datei\n");

exit(EXIT_FAILURE);

} /* Öffnen der Datei beispiel zum binären Schreiben */

Verarbeiten der Datensätze

Zeichenweise Ein-/Ausgabe

Prototyp:

int fgetc(FILE *fp);

Aktion: fgetc() holt das nächste Zeichen aus der Datei fp.

Rückgabe: Die Funktion liefert den ASCII-Wert des aktuellen Zeichens aus der korrekt geöffneten Datei mit dem FILE-Pointer fp zurück oder EOF im Fehlerfalle oder am Ende der Datei.

Prototyp:

int fputc(int c, FILE *fp);

Aktion: Das Zeichen c wird von fputc() in die Datei mit dem FILE-Pointer fp geschrieben.

Rückgabe: Das Zeichen c wird (im Sinne des numerischen Wertes) zurückgeliefert, EOF im Fehlerfalle.

Zeilenweise Ein-/Ausgabe bei Textdateien

Prototyp:

char *fgets(char *s, int length, FILE *fp);

Aktion: Es wird bis EOF, bis '\n' oder zum Erreichen der angegebenen Länge length (bzw. length-1) aus der Datei fp gelesen und in den Buffer s geschrieben, der genügend Platz bereitgestellt haben muß.

Rückgabe: Im Erfolgsfall wird ein Pointer auf s zurückgeliefert, im Fehlerfalle ein NULL-Pointer.

Beispiel:

fgets(buffer,128,fp);

Prototyp:

int fputs(char *s, FILE *fp);

Aktion: Die Zeichenkette s wird in die Datei mit dem FILE-Pointer fp geschrieben. Das Stringendezeichen '\0' wird dabei nicht in die Datei übernommen.

Rückgabe: fputs() liefert die Anzahl der übertragenen Zeichen zurück, im Fehlerfalle EOF.

Beispiel:

fputs(buffer,fp);

Formatierte Ein-/Ausgabe

Prototyp:

int fscanf(FILE *fp, char *format, ...);

Aktion: Vgl. scanf(); die Daten werden lediglich statt von stdin der übergebenen Datei fp entnommen.

Rückgabe: Im Erfolgsfalle liefert fscanf() die Anzahl der ausgelesenen und abgespeicherten Parameter zurück, ansonsten EOF.

Prototyp:

int fprintf(FILE *fp, char *format, ...);

Aktion: Analog zu printf(); fprintf() schreibt jedoch in die über den FILE-Pointer fp geöffnete Datei statt nach stdout.

Rückgabe: Die Anzahl der geschriebenen Bytes oder einen negativen Wert im Fehlerfall.

Blockweise Ein-Ausgabe

Prototyp:

size_t fread(void *buf, size_t size, size_t n, FILE *fp);

Aktion: fread() liest aus der Datei mit dem FILE-Pointer fp n*size Bytes in den übergebenen Buffer buf ein.

Rückgabe: Die Funktion liefert die Anzahl der erfolgreich gelesenen Einheiten (nicht Bytes) zurück oder 0 im Fehlerfalle.

Beispiel:

fread(buf,sizeof(struct Kundendaten),100,fp);

Prototyp:

size_t fwrite(void *buf, size_t size, size_t n, FILE *fp);

Aktion: Die Funktion fwrite() schreibt in die Datei mit dem FILE-Pointer fp n*size Bytes aus dem Buffer buf.

Rückgabe: Die Funktion liefert die Anzahl der erfolgreich geschriebenen Einheiten (nicht Bytes) zurück oder 0 im Falle eines Fehlers.

Beispiel:

fwrite(buf,sizeof(struct Kundendaten),2000,fp);

Schließen der Datei

Prototyp:

int fclose(fp);

Aktion: fclose() schließt die Datei, auf die fp zeigt.

Rückgabe: 0 im Erfolgsfalle, EOF im Falle eines Fehlers.

Beispiele zur sequentiellen Dateiarbeit

Die nachfolgenden Beispielprogramme files1.c und files2.c sollen die in den vorigen Abschnitten vorgestellten Funktionen im konkreten Programm zeigen.

/* files1.c */

#include <stdio.h>

#include <stdlib.h> /* für EXIT_SUCCESS, EXIT_FAILURE */

#define STRLEN 128

int main(void)

{

FILE *fp;

char line[STRLEN], filename[STRLEN]="/tmp/probedatei";

if (fp=fopen(filename,"w")) /* fp != NULL ? */

{

fprintf(fp,"Eine Zeile Text...");

if (fclose(fp)==EOF)

{

fprintf(stderr,"\nFehler beim Schließen der Datei "

"%s!\n",filename);

return(EXIT_FAILURE);

}

}

else

{

fprintf(stderr,"\nFehler beim Öffnen der Datei %s!\n",filename);

return(EXIT_FAILURE);

}

return(EXIT_SUCCESS);

} /* end main */

Die Erfahrung zeigt, daß insbesondere die beiden Funktionen fread() und fwrite() für Anfänger in C größere Schwierigkeiten bereiten. Darum hierzu ein konkretes Beispiel, das Programm files2.c.

/* files2.c */

#include <stdio.h>

#include <stdlib.h> /* für EXIT_SUCCESS, EXIT_FAILURE */

#define STRLEN 128

#define ZEILEN 4

#define SPALTEN 5

int main(void)

{

FILE *fp;

int a[ZEILEN][SPALTEN], i, j, wert=1;

char filename[STRLEN]="/tmp/probedatei";

/* Initialisieren des Arrays a mit Kontrolldaten */

for (i=0; i<ZEILEN; i++)

for (j=0; j<SPALTEN; j++)

a[i][j]=wert++;

/* Schreiben in die Datei filename */

if ((fp=fopen(filename,"wb"))==NULL)

{

fprintf(stderr,"\nFehler beim Öffnen der Datei %s!\n",filename);

return(EXIT_FAILURE);

} /* end if fopen-Fehler */

fwrite( &a, sizeof(int), ZEILEN*SPALTEN, fp);

if (fclose(fp)==EOF)

{

fprintf(stderr,"\nFehler beim Schließen der Datei %s!\n",filename);

return(EXIT_FAILURE);

} /* end if fclose-Fehler */

/* Zur Kontrolle: Lesen der Datei */

/* Davor: Array a mit Nullen belegen */

for (i=0; i<ZEILEN; i++)

for (j=0; j<SPALTEN; j++)

a[i][j]=0;

if ((fp=fopen(filename,"rb"))==NULL)

{

fprintf(stderr,"\nFehler beim Lesen der Datei %s!\n",filename);

return(EXIT_FAILURE);

} /* end if fopen-Fehler */

fread( a, sizeof(a), 1, fp);

if (fclose(fp)==EOF)

{

fprintf(stderr,"\nFehler beim Schließen der Datei %s!\n",filename);

return(EXIT_FAILURE);

} /* end if fclose-Fehler */

/* Kontrollausgabe des Dateiinhalts */

for (i=0; i<ZEILEN; i++)

{

for (j=0; j<SPALTEN; j++)

printf("%4d ",a[i][j]);

printf("\n");

}

return(EXIT_SUCCESS);

} /* end main */

Das erwartungsgemäße Ablauflisting dieses Programms sieht so aus:

1 2 3 4 5

6 7 8 9 10

11 12 13 14 15

16 17 18 19 20

Wahlfreier Zugriff (random access)

Neben den rein sequentiellen Routinen zum Lesen oder Schreiben einer Datei (fscanf(), fprintf()) sieht ANSI C auch einige in stdio.h deklarierte Funktionen für den wahlfreien Zugriff (random access) vor.

Mit fseek() kann der File-Pointer auf eine bestimmte Position gesetzt werden, mit rewind() wird der Dateizeiger speziell auf den Anfang der Datei gesetzt, mit ftell() kann abgefragt werden, an welcher Position sich der File-Buffer momentan befindet, mit fread() und fwrite() schließlich können Datensätze (bzw. gleich größere Datenmengen) gelesen und geschrieben werden.

Das nachstehende Programm- und Ablauflisting files3.c soll dies etwas konkreter illustrieren.

/* files3.c */

#include <stdio.h>

#include <stdlib.h> /* für EXIT_SUCCESS, EXIT_FAILURE */

#include <string.h>

typedef struct Kunde

{

int kundennr;

char nachname[30];

char vorname[25];

} KUNDE;

int main(void)

{

FILE *fp;

char *filename="/tmp/probe";

KUNDE kunde, kunde2, kunde3;

/* Zur Demonstration: Initialisierung von kunde */

strcpy(kunde.nachname,"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$");

strcpy(kunde.vorname, "!!!!!!!!!!!!!!!!!!!!!!!!");

kunde.kundennr=255;

/* Öffnen der Datei filename zum Schreiben, ggf. Anhängen */

fp=fopen(filename,"wb+");

if (fp==NULL)

{

fprintf(stderr,"\nFehler: Datei %s kann nicht "

"geöffnet werden!\n",filename);

return(EXIT_FAILURE);

} /* end if fp==NULL */

/* Schreiben von drei Datensätzen in die Datei filename */

strcpy(kunde.nachname,"Asimov");

strcpy(kunde.vorname,"Isaak");

kunde.kundennr=10;

fwrite(&kunde,sizeof(KUNDE),1,fp);

strcpy(kunde.nachname,"Böll");

strcpy(kunde.vorname,"Heinrich");

kunde.kundennr=11;

fwrite(&kunde,sizeof(KUNDE),1,fp);

strcpy(kunde.nachname,"Canetti");

strcpy(kunde.vorname,"Elias");

kunde.kundennr=12;

fwrite(&kunde,sizeof(KUNDE),1,fp);

/* "Zurückspulen" an den Anfang der Datei */

rewind(fp);

/* äquivalent zu fseek(fp,0,SEEK_SET); */

/* Lesen des ersten Datensatzes */

fread(&kunde2,sizeof(KUNDE),1,fp);

printf("Der erste Datensatz: ");

printf("%4d: %s %s\n",

kunde2.kundennr,kunde2.vorname,kunde2.nachname);

/* Lesen des dritten Datensatzes (ohne Fehlerbehandlung) */

fseek(fp,(3-1)*sizeof(KUNDE),SEEK_SET); /* Auf 3.Position */

fread(&kunde3,sizeof(KUNDE),1,fp);

printf("Der dritte Datensatz: ");

printf("%4d: %s %s\n",

kunde3.kundennr,kunde3.vorname,kunde3.nachname);

/* Ändern des zweiten Datensatzes */

fseek(fp,1L*sizeof(KUNDE),SEEK_SET); /* Auf 2.Position */

fread(&kunde,sizeof(KUNDE),1,fp);

strcpy(kunde.nachname,"Blum");

strcpy(kunde.vorname,"Katharina");

fseek(fp,1L*sizeof(KUNDE),SEEK_SET); /* Auf 2.Position */

fwrite(&kunde,sizeof(KUNDE),1,fp);

/* Anhängen eines vierten Datensatzes */

fseek(fp,0,SEEK_END); /* an die letzte Position */

/* fseek(fp,3L*sizeof(KUNDE),SEEK_SET); /* Auf 2.Position */

strcpy(kunde.nachname,"Dürrenmatt");

strcpy(kunde.vorname,"Friedrich");

kunde.kundennr=14;

fwrite(&kunde,sizeof(KUNDE),1,fp);

/* Lesen aller Datensätze der Datei filename */

rewind(fp);

printf("\nDie Datensätze in %s sind nun:\n",filename);

do

{

if (fread(&kunde,sizeof(KUNDE),1,fp)==1)

printf("%4d: %s %s\n",

kunde.kundennr,kunde.vorname,kunde.nachname);

} while (!feof(fp));

/* Programm beenden, return code EXIT_SUCCESS */

fclose(fp);

return(EXIT_SUCCESS);

} /* end main */

Nachstehend noch das Ablauflisting von files3.c sowie, vielleicht ganz interessant, ein Blick in die Datendatei /tmp/probe, so wie sie auf einem PC unter DOS von dem Programm angelegt wird. (Zusatzfrage für Kenner: woran könnten Sie der Datei ansehen, daß sie auf einem PC erstellt worden ist?)

Ablauflisting:

Der erste Datensatz: 10: Isaak Asimov

Der dritte Datensatz: 12: Elias Canetti

Die Datensätze in /tmp/probe sind nun:

10: Isaak Asimov

11: Katharina Blum

12: Elias Canetti

14: Friedrich Dürrenmatt

Inhalt der Datei tmp/probe:

hexadezimal: ASCII:

0A 00 41 73 69 6D 6F 76 00 24 24 24 24 24 24 24 ..Asimov.$$$$$$$

24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 00 $$$$$$$$$$$$$$$.

49 73 61 61 6B 00 21 21 21 21 21 21 21 21 21 21 Isaak.!!!!!!!!!!

21 21 21 21 21 21 21 21 00 2E 0B 00 42 6C 75 6D !!!!!!!!....Blum

00 76 00 24 24 24 24 24 24 24 24 24 24 24 24 24 .v.$$$$$$$$$$$$$

24 24 24 24 24 24 24 24 24 00 4B 61 74 68 61 72 $$$$$$$$$.Kathar

69 6E 61 00 21 21 21 21 21 21 21 21 21 21 21 21 ina.!!!!!!!!!!!!

21 21 00 2E 0C 00 43 61 6E 65 74 74 69 00 24 24 !!....Canetti.$$

24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 $$$$$$$$$$$$$$$$

24 24 24 00 45 6C 69 61 73 00 63 68 00 21 21 21 $$$.Elias.ch.!!!

21 21 21 21 21 21 21 21 21 21 21 21 00 2E 0D 00 !!!!!!!!!!!!....

44 81 72 72 65 6E 6D 61 74 74 00 24 24 24 24 24 Dürrenmatt.$$$$$

24 24 24 24 24 24 24 24 24 24 24 24 24 00 46 72 $$$$$$$$$$$$$.Fr

69 65 64 72 69 63 68 00 21 21 21 21 21 21 21 21 iedrich.!!!!!!!!

21 21 21 21 21 21 00 2E !!!!!!..

Ergänzungen

In diesem Kapitel sollen einige weiterführende Themen angeschnitten werden, die in der Praxis der C-Programmierung eine wichtige Rolle spielen. Dem derzeitigen Trend zum PC hin folgend werden einige dieser Ergänzungen auf der Grundlage von DOS und Windows, andere auf der originären C-Plattform UNIX vorgestellt werden. Die entsprechenden Tools (Werkzeuge) existieren jedoch so oder in sehr ähnlicher Form auch auf den jeweils anderen Betriebssystemen.

make und Makefiles

Die Entwicklung vor allem größerer Softwareprojekte wird unter UNIX und von den meisten PC-Entwicklungssystemen mit dem make-Konzept unterstützt. Alternativ dazu bietet z.B. Borland bei seinem Turbo C sogenannte Projectfiles an, die jedoch –  lediglich intern anders abgespeichert  – einen ähnlichen Ansatz verfolgen.

Das elementare make-Kommando von UNIX wurde bereits angesprochen. Mit

make beispiel

wird aus dem Sourcefile beispiel.c das Programm beispiel (statt a.out) erstellt. In dieser Form entspricht make beispiel also dem Aufruf

cc -o beispiel beispiel.c

make erkennt dabei sogar, ob eine Compilation evtl. nicht erforderlich ist, weil das ausführbare Programm jünger als der C-Quelltext ist! Näheres zu make kann unter UNIX online über man make abgerufen werden.

Komplizierter ist die Situation jedoch bei Multi-File-Projekten, also Programmen, deren Code in mehreren Dateien abgelegt ist. Hier helfen sogenannte Makefiles, die bei UNIX standardmäßig auch Makefile oder makefile heißen. In diesen werden die verschiedenen Abhängigkeiten abgespeichert, so daß das make-Kommando gezielt genau und nur die gerade erforderlichen Neu-Compilationen veranlassen muß. Bei diesem Verfahren spricht man von inkrementellem Compilieren (und Linken), weil nur das erneut compiliert bzw. gebunden wird, was nicht bereits auf dem neuesten Stand vorhanden ist.

Das UNIX-Kommando mkmf (make makefile) erstellt (zu allen .c und .h Dateien in einem Verzeichnis ein entsprechendes Makefile; dies soll anhand des nachstehenden Mini-Projektes illustriert werden.

Gehen wir aus von der folgenden Datei main.c, die ein kleines Hauptprogramm beinhaltet.

/* main.c */

#include <stdio.h>

#include "main.h"

int a[MAXIMUM][MAXIMUM];

void main(void)

{

InitArray(a);

PrintArray(a);

} /* end main */

In diesem Hauptprogramm werden zwei Funktionen aufgerufen, die nicht in dieser Quelldatei zu finden sind; deklariert werden sie in der Headerdatei main.h, die nachstehend gezeigt wird.

Das Vorgehen, mit #ifndef abzufragen, ob eine symbolische Konstante bereits definiert worden ist, ist typisch für Headerdateien: damit soll verhindert werden, daß ein und dasselbe Headerfile in einem Projekt (versehentlich) mehrfach eingebunden wird, wo es nicht erforderlich oder eventuell auch gar nicht zulässig ist.

/* main.h */

#ifndef MAIN_H__

#define MAIN_H__

#define MAXIMUM (10)

void InitArray(int a[MAXIMUM][MAXIMUM]);

void PrintArray(int a[MAXIMUM][MAXIMUM]);

#endif /* MAIN_H__ */

Die Dateien init.c und print.c beinhalten jeweils den Quelltext für die Funktionen InitArray() bzw. PrintArray(), die in main.c verwendet werden.

/* init.c */

#include "main.h"

void InitArray(int a[MAXIMUM][MAXIMUM])

{

int i, j;

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

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

a[i][j]=i*MAXIMUM+j;

} /* end InitArray */

/* print.c */

#include "main.h"

#include <stdio.h>

void PrintArray(int a[MAXIMUM][MAXIMUM])

{

int i, j;

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

{

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

printf("%4d ",a[i][j]);

printf("\n");

}

printf("\n");

} /* end PrintArray */

Wird in einem Verzeichnis, in dem sich diese vier Dateien (main.c, main.h, init.c, print.c) befinden, das Kommando mkmf abgesetzt, so entsteht (hier nur gekürzt und vereinfacht wiedergegeben) folgendes Makefile. Der Backslash \ dient innerhalb des Makefiles (wie bei C) als Fortsetzungszeichen; das Zeichen # ist bei Makefiles der Beginn eines Zeilenkommentars.

# Makefile

# Dieses Makefile wurde mit mkmf automatisch erzeugt!

HDRS = main.h

LD = cc

OBJS = init.o \

main.o \

print.o

PROGRAM = main

SRCS = init.c \

main.c \

print.c

all: $(PROGRAM)

$(PROGRAM): $(OBJS) $(LIBS)

@echo "Linking $(PROGRAM) ..."

@$(LD) $(LDFLAGS) $(OBJS) $(LIBS) -o $(PROGRAM)

@echo "done"

clean:; @rm -f $(OBJS) core

###

init.o: main.h

main.o: /usr/include/stdio.h main.h

print.o: /usr/include/stdio.h main.h

Und so arbeitet make mit dem Makefile zusammen: wird make (oder hier synonym dazu: make all) aufgerufen, so wird nachgesehen, ob es das ausführbare Programm main bereits gibt; wenn ja, dann wird überprüft, ob es jünger als alle beteiligten .o-Dateien (Objectfiles) ist. Wenn nein, dann werden die Abhängigkeiten zwischen den .o-Dateien und den im Makefile genannten Headerfiles und (implizit) den gleichnamigen .c-Dateien überprüft.

Im nachstehenden Beispiel sind zunächst main und die Objectfiles gar nicht vorhanden, so daß make alles neu erstellen muß. Es werden also sämtliche .c-Dateien compiliert, dann sämtliche .o-Dateien zusammengebunden zur ausführbaren Programmdatei main.

Ein Ablaufprotokoll mit make unter UNIX[31]:

$ mkmf /* Makefile wird erstellt */

$ make

cc -O -c init.c

cc -O -c main.c

cc -O -c print.c

Linking main ...

done

$ touch init.c

/* touch tut so, als ob init.c verändert worden wäre, */

/* konkret wird das Dateizugriffsdatum aktualisiert, */

/* make auf diese Weise also "betrogen". */

$ make

cc -O -c init.c

Linking main ...

done

$ touch main.h

$ make all

cc -O -c init.c

cc -O -c main.c

cc -O -c print.c

Linking main ...

done

$ make all

Target `main' is up to date. Stop.

$


Debugger

Ein Debugger[32] ist ein Programm, das einem Software-Entwickler hilft, den eigenen Code auf Fehler zu untersuchen. Dabei ermöglicht ein guter Debugger es, sich den Code auf Assemblerebene und – im Falle von symbolischen Debuggern – im Quelltext (z.B. in C) anzusehen und ihn schrittweise ablaufen zu lassen. Dabei kann jede Anweisung einzeln abgearbeitet werden, es kann aber auch nach jedem Funktionsaufruf oder an sonst zu definierenden Stellen, sogenannten Breakpoints, angehalten werden. Der Debugger zeigt dabei wahlweise auch die aktuellen Registerinhalte, Funktionsaufrufe, Stack- und Variablenbelegungen an.

Solche Debugger gibt es praktisch für jede Betriebssystemplattform; die Arbeitsweise ist im wesentlichen immer die gleiche: man vermutet einen Fehler in einer Funktion f() und sieht sich daher den Ablauf dieser Funktion genauer an, zum Beispiel im Einzelschrittverfahren. Dabei betrachtet man auch die jeweils aktuellen Werte der Variablen, insbesondere eventueller Pointer[33].

Betrachten wir etwa das folgende kleine Programm test1.c: wir sehen recht schnell, daß hier mit a[4] ein ungültiger Array-Zugriff erfolgt, denn das deklarierte Array besteht lediglich aus den Komponenten a[0] bis a[3]. Dies wird jedoch kein normaler C-Compiler als Fehler melden, denn hinter der Indexschreibweise steckt bekanntlich die Zeigerarithmetik.

/* test1.c - Demo zum Debugger */

void main(void)

{

int dummy=99, a[4], i;

for (i=1; i<=4; i++)

{ /* Fehlerhafter Indexzugriff! */

a[i]=i-4;

printf("a[%d]=%5d\n",i,a[i]);

}

} /* end main */

/* end of file test1.c */

In einem Debugger können wir uns nun die schrittweise Abarbeitung dieses kleinen Programms sowie die Belegung der verschiedenen Variablen-Speicherplätze ansehen. Insbesondere können wir somit nachvollziehen, wieso dieses kleine Programm in eine Endlosschleife gerät![34]

Beispielhaft für die Vielzahl von Debuggern, die es auf dem Markt gibt, werden hier zwei Schnappschüsse des Symantec C++ 7.0 Debuggers (für Windows 3.1/95 und Windows NT) gezeigt.

Hier klicken für ein Bildchen.

Im obigen Bild sehen wir die Entwicklungsumgebung des Symantec C++ Compilers[35]. Das Projekt TEST1.PRJ, bestehend aus der einen Quelltextdatei TEST1.C sowie –  windows-typisch  – einer Definitionsdatei TEST1.DEF, ist im Debug-Modus geladen, d.h. zu der ausführbaren Datei werden Debugger-Informationen hinzugefügt, die das symbolische Debuggen – also mit Referenz auf den C-Quelltext – ermöglichen.

Im nachstehenden Bildschirmschnappschuß sieht man das Quelltextfenster, in dem ein dicker Pfeil auf die aktuelle Quelltextposition verweist; daneben ist das "Application Window", in dem das ausführbare Programm –  hier ein textbasiertes DOS-Programm  – abläuft. Darunter werden in dem Fenster "Data/Object" die aktuellen Variablenbelegungen angezeigt.

Hier klicken für ein Bildchen.

Wie in dem Anwendungsfenster zu sehen ist: das Programm liefert nicht die erwarteten vier Ausgabezeilen für a[1] bis –  fehlerhafterweise  – a[4], sondern nach a[1] bis a[3] wird plötzlich a[0] gezeigt! Der Debugger hilft hier bei der Fehlersuche indem er zeigt, daß beim Index i=4 "a[4]" (d.h. *(a+4)) auf 0 gesetzt wird; und die Adresse a+4 verweist (bei diesem Compiler) gerade auf den Speicherplatz i!

Hier klicken für ein Bildchen.

Daneben können, wie das obige Bild zeigt, auch der Assemblercode, die Speicher- oder Registerinhalte angesehen werden[36].

Profiler

Unter einem Profiler versteht man ein Dienstprogramm, welches die Performance (Leistungswerte) eines Programmes analysiert, indem es feststellt, wie lange welche Quellcode-Zeilen oder Module aktiv sind, wie oft Module oder einzelne Anweisungen aufgerufen werden (und von wem) oder auf welche Dateien wie oft und für wie lange von dem Programm zugegriffen wird. Gleichzeitig können weitere Aktivitäten überwacht bzw. protokolliert werden, z.B. Druckausgaben, Systemaufrufe, Tastatureingaben. Zur Ermittlung eventuell ineffizienter Programmteile dient der Profiler ähnlich wie ein Debugger bei der Fehlersuche.

Im folgenden soll aus Übersichtlichkeitsgründen nur ein ganz kleines Beispiel eines C-Programms mit Borland's PC-Turbo Profiler ansatzweise analysiert werden. Ohne die graphische Aufbereitung stehen unter UNIX ebenfalls Profiler zur Verfügung; mit man gprof kann der Manual-Eintrag zum Profiler gprof aufgelistet werden. Dieser korrespondiert mit der C-Compileroption -G, der eine Programmdatei für die Analyse mit gprof erstellt.

Zunächst der Quelltext des kleinen Demonstrationsprogramms, in dem exemplarisch untersucht werden soll, ob eine for-Schleife der Art

for (i=0; i<strlen(s); i++)

besonders ineffizient ist, da die Funktion strlen() hier bei jedem Schleifendurchlauf erneut aufgerufen wird[37].

/* proftest.c

* Kleines Demonstrationsprogramm für den Turbo Profiler von Borland.

*/

#include <stdio.h>

#include <string.h>

void main(void)

{

char s[] = "Programmieren in C";

int i, sl;

for (i=0; i<strlen(s); i++)

putchar(s[i]);

sl=strlen(s);

for (i=0; i<sl; i++)

putchar(s[i]);

}

Das entsprechend (in diesem Fall mit Borlands Turbo C) compilierte EXE-Programm wird nun in den Profiler geladen.

Der Profiler kann nun eine Statistik über einen oder mehrere Programmläufe (im gezeigten Bild sind es zehn) erstellen. Im nachstehend gezeigten Bild sehen wir einen Bildschirmabzug des Turbo Profiler von Borland.

Hier klicken für ein Bildchen.

Im oberen Teil ist der Quellcode zu sehen, im unteren das sogenannte Execution Profile, das Ausführungsprofil. Hierbei wird gezeigt, wie oft (hier: 180 mal) und wie lange jeweils einzelne Anweisungen durchlaufen wurden. Hier wurde das putchar() in der Schleifenformulierung

for (i=0; i<strlen(s); i++)

180mal aufgerufen, dafür wurden 0,2519 Sekunden benötigt. Demgegenüber haben die putchar()-Aufrufe in der Schleifenkonstruktion

for (i=0; i<sl; i++)

bei gleicher Aufrufzahl nur 0,1775 Sekunden gebraucht.

Dieses einfache Beispiel zeigt somit schon recht gut, wie ein Profiler bei der statistischen Analyse und dem Aufspüren von Engpässen (bottle necks) behilflich sein kann.

Hier klicken für ein Bildchen.

Daneben erlauben Profiler in der Regel noch eine Reihe weiterer Untersuchungsmethoden, beispielsweise können die Aufruf-Hierarchien (welche Funktion ruft welche auf?) oder –  etwas mehr low-level orientiert  – wie im obigen Bild gezeigt der generierte Assemblercode angezeigt werden.

Cross-Reference-Listen mit cxref

Ein weiteres Hilfsmittel bei komplexeren Programmierungen sind sogenannte Cross-Reference-Listen, bei denen textlich oder graphisch dargestellt wird, welche Funktionen welche anderen Funktionen (in welchen Modulen) aufrufen und welche Konstanten (aus welchen Headerfiles) sie verwenden.

Nachstehend exemplarisch die Online-Hilfe zum UNIX-Kommando cxref, das eine solche Cross-Reference-Liste erzeugt. Danach folgt ein kleines Beispielprogramm (mit nur einem Modul) und die von cxref dazu produzierte –  hier allerdings stark verkürzt wiedergegebene  – Cross-Reference-Liste.

NAME

cxref - generate C program cross-reference

SYNOPSIS

cxref [options] files

DESCRIPTION

cxref analyzes a collection of C files and attempts to build a cross-

reference table. cxref utilizes a special version of cpp to include

#defined information in its symbol table. It produces a listing on

standard output of all symbols (auto, static, and global) in each file

separately, or with the -c option, in combination. Each symbol

contains an asterisk (*) before the declaring reference. Output is

sorted in ascending collation order (see Environment Variables below).

Options

In addition to the -D, -I, and -U options (which are identical to

their interpretation by cc (see cc(1)), cxref: recognizes the

following options:

-c Print a combined cross-reference of all input files.

-w num Width option; format output no wider than num

(decimal) columns. This option defaults to 80 if num

is not specified or is less than 51.

-o file Direct output to the named file.

-s Operate silently; do not print input file names.

-t Format listing for 80-column width.

-Aa Choose ANSI mode. If not specified, compatibility

mode (-Ac option) is selected by default.

-Ac Choose compatibility mode. This option is selected

by default if neither -Aa nor -Ac is specified.

EXTERNAL INFLUENCES

Environment Variables

LCCOLLATE determines the order in which the output is sorted.

If LCCOLLATE is not specified in the environment or is set to the

empty string, the value of LANG is used as a default. If LANG is not

specified or is set to the empty string, a default of ``C'' (see

lang(5)) is used instead of LANG. If any internationalization

variable contains an invalid setting, cxref behaves as if all

internationalization variables are set to ``C'' (see environ(5)).

International Code Set Support

Single- and multi-byte character code sets are supported with the

exception that multi-byte character file names are not supported.

DIAGNOSTICS

Error messages are unusually cryptic, but usually mean that you cannot

compile these files anyway.

EXAMPLES

Create a combined cross-reference of the files orange.c, blue.c, and

color.h:

cxref -c orange.c blue.c color.h

Create a combined cross-reference of the files orange.c, blue.c, and

color.h: and direct the output to the file rainbow.x:

cxref -c -o rainbow.x orange.c blue.c color.h

WARNINGS

cxref considers a formal argument in a #define macro definition to be

a declaration of that symbol. For example, a program that #includes

ctype.h will contain many declarations of the variable c.

cxref uses a special version of the C compiler front end. This means

that a file that will not compile probably cannot be successfully

processed by cxref. In addition, cxref generates references only for

those source lines that are actually compiled. This means that lines

that are excluded by #ifdefs and the like (see cpp(1)) are not cross-

referenced.

cxref does not parse the CCOPTS environment variable.

FILES

/lib/cpp C-preprocessor.

/lib/xpass Compatibility-mode special version of C compiler

front end.

/lib/xpass.ansi ANSI-mode special version of C compiler front end.

STANDARDS CONFORMANCE

cxref: SVID2, XPG2, XPG3

/* cxrefdemo.c */

#include <stdio.h>

void CallMe1(void);

void CallMe2(void);

void main(void)

{

CallMe1();

} /* end main */

void CallMe1(void)

{

CallMe2();

CallMe2();

} /* end CallMe1 */

void CallMe2(void)

{

printf("Hello, world!\n");

} /* end CallMe2 */

Cross-Reference-Liste zu cxrefdemo.c

cxrefdemo.c (stark gekürzt):

SYMBOL FILE FUNCTION LINE

BUFSIZ /usr/include/stdio.h -- *16

CallMe1() cxrefdemo.c -- *3 11

cxrefdemo.c main 8

CallMe2() cxrefdemo.c -- *4 17

cxrefdemo.c CallMe1 13 14

EOF /usr/include/stdio.h -- 96 *97

stdin /usr/include/stdio.h -- *100

stdout /usr/include/stdio.h -- *101



Literaturhinweise

An dieser Stelle sollen einige wenige Literaturhinweise gegeben werden.

Cuber, U. und Wenzel, H.
Das Einmaleins der C-Programmierung.

Düsseldorf, 1994.
Dem Buch liegt der MS-DOS-C/C++-Kommandozeilencompiler von Symantec in der Version 6.11 auf CD-ROM bei.

Hatton, L.
Safer C.

Berkshire, 1995.
Ein englischsprachiges Buch für etwas Fortgeschrittene, in dem es um Möglichkeiten geht, die C-Programmierung etwas sicherer zu gestalten.

Kelley, A. und Pohl, I.
C - Grundlagen und Anwendungen.

Bonn, 1987.

Kernighan, B. und Ritchie, D.
The C Programming Language (2nd edition).

Englewood Cliffs, 1988.
Das Standardwerk.

Kernighan, B. und Ritchie, D.
Programmieren in C.

(Deutsche Version des o.g. Buches.)
München, 1990.
Das Standardwerk in deutscher Sprache. Die 2.Auflage behandelt dann ANSI-C.

Küster, H.-G.
Umsteigen von COBOL auf C und C++

Vaterstetten/München, 1993.
Es soll ja noch COBOL-Programmierer geben. Diese finden hier einen Weg zurück in das Leben...

Müldner und Steele
C as a Second Language for Native Speakers of Pascal.

Reading, 1988.
Wer artig Pascal als erste Programmiersprache erlernt hat, ist mit diesem Buch gut beraten.

Plum, T.
Learning to Program in C.

Cardiff, 1983.

Stevens, W. R.
Advanced Programming in the UNIX Environment.

Reading, 1992.
Wie der Titel es sagt: speziell für die tiefergehende Programmierung unter dem Betriebssystem UNIX ist dieses Buch empfehlenswert.

Tondo, C. L. und Gimpel, S. E.
Das C-Lösungsbuch zu Kernighan & Ritchie.

München, 1987.

Ward, R.
Debugging C.

Bonn, 1988.


Das Originaldokument wurde erstellt mit Lotus Ami Pro und in das HTML-Format konvertiert mit dem Programm rtftohtml.
© 1995, 1996 Peter Baeumle, b.i.b. Bergisch Gladbach
Revision vom 11.Januar 1996.
Alle Rechte an diesem Dokument bleiben vorbehalten. Insbesondere ist das Photokopieren, Ausdrucken und Abschreibenlassen von fleißigen Mönchen dieses Dokuments nur und ausschließlich für private Zwecke gestattet.