Einführung in die Programmiersprache C Problem Finde

Transcription

Einführung in die Programmiersprache C Problem Finde
C Tutorial © Mathias Waack <[email protected]>
Page 1
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Literatur und Informationsquellen
Einführung in die Programmiersprache C
Allgemeines
1. Brian W. Kernighan, Dennis M. Ritchie: Programmieren in C, Hanser/Prentice Hall
2. ANSI: American National Standard Programming Language C, X3.159-1989
3. Usenet: de.comp.lang.c, comp.lang.c (comp.std.c)
4. WWW: http://www.lysator.liu.se/c/index.html
5. FAQ’s:
Page 2
http://www.eskimo.com/~scs/C-faq/top.html
http://www.plethora.net/~seebs/faqs/c-iaq.html
http://www-info2.informatik.uni-wuerzburg.de/staff/joscho/c/dclc-faq/
6. RRZN: Die Programmiersprache C, RRZN Hannover, SPR.C1
(vom RRZN sind auch Nachschlagewerke fuer UNIX, Windows NT und viele StandardSoftwarepakete erhältlich)
Für alle frei erhältliche Software (gcc, gdb...) ist Doku im Internet verfügbar, ebenso existieren für diese Tools und die Funktionen der C-Library man-Pages
Weitere Informationen liefern die gängigen Suchmaschinen im WWW
- C ist case sensitiv, d.h. foo und Foo sind in C unterschiedliche Namen.
- Ein Objekt ist in C ein benannter, logisch zusammenhängender Speicherbereich mit mind.
1 Byte Länge.
- Die Größe einer Datenstruktur in C (d.h. die Anzahl der Bits, in der z.B ein int gespeichert
wird) ist implementierungsabhängig und darf daher nicht als konstant angenommen werden! Die Größe eines Datentyps T kann mit Hilfe des Operators sizeof festgestellt werden.
- In C muß jeder Bezeichner vor seiner ersten Benutzung deklariert worden sein, daß heißt
dem Compiler müssen Namen und Typ des Bezeichners bekannt gemacht werden.
- C ist eine statisch getypte Sprache, d.h. jede Variable, Konstante und Funktion muß genau
einen Typ besitzen, in Zuweisungen und Vergleichsausdrücken müssen beide Seiten den
gleichen Typ besitzen. Der Typ muß zur Compile-Zeit feststellbar sein.
- Ein C-Programm besteht aus einer oder mehreren Funktionen, bei Start des Programms
wird die Funktion mit Namen main aufgerufen.
- Kommentare beginnen mit /* und werden mit */ beendet oder sie beginnen mit // und werden durch ein Zeilenende beendet
Typen
Ein Typ ist eine (endliche) Menge von Werten. Hat eine Variable den Typ T so heißt dies,
das sie nur Werte aus der Menge T annehmen darf.
Problem
Basistypen
- Integer: short int, int, long int, long long int (signed oder unsigned)
Bsp:
int i, long long int j; unsigned int k;
- Character: char, unsigned char, signed char
- Fließkomma: float, double, long double (Achtung bei Arithmetik!)
- Aufzählung: enum <Name> { <Werte_Liste> }
Bsp:
enum Woche { Mo, Di, Mi, Do, Fr, Sa, So };
enum Woche frei = So;
- Leer: void entspricht der leeren Menge
Bsp:
void test(int i);
Zusammengesetzte Typen
Finde Algorithmus zur Problemlösung
Implementiere den Algorithmus
Datenstrukturen und Operationen darauf
Implementierung
Testen
Debugging
- Zeiger: für einen Typ T ist T* vom Typ "Zeiger auf T"
Bsp:
int * k;
- Felder: Menge von Werten gleichen Types, werden über Indizes referenziert
Bsp:
char name[20], test[] = {’a’,’b’};
Profiling/Optimierung
Adresse x
x+1
Vorstellen der Lösung
x+2
x+3
x+4
i
int i, *pi;
pi
C Tutorial © Mathias Waack <[email protected]>
Page 3
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Page 4
Einführung in die Programmiersprache C
Typen
Ausdrücke
Strukturierte Typen
Ausdrücke bestehen aus einem Operanden oder aus Operanden die mit Operatoren verknüpft sind. Operanden sind Variablen, Konstanten, Funktionsaufrufe oder Ausdrücke.
- Strukturen: Menge von Werten mit verschiedenen Typen, über Namen referenziert
Bsp:
struct bsp { int i; char k; };
- Unions: besitzt verschiedene Typen (aber nur einen zur Zeit)
Bsp:
union wert { int iwert; float fwert };
- Funktionstypen: <type> f() - f ist eine Funktion vom Typ <type>
Bsp:
float square();
Operator
Assoziativität
() [] -> .
l
! ~ ++ -- + - * & (type) sizeof
r
*/%
l
+-
l
Benannte Typen
<< >>
l
Typdeklarationen können benannt werden:
< <= > >=
l
typedef <type> <bezeichner>
== !=
l
Bsp:
&
l
typedef struct { int i; char k; } bsp;
bsp a; a.i = 42;
Typqualifizierer
Sie schränken die Art und Weise des Zugriffs auf Variablen des Typs ein.
Bsp:
const int result = 42;
volatile int buf_empty;
Deklarationen und Definitionen
Deklaration: legt die Bedeutung eines Bezeichners fest
Definition: reserviert Speicherplatz für den Bezeichner
Syntax: [<speicherklasse>] <typangabe> <deklarator>
Speicherklassen: (auto), register, static, extern, inline
Typangabe: Basistypen, strukturierte Typen oder Namen eines benannten Typs (und evtl einen Qualifizierer)
Deklarator: Variablen: Name und optional ’*’ (Zeiger) und/oder ’[]’ (Felder)
Funktionen: <typangabe> Bezeichner(<parameter_liste>)
Eine Deklaration wird mit einem ’;’ abgeschlossen. Innerhalb von Deklarationen können
Klammern benutzt werden, um die Lesbarkeit zu erhöhen.
Deklarationen von Variablen sind oft besser verständlich wenn sie von rechts nach links gelesen werden (z.B. int *i[5] - ein Feld mit 5 Zeigern auf int)
Beispiele:
int i, *pi, a[10], *b[10];
double *g, h[10][10][10];
struct test { int i; double f } t[20], *f;
^
l
|
l
&&
l
||
l
?:
r
= += *= /= -= %= &= ^= |= <<= >>=
r
,
l
Anweisungen
Jede Funktion in C besteht aus einer Folge von Anweisungen. Anweisungen werden durch
’;’ abgeschlossen. Anweisungen werden streng sequentiell abgearbeitet (beachte: dies gilt
nicht innerhalb von Anweisungen!).
Einfache Anweisungen: leer, Ausdrücke, Block {}
Schleifen:
while (<ausdruck>) <anweisung>
do <anweisung> while (<ausdruck>);
for (<init_ausdruck>;<ausdruck>;<incr_ausdruck>) <ausdruck>
Sprünge:
break;
continue;
return [<ausdruck>];
Auswahlanweisungen: if (<ausdruck>) <anweisung; [else <anweisung>;]
switch (<ausdruck>) {
case <konst>: <anweisung>
case <konst>: <anweisung>
default: <anweisung>
}
C Tutorial © Mathias Waack <[email protected]>
Page 5
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Anweisungen (Beispiele)
Einführung in die Programmiersprache C
Übersetzung
Größtes Element in einem Feld finden:
Das Kompilieren eines C-Programmes läuft in 3 Schritten ab:
int size = <irgendein Wert>
int a[size], i, max = -1, index;
// initialisiere a mit positiven Werten
1. Version: while - Schleife
i = 0;
while (i<size) {
if (a[i] > max) {
max = a[i]; index = i;
}
i++; }
2. Version: for - Schleife
Der Präprozessor nimmt syntaktische Veränderungen an der Quelldatei vor,
Der C-Compiler übersetzt die Datei und erzeugt sogenannten Objektcode und
Der Linker fügt den Objektcode mit den Code aus sog. Bibliotheken oder anderen Objektdateien zusammen und erzeugt eine ausführbare Datei.
Anweisungen an den Präprozessor werden durch ein ’#’ in der 1. Spalte der jeweiligen Zeile
kenntlich gemacht. Der C-Präprozessor kennt (u.a.) folgenden Anweisungen:
include "filename" - ersetzt diese Zeile durch den Inhalt der Datei filename aus dem aktuellen Verzeichnis
include <filename> - wie zuvor, sucht die Datei filename aber in best. Verzeichnissen
for (i=0; i<size; i++)
if (a[i] == search) break;
Verschiedene Aktionen in Abhängigkeit von einem Wert ausführen:
int i = <cmd>;
switch (i) {
case <akt0>:
case <akt1>:
case <akt2>:
case <akt3>:
default: <do
}
1.
2.
3.
Präprozessor
for (i=0; i<size; i++)
if (a[i] > max) { ... }
Bestimmtes Element in einem Feld finden:
int i = <cmd>;
if (i == <akt0>) <do akt0>;
else if (i==<akt1> || i==<akt2>)
<do akt1.2>;
else if (i==<akt3>) <do akt3>;
else <do default akt>;
Page 6
<do akt0>; break;
<do akt1.2>; break;
<do akt3>; break;
default akt>;
Increment-Funktion (nur für positive Werte)
int incr(int i) {
return (i>0) i+1 : i;
}
Achtung Falle:
int i = 4;
int f() { i = i+1; return i;};
int g() { i = i-1; return i;};
int main() {
if ( f() || g() ) i = i+2;
}
Welchen Wert hat i vor der letzten Klammer ’}’ ?
Oder:
if ( f() < 5 && g() > 4 ) i = g();
Was passiert, wenn man statt && den Operator & nimmt (bzw. | statt ||) ?
define name value - alle weiteren Vorkommen von name in der aktuellen Datei werden
durch value ersetzt
define name(arglist) ausdruck - wie zuvor aber parametrisiert
ifdef name <text1> else <text2> endif - wenn name bekannt ist, wird text2 gelöscht, sonst
text1
ifndef ... - das Gegenteil von ifdef
Beispiel:
#ifdef DOS
#define DELIM ’\’
#else
#define DELIM ’/’
#endif
int main() {
int a[size];
init(a);
#ifdef DEBUG
{ int i; for (i=0;i<size;i++) printf("%d\n",a[i]); }
#endif
Praxis
Der gcc fungiert als Frontend für Präprozessor, Compiler und Linker. Ein Aufruf von gcc
file.c bewirkt das Ausführen aller 3 Schritte, als Ergebnis ensteht die ausführbare Datei
a.out. Folgende Kommandozeilenparameter sind interessant:
- E: gcc -E file.c ruft nur den Präprozessor auf
- c: gcc -c file.c führt nur die ersten beiden Schritte aus, erzeugt die Objektdatei file.o
- -o: gcc -o name file.c benennt die ausführbare Datei name
- -Wall: überprüft den Code sehr peinlich, meldet viele eventuelle Fehlerstellen
C Tutorial © Mathias Waack <[email protected]>
Page 7
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Page 8
Einführung in die Programmiersprache C
Zeiger und Felder
Funktionen
Eine Variable, die einen Feld-Typ hat, wird vom C-Compiler wie ein (konstanter) Zeiger auf
das erste Element des Feldes behandelt:
Parameter an Funktionen werden immer call-by-value übergeben, d.h. beim Aufruf werden
die Werte der aktuellen Parameter berechnet und diese an die Funktion übergeben:
int a[10], *b;
b = a;
Zeiger können mit dem Operator ’*’ dereferenziert werden:
int i = 8;
f(i);
// void f(int)
i == 8; // f kann i nicht ändern
Soll die Funktion f die Variable i ändern können (call-by-reference), kann f ein Zeiger auf i
übergeben werden:
*b == a[0];
Umgekehrt liefert ’&’ einen Zeiger auf ein Objekt:
b == &(a[0]);
Zu Zeigern können Integer-Werte addiert/subtrahiert werden. Der Ausdruck (pointer + offset) inkrementiert pointer dabei um offset*sizeof(*pointer) Bytes:
b + 3 == a[3] == a+3;
Tatsächlich wird jeder Ausdruck der Form x[y] vom Compiler in x+y umgewandelt, bevor
er übersetzt wird:
b + 3 == b[3] == 3[b];
Achtung: Zeiger und Felder sind trotzdem nicht dasselbe:
b = &a[7];
a = b; // das geht nicht!!!!
sizeof(b); // Größe eines Zeigers
sizeof(a) == 10*sizeof(int);
f(&i); // void f(int *)
i == ?; // f kann i geändert haben
Der Rückgabewert von Funktionen kann ignoriert werden:
int f(int);
f(27);
Zeiger auf Funktionen:
int f(int i) { return 2*i; }
void g() {
int (*a)(int), x;
a = f; x = a(2);
}
Beachte, daß Felder wie Zeiger behandelt werden und sie daher an Funktionen quasi by-reference übergeben werden!
Zeichenketten
Die main-Funktion
In C gibt es keinen Datentyp für Zeichenketten. Sie werden als Felder vom Typ char dargestellt mit einer 0 (nicht Zeichen ’0’ sondern der Wert 0) als letztes Zeichen:
Jedes C-Programm besitzt eine Funktion mit Namen main, diese wird beim Start des Programms aufgerufen:
char s[10]; // s kann 9 Zeichen aufnehmen + ’\0’
char u[] = "Hallo";// u ist 6 Byte gross
char *t = "Hallo"; // Wie gross ist t?
char *v[10], **w = v; // geht das?
In "" werden Zeichenketten-Konstanten eingeschlossen. Somit ist t ein Zeiger auf eine Konstante! Dagegen ist u ein Array von Zeichen die mit Konstanten initialisiert wurden.
int main(int argc, char *argv[]);
argc ist die Anzahl der Kommandozeilenparameter, die das Programm beim Aufruf erhalten
hat (einschließlich des Programmnamens), argv ist ein Feld das diese Parameter enthält. Der
Rückgabewert der main-Funktion wird an das aufrufende Programm (meist das Betriebssystem) zurückgegeben.
*t = ’G’;
// verboten!!! warum?
u[0] = ’G’;
// ok
Bestimmung der Länge eines Strings:
int strlen(char *s) {
int len;
for (len=0;s[len];len++);
return len;
}
more text.txt datei.dat
In der main-Funktion des Programmes more gilt dann:
argc == 3;
argv[0] === "more"; argv[1] === "text.txt";
argv[2] === "datei.dat"
Es ist Konvention, daß der Rückgabewert eines Programms ein Indiz für seine erfolgreiche
Ausführung ist. D.h. jedes Programm sollte den Wert 0 liefern, wenn es "normal" beendet
wurde, und einen Fehlercode != 0 wenn es infolge eines Fehlers beendet wurde.
C Tutorial © Mathias Waack <[email protected]>
Page 9
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Libraries
Für viele Standardprobleme gibt es bereits fertige Lösungen, diese werden in Form von Libraries dem Programmierer zur Verfügung gestellt. Zu jedem C-Compiler gehört eine (standardisierte) C-Library. Diese Libraries liegen schon kompiliert vor und werden nur noch
zum Programm "gelinkt". Um die dort enthaltenen Funktionen, Datenstrukturen und Variablen dem Compiler gekannt zu machen, werde diese in sogenannten Headerfiles deklariert
(und nicht definiert!). Diese Headerfiles werden mittels der #include-Anweisung des
Präprozessors in das Programm eingefügt.
#include <math.h>
int main() {
double g = exp(7.0);
}
Die Funktion double exp(double) ist in der Datei math.h deklariert und in der Library
libm.a definiert. Um obiges Programm kompilieren zu können, muß also diese Library
zum Programm gelinkt werden:
gcc test.c <path>/libm.a
Einfacher geht es mit der Compileroption -l:
gcc test.c -lm
Für die Funktionen der C-Library existieren man-pages denen man entnehmen kann was
diese Funktionen bewirken, in welchen Libraries sie definiert und in welchen Headerfiles
sie deklariert sind.
Dynamischer Speicher
Einführung in die Programmiersprache C
Dynamische Datenstrukturen
Felder und Strukturen bieten eine Möglichkeit, mehrere Datenelemente zu vereinigen. Allerdings wird deren Anzahl einmal festgelegt und ist anschließend nicht mehr änderbar. Damit lassen sich Strukturen wie verkettete Listen nicht implementieren.
In einer Liste enthält jedes Element einen Verweis auf seinen Nachfolger:
struct listentry {
<irgendwelche Daten>;
struct listentry *next; };
Das Element next der Struktur enthält also einen Zeiger auf das nächste Element der Liste.
Die Deklaration von next ist möglich, da der Compiler den Typ "struct listentry" zu diesem
Zeitpunkt schon kennt (wenn auch noch nicht komplett).
struct gehtsonicht {
struct gehtsonicht next; };
Der Struktur listentry kann man auch einen Namen geben:
free(p);
p[7]; // ist jetzt verboten!!!
malloc/free werden häufig im Zusammenhang mit dynamischen Datenstrukturen (Listen,
Bäumen usw.) benutzt.
// so nicht!!!
typedef struct listentry liste_t;
Jetzt kann man eine Variable top anlegen, die auf den Anfang der Liste verweist:
liste_t *top = NULL;
Neue Listenelement werden mit malloc erzeugt:
liste_t *neuer = malloc(sizeof(liste_t));
Dieses Element kann jetzt am Anfang der Liste eingefügt werden:
neuer->next = top; top = neuer; // (*neuer).next
In einer Schleife kann die Liste relativ einfach wieder gelöscht werden:
Die Funktion void *malloc(size_t) erlaubt es, zur Laufzeit eines Programmes Speicherplatz
anzufordern. Sie liefert einen Zeiger auf diesen Speicher zurück. Konnte kein Speicher alloziiert werden, liefert sie einen Zeiger mit dem speziellen Wert NULL. Durch Vergleich mit
diesem Wert kann festgestellt werden, ob die Anforderung erfolgreich war. Um z.B. Platz
für 10 int-Werte anzufordern, kann man folgenden Code benutzen:
int *p; p = (int *)malloc(sizeof(int)*10);
if (p == NULL) <irgendeine Fehlerbehandlung>
else p[7] = 42; // wie bei statischen Feldern
Mit malloc angeforderter Speicher kann mit der Funktion void free(void *) wieder freigegeben werden:
Page 10
list_t *tmp;
while (top) {
tmp = top; top = top->next;
free(tmp);
}
top
Daten
next
Daten
next
Daten
next
NULL
C Tutorial © Mathias Waack <[email protected]>
Page 11
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Page 12
Einführung in die Programmiersprache C
Seiteneffekte
Ausgewählte Funktionen der Standard-Library
- Zugriffe auf volatile Objekte
- verändern eines Objektes
- verändern einer Datei
- Aufruf einer Funktion, die eine der obigen Aktionen ausführt
An sog. sequence points wird garantiert, daß alle Seiteneffekte vorhergehender Anweisungen abgeschlossen sind. Die Auswertungsreihenfolge zwischen zwei sequence points ist
nicht definiert! Alle Ausdrücke, deren Wert von der Reihenfolge der Auswertung abhängt,
sind nicht erlaubt (undefined behaviour).
Dateizugriff (stdio.h)
Beispiele für solche Ausdrücke:
int a[10], b, i=0;
a[i] = i++; // a[0] oder a[1] ?
i=0;
b = i++ * i++; // b = 0 o. 1 o. 2?
b = f(&i) + g(&i); // wird i in f/g modifiziert?
Ausdrücke vermeiden , deren Bedeutung nicht beim ersten Hinschauen klar wird!
Ausgewählte Funktionen der Standard-Library
Eingabe:
- getchar liest ein Zeichen ein, gets(char *s) liest einen String
- scanf("<formatstring>",arg1,...) formatierte Eingabe
Flag
Typ
Matching
d
int *
Integer Zahl
c
char *
ein Zeichen
f
float *
reelle Zahl
x
int *
Hexadezimal-Zahl
s
char *
String (Achtung: muß groß genug sein!)
- putchar gibt ein Zeichen aus, puts(char *) gibt einen String aus
- printf("<formatstring>",arg1,...) formatierte Ausgabe
Typ
Matching
d
int
Integer Zahl
x
int
Hexadezimal-Zahl
c
char
ein Zeichen
f
double
reelle Zahl
s
char *
String
Mode
Bedeutung
r
zum Lesen öffnen
w
zum Schreiben öffnen (Inhalt wird vorher gelöscht o. Datei neu erzeugt)
a
zum Schreiben ab Dateiende öffnen (append)
r+
wie "r", zusätzlich auch Schreiben möglich
w+
wie "w", zusätzlich auch Lesen möglich
a+
wie "a", zusätzlich Lesen (ab Anfang) möglich
- int fclose(FILE *) schließt eine Datei
- int fflush(FILE *) "flusht" den Puffer (bei Ausgabe), nicht für Eingabe verwenden!
- fprintf(FILE *,const char *,...) wie printf, nur Ausgabe in Datei
- fscanf(FILE *,const char *,...) wie scanf, nur Eingabe von Datei
- int feof(FILE *) liefert EOF wenn Dateiende erreicht
- size_t fwrite(const void *ptr, size_t size, size_t nitems,
FILE *stream) schreibt max. n Items von jeweils size Bytes ab Adresse ptr in die
Datei stream, liefert die Anzahl der geschriebenen Zeichen
- size_t fread(void *ptr, size_t size, size_t nitems,
FILE *stream) liest max. n Items von jeweils size Bytes aus der Datei stream
und schreibt sie in den Speicher ab Adresse ptr, liefert die Anzahl der gelesenen Zeichen
Fehlerbehandlung
Tritt in einer Funktion der C-Library ein Fehler auf, so wird errno gesetzt. Die Datei
errno.h enthält Konstanten die angeben, welcher Fehler aufgetreten ist.
Ausgabe:
Flag
- FILE *fopen(const char *filename, const char *mode)
- void perror(const char *) gibt eine Fehlermeldung entsprechend dem Wert
errno aus
FILE *bsp = fopen("dateiname","r");
if (bsp == NULL) // also Fehler in fopen -> errno != 0
perror("dateiname");
C Tutorial © Mathias Waack <[email protected]>
Page 13
Einführung in die Programmiersprache C
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Ausgewählte Funktionen der Standard-Library
Ausgewählte Funktionen der Standard-Library (Beispiele)
Strings (string.h)
Zeilenweiser Vergleich von 2 Dateien (ohne Fehlertests!)
- char *strcpy(char *s1, const char *s2) kopiert s2 nach s1
- char *strncpy(char *s1, const char *s2, size_t n) wie strcpy,
kopiert max. n Zeichen
- char *strcat(char *s1, const *char *s2) kopiert s2 an das Ende von s1
- int strcmp(const char *s1, const char *s2)
- int strncmp(const char *s1, const char *s2, size_t n) vergleicht
max. n Zeichen
- char *strchr(const char *s, int c) liefert Zeiger auf das erste Vorkommen
von c in s
- size_t strlen(const char *s)
- void *memset(void *s, int c, size_t n) setzt den Speicher ab s auf den
Wert c für n Zeichen
- void *memcpy(void *s1,const void *s2, size_t n) kopiert n Zeichen
(char’s) ab s2 nach s1
- int memcmp(const void *s1,const *void *s2,size_t n) vergleicht
den Speicher ab s1 bzw. s2 n Zeichen lang, liefert einen Wert kleiner, gleich oder größer
0 wenn s1 kleiner, gleich oder größer als s2 ist
Die Speicherbereiche bei den Kopierfunktionen dürfen sich nicht überlappen!
#include <stdio.h>
void ungleich(int i) {
fprintf(stderr,"Ungleichheit in Zeile %d\n",i);
exit(1);
}
int main(int argc, char *argv[]) {
FILE *in0 = fopen(argv[1],"r");
FILE *in1 = fopen(argv[2],"r");
char buf0[255], buf1[255];
int lineno = 1;
while (!feof(in0) && !feof(in1)) {
fgets(buf0,255,in0); fgets(buf1,255,in1);
if (strcmp(buf0,buf1)) ungleich(lineno);
lineno++;
}
if (!feof(in0) || !feof(in1)) ungleich(lineno);
return 0;
}
Einlesen von Zahlen bis 0 eingegeben wird, dann Summe ausgeben:
double d, res = 0;
do {
scanf("%f",&d); res += d;
} while (d != 0);
printf("Summe:\t%lf\n",res);
Mögliche Implementierung von fgets():
char *fgets(char buf[], int size, FILE *f) {
char c;
int pos = 0;
while ((c=getc(f))!=EOF && c!=’\n’ && pos<size-1) buf[pos++] = c;
buf[pos] = ’\0’;
return buf;
}
Ein (schlechter) cp-Ersatz:
int main(int argc,char *argv[]) {
FILE *in = fopen(argv[1],"r");
FILE *out = fopen(argv[2],"w");
char c;
while ((c = getc(in))!=EOF) putc(c,out);
fclose(in); fclose(out);
}
Lesen/Schreiben binärer Daten:
struct bsp { <irgendwas> }
struct bsp a;
fwrite(&a,sizeof(struct bsp),1,outstream); // Schreiben
struct bsp *b = malloc(sizeof(struct bsp));
fread(b,sizeof(struct bsp),1,instream); // Lesen
Page 14
C Tutorial © Mathias Waack <[email protected]>
Page 15
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Page 16
Einführung in die Programmiersprache C
Ausgewählte Funktionen der Standard-Library
Profiling
Mathematik
Sammeln von Informationen über den Programmablauf
- sin, cos, tan, asin, acos, atan, exp, log, log10, pow, sqrt, sinh, ...
- round, trunc
Beachte, daß floating point Arithmetik aufgrund endlicher Genauigkeit nicht exakt ist. Unendliche Brüche können nicht exakt dargestellt werden. Bereits einfache Dezimalbrüche
(z.B. 1/10) besitzen keine endliche Binärbruchrepräsentation.
- Häufigkeit von Funktionsaufrufen
- Dauer von Funktionsaufrufen
Auswerten der Informationen um das Laufzeitverhalten zu verbessern
Floating point Werte sollen nie direkt auf (Un-)Gleichheit geprüft werden, d.h. statt
- Eleminieren von toten Code
- Ansatzpunkte für Optimierungen finden
Übersetzen des Codes mit der Compileroption -pg
if (x == y) ...
schreibt man besser
gcc -pg test.c
Programmlauf erzeugt eine Datei "gmon.out" welche Statistiken enthält.
if (fabs(x-y) < ε) ...
Beachte, daß bei floating point Werten das Ergebnis eines Ausdruckes stark von der Auswertungsreihenfolge abhängen kann (Auslöschung) :
(1113. + -1111.) + 7.511 = 2.000 + 7.511 = 9.511
1113. + (-1111. + 7.511) = 1113. + -1103 = 10.00
Programm gprof arbeitet diese Statistiken auf und bietet sie in lesbarer Form an
- Liste der aufgerufenen Funktionen mit Timing-Statistiken
- Call-Graph: für jede Funktion eine Liste aller aufgerufenen Funktionen
self
us/call
total
us/call
cumulative
seconds
self
seconds
44.44
0.08
0.08
16.67
0.11
0.03
125440
11.11
0.13
0.02
30001
0.67
11.11
0.15
0.02
1
20000.00
Zeitmessung
5.56
0.16
0.01
10000
1.00
- time_t time(time_t *) aktuelle Zeit (seit 1. Jan 1970, 0 Uhr)
- clock_t times(struct tms *buf) speichert Zeitinformationen über den Prozeß
in buf
- int clock_gettime(clockid_t clock_id, struct timespec *tp)
- int clock_getres(clockid_t clock_id, struct timespec *res)
Die beiden letztgenannten Funktionen sind nicht Bestandteil des C-Standards.
5.56
0.17
0.01
3856
2.59
5.56
0.18
0.01
1
10000.00
100000.00 main
0.00
0.18
0.00
1
0.00
60000.67 qsort
% time
Parsing
- int atoi(char *), long atol(char *)
- double atof(char *)
- sscanf, sprintf funktionieren wie fscanf, fprintf nur mit Strings statt mit Files
calls
name
internal_mcount
0.24
0.24 compare
0.67 .umul
57607.94 qst
3.00 _drand48_u
2.59 .div
Nach der Optimierung
57.14
0.08
0.08
7.14
0.11
0.01
125440
0.08
internal_mcount
0.00
0.14
0.00
1
0.00
0.08 compare
59998.00 main
C Tutorial © Mathias Waack <[email protected]>
Page 17
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Debugging (I)
Page 18
Einführung in die Programmiersprache C
Debugging (II)
Zusicherungen (assertions):
#include <assert.h>
liste_t *ptr;
... // hier wird mit ptr gearbeitet
assert(ptr != NULL); // ptr sollte != NULL sein
assert ist keine Funktion sondern ein Makro. assert-Anweisungen können aus dem
Code entfernt werden, indem das Makro NDEBUG definiert wird:
#define NDEBUG
// ab hier assert’s ignorieren
Ebenso kann man in #ifdef’s gekapselte Ausgabeanweisungen in den Code einfügen:
#ifndef NDEBUG
printf("Bis hierher ging gut\n");
#endif
Meistens werden solche Logging-Funktionen hinter Makros versteckt:
#ifdef DEBUG
#define LOG(x) puts(x)
#else
#define LOG(x)
#endif
Kommandozeilenoption -g für den gcc erzeugt Code mit Debugging-Information
Debugger: gdb - textorientiert
wichtige Kommandos:
- help: Liste der Kommandos, "help <cmd>" liefert Hilfe zum Kommando <cmd>
- list: "list <fkt>" listet 10 Zeilen um die Funktion <fkt>, "list" listet nächsten 10 Zeilen
- break: "break <fkt>" setzt Breakpoint an Anfang von <fkt>, "break <lineno>" in Zeile
- print: "print <var>" zeigt den Wert der Variablen <var> an
- next: führt die aktuelle Programmzeile aus (überspringt Funktionsaufrufe)
- step: wie next, aber springt in Funktionen hinein
- bt: zeigt den aktuellen Aufrufstack
- up, down: bewegt im Aufrufstack eine Ebene höher/tiefer
- run: "run <arg1> ..." startet das Programm mit den Argumenten <arg1>...
- quit: beendet gdb
gdb kann auch benutzt werden, um core-File zu analysieren:
"gdb --core core a.out" lädt das Corefile core welches vom Programm a.out
erzeugt wurde. Mit dem Kommando bt erfährt man den Aufrufstack zum Zeitpunkt des
Programmendes, mit up wechselt man in die letzte eigene Funktion und schaut sich die Variablen mit print an.
(gdb) break main
Breakpoint 1 at 0x103e8: file td.c, line 4.
(gdb) run
Starting program: /tmp/a.out
Breakpoint 1, main () at td.c:4
4
int i = 0;
(gdb) next
6
i = random();
(gdb) print i
$1 = 0
(gdb) next
8
return 0;
(gdb) print i
$2 = 2078917053
(gdb) next
9
}
(gdb) next
0x10370 in .nope ()
(gdb) next
Single stepping until exit from function .nope,
which has no line number information.
Program exited normally.
(gdb)
int main() {
int i = 0;
i = random();
return 0;
}
C Tutorial © Mathias Waack <[email protected]>
Page 19
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Page 20
Einführung in die Programmiersprache C
Speicherklassen
Größere Projekte
- automatisch: (keine Speicherklasse angegeben), existieren lokal in einem Block
- statisch: (in einem Block: static, außerhalb von Blöcken immer statisch), existieren auch
außerhalb des umgebenen Blockes
In größeren Projekten wird der Quelltext meist auf mehrer Dateien verteilt. Diese Dateien
werden einzeln kompiliert und dann gelinkt. So muß bei Änderungen in einer Datei nicht der
gesamte Sourcecode neu übersetzt werden.
int i =0;
static int k= 0;
void func(void) {
static int called = 0;
int j = 0;
j++; called++; i++; k++;
...
}
int main() {
func(); func(); func();
}
In jeder Datei befinden sich Funktionen und Variablen, die einer logischen Klasse zuzuordnen sind. Deklarationen und Typdefinitionen werden in Headerfiles verlagert.
Beispiel: Rechner für UPN:
Aufteilung: Stack für die Operanden, Rechenoperationen --> stack.c,
calc.c, calc.h,
außerdem Funktionen für Ein- und Ausgabe io.c, io.h
stack.h:
Gültigkeitsbereiche
void push(int);
int pop(void);
int empty();
~ eines Namens ist der Teil des Programms, in dem der Name benutzt werden darf.
calc.h:
Eine Variable heißt extern, wenn sie außerhalb eines Blockes definiert ist (Funktionen sind
immer extern).
void add(void); // und sub, mult, div
Der Gültigkeitsbereich externer Variablen und Funktion reicht vom Punkt ihrer Deklaration
bis zum Ende der Datei. Sollen externe Variablen vor ihrer Definition benutzt werden, so
werden sie als extern deklariert.
int read_stat(void);
extern double i;
void func() {
i+= 1.0;
}
double i=0;
int main() {
func();
}
// Deklaration, keine Definition
// geht, da Name bekannt
// Definition von i
Externe Variablen dürfen nur genau einmal definiert und initialisiert werden.
Werden externe Variablen oder Funktionen als static definiert, begrenzt das ihren Gültigkeitsbereich auf diese Datei.
static int bsp = 0; // extern, aber in anderen Dateien nicht sichtbar!
int main() {
... // hier ist bsp sichtbar
}
void func() {
.... // hier ist bsp sichtbar
}
stack.h,
io.h:
stack.c:
#include "stack.h"
static int stack[256];
static int stack_ptr = 0;
// Definition der Funktionen
calc.c:
#include "stack.h" // hier muss auf den Stack zugegriffen werden
void add() {
int op0, op1;
if (!empty()) op0 = pop();
else <error>
if (!empty()) op1 = pop();
else <error>
push(op0 + op1);
}
// und dann sub, div, mult
io.c:
#include "calc.h"
int read_stat() { ... ; return is_eof; }
Durch die #include’s wird die Konsistenz der Datenstrukturen sichergestellt.
Headerfiles werden häufig durch #ifdef’s gegen mehrfaches includen geschützt.
Headerfiles sollten nur Deklarationen und keine(!) Definitionen enthalten.
C Tutorial © Mathias Waack <[email protected]>
Page 21
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Einführung in die Programmiersprache C
Make
Prozesse und Interprozesskommunikation (I)
Werkzeug das dazu dient, automatisch Dateien, welche von anderen Dateien abhängen, neu
zu generieren, wenn sich diese anderen Dateien ändern.
pid_t - Datentyp zur Aufnahme von Prozessid’s (sys/types.h)
In einem Makefile wird beschrieben, welche Beziehungen zwischen den Dateien bestehen und wie diese "upgedated" werden. Dies geschieht durch Regeln der Form:
pid_t getpid(): PID (process ID) des aufrufenden Prozesses
<target>: <depends on sources>
TAB<command for update>
Erzeugen eines Prozesses (unistd.h)
Bsp.:
io.o: io.c io.h calc.h
gcc -c io.c
calculator: main.o io.o calc.o stack.o
gcc -o calculator main.o io.o calc.o stack.o
In Makefiles können auch Variablen deklariert werden:
CFLAGS = -c -g -Wall -DDEBUG
#CFLAGS = -c -O2 -DNDEBUG
io.o: io.c io.h calc.h
gcc $(CFLAGS) io.c
make sucht im Makefile nach der ersten Regel. In dieser Regel untersucht er alle Sourcen
daraufhin, ob für sie irgendwelche Abhängigkeiten bestehen, falls ja wird diese Source neu
generiert. Anschließend wird die Regel für das Target ausgeführt.
Beim Aufruf kann make der Name eines Target angegeben werden, z.B. "make io.o"
würde im o.g. Makefile nur die Datei io.o (evtl.) neu generieren. Mit "make -n" kann
man sich anzeigen lassen, welche Aktionen make ausführen würde, ohne daß diese wirklich
ausgeführt würden.
Page 22
Prozessidentifikation (unistd.h)
pid_t getppid(): PID des Vaterprozesses
pid_t fork()
fork erzeugt eine Kopie des aktuellen Prozesses, liefert 0 im Kindprozeß, die ID des Kindes im Vaterprozeß. Der Kindprozeß unterscheidet sich vom Vater u.a. in folgendem:
- andere PID, andere PPID (parent PID)
- Reaktion auf Signale ist anders sein
Beispiel:
if (fork() == 0) { <the child code> }
else { <the parent code> }
Prozeßstart (unistd.h)
int
int
int
int
execl(const char *path, const char *arg0, ..., char * /* NULL */ )
execv(const char *path, char *const argv[])
execlp(const char *file, const char *arg0, ..., char * /* NULL */)
execvp(const char *file, char *const argv[]);
Beispiel Drucken im Hintergrund:
void print_bg(char *filename) {
if (fork() == 0) {
execl("/bin/lp",filename,NULL);
perror("exec failed"); // dieser Code wird normalerweise nicht ausgeführt!
exit(0);
}
}
Warten auf einen (Kind-)Prozeß (sys/wait.h)
pid_t wait(int *stat)
pid_t waitpid(pid_t pid, int *stat, int options)
stat enthält Information über den Prozeß, auf den gewartet wurde:
WIFEXITED(stat) !=0 falls der Kindprozeß normal beendet wurde
WEXITSTATUS(stat) exit-Code des Kindprozesses
WIFSIGNALED(stat) !=0 falls der Kindprozeß infolge eines Signal terminierte
WTERMSIG(stat) Nummer des Signals, falls WIFSIGNALED(stat)!=0
(siehe wstat(5) ).
C Tutorial © Mathias Waack <[email protected]>
Page 23
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Einführung in die Programmiersprache C
Prozesse und Interprozesskommunikation (II)
Prozesse und Interprozesskommunikation (III)
Signals - asynchrone Ereignisse (sys/types.h, signal.h)
int kill(pid_t pid, int sig)
Mögliche Reaktion auf Empfangen eines Signals:
Mechanismen:
- Führe die Default-Aktion für dieses Signal durch (Exit, Core, Stop, Ignore)
- Ignoriere das Signal
- Empfange und verarbeite das Signal
Signalhändler installieren (signal.h)
void (*signal (int sig, void (*disp) (int))) (int)
void (*sigset (int sig, void (*disp) (int))) (int)
Mit signal installierte Signalhändler werden nach dem ersten Aufruf entfernt, mit
sigset gesetzte Händler bleiben erhalten.
#include
#include
#include
#include
<stdio.h>
<signal.h>
<unistd.h>
<stdlib.h>
void sig_handler(int sig) {
printf("\nsignal %d received\n",sig);
}
int main() {
int i;
- Signals
- (unnamed) Pipes
- FIFO (named Pipes)
- Semaphores
- shared memory
- message queues
- shared filesystem, lock files
Named Pipes - ähnlich wie Dateien (sys/types.h, sys/stat.h)
int mkfifo(const char *path, mode_t mode)
mit mode werden die Zugriffsrechte gesetzt (wie bei "normalen" Dateien)
#include
#include
#include
#include
#include
int main() {
char *tmpfilename = tmpnam(NULL);
char buf[256];
int rdes,wdes;
pid_t pid;
if (signal(SIGINT, sig_handler) == SIG_ERR) // oder sigset
{
perror("SIGINT");
exit(1);
}
if (mkfifo(tmpfilename,0644)) exit(1);
pid = fork();
if (pid > 0) { // parent
if ((wdes = open(tmpfilename,O_WRONLY | O_NDELAY))==-1) exit(1);
puts("sender is ready");
for (;;) {
gets(buf);
printf("process %d sends msg [%s] to fifo\n",getpid(),buf);
write(wdes,buf,256);
}
}
else if (pid == 0) {
// child
if ((rdes = open(tmpfilename,O_RDONLY))==-1) exit(1);
puts("receiver is ready");
for (;;) {
read(rdes,buf,256);
printf("process %d got msg [%s] from fifo\n",getpid(),buf);
sleep(1);
}
}
else {
puts("fork failed");
exit(1);
}
return 0;
for (i=0; ;i++) {
printf("%d\n",i);
sleep(1);
}
}
verwendet signal:
verwendet sigset:
mathias@ed a.out
0
1
^C
signal 2 received
2
^C
mathias@ed
mathias@ed a.out
0
^C
signal 2 received
1
^C
signal 2 received
2
^C
signal 2 received
3
<stdio.h>
<sys/types.h>
<sys/stat.h>
<unistd.h>
<fcntl.h>
}
Page 24
C Tutorial © Mathias Waack <[email protected]>
Page 25
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Page 26
Einführung in die Programmiersprache C
Sockets (II)
Sockets (I)
Kommunikation über Rechnergrenzen, Client-Server-Paradigma
SERVER
create a socket
socket( ... )
CLIENT
create a socket
socket( ... )
Akzeptieren einer Verbindung (sys/types.h, sys/socket.h)
int accept(int socket, struct sockaddr *addr, int *addrlen)
blockiert wenn keine Verbindungen in der Queue sind
addr enthält die Adreßinformationen des Clients, addrlen die Länge der Struktur
Erstellen einer Verbindung auf Client-Seite (sys/types.h, sys/socket.h)
int connect(int socket, struct sockaddr *name, int namelength)
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
assign a name to the socket
bind( ... )
establish a queue for connections
listen( ... )
<stdio.h>
<string.h>
<ctype.h>
<stdlib.h>
<unistd.h>
<sys/types.h>
<sys/socket.h>
<netdb.h>
<netinet/in.h>
<arpa/inet.h>
#define ERROR(x) { perror(x); exit(1); }
short PORT = 7777;
static char buf[BUFSIZ];
extract a connection from the queue
accept( ... )
established
initiate a connection
connect( ... )
communicate
read( ... )
write( ... )
communicate
write( ... )
read( ... )
int main(int argc, char *argv[]) {
int orig_sock, new_sock, clnt_len;
struct sockaddr_in clnt_adr, serv_adr;
int len;
if (argc == 2) PORT += atoi(argv[1]);
if ((orig_sock = socket(AF_INET,SOCK_STREAM, 0))<0) ERROR("socket");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(PORT);
if (bind(orig_sock,(struct sockaddr *)&serv_adr,sizeof(serv_adr))<0)
ERROR("bind");
if (listen(orig_sock,5) <0) ERROR("listen");
Socket erstellen (sys/types.h, sys/socket.h)
int socket(int family, int type, int protocol)
family == PF_INET, type == SOCK_STREAM, protocol == 0
dem Socket einen Namen zuweisen (sys/types.h, sys/socket.h)
int bind(int socket, const struct sockaddr *name, int namelen)
clnt_len = sizeof(clnt_adr);
if ((new_sock = accept(orig_sock,(struct sockaddr *)&clnt_adr,
&clnt_len)) < 0)
ERROR("accept");
do {
memset(buf,0,BUFSIZ);
len = read(new_sock, buf, BUFSIZ);
if (!strncmp("quit",buf,4)) {
close(new_sock);
close(orig_sock);
exit(0);
}
printf("got msg: %s\n",buf);
} while(1);
struct sockaddr { u_short sa_family, char sa_data[14]; }
struct sockaddr_in {
short sin_family;
// == AF_INET
u_short sin_port;
// 16Bit port-nr > 1024
struct in_addr sin_addr; // ref to address structure
char sin_zero[8];
// for future extension
}
Erstellen der Queue für die Verbindungen (sys/types.h, sys/socket.h)
int listen(int socket, int backlog)
backlog gibt die max. Größe der Queue an
}
C Tutorial © Mathias Waack <[email protected]>
Page 27
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Page 28
Einführung in die Programmiersprache C
Debugging (III)
Posix-Threads (I)
Debugger mit grafischer Oberfläche
Klassiker: xxgdb
~ sind unabhängige Befehlssequenzen innerhalb eines Prozesses
~ teilen sich alle Resourcen (Speicher, Filedeskriptoren,...) eines Prozesses
moderner: ddd
Vorteile:
- Fenster für aktuellen Sourcecode,
- grafische Darstellung von Datenstrukturen
- erhöhter Durchsatz,
- Ausnutzung von Shared-Memory-Systemen mit mehreren Prozessoren,
- vermeiden von Deadlocks ...
Deklarationen in pthread.h
Erzeugen eines Threads:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_fkt)(void *), void *arg);
thread liefert die Thread-ID des gestarteten Threads, attr enthält Attribute des Threads
(wenn NULL, werden Default-Werte angenommen)
Beenden eines Threads:
void pthread_exit(void *status)
status kann von einen wartenden Thread verarbeitet werden
Ein return aus der Startroutine beendet Threads ebenso
Warten auf die Beendigung eines Threads:
int pthread_join(pthread_t thread, void **status);
Weitere Funktionen:
pthread_t pthread_self(); // liefert eigene ID
int pthread_equal(pthread_t t1,pthread_t t2); // vergleicht zwei Threads
Synchronisation
Zugriff auf knappe Resourcen, atomare Operationen, gegenseitiger Ausschluß
Bsp. Producer/Consumer:
#define next(x) (( (x) + 1) % BUF_SIZE;
data buf[BUF_SIZE];
int rdptr = 0, wrptr = 0;
Producer
Consumer
while (work_to_do) {
buf[wrptr] = produce();
while (next(wrptr)==rdptr) ;
wrptr = next(wrptr);
}
while (work_to_do) {
while (wrptr == rdptr) ;
consume(buf[rdptr]);
rdptr = next(rdptr);
}
Problem: Consumer sieht schon den "neuen" wrptr aber noch die alten Daten (weakly ordered Speicherzugriffe) --> Synchronisation notwendig
C Tutorial © Mathias Waack <[email protected]>
Page 29
Einführung in die Programmiersprache C
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Posix-Threads (II)
Posix-Threads (III)
Mutex - garantiert exclusive Ausführung kritischer Code-Sequenzen
Condition Variable - warten auf Ereignisse
pthread_mutex_t lock;
pthread_cond_t cond;
Initialisierung:
Initialisierung:
int pthread_mutex_init(pthread_mutex_t *mp,
const pthread_mutexattr_t *attr);
oder statisch:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
löschen eines Mutex:
int pthread_mutex_destroy(pthread_mutex_t *mp);
Lock/Unlock:
int pthread_mutex_lock(pthread_mutex_t *mp);
int pthread_mutex_unlock(pthread_mutex_t *mp);
Ein Thread welcher einen Mutex lockt, ist sein Besitzer. Nur der Besitzer kann einen Mutex
"unlocken".
Bsp. Consumer/Producer mit Mutexen:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
Page 30
int pthread_cond_init(pthread_cond_t *cond,
const pthread_condattr_t *attr);
oder statisch:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
löschen einer Condition-Variable:
int pthread_cond_destroy(pthread_cond_t *cond);
Warten auf ein Ereignis:
int pthread_cond_wait(pthread_cond_t *cond,
pthread_mutex_t *mutex);
blockiert den aufrufenden Thread bis ein anderer Thread das Eintreten des Ereignisses cond
signalisiert. Der Mutex mutex wird unlocked bei Aufruf von pthread_cond_wait und
wieder gelockt, bevor die Funktion beendet wird.
Producer
Consumer
int pthread_cond_signal(pthread_cond_t *cond);
Condition-Variablen vermeiden ein busy-wait:
while (work_to_do) {
pthread_mutex_lock(&mutex);
buf[wrptr] = produce();
while (next(wrptr)==rdptr) ;
wrptr = next(wrptr);
pthread_mutex_unlock(&mutex);
}
while (work_to_do) {
pthread_mutex_lock(&mutex);
while (wrptr == rdptr) ;
consume(buf[rdptr]);
rdptr = next(rdptr);
pthread_mutex_unlock(&mutex);
}
for(;;) {
pthread_mutex_lock(mutex);
if (cond) break;
pthread_mutex_unlock(mutex);
}
<hier ist die Bedingung erfüllt>
pthread_mutex_unlock(mutex);
Nachteile: verbraucht sehr viel Resourcen, verlangt Programmierdisziplin
Version mit Condition-Variablen:
Vorsicht bei der Verwendung automatischer Variablen als Argumente bzw. Ergebnisse von
Threads:
pthread_t t;
void *g(int *i) {
int res;
/* berechnet irgendwas, benutzt dabei *i, Eregebnis in res */
return &res;
// das geht nicht gut! Warum?
}
void f() {
int a = 7;
pthread_create(&t, NULL, g, &a);
}
// auch das geht nicht gut! Warum?
pthread_mutex_lock(mutex);
pthread_cond_wait(cond,mutex);
<hier ist die Bedingung erfüllt>
pthread_mutex_unlock(mutex);
Es ist möglich, daß ein pthread_cond_signal mehr als einen Thread aufweckt, ebenso kann ein pthread_cond_wait scheitern. Daher sollte nach dem Aufruf von
pthread_cond_wait getestet werden, ob die Bedingung auch wirklich erfüllt ist (siehe
Bsp. )
C Tutorial © Mathias Waack <[email protected]>
Page 31
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Einführung in die Programmiersprache C
Posix-Threads (IV)
Posix-Threads (V)
Beispiel: FIFO-Queue für Datenpointer. Es soll für beliebig viele Thread möglich sein,
Pointer in das FIFO einzufügen bzw. zu entnehmen.
void put_cb_data(circ_buf_t *cbp, void *data)
{
pthread_mutex_lock(&cbp->buf_lock);
/* wait while the buffer is full */
while (cbp->num_full == QSIZE)
pthread_cond_wait(&cbp->notfull, &cbp->buf_lock);
cbp->data[(cbp->start_idx + cbp->num_full) % QSIZE] = data;
cbp->num_full += 1;
pthread_cond_signal(&cbp->notempty);
pthread_mutex_unlock(&cbp->buf_lock);
}
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define QSIZE
10
typedef struct {
pthread_mutex_t buf_lock;
int start_idx;
int num_full;
pthread_cond_t notfull;
pthread_cond_t notempty;
void *data[QSIZE];
} circ_buf_t;
/* number of pointers in the queue */
/*
/*
/*
/*
/*
/*
lock the structure */
start of valid data */
# of full locations */
full -> not full condition */
empty -> notempty condition */
Circular buffer of pointers */
void *get_cb_data(circ_buf_t *cbp)
{
void *data;
pthread_mutex_lock(&cbp->buf_lock);
/* wait while there's nothing in the buffer */
while (cbp->num_full == 0)
pthread_cond_wait(&cbp->notempty, &cbp->buf_lock);
data = cbp->data[cbp->start_idx];
cbp->start_idx = (cbp->start_idx + 1) % QSIZE;
cbp->num_full -= 1;
pthread_cond_signal(&cbp->notfull);
pthread_mutex_unlock(&cbp->buf_lock);
return (data);
circ_buf_t *new_cb()
{
circ_buf_t *cbp;
cbp = (circ_buf_t *) malloc(sizeof (circ_buf_t));
if (cbp == NULL)
return (NULL);
pthread_mutex_init(&cbp->buf_lock, NULL);
pthread_cond_init(&cbp->notfull, NULL);
pthread_cond_init(&cbp->notempty, NULL);
cbp->start_idx = 0;
cbp->num_full = 0;
return (cbp);
}
void delete_cb(circ_buf_t *cbp)
{
pthread_mutex_destroy(&cbp->buf_lock);
pthread_cond_destroy(&cbp->notfull);
pthread_cond_destroy(&cbp->notempty);
free(cbp);
}
new_cd legt einen neuen Puffer an und initialisiert den Mutex buf_lock und die Condition-Variables notfull und notempty. delete_cb löscht den Mutex und die Condition-Variables und anschließend den Puffer.
Page 32
}
put_cb_data fügt einen Datenpointer data in den Puffer cbp ein. Dazu wird wie folgt
vorgegangen: Zuerst wird der Puffer gelockt. Dann wird gewartet, bis Platz im Puffer ist.
Anschließend wird das Datum in den Puffer eingefügt, evtl. wartenden Threads signalisiert,
daß der Puffer jetzt nicht leer ist, und schließlich wird der Lock auf den Puffer wieder freigegeben.
get_cb_data liefert das älteste Datum im Puffer. Dazu wird zuerst der Puffer gelockt.
Dann wird gewartet, bis der Puffer nicht leer ist. Anschließend wird das Datum dem Puffer
entnommen, evtl. wartenden Threads signalisiert, daß der Puffer jetzt nicht voll ist, und zuletzt wird der Puffer freigegeben und das Datum zurückgeliefert.
C Tutorial © Mathias Waack <[email protected]>
Einführung in die Programmiersprache C
Weitergehend Literatur
Prozesse und -kommunikation:
John Shapley Gray: Interprocess Communications in Unix. Prentice Hall, 1997
Threads:
Nichols, Buttlar, Farrell: Phtreads Programming. O’Reilly, 1996
Kleiman, Shah, Smaalders: Programming with Threads. SunSoft Press, 1996
Lex/Yacc:
Helmut Herold: lex und yacc. Addison-Wesley, 1992
Grundlagen:
Betriebssysteme: Tanenbaum
Compilerbau (u.a. Scanner, Parser): Aho, Sethi, Ullmann: Compilerbau.
Algorithmen: Segdewick: Algorithmen
Page 33