Vorlesungsskript von E. Wallacher (WS 2005/6)

Vorlesungsskript von E. Wallacher (WS 2005/6)
Modellierung/Programmierung
Vorlesungsskript
Dipl.-Math. Erik Wallacher
FR 6.1 - Mathematik
Universit¨at des Saarlandes
Wintersemester 2005/2006
Inhaltsverzeichnis
1
Das erste Programm
1.1
1.2
1.3
1.4
1.5
1.6
2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Variablen, Datentypen und Operationen
2.1
2.2
2.3
2.4
2.5
2.6
2.7
2.8
2.9
3
1
Allgemeine Grundlagen und Organisation
Programmieren in C . . . . . . . . . . . .
Das Betriebssystem LINUX . . . . . . .
Arbeiten unter Windows . . . . . . . . .
Ein erstes C-Programm . . . . . . . . . .
Interne Details beim Compilieren . . . . .
Deklaration, Initialisierung, Definition . . . . . . . . .
Elementare Datentypen . . . . . . . . . . . . . . . . .
Felder und Strings . . . . . . . . . . . . . . . . . . . .
2.3.1 Felder . . . . . . . . . . . . . . . . . . . . . .
2.3.2 Mehrdimensionale Felder . . . . . . . . . . .
2.3.3 Zeichenketten (Strings) . . . . . . . . . . . . .
Ausdr¨ucke, Operatoren und mathematische Funktionen
2.4.1 Arithmetische Operatoren . . . . . . . . . . .
2.4.2 Vergleichsoperatoren . . . . . . . . . . . . . .
2.4.3 Logische Operatoren . . . . . . . . . . . . . .
2.4.4 Bitorientierte Operatoren . . . . . . . . . . . .
2.4.5 Inkrement- und Dekrementoperatoren . . . . .
2.4.6 Adressoperator . . . . . . . . . . . . . . . . .
2.4.7 Priorit¨aten von Operatoren . . . . . . . . . . .
Operationen mit vordefinierten Funktionen . . . . . . .
2.5.1 Mathematische Funktionen . . . . . . . . . . .
2.5.2 Funktionen f¨ur Zeichenketten (Strings) . . . .
Zusammengesetzte Anweisungen . . . . . . . . . . . .
N¨utzliche Konstanten . . . . . . . . . . . . . . . . . .
Typkonversion (cast) . . . . . . . . . . . . . . . . . .
Standardein- und -ausgabe . . . . . . . . . . . . . . .
2.9.1 Ausgabe . . . . . . . . . . . . . . . . . . . . .
2.9.2 Eingabe . . . . . . . . . . . . . . . . . . . . .
1
1
2
5
5
6
8
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
8
9
11
11
11
12
13
14
15
16
16
18
19
19
21
21
22
23
24
25
26
26
27
Programmflusskontrolle
30
3.1
30
30
Bedingte Ausf¨uhrung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1.1 Die if()-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . .
I
3.2
3.3
4
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6.2
6.3
6.4
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
39
40
41
42
43
47
Deklaration, Definition und R¨uckgabewerte
Lokale und globale Variablen . . . . . . . .
Call by value . . . . . . . . . . . . . . . .
Call by reference . . . . . . . . . . . . . .
Rekursive Programmierung . . . . . . . . .
Kommandozeilen-Parameter . . . . . . . .
Wie werden Deklarationen gelesen . . . . .
Zeiger auf Funktionen . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
47
49
51
53
54
55
57
57
60
Strukturen . . . . . . . . . . . . . . . . .
6.1.1 Deklaration von Strukturen . . . .
6.1.2 Definition von Strukturvariablen .
6.1.3 Felder von Strukturen . . . . . .
6.1.4 Zugriff auf Strukturen . . . . . .
6.1.5 Zugriff auf Strukturen mit Zeigern
6.1.6 Geschachtelte Strukturen . . . . .
6.1.7 Listen . . . . . . . . . . . . . .
Unions . . . . . . . . . . . . . . . . . . .
Aufz¨ahlungstyp . . . . . . . . . . . . . .
Allgemeine Typendefinition . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
Arbeiten mit Dateien
7.1
7.2
7.3
7.4
7.5
7.6
31
33
33
36
37
38
39
Adressen . . . . . . . . . . . . . . . . . . . .
Pointervariablen . . . . . . . . . . . . . . . . .
Adressoperator und Zugriffsoperator . . . . . .
Zusammenhang zwischen Zeigern und Feldern
Dynamische Felder mittels Zeiger . . . . . . .
Strukturierte Datentypen
6.1
7
.
.
.
.
.
.
Funktionen
5.1
5.2
5.3
5.4
5.5
5.6
5.7
5.8
6
.
.
.
.
.
.
Zeiger (Pointer)
4.1
4.2
4.3
4.4
4.5
5
3.1.2 Die switch()-Anweisung . . . . . . . . . . .
Schleifen . . . . . . . . . . . . . . . . . . . . . . .
3.2.1 Der Z¨ahlzyklus (for-Schleife) . . . . . . . .
3.2.2 Abweisender Zyklus (while-Schleife) . . . .
3.2.3 Nichtabweisender Zyklus (do-while-Schleife)
Anweisungen zur unbedingten Steuerungs¨ubergabe .
60
60
61
61
61
62
63
64
66
67
68
69
Dateien o¨ ffnen und schließen . . . .
Existenz einer Datei pr¨ufen . . . . .
Zeichenorientierte Ein- und Ausgabe
Bin¨are Ein- und Ausgabe . . . . . .
Dateien l¨oschen/umbenennen . . . .
Positionierung in einem Datenstrom
.
.
.
.
.
.
II
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
69
71
71
77
79
81
7.7
8
Dateiausgabe umlenken . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Mehrdateiprojekte
8.1
8.2
8.3
8.4
82
84
Verteilung des Codes auf Dateien . . . . . . . . .
Manuelles Kompilieren eines Mehrdateiprojektes
Automatisiertes Kompilieren mit make . . . . .
Eigene Bibliotheken . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
A
86
89
90
93
95
A.1 Zahlendarstellung im Rechner und Computerarithmetik . . . . . . . . . . . .
A.2 IEEE Gleitkommazahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . .
A.3 Computerarithmetik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Anhang
95
97
99
95
A.4 Die O-Notation . . . . . . . . . . . . .
A.5 Der Pr¨aprozessor . . . . . . . . . . . .
A.5.1 Dateien einf¨ugen (# include) . .
A.5.2 Konstanten definieren (#define)
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
101
104
104
106
Literaturverzeichnis
107
Index
108
III
1 Das erste Programm
1.1 Allgemeine Grundlagen und Organisation
Die Programmentwicklung erfolgt grunds¨atzlich in vier Schritten.
1.) Problemstellung : Welche Aufgabe soll der Computer bew¨altigen.
2.) Erarbeiten eines Algorithmus zur L¨osung der Aufgabe.
3.) Erstellen (editieren) eines Quelltextes zur Umsetzung des Algorithmus.
¨
4.) Ubersetzen
(compilieren bzw. interpretieren) des Quelltextes in einen Code, der vom Computer verstanden werden kann.
Ein Computerprogramm ist also die Umsetzung eines Algorithmus in eine Form, die von
einem Computer verarbeitet werden kann.
Der Begriff Programm wird sowohl f¨ur den in einer Programmiersprache verfassten Quelltext,
als auch f¨ur den von einem Computer ausf¨uhrbaren Maschinencode verwendet. Um aus dem
Quelltext den Maschinencode zu generieren, wird ein Compiler oder Interpreter ben¨otigt. Diese u¨ bersetzen die Befehle der Programmiersprache, die f¨ur menschliche Benutzer verst¨andlich
und bearbeitbar sein sollen, in die semantisch entsprechenden Befehle der Maschinensprache
des verwendeten Computers.
1.2 Programmieren in C
Wie aus Kapitel 1.1 schon hervorgeht wird zum Programmieren (neben einem Computer) ein
¨
Editor und ein Ubersetzer
ben¨otigt.
Editor
Ein Editor ist ein Programm zum Erstellen und Ver¨andern von Textdateien. Speziell wird es
bei der Programmierung zum Erstellen des Quellcodes verwandt.
1
¨
Ubersetzer
(Compiler, Linker)
¨
Wie bei allen Compiler-Sprachen besteht der Ubersetzer
aus zwei Hauptkomponenten: dem
eigentlichen Compiler und dem Linker. Der Compiler erzeugt aus dem Quelltext einen f¨ur
den Rechner lesbaren Objektcode. Der Linker erstellt das ausf¨uhrbare Programm, indem es in
die vom Compiler erzeugte Objektdatei Funktionen (siehe auch Kapitel 5) aus Bibliotheken
(Libraries) einbindet.
Der Begriff Compiler wird h¨aufig auch als Synonym f¨ur das gesamte Entwicklungssystem
(Compiler, Linker, Bibliotheken) verwendet.
Bemerkung:
¨
F¨ur die Bearbeitung der Ubungsaufgaben
werden Editor und C-Compiler bereitgestellt.
Es handelt sich dabei um frei erh¨altliche (kostenlose) Programme, die auf LINUXBetriebssystemen (siehe Kapitel 1.3) arbeiten. Grundlegende Kenntnisse u¨ ber LINUX
¨
werden in der ersten Ubung
vermittelt. Die Aufgaben k¨onnen an den an der Universit¨at
verf¨ugbaren Rechnern oder in Heimarbeit erledigt werden. F¨ur die Bearbeitung unter
Windows siehe Kapitel 1.4.
Warnung:
Es handelt sich bei C um eine weitgehend standardisierte Programmiersprache. Dennoch
ist C-Compiler nicht gleich C-Compiler. Die Vergangenheit hat gezeigt, dass nicht alle Programme unter verschiedenen Compilern lauff¨ahig sind. Wer mit einem anderen als den von
uns bereitgestellten Compilern arbeitet, hat unter Umst¨anden mit Schwierigkeiten bei der
¨
Kompatibilit¨at zu rechnen. In den Ubungen
kann hierauf keine R¨ucksicht genommen werden.
1.3 Das Betriebssystem LINUX
LINUX wurde von dem finnischen Informatikstudenten Linus Torvals entwickelt, um PCs
kompatibel zu den u.a. im universit¨aren Umfeld weit verbreiteten UNIX-Systemen zu betreiben. Die rasch wachsende Beliebheit f¨uhrte dazu, dass heute eine großen Programmierergemeinde zum gr¨oßten Teil unentgeltlich die Entwicklung weiterf¨uhrt. LINUX ist frei erh¨altlich
und kann kostenlos weitergeben werden. Das System wird in unterschiedlichen Varianten
(Distributionen) angeboten.
Dateien und Verzeichnisse
Daten werden auf dem Medium in Dateien zusammengefasst abgespeichert. Die Datei wird
mit einem bestimmten Dateinamen bezeichnet.
Folgendes ist f¨ur Dateinamen zu beachten:
• LINUX unterscheidet zwischen Groß- und Kleinschreibung.
2
Wurzelverzeichnis/
`
P
```
@P
PP```
PP ```
@
```
PP
```
@
P
P
`
@
P
bin/
etc/
home/
P
...
usr/
var/
@PPP
PP
@
PP
@
PP
@
P
modprog000/
X
...
uebung1/
`
...
modprog999/
HXX
HHXXX
HH XXX
XXX
HH
XX
uebung12/
PP
```
@ P
```
PP
@
PP`````
```
PP
@
``
P
@
P
aufg1.c
aufg1.out
aufg2.c
aufg2.out
Abbildung 1.1: Der Verzeichnisbaum am Beispiel von LINUX
• Der Schr¨agstrich / darf nicht verwendet werden, da er zur Trennung von Verzeichnisnamen verwendet wird.
• Sonderzeichen sollte man nicht verwenden, da einige eine spezielle Bedeutung haben.
Mehrere Dateien k¨onnen in einem Verzeichnis gesammelt werden. LINUX besitzt ein hierarchisches, baumstrukturiertes Dateisystem: Ausgehend von dem Wurzelverzeichnis / besitzt
der Verzeichnisbaum die Struktur in Abbildung 1.1.
Die Position eines beliebigen (Unter-)Verzeichnisses innerhalb des Dateisystems ist durch
den Pfad gegeben. Der Pfad gibt an, wie man vom Wurzelverzeichnis zu der gew¨unschten Datei bzw. dem gew¨unschten Verzeichnis gelangt. Die Unterverzeichnisnamen im Pfad werden
durch den Schr¨agstrich getrennt.
Pfade, die mit / beginnen, heißen absolute Pfade. Daneben gibt es die so genannten relativen
Pfade, die relativ zur aktuellen Position im Verzeichnisbaum aufzufassen sind. Die folgenden
relativen Pfadbezeichner haben eine besondere Bedeutung:
.
..
aktuelle Position im Verzeichnisbaum (Arbeitsverzeichnis)
das u¨ bergeordnee Verzeichnis
Der absolute Pfad der Datei aufg1.c lautet beispielsweise /home/modprog000/uebung1/aufg1.c.
3
Es ist m¨oglich, mehrere Dateien oder Verzeichnisse gleichzeitig anzusprechen. Dazu dienen
die so genannten Wildcards:
*
?
[zeichen1 - zeichen2]
wird durch beliebig viele Zeichen (auch keines) ersetzt
wird durch ein Einzelzeichen ersetzt
Auswahlsequenz; wird ersetzt durch alle Zeichen
zwischen zeichen1 und zeichen2
Die Shell
Ein elementares Programm ist die Shell. Sie ist ein Kommandointerpreter, der von der Kommandozeile Anweisungen einliest, auf ihre Korrektheit u¨ berpr¨uft und ausf¨uhrt. Hier ist eine
¨
kleine Ubersicht
u¨ ber die allerwichtigsten Befehle (Kommandos).
cd Verzeichnis
pwd
cp Datei Pfad
cp -R Verzeichnis1 Verzeichnis2
mv Pfad1 Pfad2
ls
ls Verzeichnis
ls -l Pfad
rm Datei
rm -i Datei
rm -f Datei
rm -Rf Verzeichnis
mkdir Verzeichnis
rmdir Verzeichnis
cat Datei
more Datei
wechsele in das angegebene Verzeichnis
zeige die aktuelle Position im Verzeichnisbaum an
kopiere Datei an die angegebene Position
kopiere rekursiv, d.h. mit allen Unterverzeichnissen
verschiebe Pfad1 nach Pfad2
zeige Inhalt des aktuellen Verzeichnisses an
zeige Inhalt von Verzeichnis an
ausf¨uhrliche Anzeige von Informationen
l¨osche Datei
l¨osche Datei mit Best¨atigung
l¨osche Datei ohne Best¨atigung
l¨osche Verzeichnis rekursiv ohne Best¨atigung
erzeuge neues Verzeichnis
l¨osche (leeres) Verzeichnis
zeige Inhalt von Datei
zeige Inhalt von Datei seitenweise
Hilfe zu Kommandos
Die man-Anweisung zeigt eine Dokumentation (Manpage) zu einem Befehl an. Man verwendet sie einfach durch
man Befehlsname
Sucht man Befehle, die mit einem bestimmten Schl¨usselbegriff zusammenh¨angen, so leistet
der Aufruf
man -k Schl¨usselbegriff
das Gew¨unschte.
4
1.4 Arbeiten unter Windows
Wie schon in Kapitel 1.2 angemerkt, kann es bei Verwendung von unterschiedlichen C-Compilern
zu Schwierigkeiten kommen. In dieser Vorlesung beziehen wir uns stets auf den unter LINUX
verwendeten GNU-C-Compiler. F¨ur die Nutzung dieses Programms unter Windows muss eine
LINUX a¨ hnliche Plattform unter Windows simuliert werden. Diese Aufgabe u¨ bernimmt das
sogenannte Programmpaket Cygwin. Wir stellen hierf¨ur eine Basisausstattung von Cygwin
bereit. Auf Anfrage erh¨alt jeder Teilnehmer leihweise eine CD mit Installationsanleitung.
1.5 Ein erstes C-Programm
Aufgabe: Der Computer soll nur eine Meldung auf dem Bildschirm schreiben.
Quelltext: (HalloWelt.c):
/* HalloWelt.c */
#include <stdio.h>
int main()
{
printf("Hallo Welt \n");
return 0;
}
/* "\n new line */
Folgende Strukturen finden wir in diesem ersten einfachen Programm vor:
• Kommentare
werden mit /* eingeleitet und mit */ beendet. Sie k¨onnen sich u¨ ber mehrere Zeilen erstrecken und werden vom Compiler (genauer vom Pr¨aprozessor ) entfernt.
• Pr¨aprozessordirektiven
werden mit # eingeleitet. Sie werden vom Pr¨aprozessor ausgewertet. Die Direktive #include bedeutet, dass die nachfolgende Headerdatei einzuf¨ugen ist. Headerdateien haben die Dateinamenendung (Suffix) .h. Die hier einzuf¨ugende Datei stdio.h enth¨alt die
ben¨otigten Informationen zur standardm¨aßigen Ein- und Ausgabe von Daten (standard
input/output).
• Das Schl¨usselwort main markiert den Beginn des Hauptprogramms, d.h. den Punkt, an
dem die Ausf¨uhrung der Anweisungen beginnt. Auf die Bedeutung des Schl¨usselwortes
void und der Klammer () wird sp¨ater detaillierter eingegangen.
• Syntaktisch (und inhaltlich) zusammengeh¨orende Anweisungen werden in Bl¨ocken zusammengefasst. Dies geschieht durch die Einschließung eines Blocks in geschweifte
Klammern:
5
{
...
Erste Anweisung
...
Letzte Anweisung
}
• Die erste Anweisung, die wir hier kennenlernen, ist printf(). Sie ist eine in stdio.h deklarierte Funktion, die Zeichenketten (Strings) auf dem Standardausgabeger¨at (Bildschirm)
ausgibt. Die auszugebende Zeichenkette wird in Anf¨uhrungsstriche gesetzt.
Zus¨atzlich wird eine Escapesequenz angef¨ugt: \n bedeutet, dass nach der Ausgabe des
Textes Hallo Welt eine neue Zeile begonnen wird. Anweisungen innerhalb eines Blocks
werden mit Semikolon ; abgeschlossen.
Der u¨ bliche Suffix f¨ur C-Quelldateien ist .c und wir nehmen an, dass der obige Code in der
Datei hallo.c abgespeichert ist.
¨
Die einfachste Form des Ubersetzungsvorgangs
ist die Verwendung des folgenden Befehls in
der Kommandozeile:
gcc hallo.c
gcc ist der Programmname des GNU-C-Compilers. Der Aufruf des Befehls erzeugt die ausf¨uhrbare
Datei a.out (a.exe unter Windows). Nach Eingabe von
./a.out (bzw. ./a.exe)
wird das Programm gestartet. Auf dem Bildschirm erscheint die Ausgabe ”Hallo Welt”.
(Das Voranstellen von ./ kann weggelassen werden, falls sich das Arbeitsverzeichnis ./ im
Suchpfad befindet. Durch Eingabe von
export PATH=$PATH:.
wird das Arbeitsverzeichnis in den Suchpfad aufgenommen).
1.6 Interne Details beim Compilieren
Der leicht ge¨anderte Aufruf zum Compilieren
gcc -v hallo.c
erzeugt eine l¨angere Bilschirmausgabe, welche mehrere Phasen des Compilierens anzeigt. Im
folgenden einige Tipps, wie man sich diese einzelnen Phasen anschauen kann, um den Ablauf
besser zu verstehen:
6
a) Pr¨aprozessing:
Headerfiles (*.h) werden zur Quelldatei hinzugef¨ugt (+ Makrodefinitionen, bedingte Compilierung)
gcc -E hallo.c > hallo.E
Der Zusatz > hallo.E lenkt die Bildschirmausgabe in die Datei hallo.E. Diese Datei hallo.E
kann mit einem Editor angesehen werden und ist eine lange C-Quelltextdatei.
¨
b) Ubersetzen
in Assemblercode:
Hier wird eine Quelltextdatei in der (prozessorspezifischen) Programmiersprache Assembler erzeugt.
gcc -S hallo.c
Die entstandene Datei hallo.s kann mit dem Editor angesehen werden.
c) Objektcode erzeugen:
Nunmehr wird eine Datei erzeugt, welche die direkten Steuerbefehle, d.h. Zahlen, f¨ur den
Prozessor beinhaltet.
gcc -c hallo.c
Die Ansicht dieser Datei mit einem normalen Texteditor liefert eine unverst¨andliche Zeichenfolge. Einblicke in die Struktur vom Objektcodedateien k¨onnen mit Hilfe eines Monitors (auch ein Editor Programm) erfolgen.
hexedit hallo.o
(Nur falls das Programm hexedit oder ein anderer Monitor installiert ist.)
d) Linken:
Verbinden aller Objektdateien und notwendigen Bibliotheken zum ausf¨uhrbaren Programm
Dateiname.out (Dateiname.exe unter Windows).
gcc -o Dateiname hallo.c
7
2 Variablen, Datentypen und
Operationen
2.1 Deklaration, Initialisierung, Definition
F¨ur die Speicherung und Manipulation von Ein- und Ausgabedaten sowie der Hilfsgr¨oßen eines Algorithmus werden bei der Programmierung Variablen eingesetzt. Je nach Art der Daten
w¨ahlt man einen von der jeweiligen Programmiersprache vorgegebenen geeigneten Datentyp
aus. Vor ihrer ersten Verwendung m¨ussen die Variablen durch Angabe ihres Typs und ihres
Namens deklariert werden. In C hat die Deklaration die folgende Form.
Datentyp Variablenname;
Man kann auch mehrere Variablen desselben Typs auf einmal deklarieren, indem man die
entsprechenden Variablennamen mit Komma auflistet:
Datentyp Variablenname1, Variablenname2, ...,VariablennameN;
Bei der Deklaration k¨onnen einer Variablen auch schon Werte zugewiesen werden, d.h. eine
Initialisierung der Variablen ist bereits m¨oglich. Zusammen mit der Deklaration gilt die Variable dann als definiert.
¨
Die Deklaration von Variablen findet vor der ersten Ausfuhrungsanweisung
statt.
¨
Dies ist bei den allermeisten Compilern nicht zwingend notwendig, dient aber der Ubersicht
des Quelltextes.
Variablennamen
Bei der Vergabe von Variablennamen ist folgendes zu beachten:
• Variablennamen d¨urfen keine Umlaute enthalten. Als einzigstes Sonderzeichen ist der
Unterstrich (engl. underscore) erlaubt.
• Variablennamen d¨urfen Zahlen enthalten, aber nicht mit ihnen beginnen.
• Groß- und Kleinschreibung von Buchstaben wird unterschieden.
8
¨
Schlusselwort
char
int
float
double
void
Datentyp
Zeichen
ganze Zahl
Gleitkommazahl mit einfacher Genauigkeit
Gleitkommazahl mit doppelter Genauigkeit
leerer Datentyp
Anzahl Bytes
1
4
4
8
Abbildung 2.1: Elementare Datentypen. Die Bytel¨ange ist von Architektur zu Architektur unterschiedlich (hier: GNU-C-Compiler unter LINUX f¨ur die x86-Architektur.
Siehe auch sizeof()).
2.2 Elementare Datentypen
¨
Die folgende Tabelle 2.1 gibt die Ubersicht
u¨ ber die wichtigsten Datentypen in C:
Anhang A widmet sich speziell der Zahlendarstellung im Rechner. Insbesondere werden dort
die Begriffe Gleitkommazahl und deren Genauigkeit er¨ortert.
Beispiel 2.1 (Deklaration, Initialisierung, Definition)
#include <stdio.h>
int main()
{
int a=4;
/* Deklaration von a als ganze Zahl */
/* + Initialisierung von a, d.h. a wird der Wert 4 zugewiesen */
printf("Die int-Variable a wurde initialisiert mit %i\n" ,a );
/* Die Formatangabe %i zeigt an, dass eine int-Variable
ausgegeben wird */
return 0;
}
Der Datentyp char wird intern als ganzzahliger Datentyp behandelt. Er kann daher mit allen
Operatoren behandelt werden, die auch f¨ur int verwendet werden. Erst durch die Abbildung
der Zahlen von 0 bis 255 auf entsprechende Zeichen (ASCII-Tabelle) entsteht die Verkn¨upfung
zu den Zeichen.
Einige dieser Datentypen k¨onnen durch Voranstellen von weiteren Schl¨usselw¨ortern modifiziert werden. Modifizierer sind:
• signed/unsigned: Gibt f¨ur die Typen int und char an, ob sie mit/ohne Vorzeichen behandelt werden (nur int und char).
• short/long: Reduziert/erh¨oht die Bytel¨ange des betreffenden Datentyps. Dabei wirkt sich
short nur auf int und long nur auf double aus.
9
• const: Eine so modifizierte Variable kann initialisiert, aber danach nicht mehr mit einem
anderen Wert belegt werden. Die Variable ist schreibgesch¨utzt“.
”
Bei den zul¨assigen Kombinationen ist die Reihenfolge
const - signed/unsigned - long/short Datentyp Variablenname
Beispiel 2.2 (Deklaration / Definition von Variablen)
Zul¨assig:
int a;
signed char zeichen1;
unsigned short int b; oder a¨ quivalent unsigned short b;
long double eps;
const int c=12;
Im letzten Beispiel wurde der Zuweisungsoperator = (s. Abschnitt 2.4) verwendet, um die
schreibgesch¨utzte Variable c zu initialisieren.
Variablen vom Typ char werden durch sogenannte Zeichenkonstanten initialisiert. Zeichenkonstanten gibt man an, indem man ein Zeichen in Hochkommata setzt, z.B.
char zeichen1=’A’;
Nicht zul¨assig:
unsigned double d;
long char zeichen1;
char 1zeichen; (unzul¨assiger Variablenname)
Die Funktion sizeof() liefert die Anzahl der Bytes zur¨uck, die f¨ur einen bestimmten Datentyp
ben¨otigt werden. Die Funktion sizeof() hat als R¨uckgabewert den Typ int.
Beispiel 2.3 (C-Anweisung : sizeof())
/* Beispiel: sizeof() */
# include <stdio.h>
int main()
{
printf("Eine int-Variable benoetigt %i Bytes", sizeof(int) );
return 0;
}
10
2.3 Felder und Strings
2.3.1 Felder
Eine M¨oglichkeit, aus elementaren Datentypen weitere Typen abzuleiten, ist das Feld (Array).
Ein Feld besteht aus n Objekten des gleichen Datentyps. Die Deklaration eines Feldes ist von
der Form
Datentyp Feldname[n];
Weitere Merkmale:
• Die Nummerierung der Feldkomponenten beginnt bei 0 und endet mit n-1.
• Die i-te Komponente des Feldes wird mit Feldname[i] angesprochen.
• Felder k¨onnen bei der Deklaration initialisiert werden. Dies geschieht unter Verwendung
des Zuweisungsoperators und der geschweiften Klammer.
Beispiel 2.4 (Felder)
#include<stdio.h>
int main()
{
float a[3]={3.2, 5, 6};
/* Deklaration und Initialisierung eines (1 x 3) float-Feldes */
printf("Die 0.-te Komponente von a hat den Wert %f" ,a[0] );
/* Die Formatangabe %f zeigt an, dass eine float bzw.
double-Variable ausgegeben wird */
return 0;
}
2.3.2 Mehrdimensionale Felder
Es ist m¨oglich, die Eintr¨age eines Feldes mehrfach zu indizieren und so h¨oherdimensionale
Objekte zu erzeugen; f¨ur d Dimensionen lautet die Deklaration dann:
Datentyp Feldname[n1 ][n2 ]...[nd ];
11
Beispiel 2.5 (Deklaration und Initialisierung einer ganzzahligen 2 x 3-Matrix)
#include <stdio.h>
int main()
{
int a[2][3]={{1, 2, 3}, {4, 5, 6}};
printf("Die [0,1].-te Komponente von a hat den Wert %i",a[0][1]);
return 0;
}
2.3.3 Zeichenketten (Strings)
Eine Sonderstellung unter den Feldern nehmen die Zeichenketten (Strings) ein. Es handelt
sich dabei um Felder aus Zeichen:
char Stringname [L¨ange]
Eine Besonderheit stellt dar, dass das Stringende durch die Zeichenkonstante ’\0’ markiert
wird. Der String Hallo wird also durch
char text[]={’H’,’a’,’l’,’l’,’o’,’\0’};
initialisiert.
Ein String kann auch durch
char text[]=“Hallo“;
initialisiert werden. Dieser String hat auch die L¨ange 6, obwohl nur 5 Zeichen zur Initialisierung benutzt wurden. Das Ende eines Strings markiert immer die Zeichenkonstante ’\0’.
Beispiel 2.6 (Deklaration und Initialisierung eines Strings)
#include <stdio.h>
int main()
{
char text[]="Hallo";
printf("%s" ,text);
/* Die Formatangabe %s zeigt an, dass ein String ausgegeben wird. */
return 0;
}
12
¨
2.4 Ausdrucke,
Operatoren und mathematische
Funktionen
Der Zuweisungsoperator
operand1 = operand2
weist dem linken Operanden den Wert des rechten Operanden zu.
Zum Beispiel ist im Ergebnis der Anweisungsfolge
Beispiel 2.7 (Zuweisungsoperator)
#include <stdio.h>
int main()
{
int x,y;
x=2;
y=x+4;
printf("x=%i und y=%i",x,y);
/* Formatangabe %i gibt dem printf-Befehl an,
* dass an dieser Stelle eine Integervariable
* ausgeben werden soll. */
return 0;
}
der Wert von x gleich 2 und der Wert von y gleich 6. Hierbei sind x, y, 0, x+4 Operanden,
wobei letzterer gleichzeitig ein Ausdruck, bestehend aus den Operanden x, 4 und dem Operator + ist. Sowohl x=2 als auch y=x+4 sind Ausdr¨ucke. Erst das abschließende Semikolon ;
wandelt diese Ausdr¨ucke in auszuf¨uhrende Anweisungen.
Es k¨onnen auch Mehrfachzuweisungen auftreten. Die folgenden drei Zuweisungen sind a¨ quivalent.
Beispiel 2.8 (Mehrfachzuweisung)
#include <stdio.h>
int main()
{
int a,b,c;
/* 1. Moeglichkeit */
a = b = c = 123;
/* 2. Moeglichkeit */
13
a = (b = (c = 123));
/* 3. Moeglichkeit (Standard) */
c = 123;
b=c;
a=b;
printf("a=%i, b=%i, c=%i",a,b,c);
return 0;
}
2.4.1 Arithmetische Operatoren
Un¨are Operatoren
Bei un¨aren Operatoren tritt nur ein Operand auf.
Operator
-
Beschreibung
Negation
Beispiel
-a
Bin¨are Operatoren
Bei bin¨aren Operatoren treten zwei Operanden auf. Der Ergebnistyp der Operation h¨angt vom
Operator ab.
Operator
+
*
/
%
Beschreibung
Addition
Subtraktion
Multiplikation
Division (Achtung bei Integerwerten !!!)
Rest bei ganzzahliger Division (Modulooperation)
Beispiel
a+b
a-b
a*b
a/b
a%b
Achtung!!! Die Division von Integerzahlen berechnet den ganzzahligen Anteil der Division,
z.B. liefert 8/3 das Ergebnis 2. Wird jedoch einer der beiden Operanden in eine Gleitkommazahl umgewandelt, so erh¨alt man das exakte Ergebnis. z.B. 8.0/3 liefert 2.66666 als Ergebnis
(siehe auch Kapitel 2.8).
Analog zur Mathematik gilt ”Punktrechnung geht vor Strichrechnung ”. Desweiteren werden
Ausdr¨ucke in runden Klammern zuerst berechnet.
Beispiel 2.9 (Arithmetische Operatoren)
% ermoeglicht das Setzen von mathematischen Ausdruecken
% wird hier fuer die Referenz benutzt
#include <stdio.h>
int main()
14
{
int a,b,c;
double x;
a=1;
a=9/8;
a=3.12;
a=-3.12;
/*
/*
/*
/*
a=1 */
a=1, Integerdivision */
a=3, abrunden wegen int-Variable */
a=-3 oder -4, Compiler abhaengig */
b=6;
/* b=6 */
c=10;
/* c=10 */
x=b/c;
/* x=0 */
x=(double) b/c; /* x=0.6 siehe Kapitel 2.8 */
x=(1+1)/2;
/* x=1 */
x=0.5+1.0/2; /* x=1 */
x=0.5+1/2;
/* x=0.5 */
x=4.2e12;
/* x=4.2*10ˆ{12} wissenschaftl. Notation */
return 0;
}
2.4.2 Vergleichsoperatoren
Vergleichsoperatoren sind bin¨are Operatoren. Der Ergebniswert ist immer ein Integerwert. Sie
liefern den Wert 0, falls die Aussage falsch, und den Wert 1, falls die Aussage richtig ist.
Operator
>
>=
<
<=
==
!=
Beschreibung
gr¨oßer
gr¨oßer oder gleich
kleiner
kleiner oder gleich
gleich (Achtung bei Gleitkommazahlen !!!)
ungleich (Achtung bei Gleitkommazahlen !!!)
Beispiel
a>b
a>=b
a<b/3
a*b<=c
a==b
a!=3.14
Achtung !!!
Ein typischer Fehler tritt beim Test auf Gleichheit auf, indem statt des Vergleichsoperators
== der Zuweisungsoperator = geschrieben wird.
Das Pr¨ufen von Gleitkommazahlen auf (Un-)gleichheit kann nur bis auf den Bereich der
Maschinengenauigkeit erfolgen und sollte daher vermieden werden.
Beispiel 2.10 (Vergleichsoperatoren)
#include <stdio.h>
int main()
15
{
int a,b;
int aussage;
float x,y;
a=3;
b=2;
aussage = a>b;
aussage = a==b;
/*
/*
/*
/*
a=3 */
b=2 */
aussage=1 ; entspricht wahr */
aussage=0 ; entspricht falsch */
x=1.0+1.0e-8;
y=1.0+2.0e-8;
aussage = (x==y);
/* x=1 + 1.0 *10ˆ{-8} */
/* y=1 + 2.0 *10ˆ{-8} */
/* aussage=0 oder 1 ; entspricht wahr,
falls eps > 10ˆ{-8}, obwohl x ungleich y */
return 0;
}
2.4.3 Logische Operatoren
Es gibt nur einen un¨aren logischen Operator
Operator
!
Beschreibung
logische Negation
Beispiel
! (3>4)
/* Ergebnis= 1; entspricht wahr */
und zwei bin¨are logische Operatoren
Operator
&&
||
Beschreibung
logisches UND
logisches ODER
Beispiel
(3>4) && (3<=4) /* Ergebnis = 0; entspricht falsch */
(3>4) | | (3<=4) /* Ergebnis = 1; entspricht wahr */
Die Wahrheitstafeln f¨ur das logische UND und das logische ODER sind aus der Algebra bekannt.
2.4.4 Bitorientierte Operatoren
Bitorientierte Operatoren sind nur auf int-Variablen (bzw. char-Variablen) anwendbar. Um die
Funktionsweise zu verstehen, muss man zun¨achst die Darstellung von Ganzzahlen innerhalb
des Rechners verstehen.
Ein Bit ist die kleinste Informationseinheit mit genau zwei m¨oglichen Zust¨anden:
(
(
(
bit ungesetzt
0
falsch
≡
≡
bit gesetzt
1
wahr
Ein Byte besteht aus 8 Bit. Eine short int-Variable besteht aus 2 Byte. Damit kann also eine
short int-Variable 216 Werte annehme. Das erste Bit bestimmt das Vorzeichen der Zahl. Gesetzt
bedeutet - (negativ); nicht gesetzt entspricht + (positiv).
16
Beispiel 2.11 ((Short)-Integerdarstellung im Rechner)
Darstellung im Rechner (bin¨ar)
0 0000000 00001010
|{z}
| {z }
| + {z
1. Byte
Dezimal
23 + 21 = 10
2. Byte
}
−(25 + 22 ) − 1 = −37
1 1111111 11011011
|{z}
| {z }
−
|
{z
} 2. Byte
1. Byte
¨ bitorientierte Operatoren
Unare
Operator
∼
Beschreibung
Bin¨arkomplement
Beispiel
∼a
¨ bitorientierte Operatoren
Binare
Operator
&
|
∧
<<
>>
Beschreibung
bitweises UND
bitweises ODER
bitweises exklusives ODER
Linksshift der Bits von op1 um op2 Stellen
Rechtsshift der Bits von op1 um op2 Stellen
Beispiel
a&1
a |1
a∧1
a << 1
a >> 2
Wahrheitstafel
x
0
0
1
1
y
0
1
0
1
x&y
0
0
0
1
x |y
0
1
1
1
x∧y
0
1
1
0
Beispiel 2.12 (Bitorientierte Operatoren)
#include <stdio.h>
int main()
{
short int a,b,c;
a=5;
b=6;
/* 00000000 00000101 = 5 */
/* 00000000 00000110 = 6 */
c= ˜ b;
c=a & b;
/* Komplement 11111111 11111001 =-(2ˆ2+2ˆ1)-1=-7 */
/* 00000000 00000101 = 5
*/
17
/*
/*
/*
/*
bit-UND &
00000000 00000110 = 6
gleich
00000000 00000100 = 4
*/
*/
*/
*/
c=a | b;
/* bit-ODER 00000000 00000111 = 7 */
c=aˆb;
/* bit-ODER exklusiv 00000000 00000011 = 3
c=a << 2;
/* 2 x Linksshift 00000000 00010100 = 20
c=a >> 1;
/* 1 x Rechtsshift & 00000000 00000010 = 2 */
*/
*/
return 0;
}
¨
Anwendungen zu bitorientierten Operatoren werden in den Ubungen
besprochen.
2.4.5 Inkrement- und Dekrementoperatoren
Pr¨afixnotation
Notation
++ operand
−− operand
Beschreibung
operand=operand+1
operand=operand-1
Beispiel 2.13 (Pr¨afixnotation)
#include <stdio.h>
int main()
{
int i=3,j;
++i; /* i=4 */
j=++i; /* i=5, j=5 */
/* oben angegebene Notation ist aequivalent zu */
i=i+1;
j=i;
return 0;
}
18
Postfixnotation
Notation
operand++
operand−−
Beschreibung
operand=operand+1
operand=operand-1
Beispiel 2.14 (Postfixnotation)
#include <stdio.h>
int main()
{
int i=3,j;
i++; /* i=4 */
j=i++; /* j=4 ,i=5 */
/* oben angegebene Notatation ist aequivalent zu */
j=i;
i=i+1;
return 0;
}
2.4.6 Adressoperator
Der Vollst¨andigkeit halber wird der Adressoperator “ & “ schon in diesem Kapitel eingef¨uhrt,
obwohl die Bedeutung erst in Kapitel 4 klar wird.
& datenobjekt
¨
2.4.7 Prioritaten
von Operatoren
Es k¨onnen beliebig viele Aussagen durch Operatoren verkn¨upft werden. Die Reihenfolge der
Ausf¨uhrung h¨angt von der Priorit¨at der jeweiligen Operatoren ab. Operatoren mit h¨oherer
Priorit¨at werden vor Operatoren niedriger Priorit¨at ausgef¨uhrt. Haben Operatoren die gleiche
Priorit¨at so werden sie gem¨aß ihrer sogenannten Assoziativit¨at von links nach rechts oder umgekehrt abgearbeitet.
Priorit¨aten von Operatoren beginnend mit der H¨ochsten
19
Priorit¨at
15
14
13
12
11
10
9
8
7
6
5
4
3
2
1
Operator
()
[]
−>
.
+
!
˜
++
−−
++
−−
&
∗
(Typ)
sizeof()
∗
/
%
+
<<
>>
<
<=
>
>=
==
!=
&
∧
|
&&
||
:?
=
∗ =, / =, + =
− =, & =, ∧ =
| =, <<= >>=
,
Beschreibung
Funktionsaufruf
Indizierung
Elementzugriff
Elementzugriff
Vorzeichen
Vorzeichen
Negation
Bitkomplement
Pr¨afix-Inkrement
Pr¨afix-Dekrement
Postfix-Inkrement
Postfix-Dekrement
Adresse
Zeigerdereferenzierung
Cast
Gr¨oße
Multiplikation
Division
Modulo
Addition
Subtraktion
Links-Shift
Rechts-Shift
kleiner
kleiner gleich
gr¨oßer
gr¨oßer gleich
gleich
ungleich
bitweises UND
bitweises exklusivers ODER
bitweises ODER
logisches UND
logisches ODER
Bedingung
Zuweisung
Zusammengesetzte Zuweisung
Zusammengesetzte Zuweisung
Zusammengesetzte Zuweisung
Komma-Operator
Im Zweifelsfall kann die Priorit¨at durch Klammerung erzwungen werden.
20
Assoz.
→
→
→
→
←
←
←
←
←
←
←
←
←
←
←
←
→
→
→
→
→
→
→
→
→
→
→
→
→
→
→
→
→
→
←
←
←
←
←
→
Beispiel 2.15 (Priorit¨aten von Operatoren)
#include <stdio.h>
int main()
{
int a=-4, b=-3, c;
c=a<b<-1;
/* c=0 ; falsch */
c=a<(b<-1);
/* c=1 ; wahr */
c=a ==-4 && b == -2; /* c=0 ; falsch */
return 0;
}
2.5 Operationen mit vordefinierten Funktionen
2.5.1 Mathematische Funktionen
Im Headerfile math.h werden u.a. Deklarationen der in Tabelle 2.1 zusammengefassten mathematischen Funktionen und Konstanten bereitgestellt:
Funktion/Konstante
sqrt(x)
exp(x)
log(x)
pow(x,y)
fabs(x)
fmod(x,y)
ceil(x)
floor(x)
sin(x), cos(x), tan(x)
asin(x), acos(x), atan(x)
ME
M PI
Beschreibung
Wurzel von x
ex
nat¨urlicher Logarithmus von x
xy
Absolutbetrag von x : |x|
realzahliger Rest von x/y
n¨achste ganze Zahl ≥ x
n¨achste ganze Zahl ≤ x
trigonometrische Funktionen
trig. Umkehrfunktionen
Eulersche Zahl e
π
Tabelle 2.1: Mathematische Funktionen
F¨ur die Zul¨assigkeit der Operation, d.h. den Definitionsbereich der Argumente, ist der Programmierer verantwortlich. Ansonsten werden Programmabbr¨uche oder unsinnige Ergebnisse
produziert.
21
Beispiel 2.16 (Mathematische Funktionen und Konstanten)
#include <stdio.h>
#include <math.h>
int main()
{
float x,y,z;
x=4.5;
/* x=4.5 */
y=sqrt(x);
/* y=2.121320, was ungefaehr = sqrt(4.5) */
z=M_PI;
/* z=3.141593, was ungefaehr = pi */
return 0;
}
¨ Zeichenketten (Strings)
2.5.2 Funktionen fur
Im Headerfile string.h werden u.a. die Deklarationen der folgenden Funktionen f¨ur Strings
bereitgestellt:
Funktion
strcat(s1,s2)
strcmp(s1,s2)
strcpy(s1,s2)
strlen(s)
strchr(s,c)
Beschreibung
Anh¨angen von s2 an s1
Lexikographischer Vergleich der Strings s1 und s2
Kopiert s2 auf s1
Anzahl der Zeichen in String s ( = sizeof(s)-1)
Sucht Zeichenkonstante (Character) c in String s
Tabelle 2.2: Funktionen f¨ur Strings
¨ Zeichenketten (Strings))
Beispiel 2.17 (Funktionen fur
#include <string.h>
#include <stdio.h>
int main()
{
int i;
char s1[]="Hallo";
/* reserviert 5+1 Byte im Speicher fuer s1
und belegt sie mit H,a,l,l,o,\0
*/
char s2[]="Welt";
/* reserviert 4+1 Byte im Speicher f¨
ur s2 */
char s3[100]="Hallo"; /* reserviert 100 Byte im Speicher f¨
ur s3
* und belegt die ersten 6 mit H,a,l,l,o,\0 */
22
/* !!!NICHT ZULAESSIG!!! (Kann zu Programmabsturz fuehren) *** */
strcat(s1,s2);
/* Im reservierten Speicherbereich von s1
* steht nun H,a,l,l,o,W
* Der Rest von s2 wird irgendwo in den
* Speicher geschrieben */
/* ZULAESSIG */
strcat(s3,s2);
/* Die ersten 10 Bytes von s3 sind nun
* belegt mit H,a,l,l,o,W,e,l,t,\0
* Der Rest ist zufaellig beschrieben */
strcpy(s1,s2);
/* Die ersten 5 Bytes von s1 sind nun
* belegt mit W,e,l,t,\0 */
i=strlen(s2);
/* i=4 */
i=strcmp(s2,s3);
/* i=15, Unterschied zwischen ’W’ und ’H’ in
* ASCII*/
return 0;
}
Achtung
Der Umgang mit Strings ist problematisch, z.B. wird bei dem Befehl strcat(s1,s2) der String
s2 an s1 angeh¨angt. Dadurch wird der Speicherbedarf f¨ur String s1 vergr¨oßert. Wurde bei der
Deklaration von s1 zu wenig Speicherplatz reserviert (allokiert) schreibt der Computer die
u¨ bersch¨ussigen Zeichen in einen nicht vorher bestimmten Speicherbereich. Dies kann unter
Umst¨anden sogar zum Absturz des Programms f¨uhren – die Alternative sind seltsame und
schwer zu findende Fehler des Programms (siehe auch Beipiel 2.17).
2.6 Zusammengesetzte Anweisungen
Wertzuweisungen der Form
op1=op1 operator op2
k¨onnen zu
op1 operator = op2
verk¨urzt werden.
Hierbei ist operator ∈ {+, −, ∗, /, %, |, ∧ , <<, >>}.
Beispiel 2.18 (Zusammengesetzte Anweisungen)
#include <stdio.h>
int main()
{
int i=7,j=3;
23
i += j; /* i=i+j; */
i >>= 1; /* i=i >> 1 (i=i/2) */
j *= i; /* j=j*i */
return 0;
}
¨
2.7 Nutzliche
Konstanten
F¨ur systemabh¨angige Zahlenbereiche, Genauigkeiten usw. ist die Auswahl der Konstanten aus
Tabelle 2.3 und Tabelle 2.4 recht hilfreich. Sie stehen dem Programmierer durch Einbinden
der Headerdateien float.h bzw. limits.h zur Verf¨ugung.
Tabelle 2.3: Konstanten aus float.h
Konstante
FLT DIG
FLT MIN
FLT MAX
FLT EPSILON
DBL
LDBL
Beschreibung
Anzahl g¨ultiger Dezimalstellen f¨ur float
Kleinste, darstellbare echt positive float Zahl
Gr¨oßte, darstellbare positive float Zahl
Kleinste positive Zahl mit 1.0 + eps , 1.0
wie oben f¨ur double
wie oben f¨ur long double
Tabelle 2.4: Konstanten aus limits.h
Konstante
INT MIN
INT MAX
SHRT
Beschreibung
Kleinste, darstellbare int Zahl
Gr¨oßte, darstellbare int Zahl
wie oben f¨ur INT
Weitere Konstanten k¨onnen in der Datei float.h nachgeschaut werden. Der genaue Speicherort
dieser Datei ist abh¨angig von der gerade verwendeten Version des gcc und der verwendeten
Distribution. Die entsprechenden Headerfiles k¨onnen auch mit dem Befehl
find /usr -name float.h -print
gesucht werden. Dieser Befehl durchsucht den entsprechenden Teil des Verzeichnisbaums
(/usr) nach der Datei namens float.h
24
2.8 Typkonversion (cast)
Beispiel 2.19 (Abgeschnittene Division)
Nach der Zuweisung hat die Variable quotient den Wert 3.0, obwohl sie als Gleitkommazahl
deklariert wurde!
Ursache: Resultat der Division zweier int-Variablen ist standardm¨asig wieder ein int-Datenobjekt.
#include <stdio.h>
int main()
{
int a=10, b=3;
float quotient;
quotient = a/b;
/* quotient = 3 */
quotient = (double) a/b; /* quotient = 3.3333 */
return 0;
}
Abhilfe schaffen hier Typumwandlungen (engl.: Casts).
Dazu setzt man den gew¨unschten Datentyp in Klammern vor das umzuwandelnde Objekt, im
obigen Beispiel:
quotient = (float) a/b;
Hierdurch wird das Ergebnis mit den Nachkommastellen u¨ bergeben.
Vorsicht bei Klammerung von Ausdr¨ucken! Die Anweisung
quotient = (float) (a/b);
f¨uhrt wegen der Klammern die Division komplett im int-Kontext durch und der Cast bleibt
wirkungslos.
Bemerkung 2.20
Die im ersten Beispiel gezeigte abgeschnittene Division erlaubt in Verbindung mit dem Modulooperator % eine einfache Programmierung der Division mit Rest.
Ist einer der Operanden eine Konstante, so kann man auch auf Casts verzichten: Statt
quotient = (float) 10/b;
kann man die Anweisung
quotient = 10.0/b;
verwenden.
25
2.9 Standardein- und -ausgabe
Eingabe: Das Programm fordert ben¨otigte Informationen/Daten vom Benutzer an.
Ausgabe: Das Programm teilt die Forderung nach Eingabedaten dem Benutzer mit und gibt
(Zwischen-) Ergebnisse aus.
2.9.1 Ausgabe
Die Ausgabe auf das Standardausgabeger¨at (Terminal, Bildschirm) erfolgt mit der printf()Bibliotheksfunktion. Die Anweisung ist von der Form
printf(Formatstringkonstante, Argumentliste);
Die Argumentliste ist eine Liste von auszugebenden Objekten, jeweils durch ein Komma getrennt (Variablennamen, arithmetische Ausdr¨ucke etc.).
Die Formatstringkonstante enth¨alt zus¨atzliche spezielle Zeichen: spezielle Zeichenkonstanten
(Escapesequenzen) und Formatangaben.
Zeichenkonstante
\n
\t
\v
\b
\\
\?
\’
\”
erzeugt
neue Zeile
Tabulator
vertikaler Tabulator
Backspace
Backslash \
Fragezeichen ?
Hochkomma
Anf¨uhrungsstriche
Die Formatangaben spezifizieren, welcher Datentyp auszugeben ist und wie er auszugeben ist.
Sie beginnen mit %.
¨
Die folgende Tabelle gibt einen Uberblick
u¨ ber die wichtigsten Formatangaben:
Formatangabe
%f
%i, %d
%u
%o
%x
%c
%s
%li, %ld
%Lf
%e
Datentyp
float, double
int, short
unsigned int
int, short oktal
int, short hexadezimal
char
Zeichenkette (String)
long
long double
float, double wissenschaftl. Notation
26
• Durch Einf¨ugen eines Leerzeichens nach % wird Platz f¨ur das Vorzeichen ausgespart.
Nur negative Vorzeichen werden angezeigt. F¨ugt man stattdessen ein + ein, so wird das
Vorzeichen immer angezeigt.
• Weitere Optionen kann man aus Beispiel 2.21 entnehmen.
Beispiel 2.21 (Ausgabe von Gleitkommazahlen)
#include <stdio.h>
int main()
{
const double pi=3.14159265;
printf("Pi
printf("Pi
printf("Pi
printf("Pi
printf("Pi
=
=
=
=
=
\%f\n",pi);
\% f\n",pi);
\%+f\n",pi);
\%.3f\n",pi);
\%.7e\n",pi);
return 0;
}
erzeugen die Bildschirmausgabe
Pi = 3.141593
Pi = 3.141593
Pi = +3.141593
Pi = 3.142
Pi = 3.1415927e+00
2.9.2 Eingabe
F¨ur das Einlesen von Tastatureingaben des Benutzers steht u.a. die Bibliotheksfunktion scanf()
zur Verf¨ugung. Ihre Verwendung ist auf den ersten Blick identisch mit der von printf().
scanf(Formatstringkonstante, Argumentliste);
Die Argumentliste bezieht sich auf die Variablen, in denen die eingegebenen Werte abgelegt
werden sollen, wobei zu beachten ist, dass in der Argumentliste nicht die Variablen selbst,
sondern ihre Adressen anzugeben sind. Dazu verwendet man den Adressoperator &.
27
Beispiel 2.22 (Einlesen einer ganzen Zahl)
#include <stdio.h>
int main()
{
int a;
printf("Geben Sie eine ganze Zahl ein: ");
scanf("%i",&a);
printf("a hat nun den Wert : %i",a);
return 0;
}
Die eingegebene Zahl wird als int interpretiert und an der Adresse der Variablen a abgelegt.
Die anderen Formatangaben sind im Wesentlichen analog zu printf(). Eine Ausnahme ist das
Einlesen von double- und long double-Variablen. Statt %f sollte man hier
• %lf f¨ur double
• %Lf f¨ur long double
verwenden. Das Verhalten variiert je nach verwendetem C-Compiler.
Achtung !
Handelt es sich bei der einzulesenden Variable um ein Feld (insbesondere String) oder eine
Zeiger Variable (siehe Kapitel 4), so entf¨allt der Adressoperator & im scanf()-Befehl.
Bsp.:
char text[100];
scanf(”%s”,text);
Die Funktion scanf ist immer wieder eine Quelle f¨ur Fehler.
int zahl;
char buchstabe;
scanf("%i", &zahl);
scanf("%c", &buchstabe);
Wenn man einen solchen Code laufen l¨asst, wird man sehen, dass das Programm den zweiten
scanf -Befehl scheinbar einfach u¨ berspringt. Der Grund ist die Art, wie scanf arbeitet. Die
Eingabe des Benutzers beim ersten scanf besteht aus zwei Teilen: einer Zahl (sagen wir 23)
und der Eingabetaste (die wir mit ’\n’ bezeichnen). Die Zahl 23 wird in die Variable zahl
kopiert, das ’\n’ steht aber immer noch im sog. Tastaturpuffer. Beim zweiten scanf liest der
Rechner dann sofort das ’\n’ aus und geht davon aus, dass der Benutzer dieses ’\n’ als Wert
f¨ur die Variable buchstabe wollte. Vermeiden kann man dies mit einem auf den ersten Blick
komplizierten Konstrukt, das daf¨ur deutlich flexibler ist.
28
int zahl;
char buchstabe;
char tempstring[80];
fgets(tempstring, sizeof(tempstring), stdin);
/* wir lesen eine ganze Zeile in den String tempstring von stdin -* das ist die Standardeingabe
*/
/* Wir haben jetzt einen ganzen String, wie teilen wir ihn auf?
* => mit der Funktion sscanf
*/
sscanf(tempstring, "%d", &zahl);
/* und nun nochmal fuer den Buchstaben */
fgets(tempstring, sizeof(tempstring), stdin);
sscanf(tempstring, "%c", &buchstabe);
Der R¨uckgabewert von fgets ist ein Zeiger; der obige Code u¨ berpr¨uft nicht, ob dies ein NULL
¨
Zeiger ist – diese Uberpr¨
ufung ist in einem Programm nat¨urlich Pflicht! Die Funktionen fgets
und sscanf sind in stdio.h deklariert.
29
3 Programmflusskontrolle
¨
3.1 Bedingte Ausfuhrung
Bei der Bedingten Ausf¨uhrung werden Ausdr¨ucke auf ihren Wahrheitswert hin u¨ berpr¨uft und
der weitere Ablauf des Programms davon abh¨angig gemacht. C sieht hierf¨ur die Anweisungen
if und switch vor:
3.1.1 Die if()-Anweisung
Die allgemeine Form der Verzweigung (Alternative) ist
if (logischer Ausdruck)
{
Anweisungen A
}
else
{
Anweisungen B
}
und z¨ahlt ihrerseits wiederum als Anweisung. Der else-Zweig kann weggelassen werden (einfache Alternative). Folgt nach dem if- bzw. else-Befehl nur eine Anweisung, so muss diese
nicht in einen Block (geschweifte Klammern) geschrieben werden. Dies sollte dann aber durch
Schreiben der einzigen Anweisung hinter den if -Befehl deutlich gemacht werden!
Beispiel 3.1 (Signum-Funktion)
Die Signum-Funktion gibt das Vorzeichen an:



1




y(x) = 
0




−1
30
x>0
x=0
x<0
int main() /* Signum Funktion */
{
float x,y;
if (x>0.0)
{
y=1.0;
}
else
{
if (x == 0.0)
{
y=0.0;
}
else
{
y=-1.0;
}
}
return 0;
}
3.1.2 Die switch()-Anweisung
Zur Unterscheidung von mehreren F¨allen ist die Verwendung von switch-case-Kombinationen
bequemer. Mit dem Schl¨usselwort switch wird ein zu u¨ berpr¨ufender Ausdruck benannt. Es
folgt ein Block mit case-Anweisungen, die f¨ur die einzelnen m¨oglichen F¨alle Anweisungsbl¨ocke vorsehen. Mit dem Schl¨usselwort default wird ein Anweisungsblock eingeleitet, der
dann auszuf¨uhren ist, wenn keiner der anderen F¨alle eingetreten ist (optional).
switch (Ausdruck)
{
case Fall 1:
{
Anweisungen f¨
ur Fall 1
break;
}
...
case Fall n:
{
Anweisungen f¨
ur Fall n
break;
}
default:
{
31
Anweisungen f¨
ur alle anderen F¨
alle
break;
}
}
Beispiel 3.2 (switch-Anweisung)
#include <stdio.h>
int main()
{
int nummer;
printf("Geben sie eine ganze Zahl an: ");
scanf("%i",&nummer);
printf("Namen der Zahlen aus {1,2,3}
switch (nummer)
{
case 1:
{
printf("Eins = %i \n",
break;
}
case 2:
{
printf("Zwei = %i \n",
break;
}
case 3:
{
printf("Drei = %i \n",
break;
}
default:
{
printf("Die Zahl liegt
break;
}
}
return 0;
}
32
\n");
nummer);
nummer);
nummer);
nicht in der Menge {1,2,3} \n");
3.2 Schleifen
Schleifen dienen dazu, die Ausf¨uhrung von Anweisungsbl¨ocken zu wiederholen. Die Anzahl
der Wiederholungen ist dabei an eine Bedingung gekn¨upft. Zur Untersuchung, ob eine Bedingung erf¨ullt ist, werden Vergleichs- und Logikoperatoren aus Kapitel 2.4 benutzt.
¨
3.2.1 Der Zahlzyklus
(for-Schleife)
Beim Z¨ahlzyklus steht die Anzahl der Zyklendurchl¨aufe a-priori fest, der Abbruchtest erfolgt
vor dem Durchlauf eines Zyklus. Die allgemeine Form ist
for (ausdruck1; ausdruck2; ausdruck3)
{
Anweisungen
}
Am besten versteht man den Z¨ahlzyklus an einem Beispiel.
¨
Beispiel 3.3 (Summe der naturlichen
Zahlen von 1 bis n)
#include <stdio.h>
int main()
{
int i,summe,n;
char tempstring[80];
/* Einlesen der oberen Schranke n
* von der Tastatur */
printf("Obere Schranke der Summe : ");
fgets(tempstring, sizeof(tempstring), stdin);
sscanf(tempstring, "%i", &n);
summe=0;
/* Setze summe auf 0
*/
for (i=1; i<=n; i=i+1)
{
summe=summe+i;
}
printf("Summe der Zahlen von 1 bis %i ist %i \n",n,summe);
return 0;
}
Im obigen Programmbeispiel ist i die Laufvariable des Z¨ahlzyklus, welche mit i=1 (ausdruck1) initialisiert wird, mit i=i+1 (ausdruck3) weitergez¨ahlt und in i <= n (ausdruck2)
bzgl. der oberen Grenze der Schleifendurchl¨aufe getestet wird. Im Schleifeninneren summe=summe+i; (anweisung) erfolgen die eigentlichen Berechnungsschritte des Zyklus. Die
Summationsvariable uss vor dem Eintritt in den Zyklus initialisiert werden.
33
Eine kompakte Version dieser Summationsschleife (korrekt, aber sehr schlecht lesbar) w¨are:
for (summe=0, i=1; i <=n; summe+=i, i++)
Man unterscheidet dabei zwischen den Abschluss einer Anweisung “;“ und dem Trennzeichen
“,“ in einer Liste von Ausdr¨ucken. Diese Listen werden von links nach rechts abgearbeitet.
Der ausdruck2 ist stets ein logischer Ausdruck und ausdruck3 ist ein arithmetischer Ausdruck
zur Manipulation der Laufvariablen. Die Laufvariable kann eine einfache Variable vom Typ
int, float oder double sein.
Achtung!!!
Vorsicht bei der Verwendung von Gleitkommazahlen (float,double) als Laufvariable. Dort
ist der korrekte Abbruchtest wegen der internen Zahlendarstellung unter Umst¨anden nicht
einfach zu realisieren.
Die folgenden Beipiele 3.4, 3.5 verdeutlichen die Problematik der begrenzten Genauigkeit von
Gleitkommazahlen in Verbindung mit Zyklen und einige Tipps zu deren Umgehung.
Beispiel 3.4
Ausgabe der diskreten Knoten xi des Intervalls [a,b], welches in n gleichgroße Teilintervalle
zerlegt wird, d.h.
xi = a + i ∗ h, i = 0, . . . , n mit h =
b−a
n
#include <stdio.h>
int main()
{
float a,b,xi,h;
int n;
char tempstring[80];
a=0.0;
b=1.0;
/* Intervall [a,b] wird initialisiert */
/* mit [0,1] */
printf("Geben Sie die Anzahl der Teilintervalle an: ");
fgets(tempstring, sizeof(tempstring), stdin);
sscanf(tempstring, "%i", &n);
h=(b-a)/n;
n=1; /* n wird nun als Hilfsvariable verwendet */
for (xi=a; xi<=b; xi=xi+h)
{
printf("%i.te Knoten : %f \n",n,xi);
n=n+1;
}
return 0;
}
34
Da Gleitkommazahlen nur eine limitierte Anzahl g¨ultiger Ziffern besitzt, kann es (meistens)
passieren, dass der letzte Knoten xn = b nicht ausgegeben wird. Auswege sind:
¨
1.) Anderung
des Abbruchtests in xi <= b + h/2.0 (jedoch ist xn immer noch fehlerbehaftet).
2.) Zyklus mit int-Variable
for (i=0; i<=n; i++)
{
xi=a+i*h;
printf(“%i.te Knoten : %f \n“,n,xi);
}
Die gemeinsame Summation kleinerer und gr¨oßerer Zahlen kann ebenfalls zu UngenauigkeiP
ten f¨uhren. Im Beispiel 3.5 wird die Summe ni=1 1/i2 auf zwei verschiedene Arten berechnet.
Beispiel 3.5
#include <stdio.h>
#include <math.h>
#include <limits.h> /* enth¨
alt die Konstante INT_MAX */
int main()
{
float summe1 = 0.0,summe2 = 0.0;
int i,n;
char tempstring[80];
printf("Der erste Algorithmus wird ungenau f¨
ur n bei ca. %f \n",
ceil(sqrt(1.0/1.0e-6)) );
/* siehe Kommentar 1.Schranke */
printf("Weitere Fehler ergeben sich f¨
ur n >= %f, \n",
ceil(sqrt(INT_MAX)) );
/* siehe Kommentar 2.Schranke */
printf("Geben Sie die obere Summationsschranke n an : ");
fgets(tempstring, sizeof(tempstring), stdin);
sscanf(tempstring, "%i", &n);
for (i=1;
{
/*
/*
/*
i<=n; i++)
1. Schranke f¨
ur i */
Der Summand 1.0/(i*i) wird bei der Addition */
nich mehr ber¨
ucksichtigt, falls 1.0/(i*i) < 1.0e-6 */
/* 2. Schranke f¨
ur i */
/* Das Produkt i*i ist als int-Variable nicht */
35
/* mehr darstellbar, falls i*i > INT_MAX */
summe1=summe1+1.0/(i*i);
}
for (i=n; i>=1; i--)
{
summe2=summe2+1.0/i/i;
}
printf("Der erste Algorithmus liefert das Ergebnis : %f \n",summe1);
printf("Der zweite Algorithmus liefert das Ergebnis : %f \n",summe2);
return 0;
}
Das numerische Resultat in summe2 ist genauer, da dort zuerst alle kleinen Zahlen addiert
werden, welche bei summe1 wegen der beschr¨ankten Anzahl g¨ultiger Ziffern keinen Beitrag
zur Summation mehr liefern k¨onnen. Gleichzeitig ist zu beachten, dass die Berechnung von i*i
nicht mehr in int-Zahlen darstellbar ist f¨ur i*i > INT MAX. Dagegen erfolgt die Berechnung
1.0/i/i vollst¨andig im Bereich von Gleitkommazahlen.
3.2.2 Abweisender Zyklus (while-Schleife)
Beim abweisenden Zyklus steht die Anzahl der Durchl¨aufe nicht a-priori fest. Der Abbruchtest erfolgt vor dem Durchlauf eines Zyklus.
Die allgemeine Form ist
while(logischer Ausdruck)
{
Anweisungen
}
Beispiel 3.6 (while-Schleife)
F¨ur eine beliebige Anzahl von Zahlen soll das Quadrat berechnet werden. Die Eingabeserie
wird durch die Eingabe von 0 beendet.
#include <stdio.h>
int main()
{
float zahl;
char tempstring[80];
printf("Geben Sie eine Zahl ein (’0’ f¨
ur Ende) : ");
fgets(tempstring, sizeof(tempstring), stdin);
sscanf(tempstring, "%f", &zahl);
36
while (zahl != 0.0)
{
printf("%f hoch 2 = %f \n",zahl,zahl*zahl);
printf("Geben Sie eine Zahl ein (’0’ f¨
ur Ende) : ");
fgets(tempstring, sizeof(tempstring), stdin);
sscanf(tempstring, "%f", &zahl);
}
return 0;
}
3.2.3 Nichtabweisender Zyklus (do-while-Schleife)
Beim nichtabweisenden Zyklus steht die Anzahl der Durchl¨aufe nicht a-priori fest. Der Abbruchtest erfolgt nach dem Durchlauf eines Zyklus. Somit durchl¨auft der nichtabweisende
Zyklus mindestens einmal die Anweisungen im Zykleninneren.
Die allgemeine Form ist
do
{
Anweisungen
}
while(logischer Ausdruck);
Beispiel 3.7 (do-while-Schleife)
Es wird solange eine Zeichenkette von der Tastatur eingelesen, bis die Eingabe eine ganze
Zahl ist.
#include <stdio.h>
#include <math.h> /* F¨
ur pow */
int main()
{
int i, n, zahl=0;
char text[100];
do
{
printf("Geben Sie eine ganze Zahl ein : ");
fgets(text, sizeof(text), stdin);
sscanf(text, "%s",text);
/* Es wird nacheinander gepr¨
uft ob text[i] */
/* eine Ziffer ist. Besteht die Eingabe */
/* nur aus Ziffern, dann wird abgebrochen */
37
i=0;
while (’0’ <= text[i] && text[i] <= ’9’)
{
/* ASCII */
i=i+1;
}
if (text[i] != ’\0’)
{
printf("%c ist keine Ziffer \n",text[i]);
}
}
while (text[i] != ’\0’);
/* Umwandlung von String zu Integer */
n=i; /* Die L¨
ange des Strings == i */
for (i=0;i<=n-1;i++)
{
zahl=zahl+ (text[i]-’0’)*pow(10,n-1-i);
}
printf("Die Eingabe %s entspricht der ganzen Zahl %i\n",text,zahl);
return 0;
}
Intern behandelt der Computer Zeichenkonstanten wie int-Variablen. Die Zuweisung erfolgt
u¨ ber die ASCII-Tabelle. So entsprechen z.B. die Zeichenkonstanten ’0’,...,’9’ den Werten
48,..,57.
¨
3.3 Anweisungen zur unbedingten Steuerungsubergabe
• break Es erfolgt der sofortige Abbruch der n¨achst¨außeren switch-, while-, do-whileoder for-Anweisung.
• continue Abbruch des aktuellen und Start des n¨achsten Zyklus einer while-, do-whileoder for-Schleife.
• goto marke Fortsetzung des Programms an der mit marke markierten Anweisung
Achtung!!!
Die goto-Anweisung sollte sparsam (besser gar nicht) verwendet werden, da sie dem strukturierten Programmieren zuwider l¨auft und den gef¨urchteten Spaghetticode erzeugt. In den
¨
Ubungen
(und in der Klausur) ist die goto-Anweisung zur L¨osung der Aufgaben nicht erlaubt.
38
4 Zeiger (Pointer)
Bislang war beim Zugriff auf eine Variable nur ihr Inhalt von Interesse. Dabei war es unwichtig, wo (an welcher Speicheradresse) der Inhalt abgelegt wurde. Ein neuer Variablentyp, der
Pointer (Zeiger), speichert Adressen unter Ber¨ucksichtigung des dort abgelegten Datentyps.
4.1 Adressen
Das folgende Programm demonstriert, wie man Speicheradressen von Variablen ermittelt.
Beispiel 4.1 (Adressen von Variablen)
#include <stdio.h>
int main()
{
int a=16;
int b=4;
double f=1.23;
float g=5.23;
/* Formatangabe %u steht
/* &a = Adresse von a */
printf("Wert von a = %i,
printf("Wert von b = %i,
printf("Wert von f = %f,
printf("Wert von g = %f,
return 0;
f¨
ur unsigned int */
\t
\t
\t
\t
Adresse
Adresse
Adresse
Adresse
von
von
von
von
a
b
f
g
=
=
=
=
%u
%u
%u
%u
\n",a,(unsigned
\n",b,(unsigned
\n",f,(unsigned
\n",g,(unsigned
int)&a);
int)&b);
int)&f);
int)&g);
}
Nach dem Start des Programms erscheint folgende Ausgabe:
Wert
Wert
Wert
Wert
von
von
von
von
a
b
f
g
=
=
=
=
16,
Adresse von a = 2289604
4,
Adresse von b = 2289600
1.230000, Adresse von f = 2289592
5.230000, Adresse von g = 2289588
Bemerkung 4.2 ( Bemerkung zu Beispiel 4.1)
1.) Dieses Programm zeigt die Werte und die Adressen der Variablen a, b, f, g an. (Die Adressangaben sind abh¨angig vom System und Compiler und variieren dementsprechend).
39
2.) Der Adressoperator im printf()-Befehl sorgt daf¨ur, dass nicht der Inhalt der jeweiligen Variable ausgegeben wird, sondern die Adresse der Variable im Speicher. Die Formatangabe
%u dient zur Ausgabe von vorzeichenlosen Integerzahlen (unsigned int). Dieser Platzhalter ist hier n¨otig, da der gesamte Wertebereich der Ganzzahl ausgesch¨opft werden soll und
negative Adressen nicht sinnvoll sind.
3.) In der letzten Zeile wird angegeben, dass die Variable g auf der Speicheradresse 2289588
liegt und die Variable f auf der Adresse 2289592. Die Differenz beruht auf der Tatsache,
dass die Variable g vom Typ float zur Speicherung sizeof(float)=4 Bytes ben¨otigt. Auch
bei den anderen Variablen kann man erkennen, wieviel Speicherplatz sie aufgrund ihres
Datentyps ben¨otigen.
4.2 Pointervariablen
Eine Pointervariable (Zeigervariable) ist eine Variable, deren Wert (Inhalt) eine Adresse ist.
Die Deklaration erfolgt durch:
Datentyp *Variablenname;
Das n¨achste Programm veranschaulicht diese Schreibweise.
Beispiel 4.3
#include <stdio.h>
int main()
{
int a=16;
int *pa;
/* Deklaration von int Zeiger pa - pa ist ein Zeiger auf
* eine Integer*/
double f=1.23;
double *pf; /* Deklaration von double Zeiger pf */
pa=&a; /* Zeiger pa wird die Adresse von a zugewiesen */
pf=&f; /* Zeiger pf wird die Adresse von f zugewiesen */
printf("Variable a : Inhalt = %i\t Adresse = %u\t
,a,(unsigned int)&a,sizeof(a));
printf("Variable pa : Inhalt = %u\t Adresse = %u\t
,(unsigned int)pa,(unsigned int)&pa,sizeof(pa));
printf("Variable f : Inhalt = %f\t Adresse = %u\t
,f,(unsigned int)&f,sizeof(f));
printf("Variable pf : Inhalt = %u\t Adresse = %u\t
,(unsigned int)pf,(unsigned int)&pf,sizeof(pf));
return 0;
}
40
Gr¨
oße %i \n"
Gr¨
oße %i \n"
Gr¨
oße %i \n"
Gr¨
oße %i \n"
Das Programm erzeugt folgende Ausgabe:
Variable
Variable
Variable
Variable
a : Inhalt = 16
Adresse = 2289604
pa : Inhalt = 2289604 Adresse = 2289600
f : Inhalt = 1.230000 Adresse = 2289592
pf : Inhalt = 2289592 Adresse = 2289588
Gr¨
oße
Gr¨
oße
Gr¨
oße
Gr¨
oße
4
4
8
4
Bemerkung 4.4 ( Bemerkung zu Beispiel 4.3)
1.) Da Pointervariablen wieder eine Speicheradresse besitzen, ist die Definition eines Pointers auf einen Pointer nicht nur sinnvoll, sondern auch n¨utzlich (siehe Beispiel 4.9).
2.) Die Gr¨oße des ben¨otigten Speicherplatzes f¨ur einen Pointer ist unabh¨angig vom Typ der
ihm zu Grunde liegt, da der Inhalt stets eine Adresse ist. Der hier verwendete Rechner
(32-Bit-System) hat einen Speicherplatzbedarf von 4 Byte (= 32 Bit).
4.3 Adressoperator und Zugriffsoperator
Der un¨are Adressoperator & (Referenzoperator)
& Variablenname
bestimmt die Adresse der Variable.
Der un¨are Zugriffsoperator * (Dereferenzoperator)
* pointer
erlaubt den (indirekten) Zugriff auf den Inhalt, auf den der Pointer zeigt. Die Daten k¨onnen
wie Variablen manipuliert werden.
Beispiel 4.5
#include <stdio.h>
int main()
{
int a=16;
int b;
int *p; /* Deklaration von int Zeiger p */
p=&a; /* Zeiger p wird die Adresse von a zugewiesen */
b=*p; /* b = Wert unter Adresse p = a = 16 */
printf("Wert von b = %i = %i = Wert von
printf("Wert von a = %i = %i = Wert von
printf("Adresse von a = %u = %u = Wert
,(unsigned int)&a,(unsigned int)p);
printf("Adresse von b = %u != %u = Wert
41
*p \n",b,*p);
*p \n",a,*p);
von p\n"
von p\n\n"
,(unsigned int)&b,(unsigned int)p);
*p=*p+2; /* Wert unter Adresse p wird um 2 erh¨
oht */
/* d.h. a=a+2
*/
printf("Wert von b = %i != %i = Wert von *p
printf("Wert von a = %i = %i = Wert von *p
printf("Adresse von a = %u = %u = Wert von
,(unsigned int)&a,(unsigned int)p);
printf("Adresse von b = %u != %u = Wert von
,(unsigned int)&b,(unsigned int)p);
return 0;
\n",b,*p);
\n",a,*p);
p\n"
p\n\n"
}
Das Programm erzeugt folgende Ausgabe:
Wert von b = 16
Wert von a = 16
Adresse von a =
Adresse von b =
= 16 = Wert von *p
= 16 = Wert von *p
2289604 = 2289604 = Wert von p
2289600 != 2289604 = Wert von p
Wert von b = 16
Wert von a = 18
Adresse von a =
Adresse von b =
!= 18 = Wert von *p
= 18 = Wert von *p
2289604 = 2289604 = Wert von p
2289600 != 2289604 = Wert von p
4.4 Zusammenhang zwischen Zeigern und Feldern
Felder nutzen das Modell des linearen Speichers, d.h. ein im Index nachfolgendes Element
ist auch physisch im unmittelbar nachfolgenden Speicherbereich abgelegt. Dieser Fakt erlaubt
die Interpretation von Zeigervariablen als Feldbezeichner und umgekehrt.
Beispiel 4.6 (Zeiger und Felder)
#include <stdio.h>
int main()
{
float ausgabe;
float f[4]={1,2,3,4};
float *pf;
pf=f; /* A¨quivalent w¨
are die Zuweisung pf=&f[0] */
42
/* Nicht Zul¨
assige Operationen mit Feldern */
/* f=g;
* f=f+1; */
/* A¨quivalente Zugriffe auf Feldelemente */
ausgabe=f[3];
ausgabe=*(f+3);
ausgabe=pf[3];
ausgabe=*(pf+3);
return 0;
}
Bemerkung 4.7 ( zu Beispiel 4.6)
1.) Das Beispiel zeigt, dass der Zugriff auf einzelne Feldelemente f¨ur Zeiger und Felder
identisch ist, obwohl es sich um unterschiedliche Datentypen handelt.
2.) Die Arithmetischen Operatoren + und - haben bei Zeigern und Feldern auch den gleichen Effekt. Der Ausdruck pf + 3 liefert als Wert
(Adresse in pf) + 3 x sizeof(Typ)
(und nicht (Adresse in pf) + 3 !!!)
3.) Der Zuweisungsoperator = ist f¨ur Felder nicht anwendbar, d.h. f=ausdruck ist nicht
zul¨assig. Einzelne Feldelemente k¨onnen jedoch wie gewohnt manipuliert werden (z.B.
f[2]=g[3] ist zul¨assig).
Die Zuweisung pf=pf+1 hingegen bewirkt, dass pf nun auf f[1] zeigt.
4.) Ein weiterer Unterschied zwischen Feldvariablen und Pointervariablen ist der ben¨otigte
Speicherplatz. Im Beipiel liefert sizeof(pf) den Wert 4 und sizeof(f) den Wert 16 (=4 x
sizeof(float)).
Die folgenden Operatoren sind auf Zeiger anwendbar :
• Vergleichsoperatoren: ==, !=, <, >, <=, >=
• Addition + und Subtraktion • Inkrement ++, Dekrement -- und zusammengesetzte Operatoren +=, -=
4.5 Dynamische Felder mittels Zeiger
Bisher wurde die L¨ange von Feldern bereits bei der Deklaration bzw. Definition angegeben.
Da viele Aufgaben und Probleme stets nach dem selben Prinzip ausgef¨uhrt werden k¨onnen,
m¨ochte man die Feldl¨ange gerne als Parameter und nicht als feste Gr¨oße in die Programmierung einbeziehen. Die ben¨otigten Datenobjekte werden dann in der entsprechenden Gr¨oße und
43
damit mit entsprechend optimalem Speicherbedarf erzeugt.
F¨ur Probleme dieser Art bietet C mehrere Funktionen (in der Headerdatei malloc.h), die den
notwendigen Speicherplatz zur Laufzeit verwalten. Dazu z¨ahlen:
malloc()
calloc()
realloc()
free()
reserviert Speicher einer bestimmten Gr¨oße
reserviert Speicher einer bestimmten Gr¨oße
und initialisiert die Feldelemente mit 0
erweitert einen reservierten Speicherbereich
gibt den Speicherbereich wieder frei
Die Funktionen malloc(), calloc() und realloc() versuchen, den angeforderten Speicher bereitzustellen (allokieren), und liefern einen Pointer auf diesen Bereich zur¨uck. Konnte die Speicheranforderung nicht erf¨ullt werden, wird ein Null-Pointer (NULL in C, d.h. Pointer zeigt
auf die 0 Adresse im Speicher) zur¨uckliefert. Die Funktion free() enth¨alt als Argument einen
so definierten Pointer und gibt den zugeh¨origen Speicherbereich wieder frei.
Das folgende Programm demonstriert die Reservierung von Speicher durch die Funktion malloc() und die Freigabe durch free().
Beispiel 4.8 ( Norm des Vektors (1,...,n))
#include <stdio.h>
#include <math.h>
#include <malloc.h>
int main()
{
int n, i;
float *vektor, norm=0.0;
printf("Geben Sie die Dimension n des Vektorraums an: ");
scanf("%i",&n);
/* Dynamische Speicher Reservierung */
vektor = (float *) malloc(n*sizeof(float));
if (vektor == NULL) /* Gen¨
ugend Speicher vorhanden? */
{
printf("Nicht gen¨
ugend Speicher vorhanden \n");
return 1;
/* Programm beendet sich und gibt einen Fehlerwert zurueck */
}
else
{
/* Initialisierung des Vektors */
/* Norm des Vektors */
for (i=0;i<n;i=i+1)
{
44
vektor[i]=i+1;
norm=norm+vektor[i]*vektor[i];
}
norm=sqrt(norm);
/* Freigabe des Speichers */
free(vektor);
printf("Die Norm des eingegebenen Vektors (1,...,%i) ist : %f \n"
,n,norm);
}
return 0;
}
Ein zweidimensionales dynamisches Feld l¨aßt sich einerseits durch ein eindimensionales dynamisches Feld darstellen, als auch durch einen Zeiger auf ein Feld von Zeigern. Dies sieht
f¨ur eine Matrix von m Zeilen und n Spalten wie folgt aus:
Beispiel 4.9 ( Dynamisches 2D Feld)
#include <stdio.h>
#include <malloc.h>
int main()
{
int n,m,i,j;
double **p; /* Zeiger auf Zeiger vom Typ double */
printf("Geben Sie die Anzahl der Zeilen der Matrix an: ");
scanf("%i",&m);
printf("Geben Sie die Anzahl der Spalten der Matrix an: ");
scanf("%i",&n);
/* Allokalisiert Speicher f¨
ur Zeiger auf die Zeilen der Matrix */
p=(double **) malloc(m*sizeof(double*));
for (i=0;i<m;i++)
{
/* Allokalisiert Speicher f¨
ur die Spalten der Matrix */
p[i]= (double *) malloc(n*sizeof(double));
}
for (i=0;i<m;i++) /* Initialisierung von Matrix p */
{
for (j=0;j<n;j++)
{
p[i][j]=(i+1)*(j+1);
printf("%f ",p[i][j]);
45
}
printf("\n");
}
for (i=0;i<m;i++)
{
free(p[i]); /* Freigabe der Spalten */
}
free(p);
/* Freigabe der Zeilenzeiger */
return 0;
}
Zuerst muss der Speicher auf die Zeilenpointer allokiert werden, erst danach kann der Speicher f¨ur die einzelnen Zeilen angefordert werden. Beim Deallokieren des Speichers m¨ussen
ebenfalls alle Spalten und danach alle Zeilen wieder freigegeben werden. F¨ur den Fall m=3
und n=4 veranschaulicht das Bild die Ablage der Daten im Speicher.
Zeiger auf Zeiger
p
Zeiger auf Feld
-
Feld
p0
-
p00
p01
p02
p03
p1
-
p10
p11
p12
p13
p2
-
p20
p21
p22
p23
Abbildung 4.1: Dynamisches 2D Feld mit Zeiger auf Zeiger
Achtung !
Es gibt keine Garantie, dass die einzelnen Zeilen der Matrix hintereinander im Speicher angeordnet sind. Somit unterscheidet sich die Speicherung des dynamischen 2D-Feldes von
der Speicherung des statischen 2D-Feldes (siehe Kapitel 2.3.2), obwohl die Syntax des Elementzugriffes p[i][j] identisch ist. Daf¨ur ist diese Matrixspeicherung flexibler, da die Zeilen
auch unterschiedliche L¨angen haben d¨urfen. Insbesondere findet das dynamische 2D-Feld
Anwendung zur Speicherreservierung bei der Bearbeitung von d¨unnbesetzten Matrizen.
46
5 Funktionen
Ein C-Programm gliedert sich ausschließlich in Funktionen. Beispiele f¨ur Funktionen wurden
bereits vorgestellt:
• Die Funktion main(), die den Ausf¨uhrungsbeginn des Programms markiert und somit
das Hauptprogramm darstellt.
• Bibliotheksfunktionen, die h¨aufiger ben¨otigte h¨ohere Funktionalit¨at bereitstellen (z.B.
printf(), scanf(), sqrt(), strcpy() etc).
C verf¨ugt nur u¨ ber einen sehr kleinen Sprachumfang, stellt jedoch eine Vielzahl an Funktionen
in Bibliotheken f¨ur fast jeden Bedarf bereit. Was aber, wenn man eine Funktion f¨ur eine ganz
spezielle Aufgabe ben¨otigt und nichts Brauchbares in den Bibliotheken vorhanden ist? Ganz
einfach: Man schreibt sich diese Funktionen selbst.
¨
5.1 Deklaration, Definition und Ruckgabewerte
Die Deklaration einer Funktion hat die Gestalt
Datentyp Funktionsname(Datentyp1,...,DatentypN);
Die Deklaration beginnt mit dem Datentyp des R¨uckgabewertes, gefolgt vom Funktionsnamen. Es folgt eine Liste der Datentypen der Funktionsparameter . Bei der Deklaration k¨onnen
auch die Namen f¨ur die Funktionsparameter vergeben werden:
Datentyp Funktionsname(Datentyp1 Variable1,...,DatentypN VariableN);
Die Deklaration legt jedoch nur das Allernotwendigste fest, so ist z.B. noch nichts dar¨uber
gesagt, was mit den Funktionsparametern im Einzelnen geschieht und wie der R¨uckgabewert
gebildet wird. Diese wichtigen Aspekte werden in der Definition der Funktion behandelt:
Datentyp Funktionsname(Datentyp1 Variable1,...,DatentypN VariableN)
{
Deklaration der Funktionsvariablen
Anweisungen
}
47
Der Funktionsrumpf besteht ggf. aus Deklarationen von weiteren Variablen (z.B. f¨ur Hilfsgr¨oßen oder den R¨uckgabewert) sowie Anweisungen.
Folgendes ist bei Funktionen zu beachten:
• Die Deklaration bzw. Definition von Funktionen wird außerhalb jeder anderen Funktion
- speziell main() - vorgenommen.
¨
• Funktionen mussen
vor ihrer Verwendung zumindestens deklariert sein. Unterl¨asst
man dies, so nimmt der Compiler eine implizite Deklaration mit Standardr¨uckgabewert
int vor, was eine h¨aufige Ursache f¨ur Laufzeitfehler darstellt.
• Deklaration/Definition k¨onnen in beliebiger Reihenfolge erfolgen.
• Deklaration und Definition m¨ussen konsistent sein, d.h. die Datentypen f¨ur R¨uckgabewert
und Funktionsparameter m¨ussen u¨ bereinstimmen.
Beispiel 5.1 ( Funktion)
#include <stdio.h>
/* Deklaration der Maximum-Funktion */
/************************************/
float maximum (float, float);
/************************************/
/* Deklaration globaler Variablen (nur f¨
ur den Debugger) */
/*********************************************************/
float *pa=(float *)&pa;
float *pb=(float *)&pb;
float *px=(float *)&px;
float *py=(float *)&py;
/*********************************************************/
/* Hauptprogramm
*/
/*****************************************************************/
int main()
{
float a=3.0,b=2.0;
pa=&a;pb=&b; /* pa, pb Zeiger auf a bzw. b */
printf("Das Maximumvon %f und %f ist %f",a,b,maximum(a,b));
return 0;
}
/*****************************************************************/
/* Definition der Maximum-Funktion */
48
/*************************************************************/
float maximum (float x, float y)
{
/* Die Funktion maximum() erstellt Kopien
* der Funktionswerte (reserviert neuen Speicher)
* und speichert sie in x bzw. y */
float maxi;
px=&x;py=&y; /* px, py Zeiger auf x bzw. y */
if (x>y) maxi=x;
else maxi=y;
return maxi;
/* R¨
uckgabewert der Funktion */
}
/*************************************************************/
5.2 Lokale und globale Variablen
Variablen lassen sich nach ihrem G¨ultigkeitsbereich unterteilen:
lokale Variablen: Sie gelten in dem Anweisungsblock (z.B. Funktionenrumpf oder Schlei-
fenrumpf), in dem sie deklariert wurden. F¨ur diesen Block gelten sie als lokal. Bei der
Ausf¨uhrung des Programms existieren die Variablen bis zum Verlassen des Anweisungsblocks.
globale Variablen: Sie werden außerhalb aller Funktionen deklariert/definiert (z.B. direkt
nach den Pr¨aprozessordirektiven) und sind zun¨achst im gesamten Programm einschließlich aller Subroutinen g¨ultig. Dies bedeutet speziell, dass jede Funktion sie ver¨andern
kann. (Achtung: unvorhergesehener Programmablauf m¨oglich.)
Variablen die auf einer h¨oheren Ebene (global oder im Rumpf einer aufrufenden Funktion)
deklariert/definiert wurden, k¨onnen durch Deklaration gleichnamiger lokaler Variablen im
Rumpf einer aufgerufenen Funktion verdeckt“ werden. In diesem Zusammenhang spricht
”
man von G¨ultigkeit bzw. Sichtbarkeit von Variablen.
¨
Beispiel 5.2 ( Gultigkeitsbereich
von Variablen)
/***********************************************/
/* Im Beispiel wird 4 x die Variable a deklariert
* Mit Hilfe eines Debuggers und der Zeiger
* pa_global, pa_main , pa_sub_main und pa_summe
* kann jeweils die Adresse und deren zugeh¨
origer
* Inhalt angezeigt werden */
/***********************************************/
49
/* Deklaration der Funktion summe() */
/************************************/
int summe (int, int);
/************************************/
/* Deklaration globaler Variablen
*/
/*************************************************/
int a=1;
/* globales a =1
*/
int *pa_global=(int *) &pa_global;
int *pa_main=(int *) &pa_main;
int *pa_sub_main=(int *) &pa_sub_main;
int *pa_summe=(int *) &pa_summe;
/*************************************************/
int main()
{
pa_global=&a; /* Zeiger auf globales a */
int a=2;
/* a in main() = 2 und
* ¨
uberdeckt globales a*/
pa_main=&a; /* Zeiger auf a in main() */
{
int a=2;
/* lokales a in main() = 2 und
* ¨
uberdeckt a in main() */
pa_sub_main=&a; /* Zeiger auf lokales a in main() */
a=a+1;
/* lokales a in main() wird um 1 erh¨
oht */
a=summe(a,a); /* lokales a in main() = 2 x lokales a in main()
* Gleichzeitig wird das globale a in
* der Funktion summe um 1 erh¨
oht */
/* lokales a in main() wird gel¨
oscht */
}
a=summe(a,a); /* a in main = 2 x lokales a in main()
* Gleichzeitig wird das globale a in
* der Funktion summe um 1 erh¨
oht */
return 0;
}
/* Definition Summen-Funktion */
/****************************************************************/
int summe (int x, int y)
{
50
/* Die Funktion summe() erstellt Kopien
* der Funktionswerte (reserviert neuen Speicher)
* und speichert sie in x bzw. y */
a=a+1; /* globales a wird um 1 erh¨
oht */
int a=0;
/* a in der Funktion summe() */
pa_summe=&a; /* Zeiger auf a in der Funkton summe */
a=x+y;
/* a in der Funktion summe = x+y */
return a;
/* a in der Funktion wird zur¨
uckgegeben */
/* a,x,y in der Funktion summe werden gel¨
oscht */
}
/****************************************************************/
5.3 Call by value
Die Standard¨ubergabe von Funktionsparametern geschieht folgendermaßen: An die Funktion
werden Kopien der Variablen als Parameter u¨ bergeben und von dieser zur Verarbeitung genutzt.
Die urspr¨ungliche Variable bleibt von den in der Funktion vorgenommenen Manipulationen
unber¨uhrt (es sei denn, sie wird durch den R¨uckgabewert der Funktion u¨ berschrieben).
Beispiel 5.3 ( Call by value I)
#include <stdio.h>
/* Deklaration der Funktion setze() */
/************************************/
void setze (int);
/************************************/
/* Deklaration globaler Variablen (nur f¨
ur den Debugger) */
/*********************************************************/
int *pb=(int *)&pb;
int *pb_setze=(int *)&pb_setze;
/*********************************************************/
/* Hauptprogramm
*/
/*****************************/
int main()
{
51
int b=0;pb=&b;
setze(b);
printf("b=%i\n",b);
return 0;
}
/*****************************/
/* Definition der Funktion setze () */
/************************************/
void setze (int b)
{
b=3;pb_setze=&b;
}
/************************************/
Die Ausgabe lautet:
b=0
Die Funktion setze() hat nur eine Kopie der Variablen b als Parameter erhalten und auf einen
neuen Wert gesetzt. Die eigentliche Variable behielt ihren Wert.
Beispiel 5.4 ( Call by value II)
#include <stdio.h>
/* Deklaration der Funktion setze() */
/************************************/
int setze (int);
/************************************/
/* Deklaration globaler Variablen (nur f¨
ur den Debugger) */
/*********************************************************/
int *pb=(int *)&pb;
int *pb_setze=(int *)&pb_setze;
/*********************************************************/
/* Hauptprogramm
*/
/*****************************/
int main()
{
int b=0;pb=&b;
b=setze(b);
printf("b=%i\n",b);
return 0;
}
52
/*****************************/
/* Definition der Funktion setze () */
/************************************/
int setze (int b)
{
b=3;pb_setze=&b;
return b;
}
/************************************/
Die Ausgabe lautet:
b=3
Die Funktion setze() hat wieder nur eine Kopie der Variablen b als Parameter erhalten und
auf einen neuen Wert gesetzt. Durch das Zur¨uckliefern und die Zuweisung an die eigentliche
¨
Variable b wurde die Anderung
wirksam.
5.4 Call by reference
Bei call by reference wird der Funktion nicht eine Kopie der Variablen selbst, sondern in Form
eines Pointers auf die Variable eine Kopie der Adresse der Variablen u¨ bergeben.
¨
Uber
die Kenntnis der Variablenadresse kann die Funktion den Variableninhalt manipulieren.
Hierzu kommen beim Aufruf der Funktion der Adressoperator und im Funktionsrumpf der
Inhaltsoperator in geeigneter Weise zum Einsatz.
Beispiel 5.5 ( Call by reference)
#include <stdio.h>
/* Deklaration der Funktion setze() */
/************************************/
void setze (int *);
/************************************/
/* Deklaration globaler Variablen (nur f¨
ur den Debugger) */
/*********************************************************/
int *pb=(int *)&pb;
/*********************************************************/
/* Hauptprogramm
*/
/*****************************/
int main()
53
{
int b=0;pb=&b;
setze(&b);
printf("b=%i\n",b);
return 0;
}
/*****************************/
/* Definition der Funktion setze () */
/************************************/
void setze (int *b)
{
*b=3;
}
/************************************/
Die Ausgabe lautet:
b=3
Der Funktion setze() wird ein Zeiger auf eine int-Variable u¨ bergeben und sie verwendet den Inhaltsoperator *, um den Wert der entsprechenden Variablen zu ver¨andern. Im Hauptprogramm
wird der Zeiger mit Hilfe des Adressoperators & erzeugt.
5.5 Rekursive Programmierung
Bisher haben Funktionen ihre Aufgabe in einem Durchgang komplett erledigt. Eine Funktion
kann ihre Aufgabe aber manchmal auch dadurch erledigen, dass sie sich selbst mehrmals aufruft und jedesmal nur eine Teilaufgabe l¨ost. Das f¨uhrt zu rekursiven Aufrufen.
Das folgende Programm berechnet xk (x ∈ R, k ∈ N) rekursiv.
Beispiel 5.6 ( Rekursive Programmierung)
#include <stdio.h>
/* Deklaration potenz-Funktion */
double potenz (double, int);
/* Hauptprogramm */
int main()
{
double x;
int k;
printf("Zahl x : "); scanf("%lf",&x);
printf("Potenz k : "); scanf("%i",&k);
54
printf("xˆk = %f \n",potenz(x,k));
}
/* Definition potenz-Funktion */
double potenz (double x, int k)
{
if (k<0) /* Falls k < 0 berechne (1/x)ˆ(-k) */
{
return potenz(1.0/x,-k);
}
else
{
if (k==0) /* Rekursionsende */
{
return 1;
}
else
{
return x*potenz(x,k-1); /* Rekursionsaufruf */
}
}
}
Die Funktion potenz ruft sich solange selbst auf, bis der Fall k == 0 eintritt. Das Ergebnis
dieses Falls liefert sie an die aufrufende Funktion zur¨uck.
Achtung
Bei der rekursiven Programmierung ist stets darauf zu achten, dass der Fall des Rekursionsabbruchs (im Beispiel k==0) immer erreicht wird, da sonst die Maschine bis zur Unendlichkeit rechnet.
5.6 Kommandozeilen-Parameter
Ausf¨uhrbaren Programmen k¨onnen beim Aufruf Parameter u¨ bergeben werden, indem man
nach dem Programmnamen eine Liste der Parameter (Kommandozeilen-Parameter) anf¨ugt. In
C-Programmen k¨onnen so der Funktion main Parameter u¨ bergeben werden. Zur Illustration
wird folgendes Beispiel betrachtet.
Beispiel 5.7
/*
/*
/*
/*
/*
/*
1
2
3
4
5
6
*/ # include <stdio.h>
*/
*/ int main(int argc, char* argv[])
*/ {
*/
int zaehler;
*/
float zahl,summe=0;
55
/* 7 */
/* 8 */
/ *9 */
/* 10 */
/* 11 */
/* 12 */
/* 13 */
/* 14 */
/* 15 */
/* 16 */
/* 17 */
/* 18 */
/* 19 */
/* 20 */
/* 21 */
/* 22 */ }
for (zaehler=0;zaehler < argc ; zaehler++)
{
printf("Parameter %i = %s \n",zaehler,argv[zaehler]);
}
printf("\n");
for (zaehler=1;zaehler < argc ; zaehler++)
{
sscanf(argv[zaehler],"%f",&zahl);
summe=summe+zahl;
}
printf("Die Summe der Kommandozeilen-Parameter : %f\n",summe);
return 0;
Nach dem Start des obigen Programms zum Beispiel durch
beispiel5_7.out 1.4 3.2 4.5
erscheint folgende Ausgabe auf dem Bildschirm
Parameter
Parameter
Parameter
Parameter
0
1
2
3
=
=
=
=
beispiel5_7
1.4
3.2
4.5
Die Summe der Kommandozeilen-Parameter : 9.100000
Die Angaben in der Kommandozeile sind an das Programm u¨ bergeben worden und konnten
hier auch verarbeitet werde.
Zeile 3: main erh¨alt vom Betriebssystem zwei Parameter. Die Variabel argc enth¨alt die Anzahl der u¨ bergebenen Parameter und argv[] die Parameter selbst. Die Namen der Variablen
argc und argv sind nat¨urlich frei w¨ahlbar, es hat sich jedoch eingeb¨urgert, die hier verwendeten Bezeichnungen zu benutzen. Sie leiten sich von argument count und argument values ab.
Bei argc ist eine Besonderheit zu beachten. Hat diese Variable zum Beispiel den Wert 1, so
bedeutet das, daß kein Kommandozeilen-Parameter eingegeben wurde. Das Betriebssystem
u¨ bergibt als ersten Parameter n¨amlich grunds¨atzlich den Namen des Programms selbst. Also
erst wenn argc gr¨oßer als 1, wurde wirklich ein Parameter eingegeben.
Die Deklaration char *argv[] bedeutet: Zeiger auf Zeiger auf Zeichen. (Man h¨atte auch char
**argv schreiben k¨onnen). Mit anderen Worten: argv ist ein Zeiger, der auf ein Feld zeigt, das
wiederum Zeiger enth¨alt. Diese Pointer zeigen schließlich auf die einzelnen KommandozeilenParameter. Die leeren eckigen Klammern weisen darauf hin, daß es sich um ein Feld unbestimmter Gr¨oße handelt. Die einzelnen Argumente k¨onnen durch Indizierung von argv ange-
56
sprochen werden. argv[1] zeigt also auf das erste Argument (”1.4”), argv[2] auf ”3.2” usw.
Zeile 15: Da es sich bei argv[i] um Strings handelt m¨ussen die Eingabeparameter eventuell (je
nach ihrer Bestimmung) in einen anderen Typ umgewandelt werden. Dies geschieht in diesem
Beispiel mit Hilfe des sscanf -Befehls.
5.7 Wie werden Deklarationen gelesen
Eine Deklaration besteht grunds¨atzlich aus einem Bezeichner (Variablennamen oder Funktionsnamen), der durch einen oder mehrere Zeiger, Feld- oder Funktions-Modifikatoren beschrieben wird. Wenn mehrere solcher Modifikatoren miteinander kombinieren, muß man darauf achten, daß Funktionen keine Funktionen oder Felder zur¨uckgeben k¨onnen und daß Felder auch keine Funktionen als Elemente haben k¨onnen. Ansonsten sind alle Kombinationen
erlaubt. Dabei haben Funktions- und Array-Modifikatoren Vorrang vor Zeiger-Modifikatoren.
Durch Klammerung kann diese Rangfolge ge¨andert werden.
Bei der Interpretation beginnt man am besten beim Bezeichner und liest nach rechts bis zum
Ende usw. bis zu einer einzelnen rechten Klammer. Dann f¨ahrt man links vom Bezeichner mit
evtl. vorhandenen Zeiger-Modifikatoren fort, bis das Ende oder eine einzelne linke Klammer
erreicht wird. Dieses Verfahren wird f¨ur jede geschachtelte Klammer von innen nach außen
wiederholt. Zum Schluß wird der Typ-Kennzeichner gelesen.
Beispiel 5.8
char |{z}
∗
(|{z}
∗
(|{z}
∗
Bezeichner
( ) ) |{z}
[20]
|{z}
| {z }) |{z}
7
6
4
2
1
3
5
Bezeichner (1) ist hier ein Zeiger (2) auf eine Funktion (3), die einen Zeiger (4) auf ein Feld
mit 20 Elementen(5) zur¨uckgibt, die Zeiger (6) auf char-Werte(7) sind!
Die folgenden vier Beispiele sollen noch einmal den Einsatz von Klammern verdeutlichen:
(i)
(ii)
(iii)
char * a[10]
char (* a)[10]
char *a(int)
(iv)
char (*a)(int)
a ist ein Feld der Gr¨oße 10 mit Zeigern auf char-Werte
a ist Zeiger auf ein Feld der Gr¨oße 10 mit char-Werte
a ist Funktion, die als Eingabeparameter einen int-Wert verlangt
und einen Zeiger auf char-Wert zur¨uckgibt
a ist Zeiger, auf eine Funktion die als Eingabeparameter einen
int-Wert verlangt und einen char-Wert zur¨uckgibt
5.8 Zeiger auf Funktionen
Manchmal ist es n¨utzlich Funktion an Funktionen zu u¨ bergeben. Dies kann mit Hilfe von
Zeigern auf Funktionen realisiert werden. Im folgenden Beispiel (5.9) wird der Funktion trapez regel ein Zeiger auf die zu integrierende Funktion mitgeliefert. Dadurch ist es m¨oglich
57
beliebige Funktionen mit Hilfe der Trapez-Regel numerisch zu integrieren.
Die Intervallgrenzen und Anzahl der St¨utzstellen sollen dem Programm durch KommandozeilenParameter u¨ bergeben werden.
Beispiel 5.9
# include <stdio.h>
# include <math.h>
/* Deklaration der Funktion trapez_regel()
*/
/*************************************************************/
double trapez_regel(double (*f)(double ),double ,double ,int );
/* Eingabeparameter : 1.) Zeiger auf Funktion mit
*
Eingabeparameter double-Wert
*
und double-R¨
uckgabewert
*
2.) double f¨
ur linke Intervallgrenze
*
3.) double f¨
ur rechte Intervallgrenze
*
4.) int f¨
ur Anzahl der St¨
utzstellen
* R¨
uckgabewert : double f¨
ur das Integral
**************************************************************/
/* Hauptprogramm
*/
/************************************************************************/
int main(int argc,char** argv)
{
int n;
double a,b,integral;
/* Zeiger auf eine Funktion die als R¨
uckgabewert
* eine double-Variable besitzt und als
* Eingabe eine double-Variable verlangt */
double (*fptr)(double);
if (argc<4)
{
printf("Programm ben¨
otigt 3 Kommandozeilenparameter :\n");
printf("1.) Linker Intervallrand (double)\n");
printf("2.) Rechter Intervallrand (double)\n");
printf("3.) Anzahl der Teilintervalle f¨
ur");
printf(" numerische Integration (int)\n");
return 1;
}
else
{
sscanf(argv[1],"%lf",&a);
58
sscanf(argv[2],"%lf",&b);
sscanf(argv[3],"%i",&n);
fptr=(double(*)(double)) cos;/* Zeiger fptr auf cos-Funktion */
integral=trapez_regel(fptr,a,b,n);
printf("Das Integral der cos-Funktion ¨
uber das Intervall");
printf(" [%f , %f]\n",a,b);
printf("betr¨
agt : \t %f (numerisch mit %i",integral,n+1);
printf(" St¨
utzstellen)\n");
printf("
\t %f (exakt)\n\n",sin(b)-sin(a));
fptr=(double (*)(double)) sin;/*Zeiger fptr auf sin-Funktion */
integral=trapez_regel(fptr,a,b,n);
printf("Das Integral der sin-Funktion ¨
uber das Intervall");
printf(" [%f , %f]\n",a,b);
printf("betr¨
agt : \t %f (numerisch mit %i",integral,n+1);
printf(" St¨
utzstellen)\n");
printf("
\t %f (exakt)\n\n",cos(a)-cos(b));
return 0;
}
}
/************************************************************************/
/* Definition der Funktion trapez_regel()
*/
/*******************************************************************/
double trapez_regel(double (*f)(double ),double a,double b,int n)
{
n=n+1;
int k;
double h=(b-a)/n;
double integral=0;
for (k=0;k<=n-1;k++) integral=integral+h/2*(f(a+k*h)+f(a+(k+1)*h));
return integral;
}
/*******************************************************************/
59
6 Strukturierte Datentypen
¨
Uber
die normalen Datentypen hinaus gibt es in C weitere, komplexere Typen, die sich aus
den einfacheren zusammensetzen. Außerdem besteht die M¨oglichkeit, eigene Synonyme f¨ur
h¨aufig verwendete Typen festzulegen.
Feld (array)
Struktur (struct)
Union (union)
Aufz¨ahlungstyp (enum)
Zusammenfassung von Elementen gleichen Typs
Zusammenfassung von Elementen verschiedenen Typs
¨
Uberlagerung
mehrerer Komponenten
verschiedenen Typs auf dem gleichen Speicherplatz
Grunddatentyp mit frei w¨ahlbarem Wertebereich
Der Typ Feld wurde bereits in Kapitel 2.3 vorgestellt.
6.1 Strukturen
Die Struktur definiert einen neuen Datentyp, welcher Komponenten unterschiedlichen Typs
vereint. Die L¨ange einer solchen Struktur ist gleich der Gesamtl¨ange der einzelnen Bestandteile. Angewendet werden Strukturen h¨aufig dann, wenn verschiedene Variablen logisch zusammengeh¨oren, wie zum Beispiel Name, Vorname, Straße etc. bei der Bearbeitung von Adressen.
Die Verwendung von Strukturen ist nicht zwingend n¨otig, man kann sie auch durch die Benutzung von mehreren einzelnen Variablen ersetzen. Sie bieten bei der Programmierung jedoch
¨
einige Vorteile, da sie die Ubersichtlichkeit
erh¨ohen und die Bearbeitung vereinfachen.
6.1.1 Deklaration von Strukturen
Zur Deklaration einer Struktur benutzt man das Schl¨usselwort struct. Ihm folgt der Name
der Struktur. In geschweiften Klammern werden dann die einzelnen Variablen aufgef¨uhrt, aus
denen die Struktur bestehen soll.
Achtung: Die Variablen innerhalb einer Stuktur k¨onnen nicht initialisiert werden.
struct Strukturname Datendeklaration ;
Im folgenden Beispiel wird eine Stuktur mit Namen Student deklariert, die aus einer intVariablen und einem String besteht.
60
Beispiel 6.1 ( Deklaration einer Struktur)
struct Student
{
int matrikel;
char name[16];
};
6.1.2 Definition von Strukturvariablen
Die Strukturschablone selbst hat noch keinen Speicherplatz belegt, sie hat lediglich ein Muster
festgelegt, mit dem die eigentlichen Variablen definiert werden. Die Definition erfolgt genau
wie bei den Standardvariablen (int a, double summe etc.), indem man den Variablentyp (struct
Student) gefolgt von einem oder mehreren Variablennamen angibt, zum Beispiel:
struct Student peter, paul;
Hierdurch werden zwei Variablen peter, paul deklariert, die beide vom Typ struct Student
sind. Neben dieser beschriebenen Methode gibt es noch eine weitere Form der Definition von
Strukturvariablen:
Beispiel 6.2 ( Deklaration einer Struktur + Definition der Strukturvariablen)
struct Student
{
int matrikel;
char name [16];
} peter, paul;
In diesem Fall erfolgt die Definition der Variablen zusammen mit der Deklaration. Dazu
m¨ussen nur eine oder mehrere Variablen direkt hinter die Deklaration gesetzt werden.
6.1.3 Felder von Strukturen
Eine Struktur wird h¨aufig nicht nur zur Aufnahme eines einzelnen Datensatzes, sondern zur
Speicherung vieler gleichartiger S¨atze verwendet, beispielsweise mit
struct Student Studenten2005[1000];
Der Zugriff auf einzelne Feldelemente erfolgt wie gewohnt mit eckigen Klammern [].
6.1.4 Zugriff auf Strukturen
Um Strukturen sinnvoll nutzen zu k¨onnen, muss man ihren Elementen Werte zuweisen und auf
diese Werte auch wieder zugreifen k¨onnen. Zu diesem Zweck kennt C den Strukturoperator
’.’, mit dem jeder Bestandteil einer Struktur direkt angesprochen werden kann.
61
Beispiel 6.3 ( Deklaration + Definition + Zuweisung)
#include <stdio.h>
#include <string.h> /* F¨
ur strcpy */
int main()
{
/* Deklaration der Struktur Student */
struct Student
{
int matrikel;
char Vorname[16];
};
/* Definition eines Feldes der Struktur Student */
struct Student Mathe[100];
/* Zugriff auf die einzelnen Elemente */
Mathe[0].matrikel=242834;
strcpy(Mathe[0].Vorname,"Peter");
Mathe[1].matrikel=343334;
strcpy(Mathe[1].Vorname,"Paul");
/* Der Zuweisungsoperator = ist auf Strukturen
gleichen Typs anwendbar */
Mathe[2]=Mathe[1];
printf("Vorname von Student 2: %s",Mathe[2].Vorname);
}
return 0;
6.1.5 Zugriff auf Strukturen mit Zeigern
Wurde eine Struktur deklariert, so kann man auch einen Zeiger auf diesen Typ wie gewohnt
definieren, wie zum Beispiel
struct Student *p;
Der Zugriff auf den Inhalt der Adresse, auf die der Zeiger verweist, erfolgt wie u¨ blich durch
den Zugriffsoperator *. Wahlweise kann auch der Auswahloperator -> benutzt werden.
62
Beispiel 6.4 ( Zugriff mit Zeigern)
#include <stdio.h>
#include <string.h> /* F¨
ur strcpy */
int main()
{
/* Deklaration der Struktur Student */
struct Student
{
int matrikel;
char Strasse[16];
};
/* Definition Strukturvariablen peter, paul */
struct Student peter, paul;
struct Student *p; /* Zeiger auf Struktur Student */
p=&peter; /* p zeigt auf peter */
/* Zugriff auf die einzelnen Elemente */
(*p).matrikel=242834;
strcpy((*p).Strasse,"Finkenweg 4");
/* Alternativ */
p=&paul;
p->matrikel=423323;
strcpy(p->Strasse,"Dorfgosse 2");
printf("Paul, Matr.-Nr.: %i , Str.: %s "
,paul.matrikel,paul.Strasse);
return 0;
}
6.1.6 Geschachtelte Strukturen
Bei der Deklaration von Strukturen kann innerhalb einer Struktur eine weitere Struktur eingebettet werden. Dabei kann es sich um eine bereits an einer anderen Stelle deklarierte Struktur
handeln, oder man verwendet an der entsprechenden Stelle nochmals das Schl¨usselwort struct
und deklariert eine Struktur innerhalb der anderen.
Das Programm 6.5 zeigt ein Beispiel f¨ur eine geschachtelte Struktur.
63
Beispiel 6.5 ( Geschachtelte Strukturen)
#include <stdio.h>
int main()
{
struct Punkt3D
{
float x;
float y;
float z;
};
struct Strecke3D
{
struct Punkt3D anfangspunkt;
struct Punkt3D endpunkt;
};
struct Strecke3D s;
/* Initialisierung einer Strecke */
s.anfangspunkt.x=1.0;
s.anfangspunkt.y=-1.0;
s.anfangspunkt.z=2.0;
s.endpunkt.x=1.1;
s.endpunkt.y=1.2;
s.endpunkt.z=1.4;
printf("%f \n",s.endpunkt.z);
return 0;
}
6.1.7 Listen
Wie in Abschnitt 6.1.6 bereits gezeigt wurde, k¨onnen Strukturen auch andere (zuvor deklarierte) Strukturen als Komponenten enthalten. Eine Struktur darf sich aber nicht selbst als
Variable enthalten!. Allerdings darf eine Struktur einen Zeiger auf sich selbst als Komponente beinhalten. Diese Datenstrukturen kommen zum Einsatz, wenn man nicht im voraus wissen
kann, wieviel Speicher man f¨ur eine Liste von Datens¨atzen reservieren muss und daher die
Verwendung von Feldern unzweckm¨aßig ist.
64
Beispiel 6.6 ( Liste)
#include <stdio.h>
#include <string.h>
int main()
{
struct Student
{
char name[16];
char familienstand[16];
char geschlecht[16];
struct Student *next;
};
struct Student Mathe[2];
struct Student Bio[1];
struct Student *startzeiger,*eintrag;
/* Initialisierung der Mathe-Liste */
strcpy(Mathe[0].name,"Kerstin");
strcpy(Mathe[0].familienstand,"ledig\t");
strcpy(Mathe[0].geschlecht,"weiblich");
Mathe[0].next=&Mathe[1]; /* next zeigt auf den n¨
achsten Eintag */
strcpy(Mathe[1].name,"Claudia");
strcpy(Mathe[1].familienstand,"verheiratet");
strcpy(Mathe[1].geschlecht,"weiblich");
Mathe[1].next=NULL; /* next zeigt auf NULL, d.h Listenende */
/* Initialisierung der Bio-Liste */
strcpy(Bio[0].name,"Peter");
strcpy(Bio[0].familienstand,"geschieden");
annlich");
strcpy(Bio[0].geschlecht,"m¨
Bio[0].next=NULL; /* next zeigt auf NULL, d.h Listenende */
/* Ausgabe der Mathe-Liste */
startzeiger=&Mathe[0];
printf("Name\tFamilienstand\tGeschlecht\n\n");
for (eintrag=startzeiger;eintrag!=NULL;eintrag=eintrag->next)
{
printf("%s\t%s\t%s\n"
,eintrag->name,eintrag->familienstand,eintrag->geschlecht);
}
/* Anh¨
angen der Bio-Liste an die Mathe-Liste */
65
Mathe[1].next=&Bio[0];
/* Ausgabe der Mathe-Bio-Liste */
printf("\n\nName\tFamilienstand\tGeschlecht\n\n");
for (eintrag=startzeiger;eintrag!=NULL;eintrag=eintrag->next)
{
printf("%s\t%s\t%s\n"
,eintrag->name,eintrag->familienstand,eintrag->geschlecht);
}
return 0;
}
6.2 Unions
W¨ahrend die Struktur sich dadurch auszeichnet, dass sie sich aus mehreren verschiedenen
Datentypen zusammensetzt, ist das Charakteristische an Unions, dass sie zu verschiedenen
Zeitpunkten jeweils einen bestimmten Datentyp aufnehmen k¨onnen.
Beispiel 6.7 ( Union)
Um die Klausurergebnisse von Studierenden zu speichern, m¨usste normalerweise zwischen
Zahlen (Notenwerte im Falle des Bestehens) und Zeichenketten ( nicht bestanden“) unter”
schieden werden. Verwendet man eine Unionvariable so kann man die jeweilige Situation
flexibel handhaben
#include <stdio.h>
#include <string.h>
int main()
{
union klausurresultat
{
float note;
char nichtbestanden[16];
};
union klausurresultat ergebnis, *ergebnispointer;
/* Zugriff mit pointer */
ergebnispointer=&ergebnis;
ergebnispointer->note=1.7;
strcpy(ergebnispointer->nichtbestanden,"nicht bestanden");
printf("%s\n",ergebnispointer->nichtbestanden);
66
/* Zugriff ohne pointer */
ergebnis.note=3.3;
strcpy(ergebnis.nichtbestanden,"nicht bestanden");
printf("%s\n",ergebnis.nichtbestanden);
return 0;
}
Der Speicherplatzbedarf einer Union richtet sich nach der gr¨oßten Komponente (im Beispiel:
16 x sizeof(char) = 16 Byte).
Gr¨unde f¨ur das Benutzen von Unions fallen nicht so stark ins Auge wie bei Strukturen. Im
wesentlichen sind es zwei Anwendungsf¨alle, in denen man sie einsetzt:
• Man m¨ochte auf einen Speicherbereich auf unterschiedliche Weise zugreifen. Dies k¨onnte
bei der obigen Union doppelt der Fall sein. Hier kann man mit Hilfe der Komponente
nichtbestanden auf die einzelnen Bytes der Komponente note zugreifen.
• Man benutzt in einer Struktur einen Bereich f¨ur verschiedene Aufgaben. Sollen beispielsweise in einer Struktur Mitarbeiterdaten gespeichert werde, so kann es sein, dass
f¨ur den einen Angaben zum Stundenlohn in der Struktur vorhanden sein sollen, w¨ahrend
der andere Speicherplatz f¨ur ein Monatsgehalt und der Dritte noch zus¨atzlich Angaben
u¨ ber Provisionen ben¨otigt. Damit man nun nicht alle Varianten in die Struktur einbauen muss, wobei jeweils zwei unbenutzt blieben, definiert man einen Speicherbereich, in
dem die jeweils ben¨otigten Informationen abgelegt werden, als Union.
¨
6.3 Aufzahlungstyp
Der Aufz¨ahlungstyp ist ein Grundtyp mit frei w¨ahlbarem Wertebereich. Veranschaulicht wird
dies am Beipiel der Wochtage.
Beispiel 6.8 ( Aufz¨ahlungstyp)
#include <stdio.h>
#include <string.h>
int main()
{
enum tag
{
montag, dienstag, mittwoch, donnerstag,
freitag, samstag, sonntag
};
enum tag wochentag;
wochentag=montag;
67
if (wochentag==montag) printf("Schlechte Laune");
return 0;
}
6.4 Allgemeine Typendefinition
Das Schl¨usselwort typedef definiert neue Namen f¨ur bestehende Datentypen. Es erlaubt eine
k¨urzere Schreibweise bei aufwendigen Deklarationen und kann Datentypen auf Wunsch aussagekr¨aftigere Namen geben. Die Syntax f¨ur die Definition eines neuen Datentypnamens sieht
wie folgt aus:
typedef typ Variablenname;
Beispiel 6.9 ( Typendefinition)
#include <stdio.h>
#include <string.h>
int main()
{
struct vektor
{
float x;
float y;
float z;
};
typedef char text[100];
typedef struct vektor punkt;
/* Deklaration der Variablen p und nachricht */
punkt p;
text nachricht;
/* Initialisierung der Variablen p und nachricht */
p.x=1.0;p.y=0.0;p.z=0.0;
strcpy(nachricht,"nachricht ist eine Variable vom Typ text");
printf("%s\n",nachricht);
return 0;
}
Interessanterweise ist eine Variable vom Typ text nunmehr stets eine Zeichenkette der (max.)
L¨ange 100. Weiterhin zeigt das Beispiel, dass auch strukturierten Datentypen eigene Typdefinitionen zugewiesen werden k¨onnen.
68
7 Arbeiten mit Dateien
Viele der Programme, die bisher vorgestellt wurden, erlaubten es, Daten einzugeben, zu bearbeiten und wieder auszugeben. Nach dem Programmende waren diese Zahlen und Texte dann
allerdings wieder verloren. Was also noch fehlt, ist die M¨oglichkeit, die Daten auf Diskette
oder Platte dauerhaft zu sichern.
¨
7.1 Dateien offnen
und schließen
Der erste Schritt beim Arbeiten mit einer Datei besteht darin, diese Datei zu o¨ ffnen. Diese
¨
Aufgabe wird mit der Bibliotheksfunktion fopen() erledigt. Nach dem Offnen
kann der Inhalt
bearbeitet werden. Ist dies beendet, wird die Datei mit der Funktion fclose() wieder geschlossen. Die Funktionen fclose() und fopen() werden in der Headerdatei stdio.h deklariert.
F¨ur die Arbeit mit Dateien, allgemeiner gesagt mit Datenstr¨omen, gibt es in C den Datentyp
FILE. Dieser Typ wird in der Headerdatei stdio.h definiert.
fopen()
Die Funktionsbibliothek deklariert fopen() folgendermaßen:
FILE * fopen (char *Dateiname, char *Modus);
Diese Beschreibung wird folgendermaßen interpretiert:
• Die Funktion heißt fopen.
• Sie hat als R¨uckgabewert einen Pointer vom Typ FILE.
• Der erste Eingabeparameter erh¨alt einen Zeiger auf den String des Dateinamens.
• Der zweite Eingabeparameter erh¨alt einen Zeiger auf den String des Bearbeitungsmodus. Er legt fest, wie auf die Datei zugegriffen werden kann.
Als Bearbeitungsmodi sind g¨ultig:
• “r“ o¨ ffnet die Datei zum Lesen.
• “w“ o¨ ffnet die Datei zum Schreiben. Wenn die Datei existiert, wird sie u¨ berschrieben,
andernfalls neu angelegt.
• “a“ o¨ ffnet die Datei zum Schreiben am Ende der Datei (Anh¨angen). Sie wird neu angelegt, falls sie noch nicht existiert.
69
• “rb“, “wb“, “ab“ o¨ ffnet die Datei wie oben f¨ur bin¨are Ein- und Ausgabe im Gegensatz
zur zeichenorientierten Ein- und Ausgabe.
• Das zus¨atzliche Anh¨angen von +“ gibt die Erlaubnis, eine Datei gleichzeitig zu mani”
pulieren und zu lesen.
¨
Schl¨agt das Offnen
einer Datei fehl, so liefert die Funktion fopen() den NULL-Pointer als
R¨uckgabewert. Die Funktion fclose() liefert den Wert 0 zur¨uck, falls eine Datei erfolgreich
geschlossen werden konnte. Um sicher zu sein, dass eine Datei wirklich geschlossen ist, sollte man stets vor Beendigung des Programms alle mit fopen() ge¨offneten Datenstr¨ome auch
wieder schließen. Die folgenden speziellen Datenstr¨ome sind vordefiniert und m¨ussen nicht
eigens ge¨offnet werden:
• stdin: Standardeingabe
• stdout: Standardausgabe
• stderr: Standardfehlerausgabe
Das Beispiel 7.1 o¨ ffnet die Dateien test1.txt und test2.txt und schließt sie anschließend wieder. Falls die Dateien bis dahin noch nicht im Verzeichnis waren, so existieren sie nach dem
Programmende. Es sind allerdings Dateien ohne Inhalt.
¨
Beispiel 7.1 ( Offnen
und schließen von Dateien)
#include <stdio.h>
int main()
{
FILE * datei_ptr;
/* Zeiger auf Datei (Datenstrom) */
char dateiname[]="test1.txt";
/* O¨ffnen der Datei "test1.txt" zum schreiben*/
datei_ptr=fopen(dateiname,"w");
fclose(datei_ptr); /* Schließen der Datei "test1.txt" */
dateiname[4]=’2’;
/* O¨ffnen der Datei "test2.txt" zum schreiben*/
datei_ptr=fopen(dateiname,"w");
fclose(datei_ptr); /* Schließen der Datei "test2.txt" */
return 0;
}
70
¨
7.2 Existenz einer Datei prufen
Manchmal ist es f¨ur ein Programm nur wichtig, ob eine bestimmte Datei existiert. Da es f¨ur
diese Aufgabe in der Standardbibliothek keine Funktion gibt, muss man sich mit einem Trick
behelfen, den auch das folgende Programm benutzt.
¨
Beispiel 7.2 ( Existenz von Dateien prufen)
#include <stdio.h>
int main()
{
FILE * datei_ptr;
/* Zeiger auf Datei (Datenstrom) */
char dateiname[]="test1.txt";
/* O¨ffnen der Datei "test1.txt" zum lesen*/
datei_ptr=fopen(dateiname,"r");
if (datei_ptr==NULL) /* Fehler beim O¨ffnen ? */
{
printf("Die Datei %s existiert nicht\n",dateiname);
}
else
{
printf("Die Datei %s wurde erfolgreich ge¨
offnet\n",dateiname);
fclose(datei_ptr); /* Schließen der Datei "test1.txt" */
}
return 0;
}
Die zu u¨ berpr¨ufende Datei test1.txt“ wird zum Lesen ge¨offnet. Schl¨agt der Versuch fehl, so
”
liefert die Funktion fopen() den NULL-Zeiger zur¨uck.
7.3 Zeichenorientierte Ein- und Ausgabe
Die folgenden in stdio.h deklarierten Funktionen dienen dem zeichen- bzw. string-orientierten
Austausch von Daten eines Programms mit Dateien.
fprintf
Deklaration: int fprintf (FILE * Dateiname, const char* Formatstring,...);
Beschreibung: Verh¨alt sich wie printf(), mit dem Unterschied, dass die einzelnen Zei-
chen des Strings nicht auf den Bildschirm, sondern in den Datenstrom (Datei) geschrieben werden. Es gilt : printf(. . . ) = fprintf(stdout,. . . );
fscanf
71
Deklaration: int fscanf (FILE * Dateiname, const char* Formatstring,...);
Beschreibung: Verh¨alt sich wie scanf(), mit dem Unterschied, dass die einzelnen Zei-
chen des Strings nicht vom Bildschirm, sondern aus dem Datenstrom (Datei) eingelesen werden. Es gilt : scanf(. . . )=fscanf(stdin,. . . );
fputc()
Deklaration: int fputc (int zeichen, FILE * Dateiname);
Beschreibung: Schreibt ein Zeichen in den Datenstrom (Datei) und liefert bei Erfolg
das Zeichen zur¨uck.
fgetc()
Deklaration: int fgetc (FILE * Dateiname);
Beschreibung: Liest ein Zeichen aus dem Datenstrom (Datei) und liefert bei Erfolg
das Zeichen zur¨uck. Bei erneutem Aufruf wird das n¨achste Zeichen von dort eingelesen.
Beispiel 7.3 (Lesen und Speichern von Matrizen)
Programmbeschreibung
Das Beispiel zeigt, wie man Matrizen in Dateien schreibt und sie wieder ausliest. Die Dimensionen der Matrizen werden durch Pr¨aprozessorvariablen realisiert. Der Name der zu betrachtenden Datei (hier: matrix.txt) wird ebenfalls durch eine Pr¨aprozessordirektive definiert.
(i) Hauptprogramm
Das Hauptprogramm bietet dem Benutzer ein Menu mit f¨unf Auswahlm¨oglichkeiten an:
1 - Der Benutzer soll eine Matrix u¨ ber die Tastatur initialisieren.
2 - Die aktuell im Speicher vorhandene Matrix wird auf dem Bildschirm ausgegeben.
3 - Die Datei matrix.txt wird ausgelesen, in den Speicher der aktuellen Matrix geschrieben und auf dem Bildschirm ausgegeben. Existiert die Datei matrix.txt nicht, so erscheint auf dem Bildschirm die Meldung: Datei existiert nicht.
4 - Die aktuell im Speicher vorhandene Matrix wird in die Datei matrix.txt geschrieben.
Zeilen werden dabei durch das Zeichen “\n“ markiert.
sonst - Abbruch.
(ii) newline (int N)
Die Funktion newline erzeugt rekursiv Zeilenumbr¨uche. Die Anzahl der Zeilenumbr¨uche
wird der Funktion als Parameter u¨ bergeben.
(iii) matrix eingeben (float **A, int zeilen, int spalten)
Initialisierung der Matrix durch den Benutzer. Anstatt des scanf()-Befehls wird die Kombination fgets() und sscanf() benutzt (siehe Kapitel ??).
72
(iv) matrix ausgeben (float **A, int zeilen, int spalten)
Ausgabe der Matrix.
(v) matrix speichern (float **A, char *dateiname, int zeilen, int spalten)
Funktion o¨ ffnet Datei namens des Strings, auf den der Pointer dateiname zeigt, zum
¨
Schreiben. Schl¨agt der Versuch fehl, so erscheint die Ausgabe: Fehler beim Offnen.
Andernfalls werden die einzelnen Matrixeintr¨age in die ge¨offnete Datei geschrieben.
Die Eintr¨age werden innerhalb der Zeile durch ein Leerzeichen getrennt. Zeilen werden
durch das Zeichen \n“ markiert.
”
(vi) matrix laden(float **A, char *dateiname, int zeilen, int spalten)
Funktion o¨ ffnet Datei namens des Strings, auf den der Pointer dateiname zeigt, zum
Lesen. Schl¨agt der Versuch fehl, so erscheint die Ausgabe: Datei existiert nicht. Die Matrixeintr¨age werden nacheinander aus der Datei gelesen und im reservierten Speicher der
Matrix abgelegt. (Auf die Parameter¨ubergabe der Dimension der Matrix k¨onnte in dieser
Funktion verzichtet werden, da die Zeilen bzw. Matrixeintr¨age durch \n“ bzw. durch
”
ein Leerzeichen bei der Speicherung getrennt wurden. Zur Reduzierung des Quelltextes
wurde dies jedoch nicht ber¨ucksichigt.)
#include <stdio.h>
#include <malloc.h>
#define M 3 /* Zeilen der Matrix */
#define N 3 /* Spalten der Matrix */
#define MATRIXDATEI "matrix.txt" /* Dateiname */
/***** Deklaration der Funktionen ************************/
void newline(int);
void matrix_eingeben(float **A, int zeilen, int spalten);
void matrix_ausgeben(float **A, int zeilen, int spalten);
void matrix_speichern(float **A, char *dateiname, int zeilen, int spalten);
void matrix_laden(float **A,char *, int, int);
/* Variablennamen koennen, muessen aber nicht in der Deklaration
* angegeben sein
*/
/***** Hauptprogramm *************************************/
int main()
{
float ** A=NULL; /* Doppelpointer f¨
ur Matrix */
int zeilen = M; /* Anzahl Zeilen der Matrix */
int spalten = N; /* Anzahl Spalten der Matrix */
char menu;
/* Menuauswahl */
char dateiname[]=MATRIXDATEI; /* Dateiname */
char tempstring[20]; /* Zum Einlesen vom Bildschirm */
int i;
73
/***** Allokalisiert Speicher f¨
ur Matrix *****/
A=(float **) malloc(M*sizeof(float));
for (i=0;i<M;i++)
{
A[i]=(float *) malloc(N*sizeof(float));
}
/***** Eingabe Menu **************************/
do
{
printf("1 - Matrix ¨
uber Bildschirm eingeben\n");
printf("2 - Matrix auf Bildschirm ausgeben\n");
printf("3 - Matrix aus Datei laden \n");
printf("4 - Matrix in Datei speichern \n");
printf("sonst - Abbruch: ");
fgets(tempstring,sizeof(tempstring),stdin);
sscanf(tempstring,"%c",&menu);
switch (menu)
{
case ’1’: {matrix_eingeben(A,zeilen,spalten);break;}
case ’2’: {matrix_ausgeben(A,zeilen,spalten);break;}
case ’3’: {matrix_laden(A,dateiname,zeilen,spalten);
break;}
case ’4’: {matrix_speichern(A,dateiname,zeilen,spalten);
break;}
default: {printf("Wiedersehn\n");break;}
}
}
while (menu >= ’1’ && menu <= ’4’);
return 0;
}
/***** Definition der Funktionen *************************/
/***** Rekursive Definition einer neuen Zeile **/
void newline(int anzahl)
{
if (anzahl > 0)
{
printf("\n");
newline(anzahl-1);
}
}
/***** Matrix vom Bildschirm einlesen **********/
74
void matrix_eingeben(float **A, int zeilen, int spalten)
{
int i=0, j=0;
char tempstring[20]="";
newline(3);
for (i=0;i<zeilen;i++)
{
for (j=0;j < spalten;j++)
{
printf("Wert f¨
ur A[%i,%i] : ",i+1,j+1);
fgets(tempstring,sizeof(tempstring),stdin);
sscanf(tempstring,"%f",&A[i][j]);
}
}
newline(3);
}
/***** Matrix auf dem Bildschirm ausgeben ******/
void matrix_ausgeben(float **A, int zeilen, int spalten)
{
int i=0, j=0;
newline(1);
for (i=0;i<zeilen;i++)
{
for (j=0;j < spalten;j++)
{
printf("%f ",A[i][j]);
}
newline(1);
}
newline(1);
}
/***** Matrix in eine Datei schreiben **********/
void matrix_speichern(float **A, char *dateiname, int zeilen, int spalten)
{
int i=0, j=0;
FILE * datei_ptr;
newline(3);
datei_ptr=fopen(dateiname,"w");
¨ffnen */
/* zum Schreiben o
if (datei_ptr == NULL)
{
printf("Fehler beim O¨ffnen\n");
fclose(datei_ptr);
75
}
else
{
for (i=0;i<zeilen;i++)
{
for (j=0;j < spalten;j++)
{
fprintf(datei_ptr,"%f ",A[i][j]);
}
fprintf(datei_ptr,"\n");
}
fclose(datei_ptr);
/* sind mit Speichern fertig */
printf("Matrix \n");
matrix_ausgeben(A,zeilen,spalten);
printf("in Datei %s geschrieben\n", dateiname);
newline(2);
}
}
/***** Matrix aus einer Datei lesen ************/
void matrix_laden(float **A ,char *dateiname,int zeilen,int spalten)
{
int i=0, j=0;
FILE * datei_ptr;
char tempchar = ’ ’;
newline(3);
datei_ptr=fopen(dateiname,"r");
¨ffnen */
/* zum Lesen o
if (datei_ptr == NULL)
{
printf("Datei existiert nicht\n");
newline(2);
}
else
{
for (i=0;i<zeilen;i++)
{
for (j=0;j < spalten;j++)
{
fscanf(datei_ptr,"%f",&A[i][j]);
}
fscanf(datei_ptr,"%c",&tempchar);
}
fclose(datei_ptr);
76
/* sind mit Einlesen fertig */
printf("Matrix \n");
matrix_ausgeben(A,zeilen,spalten);
printf("aus Datei %s eingelesen\n", dateiname);
newline(2);
}
}
Das Beispiel kann so modifiziert werden, dass die Dimension und der Dateiname durch den
Benutzer eingegeben werden kann. Nach Initialisierung und Speichern kann die Datei matrix.txt mit dem Editor betrachtet werden. Ihr Inhalt hat bis auf die Werte die Gestalt:
4.000000 4.000000 3.000000
3.000000 2.000000 2.000000
5.000000 4.000000 1.000000
Die Zeilenumbr¨uche und Leerzeichen sieht man nur indirekt auf dem Bildschirm, sie stehen
aber selbstverst¨andlich auch in der Datei. Zur Speicherung eines Zeichens ben¨otigt der Rechner ein Byte auf der Festplatte. F¨ur obiges Beispiel ben¨otigt der Computer 84 Byte (Jede Zahl
wird durch 8 Zeichen dargestellt. Nach jeder Zahl folgt ein Leerzeichen. Jede Zeile wird mit
\n“ beendet).
”
¨ Ein- und Ausgabe
7.4 Binare
Neben der zeichenorientierten Ein- und Ausgabe in Dateien gibt es noch die M¨oglichkeit, die
Daten bin¨ar zu verarbeiten. Gr¨unde, sich f¨ur diese Form der Datenausgabe zu entscheiden,
sind:
• Bei der Ausgabe von Gleitkommazahlen als Zeichen h¨angt der ben¨otigte Speicherplatz
von der Anzahl der Stellen ab. Im Bin¨arformat bestimmt der Datentyp die Gr¨oße.
• Die bin¨are Ein- und Ausgabe ist schneller.
Gr¨unde dagegen sind:
• Bin¨arformate unterscheiden sich von System zu System, so dass die entsprechenden
Dateien evtl. in das jeweilige, systemabh¨angige Bin¨arformat umzuwandeln sind. Die
Daten sind nicht portabel.
• Manche System ben¨otigen zur Kennung des Bin¨arformats den Modusbezeichner b, andere Systeme wiederum nicht. Dies hat zur Folge, dass der Quelltext nicht portabel ist.
• Man kann Daten, die im Bin¨arcode abgespeichert wurden, nicht mit einem Editor betrachten oder manipulieren.
Zur bin¨aren Ein- und Ausgabe verwendet man
77
fwrite()
Deklaration: size t fwrite (const void *ptr, size t groesse, size t anzahl, FILE * Datei);
Beschreibung: Die Funktion schreibt von der Speicherposition, auf die ptr zeigt, an-
zahl Datenobjekte der Gr¨oße groesse in den Datenstrom Datei. Der R¨uckgabewert
ist die Anzahl der erfolgreich geschriebenen Datenobjekte.
fread()
Deklaration: size t fread (const void *ptr, size t groesse, size t anzahl, FILE * Datei);
Beschreibung: Die Funktion liest aus dem Datenstrom Datei anzahl Datenobjekte der
Gr¨oße groesse und speichert sie (sequenziell) ab der Position, auf die ptr zeigt. Der
R¨uckgabewert ist die Anzahl der erfolgreich gelesenen Datenobjekte.
Beispiel 7.4 ( Bin¨are Ein- und Ausgabe)
Das Programm speichert die Matrix


4.0 4.0 3.0


A = 3.0 2.0 2.0


5.0 4.0 1.0
bin¨ar in die Datei matrix2.txt. Anschließend wird der Inhalt aus dieser Datei in die Matrix B
geladen.
#include <stdio.h>
#define M 3
#define N 3
#define MATRIXDATEI "matrix2.txt"
int main()
{
float A[M][N]={{4,4,3}, {3,2,2}, {5,4,1}};
float B[M][N];
void * ptr;
FILE * datei_ptr;
/***** Bin¨
are Ausgabe ************************/
/* Pointer auf die Adresse im Speicher setzen,
* ab der die Bytes in eine Datei geschrieben werden
* sollen */
ptr=&A[0][0];
¨ffnen */
/* Datenstrom (Datei) o
datei_ptr=fopen(MATRIXDATEI,"wb");
78
/* Schreibt N*M*sizeof(float) Bytes ab
* pointer-Adresse in den Datenstrom (Datei) */
fwrite(ptr,sizeof(float),N*M,datei_ptr);
/* Alternativ aber umst¨
andlich und langsam
* for (i=0; i < M; i++)
* {
*
for (j=0; j < N; j++)
*
{
*
fwrite(ptr,sizeof(float),1,datei_ptr);
*
ptr=ptr+sizeof(float);
*
}
* } */
/* Datenstrom (Datei) schließen */
fclose(datei_ptr);
/***** Bin¨
are Eingabe ************************/
¨ffnen */
/* Datenstrom (Datei) o
datei_ptr=fopen(MATRIXDATEI,"rb");
/* Pointer auf die Adresse im Speicher setzen,
* ab der die Bytes aus der Datei in den Speicher
* geschrieben werden sollen */
ptr=&B[0][0];
/* Liest N*M*sizeof(float) Bytes aus der Datei
* und schreibt sie sequenziell an die pointer-Adresse */
fread(ptr,sizeof(float),N*M,datei_ptr);
/* Datenstrom (Datei) schließen */
fclose(datei_ptr);
return 0;
}
¨
7.5 Dateien loschen/umbenennen
Die Anweisung, mit der Dateien gel¨oscht werden k¨onnen, lautet remove(pfadname). Diese
Funktion liefert den Wert -1, wenn nicht gel¨oscht werden konnte, ansonsten den Wert 0.
Zum Umbennen von Verzeichnissen und Dateien und zum Verschieben von Dateien wird die
Funktion rename(pfadangabe) benutzt. Liefert die Funktion einen Wert ungleich 0, dann ist
ein Fehler aufgetreten. Als Parameter werden in der Klammer zuerst der alte, danach der neue
79
Name angegeben. Weichen die Verzeichnisnamen in den beiden Pfadangaben voneinander
ab, dann wird die Datei verschoben. Verzeichnisse k¨onnen nur umbenannt, nicht verschoben
werden.
Beispiel 7.5 ( L¨oschen und Umbenennen von Dateien)
#include <stdio.h>
int main()
{
/***** L¨
oschen der Datei matrix.txt ***********/
/* Der Backslash bei der Angabe des Pfadnames * muss doppelt eingegeben
* werden, da er sonst als Steuerzeichen interpretiert wird.
* Der Backslash als Verzeichnistrenner ist Windows-spezifisch. Fuer
* LINUX ist es ein normaler Slash ’/’, fuer Macintosh ein Doppelpunkt.
*/
if (remove(".\\matrix.txt")==-1)
{
/* Ausgabe der Fehlermeldung; die Funktion perror() dient zur Ausgabe
* von Fehlermeldungen auf dem Datenstrom stderr. Der Text in Klammern
* wird um die interne Fehlermeldung erg¨
anzt.
* Die Ausgabe auf dem Bildschirm in diesem Fall lautet :
* Fehler beim L¨
oschen : No such file or directory
*/
perror("Fehler beim L¨
oschen ");
}
else
{
printf("Datei matrix.txt gel¨
oscht.\n");
}
/***** Umbenennen der Datei matrix2.txt **********/
if (rename(".\\matrix2.txt","matrix.bin") != 0)
{
perror("Fehler beim Umbenennen ");
}
else
{
printf("Datei matrix2.txt umbenannt zu matrix.bin.\n");
}
return 0;
}
80
7.6 Positionierung in einem Datenstrom
¨
Nach dem Offnen
eines Datenstroms wird die aktuelle Position in dem Datei-Pointer abgelegt.
Werden Zeichen oder Zahlen aus dem Datenstrom in den Speicher eingelesen, so erh¨oht sich
die Positionsangabe entsprechend der eingelesen Bytes. Zur Ansteuerung einer bestimmten
Stelle im Datenstrom enth¨alt der Header stdio.h die Funktion fseek().
fseek(datei-pointer,offset,ursprung)
Mit offset wird angegeben, um wieviele Bytes der datei-pointer sich vom ursprung entfernen
soll. F¨ur ursprung stehen drei Alternativen zur Auswahl:
1.) SEEK SET: Anfang des Datenstroms
2.) SEEK CUR: Aktuelle Position
3.) SEEK END: Ende des Datenstroms (EOF)
Die aktuelle Position im Datebstrom kann mit der Funktion ftell() abgefragt werden. Das
n¨achste Beispiel zeigt die Funktionsweise der beiden Funktionen.
Beispiel 7.6 ( Positionierung im Datenstrom)
#include <stdio.h>
#define zeilen 3
#define spalten 3
int main()
{
FILE *
int i;
double
double
void *
dateiptr;
A[zeilen][spalten]={{1.0,2.0,3.0}, {4.0,5.0,6.0}, {7.0,8.0,9.0}};
ausgabe;
ptr;
/***** Speichern von A im Bin¨
arformat in die Datei matrix.bin ******/
ptr=&A[0][0];
dateiptr=fopen("matrix.bin","wb");
fwrite(ptr,sizeof(A),1,dateiptr);
fclose(dateiptr);
/* Ausgabe der Diagonalen von A mit Angabe der aktuellen Position im
* Datenstrom */
dateiptr=fopen("matrix.bin","rb");
ptr=&ausgabe;
for (i=1;i<=spalten;i++)
{
81
printf("Position in Datei matrix.bin : %i \t",(int) ftell(dateiptr));
/* Einlesen einer Zahl */
fread(ptr,sizeof(double),1,dateiptr);
printf("A[%i][%i] : %f\n",i,i,ausgabe);
/* Sprung zum n¨
achsten Diagonalelement */
fseek(dateiptr,(spalten)*sizeof(double),SEEK_CUR);
}
fclose(dateiptr);
return 0;
}
Das Programm erzeugt die Ausgabe:
Position in Datei matrix.bin : 0 A[1][1] : 1.000000
Position in Datei matrix.bin : 32 A[2][2] : 5.000000
Position in Datei matrix.bin : 64 A[3][3] : 9.000000
7.7 Dateiausgabe umlenken
Wie schon in Abschnitt 7.1 bemerkt, gibt es zwei stets ge¨offnete Standardausgaben, n¨amlich
stdout und stderr. Normalerweise geben beide die Daten auf dem Bildschirm aus. Wie kann
man nun beide Datenstr¨ome so trennen, dass sie auf separate Einheiten gelenkt werden, beispielsweise stdout weiterhin auf den Bildschirm und stderr in eine Protokolldatei?
Beispiel 7.7 ( Umleiten des stderr-Stroms in die Datei error.log)
#include <stdio.h>
int main()
{
/* Umleiten des Ausgabestroms stderr.
* Anstatt der Ausgabe von stderr auf den Bildschirm, wird bei der
* Ausgabe alles in die Datei error.log geschrieben (angeh¨
angt) */
freopen("error.log","a",stderr);
if (remove(".\\matrix.bin")==-1)
{
/* Tritt ein Fehler beim L¨
oschen auf,
* so wird der Text "Fehler ..." an die
* Datei error.log angeh¨
angt */
fprintf(stderr,"Fehler beim L¨
oschen der Datei matrix.bin.\n");
}
else
{
/* Wird die Datei ordentlich gel¨
oscht, so
82
* folgt die Ausgabe "Datei ..." auf dem
* Bildschirm */
fprintf(stdout,"Datei matrix.bin erfolgreich gel¨
oscht.\n");
/* Aquivalent w¨
are printf("Datei ..."); */
}
return 0;
}
Die Funktion freopen() hat als R¨uckgabewert wieder einen Dateipointer. Die exakte Deklaration ist:
FILE * (const char * dateiname, const char * modus, FILE * datei-pointer);
Die Funktion verbindet einen g¨ultigen Dateipointer mit einem neuen Datenstrom (Datei) und
liefert als R¨uckgabewert einen Dateizeiger auf diesen Datenstrom. Die drei Parameter geben
der Reihe nach den neuen Datenstrom, den neuen Bearbeitungsmodus und einen Dateipointer
an, der zu einer ge¨offfneten Datei geh¨oren muss. Hier wird also der stets ge¨offnete Datenstrom
stderr umgelenkt auf die Datei error.log im Modus append. Die R¨uckgabe ist ebenfalls der
Dateipointer und kann auch alternativ verwendet werden.
83
8 Mehrdateiprojekte
Die bisher gezeigten Programmbeispiele waren im Umfang noch recht u¨ berschaubar, was
nat¨urlich auf ihre eingeschr¨ankte Funktionalit¨at zur¨uckzuf¨uhren ist. Gr¨oßere Softwareprojekte
werden im Allgemeinen von mehreren Entwicklern bearbeitet, was eine Aufteilung des Problems erfordert.
W¨are man in dieser Situation gezwungen, das Programm in einer einzigen Quelldatei zu halten, so w¨aren chaotische Zust¨ande im Quellcode praktisch unvermeidbar. Deshalb ist das Aufteilen der Aufgaben in C durch Mehrdateiprojekte erm¨oglicht.
1.) Pr¨aprozessordirektiven sowie die Deklaration eigener Strukturen, Unions, Typen und Subroutinen (Funktionen) werden in Headerdateien (Endung .h) zusammengefasst.
2.) Das Hauptprogramm wird in einer speziellen Datei, zumeist main.c gehalten.
3.) Die einzelnen Funktionen werden jeweils in entsprechenden Dateien gehalten (Endung
.c). Inhaltlich zusammengeh¨orige Subroutinen k¨onnen gemeinsam in der jeweiligen Datei
stehen.
¨
4.) Die Ubersetzung
der einzelnen Dateien zu Modulen (Endung .o) ist zu koordinieren. Hier
wird auf den in Abschnitt 1.6 vorgestellten mehrstufigen Kompiliervorgang zur¨uckgegriffen.
5.) Optimierte und getestete Module k¨onnen schließlich zu eigenen Programmbibliotheken
(Endung .a) zusammengefaßt werden. Andere Projekte k¨onnen dann auf die fertigen Module w¨ahrend des Linkvorgangs zugreifen.
In diesem Kapitel wird ein solches Projekt (immer noch in u¨ berschaubarem Umfang) illustriert. Dazu sollen Routinen zum Erzeugen, Laden und Speichern von beliebigen m × n Matrizen implementiert. Zus¨atzlich soll die Matrix-Multiplikation und das Anzeigen des Inhalts
einer Matrix auf dem Bildschirm zur Verf¨ugung stehen.
Begonnen wird mit der Strukturierung der Funktionalit¨aten:
1.) Matrizen werden in einer Struktur matrix gespeichert. Die Struktur enth¨alt zwei int-Variablen
f¨ur die Dimension der Matrix und einen double **-Zeiger f¨ur den Zugriff auf die einzelnen
Elemente der Matrix:
struct matrix
{
int anz_zeilen;
int anz_spalten;
double ** matrix;
};
84
2.) Laden einer Matrix von der Festplatte:
Es wird vorausgesetzt, dass die Matrizen im Zeichenformat abgespeichert wurden. Vor den
eigentlichen Matrixeintr¨agen besteht die erste Zeile der Datei aus zwei Integerwerten, die
die Anzahl der Zeilen bzw. Spalten angibt. Jede Zahl wird durch ein Leerzeichen und jede
Zeile wird durch einen Zeilenumbruch \n getrennt. Die Funktion soll als R¨uckgabewert
eine Struktur matrix haben:
struct matrix matrix laden (char * dateiname);
3.) Speichern einer Matrix auf der Festplatte:
Eine Variable von Typ struct matrix soll in dem unter 2.) angegeben Format gespeichert
werden. Formal soll die Subroutine folgende Eingabeparameter besitzen:
void matrix speichern(char * dateiname, struct matrix A);
4.) Anzeigen einer Matrix auf dem Bildschirm:
Diese Funktionen gibt eine Variable der Struktur matrix auf dem Bildschirm aus:
void matrix anzeigen(struct matrix A);
5.) Matrixmultiplikation:
Die Funktion hat als R¨uckgabewert das Matrixprodukt zweier Matrizen:
struct matrix matrix multiplizieren(struct matrix A, struct matrix B);
6.) Erzeugen einer Matrix (Speicherreservierung):
Unter Verwendung der Angabe der Dimensionen der Matrix soll entsprechend Speicher
f¨ur eine Struktur matrix reserviert werden.
struct matrix matrix allokieren(int anz zeilen,int anz spalten);
Dieses Anforderungsprofil f¨uhrt auf die folgende eigene Headerdatei, die matrix.h genannt
wird.
/* Deklaration der Struktur matrix */
struct matrix
{
int anz_zeilen;
int anz_spalten;
double ** matrix;
};
/* Deklaration der einzelnen Subroutinen */
struct matrix matrix_allokieren(int anz_zeilen,int anz_spalten);
struct matrix matrix_laden(char * dateiname);
85
struct matrix matrix_multiplizieren(struct matrix A,struct matrix B);
void matrix_speichern(char * dateiname,struct matrix A);
void matrix_anzeigen(struct matrix A);
8.1 Verteilung des Codes auf Dateien
Die Funktionalit¨aten sind nun auf die einzelnen Subroutinen verteilt und deren Deklarationen
in der Headerdatei matrix.h zusammengefaßt. Inhaltlich zusammengeh¨orige Subroutinen (wie
zum Beispiel matrix laden() und matrix speichern()) k¨onnen in einer Quelltextdatei zusammengefaßt werden. Da dieses Beispiel noch in einem u¨ berschaubarem Umfang vorliegt, wird
f¨ur jede Funktion jedoch eine eigene Quelldatei erzeugt. Schließlich muss noch der Quellcode
des Hauptprogramms in der Datei main.c abgelegt werden. Das Mehrdateiprojekt besteht mit
dem Header aus den sieben Dateien:
main.c
matrix speichern.c
matrix laden.c
matrix allokieren.c
matrix anzeigen.c
matrix multiplizieren.c
matrix.h
¨
Achtung!!! Jede einzelne Datei ist beim sp¨ateren Ubersetzen
unabh¨angig von den anderen. Speziell wissen, die einzelnen Quelldateien nicht, welche Headerdateien von main.c
eingebunden werden. Daraus folgt, dass jede einzelne Quelldatei diejenigen Headerdateien
mit #include einf¨ugen muss, die f¨ur die jeweils verwendeten Bibliotheksroutinen notwendig
sind.
Der Quellcode der einzelnen Subroutinen k¨onnte wie unten angegeben lauten. (Auf Sicherheitsabfragen bzgl. Speicheranforderungen und Dateioperationen wurde aus Platzgr¨unden ver¨
zichtet, ebenso auf die Uberpr¨
ufung der Gr¨ossen der Matrizen.)
1.) matrix speichern.c
#include <stdio.h> /* fopen, fclose, fprintf */
#include "matrix.h" /* struct matrix + Deklaration der Funktion */
void matrix_speichern(char * dateiname,struct matrix A)
{
FILE *file_ptr;
int i,j;
file_ptr=fopen(dateiname,"w");
fprintf(file_ptr,"%i %i \n",A.anz_zeilen,A.anz_spalten);
for (i=0;i < A.anz_zeilen; i++)
{
86
for (j=0;j < A.anz_spalten; j++)
{
fprintf(file_ptr,"%f ",A.matrix[i][j]);
}
fprintf(file_ptr,"\n");
}
fclose(file_ptr);
}
2.) matrix laden.c
#include <stdio.h> /* fopen, fclose, fscanf */
#include "matrix.h" /* struct matrix + Deklaration der Funktion */
struct matrix matrix_laden(char * dateiname)
{
FILE *file_ptr;
struct matrix A;
int anz_zeilen, anz_spalten;
int i,j;
file_ptr=fopen(dateiname,"r");
fscanf(file_ptr,"%i %i",&anz_zeilen,&anz_spalten);
A=matrix_allokieren(anz_zeilen,anz_spalten);
for (i=0;i < A.anz_zeilen ; i++)
{
for (j=0; j < A.anz_spalten; j ++)
{
fscanf(file_ptr,"%lf",&A.matrix[i][j]);
}
}
fclose(file_ptr);
return A;
}
3.) matrix anzeigen.c
#include <stdio.h> /* printf */
#include "matrix.h" /* struct matrix + Deklaration der Funktion */
void matrix_anzeigen(struct matrix A)
{
int i,j;
for (i=0;i< A.anz_zeilen;i++)
{
for (j=0;j < A.anz_spalten;j++)
87
{
printf("%f ",A.matrix[i][j]);
}
printf("\n");
}
}
4.) matrix allokieren.c
#include "matrix.h" /* struct matrix + Deklaration der Funktion */
#include <malloc.h> /* malloc */
struct matrix matrix_allokieren(int anz_zeilen,int anz_spalten)
{
struct matrix a;
int i;
a.matrix=(double **) malloc(anz_zeilen*sizeof(double *));
for (i=0;i<anz_zeilen;i++)
{
a.matrix[i]=(double *) malloc(anz_spalten*sizeof(double));
}
a.anz_zeilen=anz_zeilen;
a.anz_spalten=anz_spalten;
return a;
}
5.) matrix multiplizieren.c
#include "matrix.h" /* struct matrix + Deklaration der Funktion */
struct matrix matrix_multiplizieren(struct matrix A,struct matrix B)
{
struct matrix C;
int i,j,k;
C=matrix_allokieren(A.anz_zeilen,B.anz_spalten);
for (i=0;i< A.anz_zeilen; i++)
{
for (j=0;j < B.anz_spalten;j++)
{
C.matrix[i][j]=0;
for (k=0;k < A.anz_spalten;k++)
{
C.matrix[i][j]=C.matrix[i][j]
+A.matrix[i][k]*B.matrix[k][j];
}
88
}
}
return C;
}
6.) main.c
#include "matrix.h" /* structmatrix */
int main()
{
struct matrix A,B,C;
A=matrix_laden("matrixA.dat");
B=matrix_laden("matrixB.dat");
C=matrix_multiplizieren(A,B);
matrix_anzeigen(C);
matrix_speichern("matrixC.dat",C);
return 0;
}
8.2 Manuelles Kompilieren eines Mehrdateiprojektes
Mehrdateiprojekte werden in mindestens zwei Stufen zu einem ausf¨uhrbaren Programm u¨ bersetzt:
1.) Die einzelnen Quelldateien werden mit der Option -c kompiliert. Es entstehen die Objektdateien, in diesem Zusammenhang auch Module genannt:
gcc -c “Liste der .c-Dateien“
Aus jeder Quelltextdatei entsteht eine gleichnamige Datei mit der Endung .o.
2.) Die Objektdateien (Module) werden zu einer ausf¨uhrbaren Datei gelinkt. An dieser Stelle
werden eventuell ben¨otigte Bibliotheken (z.B. die Mathebibliothek) eingebunden:
gcc -o “Name des Programms“ “Liste der .o Dateien“ -lBibliothek
89
In dem einfachen Beispiel bedeutet dies:
gcc -c main.c matrix speichern.c matrix laden.c matrix anzeigen.c
gcc -c matrix allokieren.c matrix multiplizieren.c
Mit diesen Anweisungen entstehen die Objektdateien
main.o matrix speichern.o matrix laden.o matrix anzeigen.o
matrix allokieren.o matrix multiplizieren.o
Das Linken der Objektdateien zu einem ausf¨uhrbarem Programm erfolgt durch:
gcc -o Matrix main.o matrix speichern.o matrix laden.o matrix anzeigen.o matrix allokieren.o matrix multiplizieren.o
An dieser Stelle zeigt sich der Sinn des Aufteilens von Quelltexten auf mehrere Dateien sehr
deutlich: Angenommen, man m¨ochte bei der Funktion matrix anzeigen() ein anderes Format f¨ur die Matrixeintr¨age. Dann editiert man die entsprechenden Datei matrix anzeigen.c,
u¨ bersetzt nur diese,
gcc -c matrix anzeigen.c
und bindet die aktualisierte Objektdatei matrix anzeigen.o mit den bereits vom letzten Compileraufruf vorhandenen anderen Objektdateien zum Programm zusammen:
gcc -o Matrix main.o matrix speichern.o matrix laden.o matrix anzeigen.o matrix allokieren.o matrix multiplizieren.o
Man kann also Kompiliervorg¨ange durch sorgf¨altige Aufteilung auf das notwendige Maß reduzieren. Trotzdem ist es nicht immer ergonomisch, nur eine Funktion pro Datei zu halten.
Macht man manuell von dieser M¨oglichkeit Gebrauch, so ist bei hinreichend großen Projekten
die Gefahr gegeben, dass man etwa ein Modul vergisst und sich die Fehlermeldungen nicht
erkl¨aren kann.
Wichtiger ist, dass aus der Aktualisierung einer Quelldatei bzw. eines Moduls die Notwendigkeit folgen kann (und im Allgemeinen auch folgt !!), dass ein anderes Modul neu erzeugt
werden muss, weil es auf das aktualisierte Modul zugreift.
In großen Projekten werden diese Abh¨angigkeiten so komplex, dass man nur a¨ ußerst schwer
¨
den Uberblick
bewahren kann (der Quellcode des Linuxkernels z. B. besteht aus mehr als
17.000 Dateien).
8.3 Automatisiertes Kompilieren mit make
Um die am Ende des letzten Abschnitts genannten Probleme meistern zu k¨onnen, gibt es das
Programm make, das unter Ber¨ucksichtigung der Abh¨angigkeiten der Programmmodule un¨
tereinander die notwendigen Ubersetzungsvorg¨
ange vornimmt. Die Abh¨angigkeiten werden
dem Programm in einer Steuerdatei, die standardm¨aßig Makefile oder makefile heißt, mitgeteilt.
90
Das Konzept
• Der Kompiliervorgang setzt sich aus der Erzeugung von Zielen (Targets) zusammen.
Das ausf¨uhrbare Programm z.B. ist ein solches Target.
• Zu jedem Target wird im Makefile festgehalten, von welchen anderen Targets es abh¨angt.
• Anhand des Datums der letzten Datei¨anderung stellt das Programm make fest, ob ein
Target a¨ lter ist als jene, von denen es abh¨angt. Ist dies der Fall, so muss das Target
neu erzeugt werden. Im Makefile muss festgehalten werden, wie diese Erzeugung zu
geschehen hat.
• Rekursiv stellt make durch Auswertung des Makefiles die Abh¨angigkeiten fest. Auf der
untersten Ebene befinden sich z.B. die Quell- und Headerdateien, die ja zumeist durch
Editieren aktualisiert werden.
Aufbau von Makefiles
• Ein Target wird den Objekten, von denen es abh¨angt, durch einen Doppelpunkt : getrennt.
• Die Liste der Kommandos zur Erzeugung des Targets folgt in einer neuen Zeile, die
stets mit einem Tabulatorzeichen beginnen muss.
• Ein Target kann mehrmals auftreten.
• Ruft man an der Kommandozeile make auf, so wird das erste Target im Makefile erzeugt.
M¨ochte man gezielt ein anderes Target erzeugen, so verwendet man
make Name des Targets
F¨ur das Beispielprojekt hat das Makefile die Darstellung:
Beispiel 8.1 (Makefile 1)
Matrix : main.o matrix_speichern.o matrix_laden.o matrix_anzeigen.o
matrix_multiplizieren.o matrix_allokieren.o
gcc -o Matrix main.o matrix_speichern.o matrix_laden.o\
matrix_anzeigen.o matrix_multiplizieren.o matrix_allokieren.o
main.o : main.c matrix.h
gcc -c main.c -Wall
matrix_speichern.o : matrix_speichern.c matrix.h
gcc -c matrix_speichern.c -Wall
matrix_laden.o : matrix_laden.c matrix.h
gcc -c matrix_laden.c -Wall
91
matrix_anzeigen.o : matrix_anzeigen.c matrix.h
gcc -c matrix_anzeigen.c -Wall
matrix_multiplizieren.o : matrix_multiplizieren.c matrix.h
gcc -c matrix_multiplizieren.c -Wall
matrix_allokieren.o : matrix_allokieren.c matrix.h
gcc -c matrix_allokieren.c -Wall
Makros mit make
Makros dienen der Abk¨urzung und Zusammenfassung von mehreren Objekten sowie der Unterst¨utzung bei der plattformunabh¨angigen Programmierung. Muss man z.B. eine Liste von
Objektdateien mehrmals im Makefile verwenden, so leistet dies
Objekte = main.o matrix speichern.o matrix laden.o matrix anzeigen.o matrix multiplizieren.o
matrix allokieren.o
und den so zugewiesenen Inhalt des Makros Objekte liest man durch
$(Objekte)
Das obige Makefile l¨asst sich mit Hilfe von Makros vereinfachen:
Beispiel 8.2 (Makefile 2)
PROGRAMMNAME = Matrix
OBJEKTE = main.o matrix_speichern.o matrix_laden.o matrix_anzeigen.o
matrix_multiplizieren.o matrix_allokieren.o
$(PROGRAMMNAME) : $(OBJEKTE)
gcc -o $(PROGRAMMNAME) $(OBJEKTE)
main.o : main.c
gcc -c main.c -Wall matrix.h
matrix_speichern.o : matrix_speichern.c matrix.h
gcc -c matrix_speichern.c -Wall
matrix_laden.o : matrix_laden.c matrix.h
gcc -c matrix_laden.c -Wall
matrix_anzeigen.o : matrix_anzeigen.c matrix.h
gcc -c matrix_anzeigen.c -Wall
matrix_multiplizieren.o : matrix_multiplizieren.c matrix.h
92
gcc -c matrix_multiplizieren.c -Wall
matrix_allokieren.o : matrix_allokieren.c matrix.h
gcc -c matrix_allokieren.c -Wall
Der Aufruf make veranlasst also die Erzeugung des ersten Targets, in diesem Fall ist es das
ausf¨uhrbare Programm Matrix.
8.4 Eigene Bibliotheken
Sind die einzelnen Objekte (Module) auf ihre Zuverl¨assigkeit und Geschwindigkeit hin ausreichend getestet, so kann man erw¨agen, sie in einer eigenen Programmbibliothek zusammenzufassen. Wie bei der C-Standardbibliothek oder der Mathematikbibliothek ist dann bei der
Implementierung von neuen Programmen nur darauf zu achten, dass die zugeh¨orige Headerdatei mit den Deklarationen an den entsprechenden Stellen im Quelltext auftaucht und die
Bibliothek beim Linken eingebunden wird. Bibliotheken entstehen aus den Objektdateien, die
mit dem ar-Kommando zusammengef¨ugt werden.
Zur Illustration werden die Funktionen in der Bibliothek libmatrix.a zusammengefasst. Dies
geschieht mit dem Aufruf
ar -r libmatrix.a matrix speichern.o matrix laden.o matrix anzeigen.o matrix multiplizieren.o
matrix allokieren.o
und beim sp¨ateren Linken wird die Bibliothek einfach mit angegeben:
gcc -o Matrix main.o libmatrix.a
Auch diese Operation l¨asst sich mit make automatisieren. Das entsprechende Makefile sieht
zum Beispiel folgendermaßen aus:
Beispiel 8.3 (Makefile 3)
PROGRAMMNAME = Matrix
BIBLIOTHEK = ./libmatrix.a
VERZ = ./
BIBOBJEKTE = matrix_speichern.o matrix_laden.o matrix_anzeigen.o
matrix_multiplizieren.o matrix_allokieren.o
$(PROGRAMMNAME) : main.o $(BIBLIOTHEK) $(VERZ)/matrix.h
gcc -o $(PROGRAMMNAME) main.o $(BIBLIOTHEK)
main.o : $(VERZ)/main.c $(VERZ)/matrix.h
gcc -c $(VERZ)/main.c -Wall
$(BIBLIOTHEK) : $(BIBOBJEKTE)
93
ar -r $(BIBLIOTHEK) $(BIBOBJEKTE)
matrix_speichern.o : $(VERZ)/matrix_speichern.c $(VERZ)/matrix.h
gcc -c $(VERZ)/matrix_speichern.c -Wall
matrix_laden.o : $(VERZ)/matrix_laden.c $(VERZ)/matrix.h
gcc -c $(VERZ)/matrix_laden.c -Wall
matrix_anzeigen.o : $(VERZ)/matrix_anzeigen.c $(VERZ)/matrix.h
gcc -c $(VERZ)/matrix_anzeigen.c -Wall
matrix_multiplizieren.o : $(VERZ)/matrix_multiplizieren.c $(VERZ)/matrix.h
gcc -c $(VERZ)/matrix_multiplizieren.c -Wall
matrix_allokieren.o : $(VERZ)/matrix_allokieren.c $(VERZ)/matrix.h
gcc -c $(VERZ)/matrix_allokieren.c -Wall
Wird die Bibliothek anderswo untergebracht, um anderen Programmierprojekten den Zugriff
zu erleichtern und handelt es sich (¨ahnlich wie bei den Headerdateien) nicht um ein Verzeichnis im Standardsuchpfad f¨ur C-Bibliotheken, so muss das betreffende Verzeichnis beim Linken
mit der Option -L angegeben werden. Die Bibliothek wird dann mit der Option -l u¨ bergeben,
wobei das Pr¨afix lib und das Suffix .a entfallen. Da das aktuelle Verzeichnis mit . bezeichnet
wird, sind folgende Formulierungen a¨ quivalent
gcc -o Matrix main.o -L. -lmatrix
gcc -o Matrix main.o libmatrix.a
94
A
A.1 Zahlendarstellung im Rechner und
Computerarithmetik
Prinzipiell ist die Menge der im Computer darstellbaren Zahlen endlich. Wie groß“ diese
”
Menge ist, h¨angt von der Rechnerarchitektur ab. So sind bei einer 32Bit-Architektur zun¨achst
maximal 232 = 4294967296 ganze Zahlen (ohne Vorzeichen) darstellbar.
Innerhalb dieser Grenzen stellt die Addition und Multiplikation von ganzen Zahlen kein Problem dar.
Ganz anders verh¨alt es sich mit rationalen und erst recht irrationalen Zahlen: Jede Zahl x , 0
l¨asst sich bekanntlich darstellen als
∞
X
N
x = sgn(x)B
x−n B−n ,
(A.1)
n=1
wobei 2 ≤ B ∈ N, N ∈ Z und xn ∈ {0, 1, . . . , B − 1} f¨ur alle n ∈ N (B-adische Entwicklung von
x). Diese ist eindeutig, wenn gilt:
• x−1 , 0, falls x , 0,
• und es existiert ein n ∈ N mit x−n , B − 1.
Eine Maschinenzahl dagegen hat die Form
x˜ = sgn(x)B
N
l
X
x−n B−n
n=1
wobei die feste Gr¨oße l ∈ N die Mantissenl¨ange ist. Als Mantisse bezeichnet man den Ausdruck
l
X
m(x) =
x−n B−n .
(A.2)
n=1
Hieraus folgt sofort, dass irrationale Zahlen u¨ berhaupt nicht und von den rationalen Zahlen
nur ein Teil im Rechner dargestellt werden. Man unterscheidet zwei Arten der Darstellung:
1. Festkommadarstellung
Sowohl die Anzahl N der zur Darstellung verf¨ugbaren Ziffern, als auch die Anzahl N1 der
Vorkommastellen ist fixiert. Die Anzahl der maximal m¨oglichen Nachkommastellen N2
erf¨ullt notwendigerweise
N = N1 + N2 .
95
2. Gleitkommadarstellung
In (A.2) ist die Mantissenl¨ange l fixiert und der Exponent N ist begrenzt durch
N− ≤ N ≤ N+ ,
N− , N+ ∈ Z.
Alle Maschinenzahlen liegen dabei vom Betrag her im Intervall (BN− −1 , BN+ ). Zur Normalisierung der Darstellung verlangt man, dass x−1 , 0, wenn x , 0. Zahlen, die kleiner als
BN− −1 sind, werden vom Computer als 0 angesehen. Zahlen, die gr¨oßer als BN+ sind, k¨onnen
nicht verarbeitet werden (Exponenten¨uberlauf).
Die Darstellung von irrationalen Zahlen erfordert eine Projektion auf die Menge der Maschinenzahlen, die so genannte Rundung. Man unterscheidet vier Rundungsarten:
1. Aufrundung:
Zu x ∈ R w¨ahlt man die n¨achsth¨ohere Maschinenzahl x˜.
2. Abrundung:
Zu x ∈ R w¨ahlt man die n¨achstniedrigere Maschinenzahl x˜.
3. Rundung (im engeren Sinne)
Ausgehend von der Darstellung (A.1), setzt man
P
l


 n=1 x−n B−n ,
x˜ B sgn(x)BN 
P

 ln=1 x−n B−n + B−l ,
fallsx−(l+1) < B/2
fallsx−(l+1) ≥ B/2
4. Abschneiden
Verwerfen aller x−n mit n > l in der Darstellung A.1.
Die Gr¨oße
| x˜ − x|
heißt absoluter, die Gr¨oße
| x˜ − x|
|x|
heißt relativer Rundungsfehler. Man wird erwarten, dass die Rundung im engeren Sinne die
beste ist. In der Tat weist sie statistisch gesehen die geringsten Rundungsfehler auf.
F¨ur die Rundung (im engeren Sinne) gilt:
a) x˜ ist wieder eine Maschinenzahl.
b) der absolute Fehler erf¨ullt
| x˜ − x| ≤
96
1 N−l
B
2
c) Der relative Fehler erf¨ullt
| x˜ − x| 1 −l+1
≤ B
= eps
|x|
2
Die Zahl eps wird auch als Maschinengenauigkeit bezeichnet.
A.2 IEEE Gleitkommazahlen
Die IEEE (Institute of Electrical and Electronic Engineers) ist eine internationale Organisation, die bin¨are Formate f¨ur Gleitkommazahlen festlegt. Diese Formate werden von den meisten
(aber nicht allen) heutigen Computern benutzt. Die IEEE definiert zwei unterschiedliche Formate mit unterschiedlicher Pr¨azision: Einzel- und Doppelpr¨azision (single and double precision). Single precision wird in C von float Variablen und double precision von double Variablen
benutzt.
Intel’s mathematischer Co-Prozessor benutzt eine dritte, h¨ohere Pr¨azision, die sogenannte erweiterte Pr¨azision (extended precision). Alle Daten im Co-Prozessor werden mit dieser erweiterten Pr¨azision behandelt. Werden die Daten aus dem Co-Prozessor im Speicher ausgelagert, so werden sie automatisch umgewandelt in single bzw. double precision. Die erweiterte
Pr¨azision benutzt ein etwas anderes Format als die IEEE float- bzw double-Formate und werden in diesem Abschnitt nicht diskutiert.
IEEE single precision
Single precision Gleitkommazahlen benutzen 32 Bits (4 Byte) um eine Zahl zu verschl¨usseln:
Bit 1-23: Hier wird die Mantisse gespeichert, d.h. die Mantissenl¨ange l betr¨agt bei single
precision 23. Im single precision Format wird zur Mantisse noch eins addiert
Beispiel A.1 (IEEE Mantisse)
23 Bit
z
}|
{
10110110100000000000000
=0.1011011012 + 1
=1.1011011012
Bit 24-31: Hier wird der bin¨are Exponent N gespeichert. In Wirklichkeit wird der Exponent
N plus 127 gespeichert.
Bit 32: Gibt das Vorzeichen der Zahl an.
97
Beispiel A.2 (IEEE single precsion)
Welche Dezimalzahl wird duch den Bin¨arcode 01000001
| {z } 11011011
| {z } 01000000
| {z } 00000000
| {z } darByte 3
Byte 2
Byte 1
Byte 0
gestellt?
Bit 32 Bit 31-24
Bit 23-1
0
10000011
|{z}
| {z } 10110110100000000000000
|
{z
}
Zahl ist positiv N=131−127=4
Mantisse
=1.1011011012 * 2N
=11011.011012
=24 + 23 + 21 + 20 + 2−2 + 2−3 + 2−5
=27.406250
Folgendes Beispielprogramm testet ob der Computer die float Zahl 27.040625 im IEEE single
precision Format speichert.
Beispiel A.3
#
#
#
#
#
#
#
#
#
include <stdio.h>
define text1 "Der Computer testet ob die float Zahl 27.0406250 \n"
define text2 "im IEEE single Format gespeichert wird.\n\n"
define text3 "
\t
Byte 3
Byte 2
Byte 1
Byte 0
define text4 "27.0406250 = \t 01000001 11011011 01000000 00000000"
define text5 " (im IEEE Format)\n"
define text6 "
= \t
65
219
64
0"
define text7 " (Byte als unsigned char)\n\n\n"
define text8 "Ihr Rechner betrachtet die Zahl als : %f \n"
\n\n"
int main()
{
unsigned char *p;
float a;
printf(text1);
printf(text2);
printf(text3);
printf(text4);
printf(text5);
printf(text6);
printf(text7);
/*
/*
Byte 3
Byte 2
98
Byte 1
Byte 0 */
*/
/* bin¨
ar :
01000001
/* dezimal : 65
11011011
219
01000000
64
00000000 */
0
*/
p=(unsigned char *) &a;
p[0]= 0;
p[1]= 64;
p[2]= 219;
p[3]= 65;
printf(text8,a);
return 0;
}
IEEE double precision
Double precision Gleitkommazahlen benutzen 64 Bits (8 Byte) um eine Zahl zu verschl¨usseln.
Bit 1-52: Mantisse
Bit 53-63: Exponent
Bit 64: Vorzeichen
Mehr zu diesem Thema findet man zum Beispiel in [Car].
A.3 Computerarithmetik
Das Rechnen mit ganzen Zahlen ist (innerhalb der erw¨ahnten Grenzen) kein Problem: Addition und Multiplikation sind kommutativ, assoziativ und das Distributivgesetz gilt.
Beim Rechnen in der Fest- oder Gleitkommadarstellung gelten Assoziativ- und Distributivgesetz jedoch nicht!
Ein weiterer Effekt, der zu beachten ist, ist die Ausl¨oschung von f¨uhrenden Stellen:
Beipiel (Ausl¨oschnung):
Sei B = 10, l = 3. Sei x = 0.9995, y = 0.9984 und es soll im Computer die Differenz x − y
berechnet werden. Wie wirkt sich die Rundung auf das Resultat aus? Zun¨achst ist x˜ = 0.1 · 101
und y˜ = 0.998. Daher ist
x˜ − y˜ = 0.2 · 10−2 ,
das exakte Resultat ist
x − y = 0.11 · 10−2
und der relative Fehler ergibt sich zu 81.8% ! Dieser Effekt der Verst¨arkung des Rundungsfehlers tritt auf, wenn man fast identische Zahlen voneinander subtrahiert.
99
Eine Operation mit a¨ hnlich instabilen Verhalten ist die Division x/y mit x << y. Addition
zweier Zahlen mit gleichem Vorzeichen und Multiplikation sind jedoch gutartige Operationen.
100
A.4 Die O-Notation
Um eine objektive Grundlage f¨ur den Vergleich zweier Algorithmen zur L¨osung ein und derselben Aufgabe zu schaffen, untersucht man die Eigenschaften von Algorithmen anhand der
Gr¨oße (Dimension) des Problems.
Da man zum Vergleich verschiedener Algorithmen oft nur an deren asymptotischem Verhalten bez¨uglich Speicherbedarf bzw. Laufzeit interessiert ist und weniger an exakten Werten,
hat sich zur Untersuchung die so genannte O-Notation (Landau-Symbole) bew¨ahrt und eingeb¨urgert.
Definition A.4
Sei G ⊂ R, f, g : G → R und x0 ∈ G.
a) Die Funktion f heißt von der Ordnung O(g(x)) f¨ur x → x0 , wenn es eine Konstante C > 0
und ein δ > 0 gibt, so dass
∀|x − x0 | < δ
| f (x)|
≤ C.
|g(x)|
:
(A.3)
Dabei wird zus¨atzlich x , x0 gefordert, falls g (und f ) in x0 nicht sinnvoll definiert werden
k¨onnen.
b) Die Funktion f heißt von der Ordnung o (g(x)) f¨ur x → x0 , wenn es f¨ur jede Konstante
C > 0 ein δ gibt, so dass A.3 erf¨ullt ist.
Beispiel A.5 (O-Notation)
1.) Ein Polynom vom Grad N
p(x) =
N
X
an x n
n=0
ist asymptotisch (d.h. f¨ur x → ∞) von der Ordnung O(xN ) bzw. f¨ur jedes > 0 von der
Ordnung o(xN+ ).
2.) Der Satz von Taylor l¨asst sich f¨ur jede Funktion f ∈ C k (G) (G offen, k ∈ N) schreiben als
f (x + h) =
k−1 (n)
X
f (x) n
h + O(hk )
n!
n=0
bzw.
k−1 (n)
X
f (x) n
f (x + h) =
h + o(hk−1 ).
n!
n=0
In dieser Form wird h¨aufig auch die Approximationsqualit¨at numerischer Verfahren untersucht und ausgedr¨uckt.
101
Satz A.6
Das Landausymbol O hat die folgenden Eigenschaften:
(i) f (x) = O( f (x)).
(ii) f (x) = o(g(x)) ⇒ f (x)=O(g(x)).
(iii) Sei 0 < K ∈ R. Dann gilt: f (x) = K O(g(x)) ⇒ f (x)=O(g(x)).
(iv) Wenn f1 (x)=O(g1 (x)) und f2 (x)=O(g2 (x)), dann gilt
f1 (x) f2 (x) = O(g1 (x)g2 (x)).
(v) Wenn f (x)=O(g1 (x)g2 (x)), dann gilt
f (x) = g1 (x)O(g2 (x)).
Der Beweis dieser Eigenschaften sowie die Untersuchung der Frage, in wie weit analoge Aus¨
sagen f¨ur das Symbol o(·) gelten, ist als Ubung
u¨ berlassen.
Beispiel A.7 (Komplexit¨at einer Polynomauswertung)
Sei p das Polynom aus Beispiel A.5. F¨ur ein festes x0 ∈ R soll p0 (x0 ) auf die zwei folgenden
Arten ausgewertet werden:
/* Polynomauswertung 1.Methode */
1.) Setze p0 := a0
2.) F¨ur n = 1, 2, ..., N:
Setze Hilfsgr¨oße b := an ;
F¨ur m = 1, ..., n:
multipliziere b mit x0
p0 := p0 + b;
/* Polynomauswertung 2.Methode */
1.) Setze p0 = a0
Setze Hilfsgr¨oße b := x0 ;
2.) F¨ur n = 1, 2, ..., N:
Setze Hilfsgr¨oße c := an ∗ b;
p0 := p0 + c;
b = b ∗ x0 ;
102
Die Komplexit¨at der 1. Methode lautet, wenn man nur die Multiplikation als wesentliche Operationen ber¨ucksichtigt:
N
X
1
n = N(N + 1) = O(N 2 )
2
n=1
Die Komplexit¨at der 2. Methode hingegen ergibt:
2N = O(N)
103
¨
A.5 Der Praprozessor
Alle C-Compiler u¨ bersetzen die Quellprogramme in mehreren Durchg¨angen. Im ersten Durchgang werden die Programme vom sogenannten Pr¨aprozessor verarbeitet. Dieser Teil des CCompilers verarbeitet alle Anweisungen, die mit einem # beginnen, auch Pr¨aprozessor-Direktiven
genannt. Er erzeugt aus der urspr¨unglichen Quelldatei einen tempor¨aren Quellcode, der anschließend von weiteren Hilfsprogrammen des Compilers verarbeitet wird (siehe Abbildung
A.1 ).
Pr¨aprozessor
- Entfernen von
Kommentaren
Parser
- Syntaxcheck
Quellcode
- Quellcode
Quellcode
- Ausf¨uhren von
Direktiven
Compiler, Assembler
¨
- Ubersetzen
in Machinensprache
Linker
- Einbinden von Bibliotheksroutinen
- Programm
Objektdatei
Abbildung A.1: Der Weg von der Quelldatei zum ausf¨uhrbaren Programm
Dieser Vorgang bleibt f¨ur den Programmierer normalerweise unsichtbar. Man kann das Ergebnis aber auch speichern, um entweder seine Arbeit zu kontrollieren oder um sein Resultat
anderweitig zu verarbeiten.
gcc Quelltext.c -E -o dateiname
Die so erzeugte Datei kann mit dem Editor betrachtet werden (vgl. Kapitel 1.6)
¨
A.5.1 Dateien einfugen
(# include)
Die wohl wichtigste Direktive f¨ur den Pr¨aprozessor ist #include. Mit ihrer Hilfe werden die
Inhalte bestehender Dateien wie Textbausteine in das Programm eingef¨ugt. Dabei handelt es
sich im Normalfall um Headerdateien. Headerdateien enthalten u¨ blicherweise die Endung .h
104
und d¨urfen keinen ausf¨uhrbaren Code enthalten, sondern nur Deklarationen. Dies liegt daran,
dass sie unter bestimmten Umst¨anden in mehrere C-Dateien eingef¨ugt und damit mehrfach
u¨ bersetzt werden. Enthielten sie fertige Funktionen, dann l¨agen die hinterher f¨ur den Linker
mehrfach vor. Dadurch kann er keine eindeutige Bindung herstellen.
Im folgenden Beispiel wird die Deklaration der Funktion summe in eine Headerdatei summe.h geschrieben. Im Quelltext des Programms wird dann die Deklaration der Funktion durch
Einbinden des Headerfiles bewerkstelligt.
Beispiel A.8 ( summe.h)
float summe (float ,float);
¨
Beispiel A.9 ( Einfugen
des Header summe.h)
#include <stdio.h>
#include "summe.h"
/* Hauptprogramm */
int main()
{
float x=2.0;
float y=3.3;
printf("Summe von x+y = %f \n",summe(x,y));
return 0;
}
/* Definition summe-Funktion */
float summe (float x, float y)
{
return x+y;
}
Bemerkung A.10
• Die Dateinamen der Bibliotheksheader werden von spitzen Klammern <und >eingeschlossen. Derartig gekennzeichnete Dateien werden immer in den Bibliotheksverzeichnissen gesucht. Unter LINUX befindet sich die Header im Verzeichnis /usr/include.
• Eigene Dateien werden durch Anf¨uhrungszeichen begrenzt. Sie werden dadurch vom
Pr¨aprozessor im aktuellen Projektverzeichnis gesucht.
105
A.5.2 Konstanten definieren (#define)
Der Pr¨aprozessorbefehl #define erlaubt es, eine Zeichenkette im Programm durch eine andere
zu ersetzen. Die allgemeine Form sieht so aus:
# define Name Zeichenkette
Am Beispiel des folgenden Programms k¨onnen Sie die Arbeitsweise von #define nachvollziehen.
Beispiel A.11 (#define)
#include <stdio.h>
#define PI 3.14
#define Mein_PI "Mein Pi ist :"
int main()
{
float x=PI;
printf(Mein_PI);
printf(" %f\n",x);
}
Der Pr¨aprozessor manipuliert den Quellcode (vernachl¨assigt man die Direktive #include <stdio.h>
zu:
int main()
{
float x=3.14;
printf("Mein Pi ist :");
printf(" %f\n",x);
}
Der Pr¨aprozessor kennt bereits einige eingebaute Konstanten, die nicht erst definiert werden
m¨ussen und auch nicht per #define u¨ berschrieben werden sollten. Dies sind:
LINE
FILE
DATE
TIME
f¨ur die aktuelle Zeilennummer
f¨ur den Namen der kompilierten Datei
f¨ur das aktuelle Datum
f¨ur die aktuelle Uhrzeit
Weitere n¨utzliche Anwendungen von Pr¨aprozessor-Direktiven k¨onnen der Literatur (z.B. in
[Erl]) entnommen werden.
106
Literaturverzeichnis
[Knu]
D.E. Knuth: The Art of Computer Programming - Vol. 2,2nd ed., AddisonWesley
[Kof]
M. Kofler: Linux - Installation, Konfiguration, Anwendung, Addison-Wesley
[Kru]
G. Kr¨uger: Go To C-Programmierung, Addison Wesley
[Oua]
S. Oualline: Practical C Programming, O’Reilly
[WeDaDa]
M. Welsh, M.K. Dalheimer, T. Dawson: Linux- Wegweiser zur Installation &
Konfiguration, O’Reilly
[Erl]
H. Erlenk¨otter: C - Programmieren von Anfang an, rororo
[KerRit]
B.W. Kernighan, D.M. Ritchie: Programmieren in C, Hanser Verlag
[OraTal]
A. Oram, S. Talbott: Managing Projects with make, O’Reilly
[PrTeVeFl]
W.H. Press, S.A. Teukolsky, W.T. Vetterling, B.P. Flannery: Numerical Recipes
in C, Cambridge University Press
[Kir]
R. Kirsch: Einf¨uhrung in die Programmierung f¨ur Mathematiker, Vorlesungsskript Sommersemester 2004
[Car]
P.
A.
Carter
:
PC
Assembly
http://www.drpaulcarter.com/pcasm/, 2003
107
language,
Online
,
Index
O-Notation 101
goto, 38
if(), 30
malloc(), 44
perror(), 80
printf(), 6, 26
read(), 78
realloc(), 44
remove(), 79
rename(), 79
scanf(), 27
sizeof(), 10
switch(), 31
while(), 36
call by reference, 53
call by value, 51
cast, 25
Compiler, 1
compilieren, 1
Computerprogramm, 1
cygwin, 5
Abrundung, 96
Abschneiden, 96
Adressoperator, 40, 41
Allokalisieren, 44
Arbeitsverzeichnis, 3
array, 11
ASCII-Tabelle, 38
Aufrundung, 96
Aufz¨ahlungstyp, 60
Ausl¨oschnung, 99
B-adische Entwicklung, 95
Befehl, 4
Bezeichner, 57
Bibliotheken, 93
Bibliotheksfunktionen, 47
C-Anweisung
break, 38
calloc(), 44
case, 31
continue, 38
do-while(), 37
fclose(), 69
fgetc(), 72
fopen(), 69
for(), 33
fprintf(), 71
fputc(), 72
free(), 44
freopen(), 83
fscanf(), 71
fseek(), 81
ftell(), 81
fwrite(), 78
Datei, 2
Datenstrom, 69
Datenstrom:stderr, 70
Datenstrom:stdin, 70
Datenstrom:stdout, 70
Datentyp
char, 9
signed, 9
unsigned, 9
const, 10
double, 9
long, 9
enum, 60, 67
FILE, 69
float, 9
108
int, 9
short, 9
signed, 9
unsigned, 9
pointer, 39
struct, 60
strukturierte, 60
typedef, 68
union, 60, 66
void, 9
zeiger, 39
Datentypen, 8
definiert, 8
Deklaration, 8
deklarieren, 8
Dereferenzoperator, 41
Distributionen, 2
string.h, 22
Headerdatei, 5
IEEE, 97
Initialisierung, 8
Interpreter, 1
interpretieren, 1
Kommandointerpreter, 4
Kommandozeilen-Parameter, 55
Kommentare, 5
Landau-Symbole, 101
libraries, 2
LINUX, 2
LINUX-Befehl
ar, 93
cat, 4
cd, 4
cp, 4
ls, 4
make, 90
man, 4
mkdir, 4
more, 4
mv, 4
pwd, 4
rm, 4
Listen, 64
editieren, 1
Editor, 1
enum, 67
eps, 97
Exponenten¨uberlauf, 96
Felder, 11
dynamisch, 43
Festkommadarstellung, 95
Funktion
call by reference, 53
call by value, 51
Funktionsparameter, 47
Funktionsrumpf, 48
R¨uckgabewert, 47
Funktionen, 47
Funktionsparameter, 47
Funktionsrumpf, 48
make, 90
Makros, 92
Manpage, 4
Mantisse, 95
Mantissenl¨ange, 95
Maschinencode, 1
Maschinengenauigkeit, 97
Maschinenzahl, 95
Mehrdateiprojekte, 84
Modul, 89
Modulen, 84
Monitor, 7
Gleitkommadarstellung, 96
Header
float.h, 24
limits.h, 24
malloc.h, 44
math.h, 21
stdio.h, 5
offset, 81
Operationen, 8
109
Operator
Adressoperator, 40
Assoziativit¨at, 19
Auswahloperator, 62
bitorientiert, 16
dekrement, 18
Dereferenzoperator, 41
inkrement, 18
logisch, 16
Priorit¨at, 19
Referenzoperator, 41
Strukturoperator, 61
Vergleichsoperator, 15
Zugriffsoperator, 41
Signum-Funktion, 30
stderr, 70
stdin, 70
stdout, 70
String, 11
Strings, 6
Struktur, 60
Suffix, 5
Target, 91
typedef, 68
Typkonversion, 25
underscore, 8
Union, 60
union, 66
UNIX, 2
ursprung, 81
Pfad, 3
absolut, 3
relativ, 3
Pointer, 39
Pointervariable, 40
portabel, 77
Postfixnotation, 19
Pr¨afixnotation, 18
Pr¨aprozessor, 5, 104
Pr¨aprozessor-Direktiven, 104
define, 106
include, 104
Pr¨aprozessordirektiven, 5
Programmflusskontrolle, 30
Variablen, 8
G¨ultigkeit, 49
global, 49
lokal, 49
Sichtbarkeit, 49
Verzeichnis, 2
Verzeichnisbaum, 3
Verzweigung, 30
Wildcard, 4
Zeichenketten, 6
Zeichenkettten, 12
Zeiger, 39
Zeiger auf Funktionen, 57
Zeigervariable, 40
Zugriffsoperator, 41
Zyklus
abweisend, 36
nichtabweisend, 37
Z¨ahlzyklus, 33
Quelltext, 1
R¨uckgabewert, 47
Referenzoperator, 41
rekursiv, 54
Rundung, 96
Rundungsfehler, 96
absolut, 96
relativ, 96
Schleifen, 33
do-while, 37
for, 33
while, 36
Shell, 4
110
Was this manual useful for you? yes no
Thank you for your participation!

* Your assessment is very important for improving the work of artificial intelligence, which forms the content of this project

Download PDF

advertisement