Programmierung für Mathematiker

Werbung
Programmierung für Mathematiker
Prof. Dr. Thomas Schuster
M.Sc. Dipl.-Phys. Anne Wald
19.04.2017
Mathematik und ihre Anwendung am Lehrstuhl Prof. Thomas Schuster
www.num.uni-sb.de/schuster
Forschungsschwerpunkte der AG Schuster
Allgemein:
Inverse Probleme (Theorie und Anwendungen)
Numerische Analysis
Optimierung
Numerik partieller Differentialgleichungen
Anwendungen:
Vektortomographie (2D, 3D)
Parameteridentifizierungen bei anisotropen, elastischen Wellengleichungen
Terahertz-Tomographie
Magnetpartikelbildgebung
Hyperspektrale Bildgebung
Algorithmenentwicklung in der Terahertz-Tomographie
Zerstörungsfreie Prüfung eines Polyethylenblocks: Standardmethode und verbesserte
Variante
Numerisches Praktikum in der Computertomographie (9 / 12 CP)
Inhalt der Vorlesung
Phänomen,
reale Welt
Experiment
Hypothese
Vereinfachung
Abstraktion
Erfahrung
Modellfehler
(Mathematisches)
Modell
(Mathematisch)
exakte Formulierung
eines Problems
Vergleich
Evaluierung
Ergebnis
Modellverfeinerung
Parameteradaption
Implementierung
des Programms
Stabilität des
Algorithmus
Lösbarkeit und
Kondition
des Problems
Syntaktische und
semantische Fehler
Rundungsfehler
Verfahrensfehler
Datenfehler
Die Programmiersprache C
Definition (Programmiersprache)
Eine Programmiersprache ist eine künstliche Sprache, die entwickelt wurde, um
Rechenvorschriften für eine Maschine, in der Regel einen Computer, zu formulieren.
Kommunikationsmittel zwischen Mensch und Maschine
Vokabeln und Grammatik (Syntax)
>1800 Sprachen mit verschiedenen Intentionen
Warum C?
ursprünglich zur Entwicklung von Betriebssystemen
heute auch häufig in Anwendungssoftware zu finden
Compiler verfügbar für nahezu alle Prozessoren und Betriebssysteme
hardwarenah, erlaubt direkten Speicherzugriff → schnell!!
weit verbreitet, viele kostenlose Bibliotheken oder Programmbausteine im Netz
Grundlage für C++, Java, Python, Perl, PHP,...
Die integrierte Entwicklungsumgebung KDevelop
Kostenlos verfügbar unter Linux – Open Source Software
Stabil – von zahlreichen Nutzern getestet
Viele nützliche Helferlein wie Syntax Highlighting, Auto-Vervollständigung,
automatische Einrückung, integrierte Konsole, Blockausblendung,
Quelltext-Browser, Hintergrund-Parser, Debugger und vieles mehr...
Mein erstes C-Programm
$ kdevelop &
hallo_welt.c
1
2
3
4
5
6
/* hallo_welt.c - gibt Begruessung auf dem Bildschirm aus */
main()
// Achtung: C ist case-sensitive!
{
printf("Hallo Welt!\n");
}
$ gcc hallo_welt.c -o hallo_welt
hallo_welt.c: In Funktion ’main’:
hallo_welt.c:3:3: Warnung: Unverträgliche [...] Funktion ’printf’
$ ls
hallo_welt
hallo_welt.c
$ ./hallo_welt
Hallo Welt!
hallo_welt.c~
Wie entsteht ein Programm?
prog.c
Quellcode
Präprozessor
Quellcode
Compiler
prog.o
Objektcode
Linker
prog
prog.exe
Programm
Objektcode
library.so
Die Programmiersprache C
Vokabeln
reservierte Wörter: printf, scanf, struct, for, while, float, ...
Opertoren: +, &, %, <=, !, *, /
Grammatik (Syntax)
a + 3 = b;
E
b = a + 3;
printf("Hallo Welt!")
float 2wurzelx-1;
E
X
printf("Hallo Welt!");
float cMOU_2Bi5nUOz5O_bG6toBUZb;
E
a = 5; b = 5 / a;
Bedeutung (Semantik)
a = 0; b = 5 / a;
X
E
X
X
Die Software Matlab von MathWorks
Warum Matlab?
High-Level Sprache: ,→ kurze Entwicklungszeiten
Vielfältige Visualisierungsmöglichkeiten
MATLAB-Programme sind vollständig portierbar.
Integration zusätzlicher Toolboxen (PDE, Optimization, Wavelet)
Matlab ist eine kommerzielle Software, aber es gibt eine
MathWorks TAH Campuslizenz
Die Software darf von allen Studierenden der Universität des Saarlandes genutzt
werden. Das umfasst einerseits beliebig viele Installationen auf dem Campus und
andererseits auch die Nutzung auf privaten Computern.
Notwendig: Registrierung bei der Firma ASKnet AG. Weitere Informationen:
https://unisb.asknet.de/cgi-bin/program/S1552
Weitere Informationen:
http://www.hiz-saarland.de/informationen/arbeitsplatz/sw-lizenzen/
mathworks-tah-campuslizenz/
Literatur: Internet
Skripte
Gerald Kempfer: Progammieren in C - Vorlesungsbegleitendes Skript
http://public.beuth-hochschule.de/~kempfer/skript_c/c.pdf
Wikibooks: C-Programmierung
http://upload.wikimedia.org/wikibooks/de/8/8d/CProgrammierung.pdf
Springerlink: http://www.springerlink.com
Ralf Kirsch, Uwe Schmitt (Programmieren in C, 2007)
Manfred Dausmann, Ulrich Bröckl, Joachim Goll (C als erste Programmiersprache,
2008)
Jörg Birmelin, Christian Hupfer (Elementare Numerik für Techniker, 2008)
Literatur: Hardcopy
Semesterapparat
Programmierung
Fachschaft: Vorlesungsmitschriften
Bücher
Ralf Kirsch, Uwe Schmitt: Programmieren in C. Eine mathematikorientierte
Einführung, Springer 2007 (zurzeit vergriffen, 2. Auflage erscheint 2013)
Helmut Erlenkötter: C Programmieren von Anfang an, Rowohlt 1999
Brian Kernighan, Dennis Ritchie: Programmieren in C, Hanser 1990
Manfred Dausmann, Ulrich Bröckl, Joachim Goll: C als erste
Programmiersprache, Teubner 2008
Vom Quellcode zum ausführbaren Programm
Schritt 1: Code schreiben
Quellcode ist eine Textdatei mit der Endung .c
Ungeeignet: Textverarbeitungsprogramme wie MS Word oder OpenOffice Writer
Geeignet: MS Notepad (Editor), Emacs, gedit, nedit, KDevelop, Eclipse . . .
Schritt 2: Code compilieren
Kommandozeile starten und in das Verzeichnis wechseln, das den Code enthält
Befehl gcc Quellcode.c -o Programm in eine Kommandozeile eingeben
Der Compiler gcc übersetzt den Quellcode in Maschinencode, den der Computer
versteht.
Schritt 3: Programm ausführen
Das compilierte Programm wird mit dem Befehl ./Programm aufgerufen.
Ohne das vorangestellte ./ liefert das System die Fehlermeldung
bash: Programm: Kommando nicht gefunden.
Zahlendarstellung: Natürliche Zahlen
241012 = 2 · 105 + 4 · 104 + 1 · 103 + 0 · 102 + 1 · 101 + 2 · 100 =
N
X
cj B j
j=0
mit
B = 10
(→ Dezimaldarstellung)
N=5
c = (c5 , c4 , c3 , c2 , c1 , c0 ) = (2, 4, 1, 0, 1, 2)
,
cj ∈ {0, . . . , B − 1}
24 = 1 · 16 + 1 · 8 + 0 · 4 + 0 · 2 + 0 · 1
= 1 · 24 + 1 · 23 + 0 · 22 + 0 · 21 + 0 · 20 =
N
X
cj B j
j=0
mit
B=2
(→ Binärdarstellung)
N=4
c = (c4 , c3 , c2 , c1 , c0 ) = (1, 1, 0, 0, 0)|2
,
cj ∈ {0, 1}
Zahlendarstellung: Ganze Zahlen
Binärsystem (B = 2): cN wird als Vorzeichen interpretiert.
Beispiel: B = 2, N = 7
−53 = (−1)1 · (0 · 26 + 1 · 25 + 1 · 24 + 0 · 23 + 1 · 22 + 0 · 21 + 1 · 20 )
→ c = (1, 0, 1, 1, 0, 1, 0, 1)
Frühe Computer verwendeten diese Darstellung
Nachteil: 2 Versionen der Null, Ganzzahlarithmetik ineffizient
Heute üblich: „Zweierkomplement“
→ Eindeutige Darstellung der Null
→ Effizientere Arithmetik, da keine Fallunterscheidung nach Vorzeichen nötig
→ Näheres unter
en.wikipedia.org/wiki/Two’s_complement,de.wikipedia.org/wiki/Zweierkomplement
Datentypen
Definition
Ein Datentyp ist festgelegt durch einen Wertebereich und die darauf anwendbaren
Operationen.
Datentyp
char
int
float
double
"Wertebereich"
Zeichen (bspw. Buchstaben und Ziffern)
ganze Zahlen
Gleitkommazahlen mit einfacher Genauigkeit
Gleitkommazahlen mit doppelter Genauigkeit
Typmodifizierer
short
long
long long
unsigned
erlaubt für
int
int, double
int
int, char, short int, long int, long long int
Datentypen
Datentyp
char
unsigned char
int
unsigned (int)
long (int)
unsigned long
long long
unsigned long long
Größe
1 Byte (=8 Bit)
1 Byte
4 Byte
4 Byte
4 Byte∗
4 Byte∗
8 Byte
8 Byte
kleinster Wert
−27 = −128
0
−231
0
wie int
wie unsigned
−263
0
größter Wert
27 − 1 = 127
28 − 1 = 255
231 − 1
232 − 1
wie int
wie unsigned
263 − 1
264 − 1
∗ Gilt für 32-Bit-Systeme (Linux/Windows/Mac) sowie 64-Bit-Windows. Auf 64-Bit-Linux- oder
Mac-Systemen hat long eine Größe von 8 Byte = 64 Bit.
Datentyp
float
double
long double
Größe
4 Byte
8 Byte
10 Byte∗∗
betragsmäßig
kleinster Wert
≈ 1.18 · 10−38
≈ 2.23 · 10−308
≈ 3.36 · 10−4932
betragsmäßig
größter Wert
≈ 3.40 · 1038
≈ 1.80 · 10308
≈ 1.19 · 104932
∗∗ Nicht eindeutig festgelegt. Der ISO-Standard verlangt lediglich, dass long double mindestens die gleiche
Präzision aufweist wie double. Die meisten Compiler interpretieren long double als 80-Bit-Gleitkommazahl.
Variablendeklaration
Definition
Eine Variablendeklaration besteht aus der Angabe eines Datentyps sowie einer Liste
von Variablennamen.
Datentyp Variablenname1, Variablenname2,...,VariablennameN;
vor der ersten Verwendung
zu Beginn eines Anweisungsblocks
Unterscheidung zwischen Groß- und Kleinschreibung
keine Sonderzeichen, wie z.B. #, ß, %
keine Ziffern zu Beginn
max. Variablennamenlänge: 31 Zeichen
Positiv-Beispiele:
int i, j, k;
float u, w = -3.14, x, y = 1.0, z;
unsigned int N = 5; double s;
Variablendeklaration
Negativ-Beispiele:
int dummy#;
Fehler: verirrtes # im Programm
int dummy%;
Fehler: expected =, ,, ; ...
int i, double dummy;
Fehler: expected identifier or ( before
double
int i; double 2dummy;
Fehler: ungültiger Suffix dummy an
Ganzzahlkonstante
unsigned double x;
Fehler: both unsigned and double in
declaration specifiers
„Feste Variablen“: const
const Datentyp Variablenname = Wert;
Auf die entsprechende Variable kann nur noch lesend zugegriffen werden. Bsp.:
const int k = 1;
..
.
k = 2;
→ Fehler: Zuweisung der schreibgeschützten Variable k
Operatoren
Definition
Anweisung/Manipulation/Rechnung mit festen Regeln
wirkt auf einen oder mehrere Operanden
unärer Operator: 1 Operand
binärer Operator: 2 Operanden
Stellung des Operators:
Präfixform: Operator steht vor Operanden
Suffixform: Operator steht hinter Operanden
Infixform: Operator steht zwischen Operanden
Zuweisungsoperator ’=’
int a;
float x = 3.14, y, z;
a = -4;
z = y = x;
// aequivalent: z = (y = x);
binär, Infixstellung
rechtsassoziativ ( ← )
Operatoren: Arithmetische Operatoren
Arithmetische Opertoren ( + , - , * , / , % )
Name
Minus (unär)
Plus
Minus (binär)
Multiplikation
Verwendung
-Op1
Op1 + Op2
Op1 - Op2
Op1 * Op2
Division
Op1 / Op2
Modulo
Op1 % Op2
Operandentyp
int, float
int, float
int, float
int, float
int
float
int
<
Resultat
Vorzeichenwechsel
Summe
Differenz
Produkt
ganzzahlige Division
Quotient
Rest bei ganzzahliger Division
Regeln
Sind die Operanden vom gleichen Datentyp, so auch das Ergebnis
Sind die Operanden von verschiedenen Typen, so ist das Resultat vom
„genaueren“Datentyp: ’int’ + ’double’ = ’double’
Arithmetische Operatoren sind linksassoziativ ( → ) , d.h. a + b + c = (a + b) + c
Priorität bei binären Operatoren: „Punkt vor Strich“
Operatoren: Arithmetische Zuweisung und Inkrement
Arithmetische Zuweisungsoperatoren
Operation
Op1 += Op2;
Op1 -= Op2;
Op1 *= Op2;
Op1 /= Op2;
Op1 %= Op2;
Bezeichnung
Additionszuweisung
Subtraktionszuweisung
Multiplikationszuweisung
Divisionszuweisung
Modulozuweisung
Äquivalent
Op1 = Op1
Op1 = Op1
Op1 = Op1
Op1 = Op1
Op1 = Op1
zu
+ Op2;
- Op2;
* Op2;
/ Op2;
% Op2;
Inkrement- und Dekrementoperatoren ++ und -Für eine Variable i vom Typ int sind äquivalent:
i = i+1;
⇐⇒ i += 1;
⇐⇒
i++;
i = i-1;
⇐⇒ i -= 1;
⇐⇒
i--;
Unterscheidung Postfix- und Präfixnotation
sum += i++;
⇐⇒
sum = sum + i;
i = i + 1;
sum += ++i;
⇐⇒
i = i + 1;
sum = sum + i;
Operatoren: Arithmetische Zuweisung und Inkrement
int a = 3, b, c;
b = ++a * 3; // a = 4, b = 12
c = a++ * 3; // c = 12, a = 5
ist äquivalent zu
int a = 3, b, c;
a++;
// oder ++a;
b = a * 3;
c = a * 3;
++a;
// oder a++;
Publikumsfrage: Seien a und b vom Typ int. Was ist zu
a) -b
= a - 1;
b)
b -= (-a);
c)
b
d)
b -= (--a);
= -(a--);
b -= (a-1);
äquivalent?
Operatoren: Division und Modulorechnung
Mathematik (Zahlentheorie):
Seien a, 0 6= b ∈ Z. Dann existieren eindeutig bestimmte Zahlen q, r ∈ Z mit
a = q · b + r,
0 ≤ r < |b| .
Computer Science:
Seien a, 0 6= b ∈ Z. Dann existieren (eindeutig bestimmte) Zahlen q, r ∈ Z mit
a = q · b + r,
−|b| < r < |b| .
Es gilt:
q = a / b;
r = a % b;
→ ganzzahlige Division
→ a modulo b
Beispiele:
a = 45 , b = 7:
a = −27 , b = 5:
45 = 6 · 7 + 3
45 = 7 · 7 − 4
−27 = (−6) · 5 + 3
−27 = (−5) · 5 − 2
;
;
q=6,
q=7,
;
;
r = 3
r = −4
q = −6,
q = −5,
r = 3
r = −2
Operatoren: Implizite Typumwandlung und Casts
Unterscheide!
int
a = -3;
float c;
c = a / 2;
int
a = -3;
float c;
c = a / 2.0;
; c = -1.0
; c = -1.5
Explizite Typumwandlung durch Casts
(Datentyp) Term;
Beispiel:
int
a = 3, b = 2;
float c = (float) a / b;
; c = 1.5
Merke: Casts besitzen höhere Priorität als binäre arithmetische Operatoren
Äquivalent:
float c = ((float) a) / b;
Nicht äquivalent:
float c = (float) (a / b);
Operatoren: Vergleichende und logische Operatoren
Vergleichsoperatoren
Überprüft werden Wahrheitswerte von Aussagen wie etwa x > 0, j ≤ N,
a ≤ f (x ) ≤ b usw.
Notation in C
a < b
a > b
a <= b
a >= b
a == b
a != b
math. Notation
a<b
a>b
a≤b
a≥b
a=b
a 6= b
Häufig verwendet in „wenn, . . . dann . . . “-Konstruktionen
in C:
Wert 0 (auch 0.0)
Werte ungleich 0 (etwa 1, -2.5)
; "falsch"
; "wahr"
(false)
(true)
Unterscheide "a = b" (Zuweisung) und "a == b" (Vergleich)
Merke: Der Wert einer Zuweisung entspricht dem zugewiesenen Wert.
Vorsicht beim Test auf Gleichheit bei floats (s. arithm. Operatoren)
−→
Vergleichsoperatoren sind linksassoziativ (−→
−→)
Operatoren: Vergleichende und logische Operatoren
Logische Operatoren: && , || , !
Ausdrücke (arithmetische, vergleichende, zuweisende) werden als Aussagen
miteinander verknüpft
Die Werte solcher Verknüpfungen sind immer 1 (wahr) oder 0 (falsch)
Notation in C
A && B
A || B
!A
math. Notation
A∧B
A∨B
¬A
Bedeutung
Konjunktion (und)
Disjunktion (oder)
Negation
Das "oder" ist einschließend zu verstehen und nicht als "entweder oder"
Logische Operatoren sind linksassoziativ (−→)
Verknüpfungstafeln:
&&
0
1
0
0
0
1
0
1
||
0
1
0
0
1
1
1
1
0
1
!
1
0
Operatoren: Vergleichende und logische Operatoren
Beispiel:
int A = -4, C = 1;
double B = -0.5;
Ausdruck
A
!A
A - B
!(A - B)
!!(A - B)
!A - B
!A -!B
Wert
-4
0
-3.5
0
1
0.5
0
Ausdruck
A < B < C
A < B <= C
A < (B<C)
A<B && B<C
C>A && B
A==2 || B>C
A=2 || B>C
Wert
0
1
1
1
1
0
1
Ausgabe auf der Kommandozeile: printf
dient zur Ausgabe von Text und numerischen Werten auf dem Bildschirm
erfordert Präprozessordirektive:
Syntax:
#include <stdio.h>
printf(Formatstring, Parameterliste);
Formatstring = Text und Platzhalter in Anführungszeichen
Beispiel:
int a = -5;
float b = 3.1415926535;
printf("a hat den Wert %d, b hat den Wert %f.\n", a, b);
Ausgabe:
a hat den Wert -5, b hat den Wert 3.141593.
Zeichenkonstanten (vgl. Erlenkötter, Kap. 8.5)
\n neue Zeile (new line)
\t horizontaler Tabulator
Ein- und Ausgabe: Platzhalter
%[flags][weite][.genauigkeit][modifizierer]typ
typ
ganzzahlig
unsigned integer
Gleitkomma
wissenschaftl.
d/i
u
f
e
modifizierer
l, z.B. %lf bei Verwendung von double
L, z.B. %Lf bei Verwendung von long double
genauigkeit
Anzahl der Nachkommastellen
weite
Mindestanzahl an Zeichen („.“ mitgezählt)
flags
links- oder rechtsbündig, Vorzeichen, führende Nullen
3218
3218
3218.000000
3.218000e+03
Beispiel: ”%7.3f” ist Platzhalter für eine Gleitkommazahl mit 3 Nachkommastellen
und einer Feldbreite von mindestens 7 Zeichen.
Hilfe zu printf: Befehl man 3 printf in die Kommandozeile eingeben
Weitere Beispiele: Übung
Programmierung für Mathematiker
Prof. Dr. Thomas Schuster
M.Sc. Dipl.-Phys. Anne Wald
26.04.2017
Wiederholung: Ausgabe auf der Kommandozeile: printf
dient zur Ausgabe von Text und numerischen Werten auf dem Bildschirm
erfordert Präprozessordirektive:
Syntax:
#include <stdio.h>
printf(Formatstring, Parameterliste);
Formatstring = Text und Platzhalter in Anführungszeichen
Beispiel:
int a = -5;
float b = 3.1415926535;
printf("a hat den Wert %d, b hat den Wert %f.\n", a, b);
Ausgabe:
a hat den Wert -5, b hat den Wert 3.141593.
Zeichenkonstanten (vgl. Erlenkötter, Kap. 8.5)
\n neue Zeile (new line)
\t horizontaler Tabulator
Ein- und Ausgabe: Platzhalter
%[flags][weite][.genauigkeit][modifizierer]typ
typ
ganzzahlig
unsigned integer
Gleitkomma
wissenschaftl.
d/i
u
f
e
3218
3218
3218.000000
3.218000e+03
modifizierer
l, z.B. %lf bei Verwendung von double
L, z.B. %Lf bei Verwendung von long double
genauigkeit
Anzahl der Nachkommastellen
weite
Mindestanzahl an Zeichen („.“ mitgezählt)
flags
links- oder rechtsbündig, Vorzeichen, führende Nullen
Beispiel: ”%7.3f” ist Platzhalter für eine Gleitkommazahl mit 3 Nachkommastellen
und einer Feldbreite von mindestens 7 Zeichen.
Eingabe durch den Benutzer: scanf
dient u.a. zum Einlesen von Zahlen
erfordert Präprozessordirektive:
#include<stdio.h>
Syntax:
scanf(Formatstring, Parameter);
Formatstring: %[modifizierer]typ
Parameter: & vor Variablennamen!
Beispiel:
#include <stdio.h>
main()
{
double zahl;
printf("Bitte geben Sie eine Zahl ein: ");
scanf("%lf", &zahl);
printf("%lf zum Quadrat ist %lf\n", zahl, zahl*zahl);
}
Zeichen
Zeichen (engl. character ) werden intern wie (positive) ganzzahlige Werte
behandelt, der Datentyp ist char. Variablen vom Typ char belegen im Speicher 1
Byte.
Die standardisierte Zuordnung Zeichen ←→ Zahl erfolgt gemäß der ASCIITabelle. ASCII = American Standard Code for Information Interchange. Die
Kodierung definiert 128 Zeichen, bestehend aus 33 nicht-druckbaren sowie 95
druckbaren:
0
Nullzeichen
1–32
Steuerzeichen
33–126
Ziffern, Buchstaben, Symbole, usw.
Auszug:
37
38
48
49
%
&
0
1
65
66
67
92
A
B
C
\
97
98
99
123
a
b
c
{
124
167
181
223
Es sind äquivalent
char
char
char
char
c
c
c
c
=
=
=
=
'A';
65;
0101; // oktal:
65 = 1*64 + 0*8 + 1*1
0x41; // hexadezimal: 65 = 4*16 + 1*1
}
$
µ
ß
Zeichen
Ausgabe mittels printf:
char c = 88;
printf("88 interpretiert als Zeichen: %c\n",c);
printf("88 interpretiert als integer: %d\n",c);
Zeichenarithmetik
char c = 'A';
int diff = 'C' - c; // diff = 2
c = 'B' + diff;
// c = 68
printf("Das zu c korrespondierende Zeichen ist %c\n",c);
Einlesen von Zeichen via der Funktion getchar
int getchar(void)
Beispiel
int c;
c = getchar();
Anweisungsblöcke
Folge von Anweisungen, die von geschweiften Klammern eingeschlossen sind
Anweisungsblock ist syntaktisch äquivalent zu einer einzelnen Anweisung
eine einzelne Anweisung bedarf keiner Klammer
Anweisungsblöcke können geschachtelt sein
Beispiele:
printf("Hallo Welt!\n");
{
}
{
}
printf("Hallo Welt!\n");
printf("Ich gehoere zum inneren Anweisungsblock.\n");
printf("Ich bin im uebergeordneten Anweisungsblock!\n");
{
printf("Hallo Welt!\n");
printf("Ich gehoere zum inneren Anweisungsblock.\n");
}
if-Anweisung
if (Bedingung)
Anweisungsblock
oder
if (Bedingung)
Anweisungsblock_1
else
Anweisungsblock_2
Wenn-Dann
Wenn-Dann-Andernfalls
Beispiel: Vorzeichenfunktion sign
Mathematische Definition:
int a;
if (a>0)
printf("sign(a) = +1\n");
else
{
if (a==0)
printf("sign(a) = 0\n");
else
printf("sign(a) = -1\n");
}
sign(a) =

+1,
0,
−1,
falls a > 0
falls a = 0
falls a < 0
Mehrfache Alternativen: else if
Syntax
if (Bedingung_1)
Anweisungsblock_1
else if (Bedingung_2)
Anweisungsblock_2
..
.
else if (Bedingung_N)
Anweisungsblock_N
else
// optional
Anweisungsblock
// optional
Beispiel
if (a > 0)
printf("sign(a) = +1\n");
else if (a == 0)
printf("sign(a) = 0\n");
else // oder else if (a < 0)
printf("sign(a) = -1\n");
Viele Alternativen: switch
Syntax
switch (Variable)
{
case Wert_1:
Anweisungsblock_1
break;
// optional
case Wert_2:
Anweisungsblock_2
..
.
case Wert_N:
Anweisungsblock_N
default:
// optional
Anweisungsblock // optional
}
Bei der Ausführung wird zu dem case label gesprungen, an der zum ersten Mal
Variable und Wert übereinstimmt.
Der gesamte folgende Code bis zum ersten break oder zum Ende des Blocks
wird ausgeführt. Das schließt auch Code außerhalb des angesprungenen case
labels ein.
Als Wert im case label sind nur Konstanten zulässig.
Viele Alternativen: switch
Beispiel
unsigned char eingabe;
printf("Bitte Befehl eingeben: ");
scanf("%c", &eingabe);
switch (eingabe)
{
case 'q':
printf("Programm wird beendet!\n");
break;
case 'p':
case 'P':
printf("Drucken...\n");
break;
case 'h':
printf("Hilfe wird aufgerufen.\n");
break;
default:
printf("Eingabe nicht erkannt!\n");
break;
}
Schleifen
Schleifen wiederholen einen Anweisungsblock so lange bis ein bestimmtes
Abbruchkriterium erfüllt ist
Die wichtigsten Schleifen in C sind for und while
while-Schleife
oft verwendet, wenn die Anzahl der Wiederholungen nicht vorherbestimmt ist
Syntax:
while (Bedingung)
Anweisungsblock
wiederholt Anweisungsblock bis Bedingung "falsch", d.h. gleich 0 ist
Autor ist selbst verantwortlich, dass das Abbruchkriterium irgendwann erfüllt ist
→ Gefahr einer Endlosschleife!
Variablen in Bedingung müssen deklariert und ggf. initialisiert werden
Anweisungsblock = "Rumpf" der Schleife
Beispiel:
Programm soll eine natürliche Zahl N einlesen und die Summe 1 + 2 + 3 + . . . + N
ausgeben.
Beispiel: while-Schleife
1
#include<stdio.h>
2
3
4
5
main()
{
int i = 1, sum = 0, N;
6
printf("Geben Sie eine natuerliche Zahl ein: N= ");
scanf("%d", &N);
7
8
9
while (i <= N)
{
sum += i;
i++;
}
10
11
12
13
14
15
16
17
18
}
printf("Die Summe der ersten %d natuerlichen Zahlen ", N);
printf("betraegt %d\n", sum);
Beispiel: while-Schleife
Es sind äquivalent:
int i = 1;
int i = 0;
while (i <= N)
{
sum += i;
i++;
}
while (i < N)
{
i++;
sum += i;
}
int i = 0;
int i = 0;
while (i++ < N)
sum += i;
while (++i <= N)
sum += i;
; Kein guter Stil, da fehleranfällig!
do-while-Schleife
Anweisungsblock wird mindestens ein Mal ausgeführt
beachte Semikolon am Ende
do
Anweisungsblock
while (Bedingung);
äquivalent zu:
Anweisungsblock
while (Bedingung)
Anweisungsblock
Beispiel
int N;
do {
printf("Bitte geben Sie eine ganze Zahl zwischen 5 und 15 ein:");
scanf("%i", &N);
} while(N<5 || N>15);
for-Schleife
vermutlich die am häufigsten verwendete Schleifenvariante
kommt zum Einsatz wenn das Update immer gleich ist
Anzahl der Wiederholungen ist a priori bekannt
Syntax:
for (Initialisierung; Bedingung; Update)
Anweisungsblock
äquivalent zu:
Initialisierung;
while (Bedingung)
{
Anweisungsblock
Update;
}
Beispiel:
int i;
for (i=1; i<=N; i++)
sum += i;
Stolperfallen
Abbruchkriterium fehlerhaft: Zuweisung statt Vergleich
for(i=1; i=N; i++)
sum += i;
→ Endlosschleife!
Leerer Anweisungsblock: Falsch platziertes Semikolon
for(i=1; i<=N; i++);
sum += i;
Unter- oder Überlauf
unsigned i;
for(i=N; i>=0; i--)
sum += i;
unsigned char i;
for(i=1; i<=N; i++)
sum += i;
→ Endlosschleife für N>255!
Schachtelung
Beispiel:
1
#include <stdio.h>
2
3
4
5
main()
{
int i, j;
6
for(i=1; i<=5; i++)
{
for(j=1; j<=5; j++)
printf("%2d ", i*j);
7
8
9
10
// aeussere Schleife
// innere Schleife
// Feldbreite 2 -> Zahlen rechtsbuendig
11
12
13
14
}
}
printf("\n");
Ausgabe:
1 2 3 4
2 4 6 8
3 6 9 12
4 8 12 16
5 10 15 20
5
10
15
20
25
// wieder aussen
Steuerung von Wiederholungen
break beendet aktuelle Wiederholungsansweisung
continue Rest der Schleife wird übersprungen und der nächste
Schleifendurchlauf gestartet
Merke: break und continue sollten sparsam eingesetzt werden, da
sonst das Programm unübersichtlich wird.
return beendet aktuelle Funktion (später mehr!)
Absolut verpönt:
goto bewirkt einen Sprung im Programm an eine zurvor definierte Stelle
Merke: Anwendung von goto ist verboten!
Steuerung von Wiederholungen
Beispiel:
1
#include <stdio.h>
2
3
4
5
main()
{
unsigned eingabe;
6
while(1)
// ohne break eine Endlosschleife!
{
printf("Bitte eine natuerliche Zahl kleiner als 100");
printf(" eingeben: ");
scanf("%u", &eingabe); // Vorsicht: Unterlauf moeglich!
7
8
9
10
11
12
13
14
}
15
if (eingabe < 100)
break;
16
17
18
}
printf("Die Zahl war %u.\n", eingabe);
Zufallszahlen
Definition
Ein Zufallsexperiment ist ein Experiment, dessen Ausgang nicht aus den vorherigen
Ergebnissen vorausgesagt werden kann. Eine Zufallszahl ist eine Zahl, die sich aus
dem Ergebnis eines Zufallsexperiments ableitet.
„Echte“ Zufallszahlen sind prinzipiell nur solche, die von wirklich zufälligen
(physikalischen) Prozessen abgeleitet werden (Beispiel: radioaktiver Zerfall).
Viele physikalische Prozesse sind zwar deterministisch (vorhersagbar), jedoch
nicht praktisch berechenbar (Beispiel: Temperatur am 9. Dezember des
Folgejahres) → Pseudo-Zufall
Definition
Ein Experiment gilt als pseudo-zufällig, wenn sich sein Ergebnis zwar theoretisch
voraussagen lässt, ohne Kenntnis der genauen Berechnungsvorschrift jedoch eine
Prognose unmöglich ist.
Zufallszahlen
Computer kann nur Pseudo-Zufallszahlen erzeugen
Ziel (zunächst): Gleichverteilte Pseudo-Zufallszahlen auf [0, 1[ oder auf [0, M[
erzeugt
Gute Generatoren müssen eine Reihe von statistischen Tests bestehen
Am weitesten verbreitet ist die lineare Kongruenzmethode:
1
Gib einen seed-Wert n0 vor (Benutzereingabe / anderweitige Berechnung)
2
Berechne für feste natürliche Zahlen a, b, M und k = 0, 1, . . .:
nk+1 = (a · nk + b)
mod M
Soll das Ergebnis eine Gleitkommazahl in [0, 1[ sein, berechne
xk+1 = nk+1 /M.
Die Qualität des Generators hängt entscheidend von den Parametern a, b und M ab!
Zufallszahlen
Beispiel für einen „schlechten“ Generator (aus Kirsch/Schmitt, Kap. 12):
Wähle a = 216 + 3, b = 0, M = 231
Visualisierung: Fasse je 3 aufeinanderfolgende Zahlen zu Vektoren in R3 zusammen.
Ergebnis: Jeder Vektor liegt in einer von 15 festen Ebenen!
Erzeugung von Zufallszahlen in C
Präprozessordirektive #include <stdlib.h> notwendig
Für int-Zufallszahlen auf [0, INT_MAX]: Funktion rand()
Seed-Wert für den rand-Generator wird mit srand(seed) festgelegt. Dabei wird
seed als unsigned int interpretiert.
Für double-Zufallszahlen auf [0, 1]: Funktion drand48()
Seed-Wert für den drand48-Generator wird mit srand48(seed) festgelegt. Dabei
wird seed als long int interpretiert.
Verwendung:
unsigned seed = 884722;
int zz;
long seed = 88472250439203531568;
double zz;
srand(seed);
zz = rand();
srand(seed);
zz = drand48();
printf("zz = %d\n", zz);
printf("zz = %f\n", zz);
Beispiel: (primitive) Simulation eines Aktienkurses
1
2
#include <stdio.h>
#include <stdlib.h>
3
4
5
6
7
8
9
main()
{
long seed;
int i, periode = 20;
// Anzahl Tage
double wert,
// aktueller Wert
max_schwankung = 10; // Tagesschwankung maximal 10 Euro
10
printf("Bitte Seed-Wert eingeben: ");
scanf("%ld", &seed);
srand48(seed);
11
12
13
14
printf("Wert der Aktie zu Beginn (Euro): ");
scanf("%lf", &wert);
15
16
17
for(i=0; i<periode; i++)
wert += 2 * (drand48() - 0.49) * max_schwankung;
// Skalierung auf [-0.99,1.01]
18
19
20
21
22
23
}
printf("Wert nach 20 Tagen: %.2f Euro\n", wert);
Monte-Carlo-Methoden
Oberbegriff für mathematische Verfahren, deren Funktionsprinzip der Zufall ist
Name geht zurück auf John v. Neumann und reflektiert die Tatsache, dass immer
wieder „gewürfelt“ wird.
Grundlegendes Prinzip ist einfach zu verstehen → bei Anwendern beliebt
Trotzdem vielseitig und effizient verwendbar
Kommen vor allem dann zum Einsatz, wenn ein zufälliger Prozess simuliert
werden soll (z. B. Finanzmathematik, Dynamik von Gasen / Partikeln /
Elektronen in Materie, Ausbreitung von Krankheiten, . . .)
Mathematische Basis:
Gesetz der großen Zahlen (einfache Fassung)
Je öfter man ein Zufallsexperiment durchführt, desto mehr nähert sich die relative
Häufigkeit eines Ereignisses der Wahrscheinlichkeit desselben Ereignisses an.
Beispiel: Idealer Würfel
P(X = 1) = 16 – Wahrscheinlichkeit, dass eine Eins gewürfelt wird
N1
– Anzahl der gewürfelten Einsen nach N Würfen
Gesetz der großen Zahlen:
lim
N→∞
N1
1
=
N
6
Monte-Carlo-Integration
|A| = Flächeninhalt von A = ?
Z
A
x dy − y dx
A=
γ
γ Umrandungskurve von A
Parametrisierung von γ notwendig
Q = [0,1]x[0,1]
}h
Klassische Vorgehensweise (Quadratur):
Unterteile Q in N · N Quadrate der
Kantenlänge h = 1/N
Bestimme die Anzahl NA der
Quadrate, deren Mittelpunkte in A
liegen
Berechne |A| ≈ NA · h2
Nachteil: sehr teuer in höheren
Dimensionen (≥ 3)
Monte-Carlo-Integration
Stochastischer Zugang:
Wahrscheinlichkeit, dass ein zufällig erzeugter Punkt z ∈ Q in A landet, ist
gegeben durch
|A|
pA = P(z ∈ A) =
= |A|
|Q|
Gesetz der großen Zahlen: Für eine hinreichend große Anzahl N von zufällig
erzeugten Punkten ist
NA
#(Punkte in A)
=
≈ pA = |A|
#(Punkte insgesamt)
N
Stochastische Vorgehensweise:
Erzeuge N zufällige Punkte in Q
Zähle die Anzahl NA der Punkte,
die in A liegen
Berechne |A| =
NA
N
[·|Q|]
Vorteile:
simpel, einfach zu
implementieren
man muss nur zwischen z ∈ A
und z 6∈ A unterscheiden können
in höheren Dimensionen sehr
effizient
Monte-Carlo-Integration
Beispiel: Einheitskreisscheibe im 1. Quadranten
1
Umgebendes Quadrat
Q = [0, 1]2 ⇒ |Q| = 1
0.8
Einheitskreis-Viertel
A = (x , y ) ∈ Q | x 2 + y 2 ≤ 1
0.6
y
Pseudocode:
A
0.4
1
Lies eine Zahl N ein
2
Für i = 1, . . . , N:
Erzeuge zufällige Zahlen x und y
Falls x 2 + y 2 ≤ 1:
0.2
NA ← NA + 1
3
0
0
0.2
0.4
0.6
x
0.8
1
Gib Flächeninhalt = NA /N aus
Monte-Carlo-Integration: Ergebnisse
1
1
0.8
0.8
0.6
0.6
0.4
0.4
0.2
0.2
0
0
0
0.2
0.4
0.6
N = 10, Fehler: 2.85 ·
0.8
10−1
1
(36.3 %)
0
0.4
0.6
N = 100, Fehler: 4.54 ·
1
1
0.8
0.8
0.6
0.6
0.4
0.4
0.2
0.2
0
0.2
0.8
10−2
1
(5.8 %)
0
0
0.2
0.4
0.6
0.8
1
N = 1000, Fehler: 2.26 · 10−2 (2.9 %)
0
0.2
0.4
0.6
0.8
1
N = 10000, Fehler: 4.80 · 10−3 (0.6 %)
Monte-Carlo-Simulation: Radioaktiver Zerfall
Radioaktive Nuklide (Atomsorten) sind instabil und werden unter Aussendung von
Strahlung in andere, stabile Atomsorten umgewandelt.
Der Zerfall eines einzelnen Atoms geschieht spontan und kann als zufälliges
Ereignis angesehen werden.
Das Isotop 131 I (Jod-131) ist ein Betastrahler. Es wird zum stabilen Isotop 131 Xe
(Xenon-131) umgewandelt. Dabei wird ein Elektron (Beta-Teilchen) emittiert.
Jod-131 besitzt eine Halbwertszeit von T1/2 = 8.02070 Tagen, d. h. nach T1/2 ist
(etwa) die Hälfte einer betrachteten Menge Jod-131 zerfallen.
Experimente zeigen den Zusammenhang
∆N = −λ · N · ∆t
mit
∆t : kleines Zeitintervall
N : Anzahl der radioaktiven Kerne
∆N : Änderung von N im Zeitintervall ∆t
λ > 0 : Zerfallskonstante (Proportionalitätsfaktor, materialabhängig)
Monte-Carlo-Simulation: Radioaktiver Zerfall
Klassische Herangehensweise (für große Anzahl N):
∆N(t) = −λN(t)∆t
mit ∆N(t) = N(t + ∆t) − N(t). Division durch ∆t und der anschließende
Grenzübergang ∆t → 0 liefern die Differentialgleichung
Ṅ(t) = −λN(t).
Die (eindeutige) Lösung ist gegeben durch
N(t) = N(0) e −λt .
Die Halbwertszeit T1/2 ist der Zeitraum, nach dem die Hälfte der Kerne zerfallen ist,
d. h. N(T1/2 ) = N(0)/2. Durch eine einfache Umformung ergibt sich
T1/2 =
ln 2
λ
bzw.
λ=
ln 2
.
T1/2
Halbwertszeiten radioaktiver Isotope sind üblicherweise tabelliert.
Stochastische Herangehensweise:
λ∆t = Wahrscheinlichkeit, dass ein Kern im Zeitraum ∆t zerfällt
In jedem Zeitschritt wird für jeden Kern zufällig entschieden, ob er zerfällt.
Monte-Carlo-Simulation: Radioaktiver Zerfall
Beispiel: Jod-131
Als Zeitschritt wählen wir ∆t = 1 min.
T1/2 = 8.0207 d = 11549.808 min ⇒
λ=
ln 2
= 6.001 · 10−5 min−1
T1/2
Die Wahrscheinlichkeit, dass ein Kern innerhalb einer Minute zerfällt, ist gegeben
durch
p = λ∆t = 6.001 · 10−5 .
Pseudocode:
1
Lies eine Zahl N (Anzahl zu Beginn) und eine Zahl T (Endzeitpunkt) ein
2
Für i = 1, . . . , T :
Für j = 1, . . . , N:
• Erzeuge eine zufällige Zahl x ∈ [0, 1[
• Falls x < p, setze N ← N − 1
3
Gib die Anzahl N der übriggebliebenen Kerne nach T Minuten aus
Monte-Carlo-Simulation: Radioaktiver Zerfall – Ergebnisse
1
1
N(0)=10
N(0)=100
N(0)=10000
Analytisch
0.8
Vorhandene Kerne / Anfangsbestand
Vorhandene Kerne / Anfangsbestand
0.8
Realisierung 1
Realisierung 2
Realisierung 3
0.6
0.4
0.2
0.6
0.4
0.2
0
0
2000
4000
6000
Zeit [min]
8000
10000
Zerfallskurven für verschiedene
Anfangsbestände N(0)
0
0
2000
4000
6000
Zeit [min]
8000
10000
3 Realisierungen für N(0) = 10
Monte-Carlo-Simulation: Räuber-Beute-Modell
Grundlegendes Modell in der Theorie der dynamischen Systeme
Beschreibt die Zusammenhänge, welche die Entwicklung mehrerer interagierender
Populationen (konkurrierende Spezies) bestimmen
Anwendungsgebiete: Systembiologie, Epidemologie, Ökonomie, . . .
Beispiel:
Hecht
vs.
Karpfen
Räuber
Beute
sehr gefräßig
harmlos
mag Karpfen
lecker
Monte-Carlo-Simulation: Räuber-Beute-Modell
Grundannahmen:
In einem großen Weiher leben ausschließlich Karpfen (K ) und Hechte (H).
Der Nahrungsvorrat für Karpfen ist unbegrenzt. Ihr Wachstum in einem
Zeitintervall ∆t ist proportional zur Anzahl der vorhandenen Individuen:
∆K ∼ K ∆t
Hechte, die keine Nahrung finden, fressen andere Hechte, d. h. der Rückgang
ihres Bestandes in einem Zeitintervall ∆t ist proportional zur Zahl der
vorhandenen Exemplare:
∆H ∼ −H∆t
; Ohne Interaktion gilt für beide Spezies das gleiche Modell wie beim
radioaktiven Zerfall!
Bei einer Begegnung wird der Karpfen vom Hecht gefressen. Hat ein Hecht eine
bestimmte Anzahl Karpfen gefressen, „entsteht“ ein zusätzlicher Hecht. Je mehr
Individuen es von beiden Sorten gibt, desto wahrscheinlicher ist solch eine
Begegnung:
∆K ∼ −KH∆t
∆H ∼
KH∆t
Monte-Carlo-Simulation: Räuber-Beute-Modell
Gekoppeltes Gesamtmodell (∆t = 1):
∆K =
λK K − p KH
∆H = n−1 p KH − λH H
∆K /H : Änderung des Bestandes von K /H
λK : Zuwachsrate K
λH : Sterberate H
p : Wahrscheinlichkeit einer Begegnung
n : Anzahl der K , die ein H zur Reproduktion fressen muss
Parameterwahlen für die Simulation:
λK = 2 · 10−3 ,
−6
p = 5 · 10
λH = 5 · 10−4 ,
,
n = 5.
Aus dem kontinuierlichen Modell bekannt:
Ein stabiler Gleichgewichtszustand ist gegeben durch
K∗ =
nλH
= 500,
p
H∗ =
λK
= 400.
p
Monte-Carlo-Simulation: Räuber-Beute-Modell
Pseudocode:
1. Lies die Anfangsbestände K und H sowie die Anzahl T der Zeitschritte ein
2. Für i = 1, . . . , T :
Für k = 1, . . . K :
• Erzeuge eine zufällige Zahl x ∈ [0, 1[
• Falls x < λK : Setze K ← K + 1
Für k = 1, . . . , K :
Für j = 1, . . . , H:
• Erzeuge eine zufällige Zahl x ∈ [0, 1[
• Falls x < p: Setze K ← K − 1
Setze ntemp ← ntemp + 1
Falls ntemp = n: Setze H ← H + 1 und ntemp ← 0
break;
Für j = 1, . . . , H:
• Erzeuge eine zufällige Zahl x ∈ [0, 1[
• Falls x < λH : Setze H ← H − 1
Gib die Zahlen K und H am Bildschirm aus
Monte-Carlo-Simulation: Räuber-Beute-Modell – Ergebnisse
650
900
Karpfen
Hechte
Karpfen
Hechte
800
600
700
Population
Population
550
500
600
500
450
400
400
300
350
200
0
2000
4000
6000
8000
10000 12000
Zeitschritte
14000
16000
18000
20000
0
2000
K = 500, H = 400
2000
6000
8000
10000 12000
Zeitschritte
14000
16000
18000
20000
K = 800, H = 400
4000
Karpfen
Hechte
1800
4000
Karpfen
Hechte
3500
1600
3000
1200
Population
Population
1400
1000
800
2500
2000
1500
600
1000
400
500
200
0
0
0
2000
4000
6000
8000
10000 12000
Zeitschritte
14000
K = 900, H = 800
16000
18000
20000
0
2000
4000
6000
8000
10000 12000
Zeitschritte
14000
K = 800, H = 1000
16000
18000
20000
Monte-Carlo-Simulation: Räuber-Beute-Modell – Ergebnisse
1400
K=500,
K=800,
K=900,
K=800,
1200
H=400
H=400
H=800
H=1000
Hechte
1000
800
600
400
200
0
0
500
1000
1500
2000
Karpfen
2500
3000
Phasenraumdiagramm für verschiedene Startwerte
3500
4000
Programmierung für Mathematiker
Prof. Dr. Thomas Schuster
M.Sc. Dipl.-Phys. Anne Wald
03.05.2017
Funktionen
Funktion ≈ Zusammenfassung eines Anweisungsblocks zu einer aufrufbaren
Einheit
Gehört zu den wichtigsten Konzepten fast aller Programmiersprachen
Aufgaben von Funktionen:
1
2
3
4
Wiederverwendbarkeit von einmal geschriebenem Code
Strukturierung und Vereinfachung von Code
; bessere Übersicht und Lesbarkeit
einfachere Fehlersuche, separates Testen möglich
leichteres Hinzufügen weiterer Funktionalitäten
; Die sinnvolle Strukturierung eines Programms in Unterprogramme ist einer der
wichtigsten Schritte bei der Programmierung!
Charakterisierung von Funktionen
Eine Funktion
besitzt einen sinnvollen (=sprechenden) Namen, mit dem sie aufgerufen wird.
nimmt eine (möglicherweise leere) Liste von Parametern mit festgelegten
Datentypen als Eingabe.
hat einen Anweisungsblock, der bei ihrem Aufruf ausgeführt wird.
liefert nichts oder einen Wert eines festgelegten Datentyps als Ausgabe
„Mathematische“ Schreibweise:
Funktion : {int, float, ...}N −→ {int, float, ...} oder {}, N ≥ 0
(Parameter1, . . . , ParameterN)
7−→
Ausgabewert
Anweisungen
Im Gegensatz zu mathematischen Funktionen kann der Anweisungsblock auch
Befehle enthalten, die nicht direkt etwas mit der Ausgabe zu tun haben (auch
„Funktion: {} → {}“ kann in C sinnvoll sein).
Syntax
Deklaration
Rückgabetyp Funktionsname(ParTyp1 [Par1], ..., ParTypN [ParN]);
Definition
Rückgabetyp Funktionsname(ParTyp1 Par1, ..., ParTypN ParN)
{
Anweisungsblock
}
Unterschied zwischen Deklaration und Definition: verschiedene Abstraktionsebenen!
Deklaration (= Signatur)
legt fest, was eine Funktion tut.
muss vor dem erstmaligen Aufruf
und außerhalb von main im Code
stehen.
Definition
legt fest, wie eine Funktion etwas
tut.
kann an einer beliebigen Stelle
außerhalb von main im Code stehen.
endet mit einem Semikolon.
hat kein Semikolon am Ende.
Parameter-Datentypen genügen.
Parameter sind mit Typ und Name
anzugeben.
Beispiel
1
2
/** Praeprozessordirektive ****/
#include <stdio.h>
3
4
5
6
/** Funktionsdeklarationen ****/
float summe(float a, float b);
7
// auch moeglich:
// float summe(float, float);
8
9
10
11
12
13
14
15
/** Hauptprogramm ****/
int main(void)
{
float sum;
sum = summe(3.5, 1);
}
// Funktionsaufruf
16
17
18
19
20
21
22
23
/** Funktionsdefinitionen ****/
float summe(float a, float b)
{
float sum = a + b;
return sum;
}
// Funktionsrumpf
// Funktionsrumpf
Der Datentyp void
Funktionen ohne Rückgabe:
Deklaration: void Funktion(Parameterliste);
Beispiel:
void srand48(long seed);
Verwendung: z. B. Bildschirmausgabe, Statusänderung eines externen
„Mechanismus“
In anderen Programmiersprachen (z.B. Pascal) oft als Prozedur bezeichnet. In C
gibt es keine Differenzierung zwischen Funktion und Prozedur.
Funktionen ohne Parameter:
Deklaration: Rückgabetyp Funktion(void);
Beispiel:
double drand48 (void);
Verwendung: z. B. Ausführung von externen „Mechanismen“
Kombination: z. B. void abort(void); (Gewaltsames Ende des Programms)
return und main
return
Mit der Ausführung der return-Anweisung wird die aktuelle Funktion sofort
beendet.
Enthält der Funktionsrumpf keine return-Anweisung, so endet die Ausführung
des Rumpfes bei Erreichen der letzten schließenden geschweiften Klammer.
→ schlechter Stil!
Beachte: Es kann immer nur ein (skalarer) Wert zurückgegeben werden.
main
main ist in Wirklichkeit eine Funktion (mit Rückgabetyp int).
Quelltext innerhalb von main wird als Hauptprogramm bezeichnet.
Es sind äquivalent:
main()
int main()
int main(void)
main liefert standardmäßig den Rückgabewert 0 wenn keine Fehler aufgetreten
sind.
Mit return . . .; können andere Werte zurückgeliefert werden.
→ Fehlerbehandlung
Beispiel 1: Funktion mit Rückgabewert
Die charakteristische Funktion eines Intervalls [a, b] mit a < b ist definiert als
χ[a,b] (x ) :=
1
0
,
,
x ∈ [a, b]
sonst.
Implementierung: je nach Wert von x wird 1.0 oder 0.0 zurückgegeben.
Definition im Code:
// Charakteristische Funktion des Intervalls [a,b]
// (1 innerhalb, 0 ausserhalb)
float charFunkIntervall(const float a, const float b, float x)
{
if(a >= b)
// Hier muss eine Fehlerbehandlung hin
}
if ((x >= a) && (x <= b))
return 1.0;
else
return 0.0;
Beispiel 2: Funktion ohne Rückgabewert
void geplapper(int zahl1, double zahl2, char c)
{
printf("Diese Funktion erzeugt eine Menge (sinnloser) Ausgaben ");
printf("am Bildschirm.\n\n");
printf("Jetzt noch eine horizontale Linie, dann geht's los!\n");
printf("---------------------------------------------------\n");
printf("Zahl1 = %d, Zahl2 = %f\n", zahl1, zahl2);
if(c == '+')
printf("Die Summe der beiden Zahlen ist %f\n\n", zahl1+zahl2);
printf("So, jetzt bin ich fertig!\n");
}
return;
Mathematische Funktionen
Nutzung erfordert Präprozessordirektive
#include <math.h>
Compilierung
gcc ProgrammName.c -o ProgrammName -lm
Signatur
int abs(int a)
Bedeutung
|a|
float fabsf(float a)
|a|
double fabs(double a)
|a|
√
x
double sqrt(double x)
double pow(double b, double e)
be
double exp(double x)
ex
double log(double x)
double log10(double x)
ln(x )
log10 (x )
Mathematische Funktionen
Signatur
double sin(double x)
Bedeutung
sin(x )
double cos(double x)
cos(x )
double tan(double x)
tan(x )
double asin(double x)
arcsin(x )
double acos(double x)
arccos(x )
double atan(double q)
arctan(q) ∈ (− π2 ,
double atan2(double x, double y)
arctan(y /x ) ∈ (−π, π]
double sinh(double x)
sinh(x )
double cosh(double x)
cosh(x )
double floor(double x)
bx c
double ceil(double x)
dx e
π
)
2
Konstanten in math.h
Name
M_E
Bedeutung
e
M_LOG2E
log2 (e)
M_LOG10E
log10 (e)
M_LN2
ln(2)
M_LN10
ln(10)
M_PI
π
M_PI_2
π/2
M_PI_4
π/4
M_1_PI
1/π
√
2
√
1/ 2
M_SQRT2
M_SQRT1_2
Bemerkung: Die Namen der Konstanten werden vom Präprozessor im Code textuell
durch die entsprechenden Werte ersetzt, z. B. M_PI durch 3.14159265358979323846.
(Gleitkommazahlen werden automatisch als double interpretiert.)
Call by Value
Beispiel: Vertauschen zweier Werte (?)
1
#include <stdio.h>
2
3
4
// Funktionsdeklaration
void vertausche(int p, int q);
// oder (int, int)
5
6
7
8
9
10
11
12
13
14
// Hauptprogramm
int main(void)
{
int a = 1, b = 3;
vertausche(a, b);
printf("a = %d, b = %d\n", a, b);
return 0;
// guter Stil
}
15
16
17
18
19
20
21
22
23
// Funktionsdefinition
void vertausche(int p, int q)
{
int hilf = p;
p = q;
q = hilf;
return;
}
Ausgabe:
a = 1, b = 3
Die Werte wurden gar nicht vertauscht! Warum?
Call by value
Definition
Bei einem Funktionsaufruf werden nicht die Variablen als solche, sondern lediglich ihre
Werte, d.h. Kopien der Variableninhalte übergeben.
+ Funktionsaufrufe können direkt als Parameter für eine andere Funktion verwendet
werden, da der Wert und nicht die Funktion selbst übergeben wird.
Beispiel: printf("Wurzel von 2 = %f\n", sqrt(2.0));
+ Unbeabsichtigte Manipulation der Variablen durch Funktionen wird vermieden.
– Der Manipulation von Variablen sind Grenzen gesetzt, da immer nur ein (skalarer)
Wert zurückgeliefert werden kann. (Ausweg: Zeiger und Call by reference, später)
Häufige Fehlerquelle: Annahme, dass Funktionsparameter durch die Funktion
verändert werden können. Dem ist nicht so!
Beispiel: float a = 2.0; sqrt(a);
Hier wird der Rückgabewert von sqrt mit Parameter 2.0 nicht wieder in a
gespeichert, sondern verworfen!
Scope und Lifetime
Definition (Scope)
Der Scope (Sichtbarkeit) eines deklarierten Objekts (Variable oder Funktion) ist der
Bereich im Quelltext, in dem es bekannt, d. h. mit seinem Namen aufrufbar ist.
Generell gilt:
Variablen sind innerhalb des textuellen Codeblocks sichtbar, in dem sie deklariert
wurden.
Funktionen sind ab ihrer Deklaration in der gesamten Datei sichtbar.
Definition (Lifetime)
Die Lifetime (Lebenszeit) einer Variablen beschreibt den Zeitraum, in dem der
Speicherbereich der Variablen für sie reserviert ist.
Grundregel:
Eine Variable existiert vom Moment ihrer Deklaration bis zu dem Zeitpunkt, an
dem der Block, welcher die Deklaration umschließt, verlassen wird.
Lokale und globale Variablen
Lokale Variablen
Lokale Variablen werden zu Beginn eines Anweisungsblocks deklariert.
Lifetime: Bis zum Ende des Anweisungsblocks, also auch in inneren Blöcken
Scope: Innerhalb des Blocks, sofern sie nicht durch Variablen gleichen Namens in
untergeordneten Blöcken überdeckt werden
Funktionen liegen textuell außerhalb jedes anderen Blocks → lokale Variablen
sind dort generell nicht sichtbar.
Globale Variablen
Globale Variablen werden außerhalb aller Funktionen (einschl. main) deklariert.
Namenskonvention: Unterstrich am Ende des Namens, z. B. int var_ = 42;
Lifetime: Gesamte Dauer der Programmausführung (auch über Dateigrenzen
hinweg → später)
Scope: Überall (auch in Funktionen)
Gefahren: Namenskonflikte, unkontrollierte Manipulation, Chaos
→ Nutzung globaler Variablen auf ein Minimum reduzieren!
Sichtbarkeit: Beispiel 1
1
#include <stdio.h>
2
3
4
5
6
7
8
9
10
int main(void)
{
int a = 4;
{
int a = 5;
printf("Innen: a = %d\n", a);
}
printf("Aussen: a = %d\n", a);
11
12
13
}
return 0;
Ausgabe:
Innen: a = 5
Aussen: a = 4
Sichtbarkeit: Beispiel 2
1
#include <stdio.h>
2
3
4
5
int main(void)
{
int i;
6
for (i=5; i<10; i++)
{
int i=0;
i++;
printf("In Schleife: i = %2d\n", i);
}
7
8
9
10
11
12
13
14
15
16
}
printf("Nach Schleife: i = %2d\n", i);
return 0;
Ausgabe:
In Schleife:
In Schleife:
In Schleife:
In Schleife:
In Schleife:
Nach Schleife:
i
i
i
i
i
i
= 1
= 1
= 1
= 1
= 1
= 10
Lokale und globale Variablen
1
#include <stdio.h>
2
3
int a_ = 10;
// globale Variable
4
5
6
int funktion(int);
void prozedur(void);
7
8
9
10
11
12
13
14
15
int main(void)
{
prozedur();
prozedur();
funktion(a_);
printf("a_ = %d\n", a_);
return 0;
}
16
17
18
19
20
21
void prozedur(void)
{
a_ *= a_;
return;
}
22
23
24
25
26
int funktion(int a_)
{
return(--a_);
}
Ausgabe: a_ = 10000
// schlechter Stil: nur globale Variablen
// sollten mit "_" enden!
Automatische und statische Variablen
Bisher waren alle Variablen automatische Variablen, d.h. sie existieren bis zu dem
Zeitpunkt, an dem der Block, welcher die Deklaration umschließt, verlassen wird.
Statische Variablen
Möglichkeit, dass eine Funktion beim nächsten Durchlauf die Information, die in
der Variablen gespeichert wurde, verwenden kann (wie in einem Gedächtnis)
Sichtbarkeit: Innerhalb des Blocks, sofern sie nicht durch Variablen gleichen
Namens in untergeordneten Blöcken überdeckt werden
Deklaration: static Datentyp Name=Wert;
Beispiel:
1
2
3
4
5
void zaehle() {
static int i = 1; // i wird (nur) beim ersten Aufruf von zaehle initialisiert
printf("%d\n", i);
i = i + 1;
}
Rekursive Programmierung
Rekursion
Aufruf einer Funktion durch sich selbst.
Iteration
Wiederholung eines Anweisungsblocks.
Bemerkungen zur rekursiven Programmierung:
Man muss die Schachtelungstiefe der Rekursion selbst überwachen, sonst kann es
zum sog. Stapelüberlauf (engl. stack overflow ) kommen.
Meist sind Iteration und Rekursion äquivalent, aber häufig ist die Überführung in
die jeweils andere Variante nicht offensichtlich.
Je nach Fragestellung (Laufzeit-, Speicher-, Lesbarkeitsoptimierung) entscheidet
man sich für eine der beiden Methoden.
Viele effiziente Sortieralgorithmen oder Divide-and-Conquer -Techniken basieren
auf dem Prinzip der Rekursion.
Rekursive Programmierung
Einfaches Beispiel: Fakultät n! = n(n − 1) · · · 1 = fac(n)
Es gilt:
fac(n) =
n * fac(n-1)
1
,n > 1
, n ∈ {0, 1}
Interessanteres Beispiel: Quersumme einer natürlichen Zahl
Algorithmus
1
Die Quersumme einer einstelligen Zahl ist die Zahl selbst.
2
Die Quersumme einer mehrstelligen Zahl ist die Summe der letzten Ziffer und der
Quersumme der Zahl ohne ihre letzte Ziffer.
Beispiel:
Quersumme(5) = 5
Quersumme (31415) = Quersumme (3141) + 5
Quersumme iterativ
1
#include <stdio.h>
2
3
4
5
int main(void)
{
int zahl, qsumme = 0;
6
printf("Zahl = ");
scanf("%d", &zahl);
7
8
9
printf("Quersumme(%d) = ", zahl);
10
11
while (zahl)
{
qsumme += zahl % 10;
zahl /= 10;
}
12
13
14
15
16
17
printf("%d\n", qsumme);
18
19
20
21
}
return 0;
Quersumme rekursiv
1
#include <stdio.h>
2
3
int qsumme(int);
4
5
6
7
8
9
int main(void)
{
int zahl;
printf("Zahl = ");
scanf("%d", &zahl);
10
printf("\nQuersumme(%d) = %d\n",zahl, qsumme(zahl));
11
12
13
14
}
return 0;
15
16
17
18
19
20
int qsumme(int zahl)
{
if (zahl / 10)
return zahl % 10 + qsumme(zahl / 10);
21
22
23
}
return zahl;
Funktionen – elementare Merkregeln
Deklaration und Definition
Die Funktionsdeklaration steht im Code vor main.
Die Funktionsdefinition kommt ans Ende der Datei (unauffällig „versteckt“).
Anzahl und Datentypen der Parameter sowie Rückgabetyp müssen
übereinstimmen.
Guter Stil: Deklaration mit Kommentar versehen, der Parameter und
Rückgabewert erläutert
Der Name soll die Tätigkeit (Rückgabetyp void) bzw. den zurückgegebenen
Wert (nichtleerer Rückgabetyp) widerspiegeln.
Jede Funktion sollte durch ein return [Wert]; beendet werden.
Aufruf
Die Anzahl der Parameter muss konsistent mit der Deklaration sein.
Es findet Call by value statt, d. h. die Funktion hat nicht die Parameter selbst,
sondern nur die darin gespeicherten Werte zur Verfügung. Daher können diese
auch nicht permanent verändert werden!
Die übergebenen Werte werden automatisch in die Datentypen laut Deklaration
umgewandelt („gecastet“). Bei inkompatiblen Typen warnt der Compiler lediglich.
Funktionen „sehen“ nur globale Variablen, übergebene Parameter sowie lokale
Variablen im Funktionsrumpf.
Statische Felder
Definition
Ein Feld (engl. array ) ist die Zusammenfassung von Elementen gleichen Typs zu einer
aufrufbaren Einheit.
Deklaration:
Datentyp Feldname[Anzahl];
Legt ein Feld von Anzahl Elementen des Typs Datentyp an.
Achtung: Anzahl muss ein positiver ganzzahliger Wert sein.
Wie bei primitiven Datentypen ist eine Initialisierung bei der Deklaration möglich:
int N[4] = {1, 3, -5, 42};
double x[] = {1.9, -3.1415, 5.73e+21};
// Die Groesse (3) wird hier vom Compiler automatisch bestimmt
int p = 23;
unsigned j[p] = {1, 0, 3};
// Wichtig: p initialisieren!
// Rest bleibt uninitialisiert
float y[2] = {1.0, 3, -7.2};
// Fehler: zu viele Elemente!
Statische Felder
Die Deklaration float x[5]; legt ein Feld der Länge 5 an. Die Komponenten
(Feldeinträge, Feldelemente) sind dabei alle vom Typ float.
Folgerung: Das Feld mit Bezeichner x belegt im Arbeitsspeicher 5 · 4 = 20 Byte.
Auf die Komponenten kann mittels x[0], x[1], x[2], x[3] und x[4] zugegriffen
werden. Beispiel:
x[0]
x[1]
x[2]
x[3]
x[4]
=
=
=
=
=
11.0;
12.0;
13.0;
14.0;
15.0;
Merke: Die Indizierung von Feldeinträgen beginnt in C stets mit 0!
1. Komp.
4 Byte
x[0]
11.0
2. Komp.
4 Byte
x[1]
12.0
3. Komp.
4 Byte
x[2]
13.0
4. Komp.
4 Byte
x[3]
14.0
5. Komp.
4 Byte
x[4]
15.0
← Größe
← "Name"
← Inhalt
Merke: Alle Komponenten eines Feldes werden vom Compiler direkt
hintereinander im Arbeitsspeicher abgelegt.
Einschränkungen und Stolperfallen
Einmal festgelegt, kann die Größe eines statischen Feldes nicht mehr verändert
werden.
Der maximalen Größe eines Feldes sind enge Grenzen gesetzt (Gefahr eines stack
overflow = Stapelüberlauf).
Die Rückgabe eines Feldes durch eine Funktion oder die Übergabe eines Feldes
als Parameter einer Funktion ist nicht möglich, da es sich nicht um einen
primitiven, sondern einen zusammengesetzten Datentyp handelt.
Beim Zugriff auf ein Feldelement außerhalb des zulässigen Indexbereichs erfolgt
im allgemeinen keine Fehlermeldung! Auch der Compiler warnt nicht!
Die Zuweisung x[7] = 241.98 schreibt in den Bereich, der (zufällig) von alpha
belegt wird.
Mögliche Folge: Das Programm wird völlig unberechenbar!
Häufigste Fehlerquelle:
int x[N]; ... x[N] = 1;
Tritt zumeist dann auf, wenn x[1] statt x[0] als erster Eintrag interpretiert wird.
Beispiel: Euklidische Norm eines Vektors im R3
1
2
#include <stdio.h>
#include <math.h>
3
4
5
6
7
int main(void)
{
double x[3], norm2 = 0.0, norm;
int i;
8
// Vektor einlesen
for (i=0; i<3; i++)
// Vorsicht: Indices beginnen bei 0!
{
printf("Geben Sie die %d-te Komponente ein: ", i+1);
scanf("%lf", &x[i]);
}
9
10
11
12
13
14
15
// Vektor ausgeben
printf("\nDer Vektor hat folgende Eintraege:\n");
for (i=0; i<3; i++)
printf("x[%d] = % 7.4lf\n", i, x[i]);
16
17
18
19
20
// Berechnung der Summe der Komponentenquadrate
for (i=0; i<3; i++)
norm2 += x[i]*x[i];
21
22
23
24
norm = sqrt(norm2);
25
// euklidische Norm
26
27
28
29
}
printf("\nDie Norm des Vektors ist % .4lf.\n\n", norm);
return 0;
Beispiel: Euklidische Norm eines Vektors im R3
Ausgabe:
Geben Sie die 1-te Komponente ein: 1
Geben Sie die 2-te Komponente ein: -2
Geben Sie die 3-te Komponente ein: 2
Der Vektor hat folgende Eintraege:
x[0] = 1.0000
x[1] = -2.0000
x[2] = 2.0000
Die Norm des Vektors ist
3.0000.
Mehrdimensionale Felder
Deklaration:
Datentyp Feldname[dim1][dim2]...[dimN];
Beachte: A[4][3] wird (unabhängig vom Typ) im Speicher „zeilenweise“ abgelegt:
A[0][0], A[0][1], A[0][2], A[1][0], . . . , A[1][2], A[2][0], . . . , A[3][2]
Mathematische Interpretation:
A = (aij ) 1≤i≤4 ∈ R4×3
1≤j≤3
A[0][0]
A[1][0]
A[2][0]
A[3][0]

A[0][1]
A[1][1]
A[2][1]
A[3][1]
A[0][2]
a11
A[1][2]
a21

←→
A[2][2]
a31
A[3][2]
a41


a12
a22
a32
a42

a13
a23 
a33
a43
2D-Felder als Matrizen
Beispiel: Zeilenweises Einlesen der Komponenten einer 2 × 3-Matrix vom Typ int
1
#include <stdio.h>
2
3
4
5
6
int main(void)
{
int i, j;
int A[2][3];
7
for(i=0; i<2; i++)
{
for(j=0; j<3; j++)
{
printf("A[%d][%d] = ", i, j);
scanf("%d", &A[i][j]);
} // for j
} // for i
8
9
10
11
12
13
14
15
16
17
18
}
return 0;
2D-Felder als Matrizen
Auch bei mehrdimensionalen Feldern ist eine direkte Initialisierung möglich, bspw.
int A[3][2] = {{11,12},{21,22},{31,32}};
Da die Komponenten im Speicher in einer Reihe angeordnet sind, ist obige Zeile
äquivalent zu
int A[3][2] = {11,12,21,22,31,32};
Erfolgt bei der Deklaration eine partielle Initialisierung, so wird mit Nullen aufgefüllt:
int A[3][3] = {{1,2},{3},{4,5}};
generiert die Matrix
"
A=
1
3
4
2
0
5
0
0
0
#
.
Programmierung für Mathematiker
Prof. Dr. Thomas Schuster
M.Sc. Dipl.-Phys. Anne Wald
10.05.2017
Zeichenketten
Zeichenketten (engl. strings) sind formal nichts anderes als Felder vom Typ char.
Eine Zeichenkette ("Hallo Welt!\n") wird vom Compiler automatisch als Feld
von Zeichen dargestellt. Dabei wird am Schluss automatisch ein zusätzliches
Zeichen ’\0’ (Nullzeichen) angehängt, um das Stringende zu markieren.
Stringverarbeitungsfunktionen benötigen unbedingt das Nullzeichen, damit sie das
Ende eines Strings erkennen. Somit muss bei der Deklaration ein zusätzlicher
Speicherplatz für ’\0’ eingeplant werden.
Beispiel:
char wort[6] = "Hallo";
0
H
1
a
2
l
3
l
4
o
5
\0
printf("Der 2. Buchstabe von wort ist ein '%c'.\n", wort[1]);
Ausgabe: Der 2. Buchstabe von wort ist ein ’a’.
Zeichenketten
Direkte Initialisierung: als String
char wort[] = "Hallo";
oder als Feld von Zeichen
char wort[] = {'H', 'a', 'l', 'l', 'o', '\0'};
Einlesen von Zeichenketten mittels fgets:
fgets(Zielstring, Anzahl + 1, stdin);
liest Zeichen von der Tastatur ein, bis ein ’Return’ eingegeben wird, und speichert
die ersten Anzahl Zeichen und ein abschließendes ’\0’ im Zielstring.
Bemerkung: Die „einfachere“ Funktion gets prüft nicht die Länge des
Zielstrings. Daraus ergeben sich u. U. gravierende Sicherheitsmängel (Gefahr
eines Überlaufs), weshalb die Funktion unter Programmierern „geächtet“ ist.
Ausgabe via printf wie üblich. Der Platzhalter ist %s.
printf("%s\n", wort);
Operationen mit Strings (kleine Auswahl)
#include <string.h>
strlen(s)
liefert Länge von s, abschließendes ’\0’ nicht mitgezählt
strncpy(s, t, n)
kopiert höchstens n Zeichen von t nach s (*)
strncat(s, t, n)
hängt n Zeichen von t an das Ende von s an (*)
(
strcmp(s, t)
>0
<0
0
,
,
,
falls s lexikographisch kleiner als t ist
falls s lexikographisch größer als t ist
falls s und t identisch sind
Beachte: if (strcmp(s, t)) testet auf Ungleichheit!
strncmp(s, t, n)
wie strcmp, aber nur für die ersten n Zeichen
(*) Vorsicht: Der Programmierer ist dafür verantwortlich, dass in s genügend Platz
vorhanden ist, um n Zeichen zu speichern. Das sollte immer geprüft werden!
Operationen mit Strings: Beispiel
1
2
#include <stdio.h>
#include <string.h>
3
4
5
6
int main(void)
{
char str1[] = "Modellierung", vl_eigenschaft[8], zu_kurz[3], vl_name[51];
7
strncpy(vl_name, str1, strlen(str1));
8
// okay
9
// simpler Test auf ausreichende Laenge
if (50 >= strlen(vl_name) + strlen(" und ") + strlen("Programmierung"))
{
strncat(vl_name, " und ", 5);
strncat(vl_name, "Programmierung", strlen("Programmierung"));
}
10
11
12
13
14
15
16
printf("Wie findest du %s? ", vl_name);
fgets(vl_eigenschaft, 8, stdin);
17
18
19
printf("\n%s ist deiner Meinung nach %s\n", vl_name, vl_eigenschaft);
20
21
strncpy(zu_kurz, str1, strlen(str1));
// Ueberlauf!
printf("Inhalt von zu_kurz: %s\n", zu_kurz);
printf("Inhalt von vl_eigenschaft: %s\n", vl_eigenschaft);
22
23
24
25
26
27
}
return 0;
Operationen mit Strings: Beispiel
Ausgabe:
Wie findest du Modellierung und Programmierung? okay
Modellierung und Programmierung ist deiner Meinung nach okay
Inhalt von zu_kurz: Modellierungodellierung
Inhalt von vl_eigenschaft: ellierungodellierung
(Die Ausgabe der letzten beiden Zeilen hängt vom Speicherlayout ab.)
Vorsicht:
Offenbar warnt der Compiler nicht vor dem Überlauf in Zeile 22, und auch zur
Laufzeit tritt kein Fehler auf.
Die Funktion strncpy schreibt über die Feldgrenzen von zu_kurz hinaus in einen
Bereich, der (möglicherweise) von einer anderen Variable belegt wird.
Da ein String erst mit dem Nullzeichen als beendet gilt, wird von printf bei der
Ausgabe des fehlerhaften Strings aus einem fremden Bereich gelesen!
Ergebnis: Ein unberechenbares Programm mit einem äußerst schwierig zu
lokalisierenden Fehler.
Umwandlung von Strings
#include <stdlib.h>
String −→ Ganzzahl: int atoi(Zeichenkette)
int n;
char s[11];
printf("Es ist atoi(\"101\") = %d \n", atoi("101"));
n = atoi("3218");
printf("Es ist atoi(\"3218\") = %d \n", n);
strncpy(s, "-157", 10);
printf("Es ist atoi(s) = %d \n", atoi(s));
Ausgabe:
Es ist atoi("101") = 101
Es ist atoi("3218") = 3218
Es ist atoi(s) = -157
Umwandlung von Strings
#include <stdlib.h>
String −→ Gleitkommazahl: double atof(Zeichenkette)
double x;
char s[11];
printf("Es ist atof("101.32") = %lf \n", atof("101.32"));
x = atof("3218.927");
printf("Es ist atof("3218.927") = %lf \n", x);
strncpy(s, "-157.58", 10);
printf("Es ist atof(s) = %lf \n", atof(s));
Ausgabe:
Es ist atof("101.32") = 101.320000
Es ist atof("3218.927") = 3218.927000
Es ist atof(s) = -157.580000
Größe von Datentypen und Speicherobjekten: sizeof
Allgemein
#include <stdlib.h>
Rückgabetyp von sizeof ist size_t.
Dabei gilt: size_t ist ganzzahlig und vorzeichenlos, entspricht unsigned (int)
oder unsigned long.
sizeof gibt den Speicherbedarf eines Datentyps in Byte aus:
size_t sizeof(Datentyp);
printf("sizeof(char) = %u, ",sizeof(char));
printf("sizeof(int) = %u\n",sizeof(int));
printf("sizeof(float) = %u, ",sizeof(float));
printf("sizeof(double) = %u\n",sizeof(double));
Ausgabe:
sizeof(char) = 1, sizeof(int) = 4
sizeof(float) = 4, sizeof(double) = 8
Größe von Datentypen und Speicherobjekten: sizeof
sizeof liefert den Speicherbedarf eines deklarierten Speicherobjekts in Byte:
size_t sizeof Speicherobjekt;
oder
size_t sizeof(Speicherobjekt);
Beispiel
int i;
int j=42;
float x;
float y=3.1415;
double z=M_PI;
char s[10];
char t[]= "Ay, caramba!";
int a[6];
int b[]={3,2,1,8};
float c[]={1.1,2.2,3.3};
double A[3][2];
int B[][] = {{11,12,13},{21,22,23}};
sizeof
sizeof
sizeof
sizeof
sizeof
sizeof
sizeof
sizeof
sizeof
sizeof
sizeof
sizeof
i
j
x
y
z
s
t
a
b
c
A
B
;
;
;
;
;
;
;
;
;
;
;
;
4
4
4
4
8
10
13
24
16
12
48
error
Speicheradressen
Adress-Operator &
Erinnerung:
int a;
scanf("%d",&a);
Beispiel: Adressen von Skalaren:
int i=1, j=9;
double x=2.7, y=M_PI;
printf("Adresse von i = %p\t",&i);
printf("Wert von i = %d\n", i);
printf("Adresse von j = %p\t",&j);
printf("Wert von j = %d\n", j);
printf("Adresse von x = %p\t",&x);
printf("Wert von x = %lf\n", x);
printf("Adresse von y = %p\t",&y);
printf("Wert von y = %lf\n", y);
Ausgabe:
Adresse
Adresse
Adresse
Adresse
von
von
von
von
i
j
x
y
=
=
=
=
0x28abf4
0x28abf0
0x28abe8
0x28abe0
Wert
Wert
Wert
Wert
von
von
von
von
i
j
x
y
=
=
=
=
1
9
2.700000
3.141593
Speicheradressen
Beispiel: Adressen von eindimensionalen Feldkomponenten
char c[] ="Wetterwachs";
int a[] = {1,2,3};
printf("Adresse von c[0]
printf("Adresse von c[1]
printf("Adresse von c[2]
printf("Adresse von c[3]
printf("Adresse von a[0]
printf("Adresse von a[1]
printf("Adresse von a[2]
=
=
=
=
=
=
=
Ausgabe:
Adresse
Adresse
Adresse
Adresse
von
von
von
von
c[0]
c[1]
c[2]
c[3]
=
=
=
=
0x22ccb0
0x22ccb1
0x22ccb2
0x22ccb3
Adresse von a[0] = 0x22cca0
Adresse von a[1] = 0x22cca4
Adresse von a[2] = 0x22cca8
%p\n",&c[0]);
%p\n",&c[1]);
%p\n",&c[2]);
%p\n",&c[3]);
%p\n",&a[0]);
%p\n",&a[1]);
%p\n",&a[2]);
Speicheradressen
Beispiel: Adressen von mehrdimensionalen Feldkomponenten
double A[3][2] = {{11,12},{21,22},{31,32}};
printf("Adresse von A[0][0] = %u\n",&A[0][0]);
printf("Adresse von A[0][1] = %u\n",&A[0][1]);
printf("Adresse von A[1][0] = %u\n",&A[1][0]);
printf("Adresse von A[1][1] = %u\n",&A[1][1]);
printf("Adresse von A[2][0] = %u\n",&A[2][0]);
printf("Adresse von A[2][1] = %u\n",&A[2][1]);
Adresse
Adresse
Adresse
Adresse
Adresse
Adresse
von
von
von
von
von
von
A[0][0]
A[0][1]
A[1][0]
A[1][1]
A[2][0]
A[2][1]
=
=
=
=
=
=
2280560
2280568
2280576
2280584
2280592
2280600
Überlegungen:
Wenn man die Adresse einer Variable kennt, wird man auch ihren Inhalt, d.h.
ihren Wert manipulieren können.
Da man direkt auf den Arbeitsspeicher zugreift, sollte dies auch von jeder Stelle
im Programm aus möglich sein - vorausgesetzt die Adresse im Speicher ist
bekannt.
Zeiger: Definition und Prinzip
Definition
Ein Zeiger (engl. pointer ) ist eine Variable, deren Inhalt eine Speicheradresse und der
zugehörige Datentyp ist.
Funktionsprinzip:
Zeiger
...
Variable
...
Arbeitsspeicher
Zugriff über Variablennamen
...
Variable
...
Arbeitsspeicher
Zugriff über Zeiger
Variablenname steht stellvertretend
für einen Speicherbereich
Speicherbereich ist Inhalt der
Zeigervariablen
Verknüpfung ist mit Deklaration
festgelegt und unveränderbar
Dieser Inhalt ist selbstverständlich
veränderbar → Zeiger kann
„umgebogen“ werden
Bei Zugriff wird die
Speicher-Referenz intern aufgelöst
und der Wert anschließend gelesen
bzw. geschrieben.
Auflösung der Speicherreferenz
geschieht explizit, d. h. durch den
Programmierer
Zeiger
Wozu überhaupt Zeiger?
Indem man die Speicheradresse einer Variablen an eine Funktion übergibt,
ermöglicht man es, den Inhalt der Variablen innerhalb der Funktion permanent zu
verändern (Call by reference).
Mit Zeigern lässt sich wie mit Feldern umgehen, mit dem Unterschied, dass sie als
Parameter und als Rückgabewert von Funktionen verwendet werden können
(dynamische Felder).
Die gesamte Verwaltung von Speicherbereichen zur Laufzeit geschieht mit Hilfe
von Zeigern (dynamische Speicherverwaltung).
Zeiger ermöglichen es, aus einer Menge von Funktionen zur Ausführung einer
bestimmten Aufgabe zur Laufzeit eine Variante auszuwählen (Callback-Prinzip).
Zeiger
Deklaration:
Datentyp *Zeigername;
bzw.
Datentyp *z1,..., *zN;
Achtung: vor jedem Zeigername muss ein „*“ stehen!
Beispiel:
int a, *p;
erzeugt eine int-Variable und einen Zeiger auf int.
Bedeutung des Datentyps: Wird über einen Zeiger auf eine Speicheradresse
zugegriffen, so gibt der Datentyp an, wie viele Byte von der (Start-)Adresse an gelesen
bzw. geschrieben werden sollen.
Achtung: Fehlinterpretationen, d. h. die Verwendung eines Zeigers auf einen anderen
Datentyp als vorgesehen, kann zu ungewollten Ergebnissen führen.
Beispiel: Zeiger auf int statt Zeiger auf char.
Eine char-Variable wurde deklariert
int *
char
Fälschlicherweise wurde ein Zeiger auf int
verwendet, um über die Speicheradresse
auf die char-Variable zugreifen zu können.
int
Bei einem Zugriff werden statt 1 Byte
4 Byte angesprochen, von denen 3 nicht
mehr zum „zulässigen“ Bereich gehören.
Vor solchen Fällen warnt der Compiler
bestenfalls!
Arbeiten mit Zeigern: Referenzen
Adress- oder Referenzoperator: &
kann auf jedes beliebige Speicherobjekt (Variablen, Funktionen,. . . ) angewandt
werden.
hat Vorrang vor Vergleichs- und arithmetischen Operatoren, nicht jedoch vor dem
„Array/Index-Operator“ [].
liefert als Ergebnis einen konstanten Zeiger auf die Adresse (=Referenz) des
Objekts.
Beispiel:
int i = 1;
double x = M_E,
y[] = {3.0, 0.0, 42.0};
printf("Adresse von i: %p\n", &i);
printf("Wert von i: %d\n\n", i);
printf("Adresse von x: %p\n", &x);
printf("Wert von x: %lf\n\n", x);
printf("Adresse von y[2]: %p\n",
&y[2]); // oder &(y[2])
printf("Wert von y[2]: %lf", y[2]);
Ausgabe:
Adresse von i: 0x7fffb476d89c
Wert von i: 1
Adresse von x: 0x7fff45786740
Wert von x: 2.718282
Adresse von y[2]: 0x7fff45786730
Wert von y[2]: 42.000000
Bemerkung: %p ist der Platzhalter für
Adressen im Formatstring.
Arbeiten mit Zeigern: Referenzen
Inhalts- oder Dereferenzierungsoperator: *
lässt sich auf Zeiger anwenden.
steht auf der gleichen Prioritätsstufe wie der Adressoperator &.
liefert den Wert, der an der Adresse gespeichert ist, auf die der Zeiger verweist.
Beispiel:
int i = 1;
double x = M_E, *px = &x,
y[] = {3.0, 0.0, 42.0};
printf("Adresse von i: %p\n", &i);
printf("Wert von i: %d\n\n", *(&i));
printf("Adresse von x: %p\n", px);
printf("Wert von x: %lf\n\n", *px);
printf("Adresse von y: %p\n", y);
printf("\"Wert\" von y: %lf\n", *y);
Ausgabe:
Adresse von i: 0x7fffb476d89c
Wert von i: 1
Adresse von x: 0x7fff45786740
Wert von x: 2.718282
Adresse von y: 0x7fff45786720
“Wert” von y: 3.000000
Bemerkung: Der *-Operator kann auch
auf Felder angewandt werden!?
Arbeiten mit Zeigern: indirekter Zugriff auf Variablen
Zeiger erlauben indirekten lesenden und schreibenden Zugriff auf zuvor deklarierte
Variablen, indem man sie auf die entsprechenden Speicheradressen zeigen lässt.
Beispiel:
double x = M_PI, *p1 = &x, *p2;
Ausgabe:
Wert von x: 3.141593
printf("Wert von x: %lf\n\n", x);
Zugriff via p1: x = 3.141593
printf("Zugriff via p1: x = %lf\n\n",*p1);
Nach Manipulation,
Zugriff via p1: x = -0.318310
x = -M_1_PI;
printf("Nach Manipulation,\n");
printf("Zugriff via p1: x = %lf\n\n",*p1);
p2=p1;
*p2 = 0.1;
printf("Nach Manipulation via p2,\n");
printf("direkter Zugriff: x = %lf\n",x);
Nach Manipulation via p2,
direkter Zugriff: x = 0.100000
Achtung: In der Deklaration steht
„*p1“ nicht für Dereferenzierung,
sondern dafür, dass es sich bei der
folgenden Variablen um einen Zeiger
handelt!
Arbeiten mit Zeigern: „Umbiegen“
Einem Zeiger kann als Variable, deren Inhalt eine Adresse ist, (fast) jeder beliebige
(Adress-)Wert zugewiesen werden. Man spricht dann vom „Umbiegen“ des Zeigers.
Beispiel:
int a = -5, b = 42,
*p1 = &a, *p2 = &a;
printf("p1 zeigt auf a:\n");
printf("&a = %p\n", &a);
printf("p1 = %p\n\n", p1);
p1 = &b;
printf("Nach Umbiegen auf b:\n");
printf("&b = %p\n", &b);
printf("p1 = %p\n\n", p1);
p1 = p2;
printf("Zurueckbiegen mit p2:\n");
printf("p1 = %p\n", p1);
Ausgabe:
p1 zeigt auf a:
&a = 0x7fff72c3ecac
p1 = 0x7fff72c3ecac
Nach Umbiegen auf b:
&b = 0x7fff72c3eca8
p1 = 0x7fff72c3eca8
Zurueckbiegen mit p2:
p1 = 0x7fff72c3ecac
Typische Pointer-Fehler I
Falsche Annahme, dass Name und Funktion automatisch zusammenhängen.
Beispiele:
int a, *a;
; Fehler: In Konflikt stehende Typen für a
double x = 3.14, *px;
printf("%lf\n", *px);
; Warnung: px is used uninitialized in this function
Vergessenes „&“ oder „*“
Beispiel:
int a = 42, *pa;
pa = a;
printf("Wert von a: %d\n", pa);
; Bei der Zuweisung pa = a wird der Wert von a (42) als Adresse aufgefasst.
; In der printf-Anweisung wird die (vermeintliche) Referenz nicht aufgelöst,
sondern die Adresse, die in pa gespeichert wurde, als int ausgegeben.
Warnung: Durch (schreibenden) Zugriff auf uninitialisierte oder wild verbogene Zeiger
oder Zeiger auf einen inkompatiblen Datentyp lässt sich jedes Programm ins Chaos
stürzen (z. B. wenn durch den falschen Zugriff ein anderer Zeiger verbogen wird).
Sonderfälle: void * und NULL
Der Datentyp void *
Ein Zeiger vom Typ void * ist kein „Zeiger auf Nichts“, sondern ein (universeller)
Zeiger, der wie üblich eine Adresse speichert, dessen Typ jedoch (noch) nicht
festgelegt ist.
Einem Zeiger vom Typ void * können Adressen von typisierten Speicherobjekten
zugewiesen werden.
Durch einen korrekten Cast kann über den Zeiger auf Inhalte der Speicherobjekte
zugegriffen werden. Dieser Cast sollte immer explizit sein.
Der Nullzeiger NULL
Formal ist NULL = (void *)0, d. h. die Zuweisungen p = NULL und p = 0 sind
für einen Zeiger äquivalent. Verwendung von NULL erhöht aber die Lesbarkeit.
Der Versuch, NULL mittels „*“ zu dereferenzieren, führt unmittelbar zu einem
Speicherzugriffsfehler.
Bei der Deklaration eines Zeigers ist es ratsam, mit NULL (bzw. einem anderen
Wert) zu initialisieren. Dadurch vermeidet man ungewollte Zugriffe auf fremde
Speicherbereiche.
Viele Funktionen mit einem Zeiger-Datentyp als Rückgabewert liefern bei
Scheitern NULL. Dieser Fall kann zur Kontrolle abgefragt werden.
Typische Pointer-Fehler II
Vergessener Cast
Beispiel:
int a;
void *z = &a;
printf("a = %d\n", *z);
; Warnung: Dereferenzierung eines void *-Zeigers
; Fehler: falsche Benutzung eines void-Ausdruckes
Cast eines void * auf einen falschen Datentyp (Fehlinterpretation des void *)
Beispiel:
int a; void *z = &a;
double *p = (double *)z;
printf("a = %lf\n", *p);
; a = 573791543154187132539735239137854912273... (undefinierter Wert)
Zuweisung statt Vergleich mit NULL
Beispiel:
int *p = NULL;
p = ...
if (p = NULL)
return 1;
; Der Fall, dass p den Wert NULL hat, wird garantiert nicht wie beabsichtigt
abgefangen. (Wieso nicht?)
Call by reference
Definition
An eine Funktion werden Zeiger auf die Adressen von Variablen übergeben, mit deren
Hilfe der Variableninhalt verändert werden kann.
Zur Erinnerung: Call by value
void vertausche(int a, int b)
{
int hilf = b;
b = a; a = hilf;
return;
}
Ergebnis: Beim Aufruf von vertausche in
main passiert effektiv nichts, da nur die Werte
der Funktionsparameter übergeben werden.
Call by reference
void vertausche(int *a, int *b)
{
int hilf = *b;
*b = *a; *a = hilf;
return;
}
In main:
int x = 1, y = -2;
vertausche(&x, &y);
Ergebnis: Die Werte von x und y werden
tatsächlich vertauscht!
Wie funktioniert Call by reference?
Beispiel: Eine Funktion fkt soll zwei Gleitkommazahlen als Ergebnis einer Berechnung
liefern (z. B. die Koordinaten eines berechneten Punktes in R2 ). Mit Hilfe von return
kann jedoch nur ein Wert zurückgegeben werden.
Idee: Es werden zwei double-Variablen erg1 und erg2 an die Funktion übergeben, in
welche die Ergebnisse gespeichert werden sollen.
Der Programmcode sieht wie folgt aus:
Deklaration von fkt als
void fkt(double *e1, double *e2);
Aufruf in main in der Form
fkt(&erg1, &erg2);
Definition von fkt enthält Zuweisungen
*e1 = ...; *e2 = ...;
Folgendes passiert bei der Ausführung:
Durch Anwendung des &-Operators entstehen zwei Zeiger auf double.
Diese beiden Zeiger werden als Parameter (in Übereinstimmung mit der
Deklaration) an fkt übergeben.
Die Funktion fkt „sieht“ zwar die beiden Variablen nicht, verfügt aber mit den
Zeigern über ihre Adressen.
Mit dem *-Operator wird die Referenz aufgelöst, und die Zuweisung von Werten
an die betreffenden Speicherbereiche wird möglich.
Fazit: Call by reference überwindet die Grenze des scope von Variablen.
Typische Pointer-Fehler III
Call by reference überwindet zwar die scope-Grenze, jedoch nicht die
lifetime-Grenze!
Beispiel:
int *neuer_zeiger(void)
{
int a;
return &a;
}
; Nach Beendigung der Funktion ist die
Lebenszeit der Variablen a vorbei.
; Der (gültige!) erzeugte Zeiger verweist
auf irgendeine Speicherstelle, die
längst anderweitig vergeben sein kann.
Call by value mit Pointern statt Call by reference
Beispiel:
void biege(int *a, int *b)
{
int *hilf = b;
b = a; a = hilf;
return;
}
; Die Speicherreferenzen werden
nicht aufgelöst. Stattdessen finden
(ausschließlich lokale!)
Zuweisungen von Adressen an
Zeigervariablen statt.
; Pointer, die als Parameter
übergeben werden, bleiben
unverändert. (Call by value!!)
Zeigerarithmetik
Erlaubte Operationen mit Zeigervariablen
Addition und Subtraktion von Integer-Werten.
Arithmetische Zuweisungen += und -= .
Inkrement ++ und Dekrement -- .
Differenzbildung zweier Zeiger.
Beispiel:
double x1=5.5, x2=7, *p1=&x1, *p2=&x2;
printf("&x1 = %p \n" ,&x1);
printf("&x2 = %p \n\n",&x2);
printf("&p1 = %p, p1 = %p \n*p1 = %lf\n",
&p1,p1,*p1);
printf("&p2 = %p, p2 = %p \n*p2 = %lf\n\n",
&p2,p2,*p2);
p2 = p1 + 2;
printf("&p2=%p, p2=%p \n*p2=%lf\n\n",
&p2,p2,*p2);
p2--;p2--;
printf("&p2=%p, p2=%p \n*p2=%lf\n",
&p2,p2,*p2);
Ausgabe:
&x1 = 0x28ac08
&x2 = 0x28ac00
&p1
*p1
&p2
*p2
=
=
=
=
0x28abfc p1 = 0x28ac08
5.500000
0x28abf8 p2 = 0x28ac00
7.000000
&p2 = 0x28abf8 p2 = 0x28ac18
*p2 = 1807675861650890483
&p2 = 0x28abf8 p2 = 0x28ac08
*p2 = 5.500000
Zeiger und Felder
Erinnerung: Der *-Operator lässt sich auf statische Felder anwenden und liefert das
erste Feldelement.
Frage: Wieso?
Antwort:
int main(void)
{
double x[] = {-3.14, 0.0, 4};
printf("Werte:\n");
printf(" *x = %lf\n", *x);
printf("x[0] = %lf\n", x[0]);
printf("\nAdressen:\n");
printf(" x = %p\n", x);
printf(" &x = %p\n", &x);
printf("&x[0] = %p\n", &x[0]);
}
return 0;
Werte:
*x = -3.140000
x[0] = -3.140000
Adressen:
x = 0x7fffc291a0b0
&x = 0x7fffc291a0b0
&x[0] = 0x7fffc291a0b0
Fazit: Der Feldname ist zugleich ein konstanter
Zeiger auf das erste Feldelement!
,→ Manipulationsversuche von x der Form
x=x+2; x++; usw.
scheitern.
Folgerung: Mit einem Zeiger auf den Datentyp der Feldelemente kann man ebenfalls
auf das Feld zugreifen!
Zeiger und Felder
Frage: Das erste Feldelement ist schon mal ein Anfang, aber wie erhält man die
restlichen mit Hilfe eines anderen Zeigers?
Antwort: Genau wie beim Original-Feld mit „[]“.
int main(void)
{
double x[] = {-3.14, 0.1, 4};
double *p = x; // oder &x
// oder &x[0]
printf("x[2] = %lf\n", x[2]);
printf("*(x+2) = %lf\n", *(x+2));
printf("\n*p++ = %lf\n",*p++);
// dauerhafte Veraenderung von p!
printf("*p = %lf\n", *p);
printf(" p[0] = %lf\n", p[0]);
printf(" p[1] = %lf\n", p[1]);
}
return 0;
x[2] = 4.000000
*(x+2) = 4.000000
*p++
*p =
p[0]
p[1]
= -3.140000
0.100000
= 0.100000
= 4.000000
Da die Elemente hintereinander im
Speicher angeordnet sind, wird „x[n]“
intern interpretiert als „springe um n
Speicherpositionen weiter und gib den
Wert an dieser Stelle zurück“.
In Pointer-Sprache ausgedrückt: *(x+n)
Zeigerarithmetik: Zeiger und Felder
Bemerkungen
Klammer bei *(x+2) ist nötig, da der Inhaltsoperator * eine höhere Priorität
als der Additionsoperator besitzt.
Inkrement- und Inhaltsoperator sind beide von der selben Prioritätsstufe.
Wichtig: der Inkrementoperator bezieht sich auf den Zeiger und nicht auf den
dereferenzierten Inhalt. Der Zeiger wird dauerhaft umgesetzt, d.h. er zeigt nun
auf eine andere Adresse im Speicher.
Merke
Sei A ein Feld von einem beliebigen Typ. Dann sind die jeweiligen Ausdrücke in den
folgenden Boxen äquivalent:
A[i]
,
*(A+i)
Der zweite Ausdruck ist jedoch schwerer lesbar und wird als
stilistisch schlecht angesehen.
&A[i]
A
,
,
&A
A+i
,
&A[0]
,
A+0
Zeigerarithmetik: Zeiger und Felder
Programmierung für Mathematiker
Prof. Dr. Thomas Schuster
M.Sc. Dipl.-Phys. Anne Wald
24.05.2017
Zeiger und Felder
Zur Erinnerung:
Der Name eines statischen Feldes ist zugleich ein Zeiger auf das erste
Feldelement.
Auf Zeiger kann der Index/Array-Operator [] angewandt werden, mit dem
gleichen Effekt wie bei statischen Feldern.
Statische Felder können anstelle von Zeigern auf den gleichen Typ an Funktionen
übergeben werden.
Beispiel: Der folgende Code ist semantisch korrekt und liefert wie erwartet die
Ausgabe „1. Element: 3.141500“.
void zeigeErstesElement(
double *array)
{
printf("1. Element: %lf\n",
array[0]);
return;
}
int main(void)
{
double x[2] = {3.1415, -1.0};
zeigeErstesElement(x);
return 0;
}
Namen statischer Felder können als konstante Zeiger nicht umgebogen werden.
Zuweisungen wie feld1 = feld2; schlagen fehl.
Call by reference: Beispiel
void vektor_ausgeben(double *vec,int n) // Funktion
{
int i;
printf("(");
for(i=0;i<n-1;i++)
printf(" %2.1lf ,",vec[i]);
//alternativ: *(vec+i)
printf(" %2.1lf )\n",vec[n-1]);
}
void vektor_erhoehen(double *vec,int n,int summand) // Funktion
{
int i;
for(i=0;i<n;i++)
vec[i] +=summand;
}
int main(void) // Hauptprogramm
{
double vec[6]={1,1,1,1.5,1.5,1.5};
vektor_ausgeben(vec,6);
vektor_erhoehen(vec,6,2);
vektor_ausgeben(vec,6);
}
Ausgabe: ( 1.0 , 1.0 , 1.0 , 1.5 , 1.5 , 1.5 )
( 3.0 , 3.0 , 3.0 , 3.5 , 3.5 , 3.5 )
Dynamische Speicherverwaltung
Problem: Mit den bisherigen Mitteln können nur Speicherobjekte angelegt werden,
deren Größe zur Compilezeit bekannt ist.
Beispiel: Einfacher Texteditor
Umsetzung: der aktuell bearbeitete Text wird zeilenweise in einem
zweidimensionalen Feld (z. B. char text[zeilen][spalten]) gespeichert.
Frage 1: Wie kann man im Vorfeld eine sinnvolle Zeilenbreite festlegen?
Antwort: Gar nicht! Es sollte berücksichtigt werden, wie viele Zeichen zum
Zeitpunkt der Ausführung auf dem Bildschirm in einer Zeile darstellbar sind.
Frage 2: Wie viele Zeilen sollte das Feld umfassen, damit
einerseits ein Arbeiten mit Texten „gängiger“ Länge möglich ist und
andererseits das Programm nicht unnötig viel Speicher verbraucht?
Antwort: Offenbar lassen sich nicht beide Bedingungen gleichzeitig erfüllen!
Frage 3: Ist es sinnvoll, diese Größe festzulegen, ohne zu berücksichtigen,
wie viel Speicher zur Verfügung steht,
wie lang der bearbeitete Text tatsächlich ist und
dass eventuell mehrere Texte gleichzeitig bearbeitet werden?
Offensichtliche Antwort: Nein!
Fazit: Mit diesen Speicherobjekten kommt man nicht besonders weit!
Dynamische Speicherverwaltung
Naheliegende Lösung: Das Programm soll zur Laufzeit entscheiden, wann wieviel
Speicher benötigt wird.
Programmiertechnische Umsetzung: mit Pointern!
Im Code wird explizit ein Speicherblock einer bestimmten Größe angefordert,
indem eine speziell dafür vorgesehene Funktion aufgerufen wird.
Die Funktion versucht, den Speicher zu reservieren („allokieren“) und liefert bei
Erfolg einen Zeiger auf die Startadresse des Speicherblocks, den sogenannten
Basis-Zeiger (engl. base pointer ).
Wird der Speicher nicht mehr benötigt, ruft man eine weitere Funktion auf, um
Block explizit wieder freizugeben.
Der Basis-Zeiger ist die einzige Referenz auf den allokierten Speicherblock
(solange man keinen anderen Pointer ebenfalls dorthin zeigen lässt). Man darf ihn
auf keinen Fall verlieren (umbiegen, Lebenszeit ablaufen lassen,. . . ), denn sonst
kann der Block nicht mehr freigegeben werden.
; „speicherfressende“ Programme
Funktionen zur Speicherverwaltung
Header: #include <stdlib.h>
Anforderung von Speicherplatz
void *malloc(size_t size);
(memory allocation)
Fordert einen (uninitialisierten!) Speicherblock der Größe size Byte an und gibt
einen Zeiger auf die Startadresse zurück. Bei Scheitern wir NULL zurückgegeben.
void *calloc(size_t anzahl, size_t size);
(cleared memory allocation)
Fordert einen Speicherblock für anzahl Elemente der Größe size an und
initialisiert mit Nullen. Ansonsten wie malloc.
Vergrößerung / Verkleinerung eines Speicherblocks
void *realloc(void *ptr, size_t size);
Ändert die Größe des Blocks zum Basis-Zeiger ptr auf size Byte. Der Inhalt des
alten Blocks bleibt unverändert, neu angeforderter Speicher bleibt uninitialisiert.
Rückgabewert ist der zum neuen Speicherbereich gehörige Base-Pointer, welcher
sich von ptr unterscheiden kann(!). Bei Scheitern wird nichts unternommen.
Freigabe eines Speicherblocks
void free(void *ptr);
Gibt den Speicher frei, auf den ptr zeigt.
Funktionen zur Speicherverwaltung
Beispiel
1
2
#include <stdio.h>
#include <stdlib.h>
3
4
5
6
7
int main(void)
{
int i;
double *dyn_feld;
8
dyn_feld = (double *) malloc(5 * sizeof(double));
// Cast!
// Platz fuer 5 double-Eintraege
9
10
11
for(i = 0; i < 5; i++)
// Initialisierung
dyn_feld[i] = 2.0 * i;
for(i = 0; i < 5; i++)
// Ausgabe
printf("dyn_feld[%d] = %lf\n", i, dyn_feld[i]);
12
13
14
15
16
dyn_feld = (double *) realloc(dyn_feld, 8 * sizeof(double));
// Vergroesserung auf 8 double-Eintraege
for(i = 5; i < 8; i++)
dyn_feld[i] = 2.0 * i;
// Initialisierung der neuen Eintraege
for(i = 0; i < 8; i++)
printf("dyn_feld[%d] = %lf\n", i, dyn_feld[i]);
17
18
19
20
21
22
23
free(dyn_feld); dyn_feld = NULL;
24
25
return 0;
26
27
}
// Vorsichtsmassnahme
Der sizeof-Operator
Der Speicherbedarf wird niemals direkt als „magische“ Zahl angegeben, sondern
immer mit Hilfe des sizeof-Operators berechnet.
Erinnerung:
sizeof(Datentyp)
oder
sizeof(Speicherobjekt)
liefert den Speicherbedarf eines allgemeinen Datentyps oder eines deklarierten
Speicherobjekts in Byte.
Achtung: Bei statischen Feldern liefert sizeof die tatsächliche Größe des Feldes, bei
dynamischen Feldern hingegen nur den Speicherbedarf der Pointervariable. Mit
sizeof lässt sich die Größe eines dynamischen Feldes nicht feststellen!
Bemerkung: Da Pointer Adressen speichern, ist sizeof(Datentyp *) unabhängig
vom Datentyp immer gleich sizeof(size_t).
Typische Fehler bei der Speicherverwaltung
Nicht mehr benötigter Speicher wird vergessen freizugeben.
double berechnung(double *vektor, int n)
{
double ergebnis,
*zw_speicher = (double *) malloc(n*n * sizeof(double));
// Hier wird etwas berechnet
return ergebnis;
}
Folge: Speicher bleibt bis zum Ende des Programms belegt, schlimmstenfalls wird
der gesamte Speicher aufgebraucht und das System geht in die Knie.
; Speicher so früh wie möglich und in jedem Fall wieder freigeben
Auf bereits freigegebenen Speicher wird zugegriffen.
int *arr = (int *) malloc(2 * sizeof(int));
free(arr);
arr[0] = -24; arr[1] = 42;
printf("ptr2[0] = %d\n", arr[0]);
printf("ptr2[1] = %d\n", arr[1]);
Folge: Möglicherweise stehen dort noch immer die gleichen Werte, doch dafür
gibt es keine Garantie. Vor einem solchen Zugriff wird nicht gewarnt.
; Basis-Pointer von freigegebenen Blöcken auf NULL setzen
Typische Fehler bei der Speicherverwaltung
Ein umgebogener Base-Pointer wird in free() verwendet.
int i, *arr = (int *) malloc(20 * sizeof(int));
for (i = 0; i < 20; i++)
*(arr++) = 5 * i;
free(arr);
Folge: Speicherzugriffsfehler
Es werden statische statt dynamischer Felder verwendet.
int feld[5], *ptr;
ptr = feld;
ptr[4] = 42;
// Alternativer Zugriff
// Code
free(ptr);
Folge: Speicherzugriffsfehler
Dynamische Speicherverwaltung
Speicherverwaltung während der Programmlaufzeit:
Speicherbereich
Verwendung
Code
Maschinencode des Programms
Daten-Segment
Statische und globale Variablen
Stack (dt. Stapel)
Funktionsaufrufe und lokale Variablen
Heap (dt. Halde)
Dynamisch reservierter Speicher
Variablentypen
Lokale, globale und statische Variablen
Name und Typ werden im Programmcode durch eine Deklaration festgelegt.
Variablen werden direkt über ihren Namen angesprochen.
Scope und Lifetime sind durch die statische Struktur des Programms festgelegt.
Speicherverwaltung erfolgt implizit:
Lokale Variable werden im Stack angelegt.
Globale und statische Variablen werden im Daten-Segment angelegt.
Dynamische Variablen
Erscheinen in keiner Deklaration, tragen keinen Namen.
Variablen werden über ihre Adressen mit einem Pointer angesprochen.
Scope und Lifetime folgen nicht den Regeln statischer Variablen.
Speicherverwaltung im Heap geschieht explizit im Programm.
Dynamische Implementierung von Vektoren
Vorüberlegung: Vektoren x ∈ Rn lassen sich offenbar als Felder double x[n]
implementieren. Um flexibler zu sein, bietet sich eine Implementierung als dynamisches
Feld double *x in Verbindung mit den Speicherverwaltungsfunktionen an.
Erzeugung eines neuen Vektors:
double *neuerVektor(int laenge)
{
double *v;
v = (double *) malloc(laenge * sizeof(double));
return v; // Bei Scheitern automatisch NULL
}
Erzeugung eines Nullvektors:
double *neuerNullvektor(int laenge)
{
double *v;
v = (double *) calloc(laenge, sizeof(double));
return v; // Bei Scheitern automatisch NULL
}
Dynamische Implementierung von Vektoren
Ausgabe am Bildschirm:
void zeigeVektor(double *vektor, int laenge)
{
int i;
for (i = 0; i < laenge; i++)
printf("%lf\n", vektor[i]);
return;
}
Löschung:
void loescheVektor(double *vektor)
{
free(vektor);
return;
}
Dynamische Implementierung von Vektoren
Kopie anlegen:
void kopiereVektor(double *ziel, double *quelle, int laenge)
{
int i;
for (i = 0; i < laenge; i++)
ziel[i] = quelle[i];
return;
}
Weitere Funktionen:
Arithmetische Operationen mit Vektoren (elementweise)
Skalierung, Addition einer Konstanten
(Betragsmäßig) maximales / minimales Element finden
Untervektoren: Block, jedes n-te Element
Permutationen
Minimales Verwendungsbeispiel
1
2
#include <stdio.h>
#include <stdlib.h>
3
4
5
6
7
8
double *neuerVektor(int laenge);
void zeigeVektor(double *vektor, int laenge);
void loescheVektor(double *vektor);
void kopiereVektor(double *ziel, double *quelle,
int laenge);
9
10
11
12
13
int main(void)
{
int i, N = 10;
double *v = neuerVektor(N), *w = neuerVektor(N);
14
15
16
17
18
19
20
21
22
if (v==NULL || w==NULL)
{
printf("Zu wenig freier Speicher!");
free(v);free(w);
return 1;
}
for (i = 0; i < N; i++)
v[i] = 2.0 * i;
23
24
25
26
27
28
29
30
kopiereVektor(w, v, N);
printf("w =\n");
zeigeVektor(w, N);
loescheVektor(v); loescheVektor(w);
return 0;
}
// Hier kommen noch die Funktionsdefinitionen
Ausgabe:
w =
0.000000
2.000000
4.000000
6.000000
8.000000
10.000000
12.000000
14.000000
16.000000
18.000000
Dynamische Implementierung von Matrizen
Vorüberlegungen:
Felder in C sind eindimensional, Matrizen jedoch (mindestens) zweidimensional.
Jede Zeile einer 2D-Matrix kann als 1D-Vektor aufgefasst werden.
Idee 1: Eine Matrix A ∈ Rm×n wird als Feld von Vektoren Ai ∈ Rn interpretiert.
A1
A2
A2,3
A3
A4
Idee 2: Die Zeilen einer Matrix A ∈ Rm×n werden aneinandergereiht, so dass ein
langer Vektor a ∈ Rm·n entsteht. Es gilt der Zusammenhang Ai,j = a(i−1)n+(j−1) .
A1
A2
A3
a11
a
A4
Dynamische Implementierung von Matrizen
Umsetzung in C – Variante 1: Beispiel A ∈ R4×8
double **
double *
A
A[0]
A[0][0]
A[1]
A[1][0]
A[2]
A[2][0]
A[3]
A[3][0]
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
A[0][7]
A[1][7]
A[2][7]
A[3][7]
Ein Vektor v ist ein (dynamisches) Feld vom Typ double, implementiert als
double *v.
Folglich ist eine Matrix A ein (dynamisches) Feld von Vektoren A[i] vom Typ
double *.
Technisch gesehen ist A ein Zeiger auf double *, und jedes A[i] ist ein Zeiger
auf double.
Deklaration:
double **A;
Zugriff auf Feldelemente: Ai,j =
ˆ A[i-1][j-1]
Dynamische Implementierung von Matrizen: Funktionen
Erzeugung einer Matrix:
1
2
3
4
double **neueMatrix(int n_zeilen, int n_spalten)
{
int i, j;
double **matrix;
5
matrix = (double **) malloc(n_zeilen * sizeof(double *));
if (matrix == NULL) // Speicheranforderung gescheitert
return NULL;
6
7
8
9
for (i = 0; i < n_zeilen; i++)
{
matrix[i] = (double *) malloc(n_spalten * sizeof(double));
if (matrix[i] == NULL)
{
for (j = 0; j < i; j++)
free(matrix[j]);
10
11
12
13
14
15
16
17
free(matrix);
return NULL;
18
19
}
20
}
21
22
return matrix;
23
24
}
Dynamische Implementierung von Matrizen: Funktionen
Bei der Speicheranforderung für die Matrix (double **) muss der
Größenmultiplikator sizeof(double *) sein, denn jeder Eintrag in matrix ist
schließlich ein double *!
matrix = (double **) malloc(N_zeilen * sizeof(double *));
Scheitert die Anforderung, wird NULL zurückgegeben.
if (matrix == NULL)
return NULL;
Ist die Speicheranforderung für eine Zeile A[i] erfolglos, so muss der bisher
reservierte Speicher - also sämtliche Zeilenvektoren A[j] (j < i) sowie matrix
selbst - wieder freigegeben werden, bevor die Funktion mit Rückgabewert NULL
endet.
if (matrix[i] == NULL)
{
for (j = 0; j < i; j++)
free(matrix[j]);
free(matrix);
return NULL;
}
Dynamische Implementierung von Matrizen: Funktionen
Ausgabe einer Matrix auf dem Bildschirm:
1
2
3
void zeigeMatrix(double **matrix, int n_zeilen, int n_spalten)
{
int i, j;
4
for (i = 0; i < n_zeilen; i++)
{
for (j = 0; j < n_spalten; j++)
printf("%.4lf ", matrix[i][j]);
5
6
7
8
9
printf("\n");
10
}
11
12
return;
13
14
}
Beachte:
Die Ausgabe auf der Kommandozeile funktioniert nur zeilenweise.
Nach dem Ende der Zeile den Newline-Character ’\n’ nicht vergessen!
Dynamische Implementierung von Matrizen: Funktionen
Löschung einer Matrix:
1
2
3
void loescheMatrix(double **matrix, int n_zeilen)
{
int i;
4
for (i = 0; i < n_zeilen; i++)
free(matrix[i]);
5
6
7
free(matrix);
8
9
}
Beachte:
Es genügt nicht, das übergeordnete Feld matrix zu befreien. Jedes Feld
matrix[i] muss einzeln an free() übergeben werden.
Sicherheitshalber sollte nach dem Aufruf loescheMatrix(A, n); noch die
Zuweisung A = NULL; vorgenommen werden, um zukünftige Zugriffe auf den
freigegebenen Bereich zu verhindern.
Anwendungsbeispiel
1
2
#include<stdio.h>
#include<stdlib.h>
3
4
5
6
double **neueMatrix(int n_zeilen, int n_spalten);
void zeigeMatrix(double** matrix, int n_zeilen, int n_spalten);
void loescheMatrix(double** matrix, int n_zeilen);
7
8
9
10
11
int main(void)
{
int i, j, n = 10;
double **A;
12
A = neueMatrix(n, n);
13
14
for (i = 0; i < n; i++)
{
for (j = 0; j < n; j++)
A[i][j] = i+0.1*j;
}
15
16
17
18
19
20
zeigeMatrix(A, n, n);
loescheMatrix(A, n);
21
22
23
return 0;
24
25
}
26
27
// Ab hier noch die Funktionsdefinitionen
Dynamische Implementierung von Matrizen
Umsetzung in C – Variante 2:
A1
A2
A3
A4
a11
a
Deklaration:
double *a;
Als Funktionen zum Erzeugen und Löschen einer Matrix können die
Vektor-Funktionen neuerVektor() und loescheVektor() verwendet bzw.
umbenannt werden.
Zum Zugriff auf das Element Ai,j verwendet man den Ausdruck
a[i * n_spalten + j] .
Umgekehrt lassen sich Zeilen- und Spaltenindex in a[index] berechnen mittels
i = index / n_spalten; j = index % n_spalten;
Vorteil gegenüber Doppelpointer-Variante: Vermeidung von Doppelschleifen →
schneller bei Operationen auf der ganzen Matrix und bei Multiplikation.
Viele numerische Programmbibliotheken (GSL, BLAS, NumPy,. . . ) verwenden
intern diese Darstellung.
Nachteil: Operationen basierend auf Zeilen- und Spaltenindex (z. B.
transponieren) sind wegen des zusätzlichen Berechnungsschritts aufwändiger.
Programmierung für Mathematiker
Prof. Dr. Thomas Schuster
M.Sc. Dipl.-Phys. Anne Wald
31.05.2017
Strukturen: Motivation
Situation: Mit Funktionen verfügen wir über ein wirksames Mittel, um Programmcode
sinnvoll zu gliedern und Komplexität zu verbergen. Für Daten haben wir bisher kein
solches Konstrukt!
Beispielproblem: Adressdatenbank
Eine Adresse (in Deutschland) besteht aus
Straße
Hausnummer
Stockwerk
Postleitzahl
Ort
Zeichenkette
positive Ganzzahl (Vorsicht: „21a“ nicht darstellbar!)
positive Ganzzahl (optional)
positive Ganzzahl
Zeichenkette
Umsetzung: jede Informationseinheit wird in einem entsprechenden Feld gespeichert,
und die Einträge mit gleichen Feldindices gehören zueinander.
char **strassen, unsigned *hausnummern, unsigned *stockwerke, unsigned
*postleitzahlen, char **orte
Nachteile:
Unlogische Gruppierung von Daten, die miteinander nichts zu tun haben
Zuordnung über den Feldindex ist fehleranfällig
Strukturen
Bessere Lösung: jede Adresse ist wie eine Variable ansprechbar, deren Elemente die
einzelnen Informationseinheiten sind. Dazu gibt es in C das Konzept der Strukturen.
Definition
Eine Struktur ist die Zusammenfassung einer bestimmten Anzahl von Daten
(möglicherweise) verschiedenen Typs zu einer Einheit, die mit einem festgelegten
Namen angesprochen werden kann.
Deklaration
eines Strukturtyps:
struct Etikett;
Teilt dem Compiler mit, dass es einen Strukturtyp mit einem bestimmten Etikett
(engl. structure tag) gibt. Muss außerhalb von main stehen.
einer Strukturvariablen:
struct Etikett Variablenname;
Teilt dem Compiler mit, dass eine Variable eines bestimmten Namens gibt, die
vom Typ struct Etikett ist.
Definition eines Strukturtyps:
struct Etikett {Codeblock};
Legt fest, welche Komponenten (engl. members) zu einer Struktur dieses Typs
gehören. Muss außerhalb von main stehen (meistens direkt nach oder sogar statt der
Deklaration).
Strukturen
Anwendung auf das Beispielproblem:
1
struct Adresse;
// Deklaration des Strukturtyps "Adresse"
struct Adresse
{
char *strasse;
unsigned hausnr;
int stock;
unsigned plz;
char *ort;
};
// Definition der Struktur
2
3
4
5
6
7
8
9
10
// 0 = "keine Angabe"
11
12
13
14
15
16
17
18
19
int main(void)
{
// Deklaration einer Variablen vom Typ "struct Adresse"
struct Adresse meine_adr;
printf("sizeof(struct Adresse) = %lu\n", sizeof(struct Adresse));
printf("alternativ: sizeof(meine_adr) = %lu\n", sizeof(meine_adr));
return 0;
}
Die Definition macht in diesem Beispiel die Deklaration überflüssig.
Anzahl und Namen der Komponenten der Struktur sind mit der Definition
festgelegt und im Nachhinein nicht mehr veränderbar.
Hinter der schließenden geschweiften Klammer muss ein Semikolon stehen!
„struct Adresse“ kann wie ein gewöhnlicher Datentyp behandelt werden.
Strukturen: Zugriff auf Komponenten
Die Komponenten einer Struktur werden mit ihrem Namen angesprochen, im
Gegensatz zum Zugriff über Indices bei Feldern. Das Pendant zum Index-Operator
„[]“ ist der Strukturkomponenten-Operator „.“.
Verwendung:
Strukturvariable.Komponentenname
Greift auf die Komponente des entsprechenden Namens einer zuvor deklarierten
Strukturvariable zu.
Der .-Operator hat wie [] höchste Priorität, insbesondere höher als *, &, ++,
Cast und arithmetische Operatoren.
Beispiel:
meine_adr.strasse = "Stuhlsatzenhausweg";
meine_adr.hausnr = 103;
meine_adr.stock = 0;
meine_adr.plz = 66123;
meine_adr.ort = "Saarbruecken";
printf("%s %u\n", meine_adr.strasse,
meine_adr.hausnr);
printf("%u %s\n", meine_adr.plz,
meine_adr.ort);
Ausgabe:
Stuhlsatzenhausweg 103
66123 Saarbruecken
Strukturen: Weitere Eigenschaften
Initialisierung bei Deklaration: wie bei statischen Feldern in geschweiften
Klammern
struct Adresse meine_adr = {"Stuhlsatzenhausweg", 103, 0, 66123,
"Saarbruecken"};
Strukturen können andere Strukturen enthalten (Schachtelung)
struct Anschrift
{
char *nachname;
char *vorname;
struct Adresse adresse;
};
Initialisierung:
struct Anschrift meine_anschrift = {"mit Biergarten",
"Restaurant",
{"Stuhlsatzenhausweg", 103, 0, 66123, "Saarbruecken"} };
Zugriff auf geschachtelte Strukturen: z. B.
meine_anschrift.adresse.hausnr
Achtung: Zu tiefe Schachtelung macht Code unlesbar!
Strukturen: Weitere Eigenschaften
Zeiger auf Strukturen:
struct Etikett *pointer
Deklariert einen Zeiger auf den Datentyp struct Etikett
Für den Zugriff (*pointer).Komponente auf Komponenten mit Hilfe des
Pointers gibt es die vereinfachte Schreibweise pointer->Komponente
Strukturen können einen Zeiger auf den eigenen Typ enthalten.Dazu müssen
jedoch Deklaration und Definition getrennt werden:
struct Anschrift;
struct Anschrift
{
char *nachname;
char *vorname;
struct Adresse adresse;
struct Anschrift *li_nachbar;
struct Anschrift *re_nachbar;
} meine_anschrift_;
Zugriff auf Nachbarelement: z. B.
Bemerkung: In diesem Programmcode
wird in Verbindung mit der Definition
der Struktur Anschrift eine globale
Instanz meine_anschrift_ deklariert.
meine_anschrift_.li_nachbar->hausnr
Anwendung: Verkettung von Daten, z. B. Listen oder Bäume
Strukturen: Weitere Eigenschaften
Zeiger auf Strukturen können an Funktionen als Parameter übergeben und als
Rückgabewert zurückgegeben werden.
Strukturen, die andere Strukturen oder dynamische Speicherobjekte enthalten,
sollten nicht mehr „von Hand“, sondern mit speziell dafür vorgesehenen
Funktionen (Erstellung, Manipulation von Komponenten, Kopie, Löschung, . . . )
verwaltet werden
; Robuste Programme, Anfänge der „Objektorientierten Programmierung“
Vorsicht Strukturen können nur komponentenweise verglichen werden. Der
Vergleich
struct Vektor u,v;
if (u==v)
printf("Vektoren stimmen überein");
führt zu der Fehlermeldung: invalid operands to binary ==
Strukturen: Weitere Eigenschaften
Vorsicht: Bei Zuweisung wird nur die oberste Ebene betrachtet.
1
2
3
4
5
6
struct Vektor
{
int dim;
char *label;
int *koord;
};
7
8
9
10
11
int main(void)
{
struct Vektor u = {2, "Ursprung", NULL}, e1;
u.koord = (int *) malloc(u.dim * sizeof(int));
12
e1 = u;
// Shallow copy
e1.label = "1. Einheitsvektor";
//umbiegen des Zeigers e1.label
e1.koord[0] = 1;
13
14
15
16
17
printf("%s u = (%d,%d), ", u.label, u.koord[0], u.koord[1]);
printf("%s e1 = (%d,%d)\n", e1.label, e1.koord[0], e1.koord[1]);
18
19
20
21
22
23
}
free(u.koord);
return 0;
Ausgabe: Ursprung u = (1,0), 1. Einheitsvektor e1 = (1,0)
; Es wurden sämtliche Variablenwerte (= Adressen bei Zeigern!) kopiert, nicht
jedoch das dynamische Feld! (Woher soll der Compiler auch davon wissen?)
Typendefinition
Mit Hilfe des Schlüsselworts typedef lassen sich in C eigene Datentypen definieren.
Syntax: typedef AlterDatentyp NeuerDatentyp;
Beispiel 1: primitive Datentypen
typedef unsigned long int size_t;
Anwendung: architekturunabhängige Programmierung
Beispiel 2: in Verbindung mit Strukturen
typedef struct Adresse Adresse;
Im folgenden Code kann statt „struct Adresse“ einfach „Adresse“ geschrieben
werden. Folge: erhöhte Lesbarkeit
Die Typendefinition lässt sich mit der Strukturdefinition verbinden:
typedef struct _Adresse_ Adresse;
struct _Adresse_
{
char *strasse;
unsigned hausnr;
int stock;
unsigned plz;
char *ort;
};
oder äquivalent:
typedef struct
{
char *strasse;
unsigned hausnr;
int stock;
unsigned plz;
char *ort;
} Adresse;
Beispiel: Erstellen/Kopieren eines Vektors
1
2
3
4
5
6
7
8
9
10
Vektor *neuerVektor(int dim, char *label, int *koord)
{ Vektor *vec=(Vektor *) malloc(sizeof(Vektor));int i;
if(vec==NULL)
printf("Zu wenig speicher");
vec->dim = dim;
vec->koord = (int *) malloc(dim*sizeof(int));
for (i=0;i<dim;i++)
vec->koord[i] = koord[i];
vec->label = dupliziereString(label);
return vec;}
11
12
13
14
15
16
17
18
19
20
char *dupliziereString(char *string)
{char *clone = NULL;
if (string)
{
if ( !(clone = (char *) malloc(sizeof(char)*(strlen(string)+1))))
printf("Zu wenig Speicher!");
strcpy(clone,string);
}
return clone;}
Ausschnitt aus dem Hauptprogramm:
int dimension=2;
koord=(int*) malloc(dimension*sizeof(int));
char ulabel[]="Ursprung";
struct Vektor *u=neuerVektor(dimension,ulabel,koord); //neuen Vektor erzeugen
struct Vektor *e1=neuerVektor(u->dim,u->label,u->koord); //Vektor kopieren
Beispiel: komplexe Zahlen
Deklarationen und Definitionen
1
2
3
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
4
5
6
7
8
9
typedef struct
{
double re;
double im;
} Complex;
10
11
12
13
Complex *newComplexPolar(double radius, double angle);
Complex *complexProduct(Complex *z1, Complex *z2);
//--------------------------------------------------------
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Complex *newComplexPolar(double radius, double winkel)
{
Complex *z = (Complex *) malloc(sizeof(Complex));
z->re = radius * cos(winkel);
z->im = radius * sin(winkel);
return z;
}
//-------------------------------------------------------Complex *complexProduct(Complex *z1, Complex *z2)
{
Complex *z = (Complex *) malloc(sizeof(Complex));
z->re = z1->re*z2->re - z1->im*z2->im;
z->im = z1->re*z2->im + z1->im*z2->re;
return z;
}
Beispiel: komplexe Zahlen
Hauptprogramm
1
2
3
4
5
int main(void)
{
Complex a = {1.0, 1.0};
Complex *b = newComplexPolar(2.0, 3*M_PI/4.0);
Complex *c = complexProduct(&a, b);
6
printf("a = %lf+%lfi\n", a.re, a.im);
printf("b = %lf+%lfi\n", b->re, b->im);
printf("a*b = %lf+%lfi\n", c->re, c->im);
c->re = 0;
c->im = 2.5;
b = complexProduct(&a, c);
printf("a*c = %lf+%lfi\n", b->re, b->im);
7
8
9
10
11
12
13
14
15
16
}
return 0;
Ausgabe:
a
b
a*b
a*c
= 1.000000+1.000000i
= -1.414214+1.414214i
= -2.828427+0.000000i
= -2.500000+2.500000i
Felder von Strukturen
1
2
3
4
5
int main(void)
{
int j, n = 10;
Complex *dyn_vek = (Complex *) calloc(n, sizeof(Complex));
Complex stat_vek[10];
6
for (j=0; j<n; j++)
{
dyn_vek[j].re = 2.0 * j;
dyn_vek[j].im = 0.5 * j;
stat_vek[j].re = 2.0 * j;
stat_vek[j].im = 0.5 * j;
}
printf(" dyn_vek[7] = %lf+%lfi\n", dyn_vek[7].re, dyn_vek[7].im);
printf("stat_vek[7] = %lf+%lfi\n", stat_vek[7].re, stat_vek[7].im);
7
8
9
10
11
12
13
14
15
16
17
18
19
}
free(dyn_vek);
return 0;
Ausgabe:
dyn_vek[7] =
stat_vek[7] =
14.000000+3.500000i
14.000000+3.500000i
; Statische und dynamische Felder von Strukturen können wie gewohnt verwaltet
werden.
Allgemeine Merkregeln für Strukturen
Strukturen dienen dazu, Einzelinformationen zu einer sinnvollen Einheit
zusammenzufassen.
Häufig sind Strukturen Abbilder von Konzepten aus dem Alltag oder von
mathematischen Objekten (siehe Beispiele zu Adressen und komplexen Zahlen).
Geschachtelte Strukturen sind möglich und oft sinnvoll, bergen aber die Gefahr,
zu große Komplexität zu erzeugen und das Ziel größerer Ordnung zu verfehlen.
Zudem steigt das Risiko von Programmierfehlern (s. shallow-copy -Problem).
Strukturen, die andere Strukturen oder dynamische Speicherobjekte enthalten,
sollten nicht mehr „von Hand“, sondern mit speziell dafür vorgesehenen
Funktionen verwaltet werden (Erstellung, Manipulation von Komponenten, Kopie,
Löschung, . . . ) ; Anfänge der „Objektorientierten Programmierung“
Zur Erhöhung der Lesbarkeit sollten Strukturen immer mit einem typedef als
neuer Datentyp definiert werden.
Zur Namensgebung hat sich folgende Konvention durchgesetzt:
ErsterBuchstabeGrossOhneUnterstriche für eigene typedef-Datentypen
_VorneUndHintenMitUnterstrichen_ für Strukturen, die nicht direkt, sondern
nur in Verbindung mit typedef verwended werden.
Beispiel:
typedef struct _Adresse_ Adresse;
Verwendung nur über Adresse, niemals über struct _Adresse_
Funktionszeiger: Vorüberlegungen
Funktionsaufrufe sind bis jetzt im Code mit Name explizit angegeben
(„hartcodiert“)
Folge: Zur Compilierzeit muss bekannt sein, welche Funktion eine bestimmte
Aufgabe erfüllen soll.
Beispielszenario: Wir haben einen Sortieralgorithmus geschrieben, der ein Feld
von Zahlen sortiert. Dabei wollen wir flexibel bestimmen können, nach welchem
Vergleichskriterium sortiert werden soll (größer, kleiner, 5. Ziffer in
Dezimaldarstellung größer, Quersumme kleiner, . . . ).
Mit den bisherigen Mitteln müsste im Algorithmus nach Vergleichskriterien
unterschieden werden, z. B. mittels if-else oder switch.
Alternativ müsste für jedes Vergleichskriterium eine separate Funktion
geschrieben werden.
Einleuchtendere Herangehensweise:
Es gibt einen allgemeinen Sortieralgorithmus, der das Feld nach einer
bestimmten Methode durchsucht und beim Vergleich zweier Elemente
irgendein (variables!) Vergleichskriterium heranzieht.
Zum Vergleich gibt es eine Reihe von Vergleichsfunktionen, die getrennt
vom Sortierverfahren deklariert und definiert sind.
Beim Aufruf des Sortieralgorithmus wird eine der Vergleichsfunktionen als
Parameter mit angegeben.
Genau dieses Verhalten lässt sich mit Funktionszeigern erzeugen!
Funktionszeiger:Deklaration
Datentyp (*FktZeigerName)(Parameter(typ)liste);
Deklariert einen Zeiger auf eine Funktion, welche die Signatur
Datentyp Funktion(Parameter(typ)liste)
besitzt.
Beispiele:
void (*InitFkt)(double *, double);
Zeiger auf eine Funktion, die einen double *- und einen double-Parameter
nimmt und nichts (void) zurückgibt.
double (*ZufGen)(void)
Zeiger auf eine Funktion, die keine Parameter nimmt und einen double-Wert
zurückgibt.
double *(*NeuesFeld)(unsigned)
Zeiger auf eine Funktion, die einen unsigned-Parameter nimmt und einen Pointer
auf double zurückgibt.
Funktionszeiger: Beispiel
Deklaration und Definition von zwei Anzeigevarianten
1
#include <stdio.h>
2
3
4
void anzeigeVariante1(char *text);
void anzeigeVariante2(char *text);
5
6
//--------------------------------------------------
7
8
9
10
11
12
void anzeigeVariante1(char* text)
{
printf("\n %s\n", text);
return;
}
13
14
//--------------------------------------------------
15
16
17
18
19
20
21
22
void anzeigeVariante2(char* text)
{
printf("\n **********************************");
printf("\n * %-30s *\n", text);
printf(" **********************************\n");
return;
}
Funktionszeiger: Beispiel
Hauptprogramm
1
2
3
int main(void)
{
void (*AnzFkt)(char *);
4
AnzFkt = anzeigeVariante1;
AnzFkt("Test Variante 1");
5
6
7
AnzFkt = anzeigeVariante2;
AnzFkt("Test Variante 2");
8
9
10
11
12
}
return 0;
Ausgabe:
Test Variante 1
**********************************
* Test Variante 2
*
**********************************
; Vor dem Funktionsnamen in der Zuweisung steht kein &-Operator. Wie bei
statischen Feldern hätte er keine Auswirkung.
; Beim Aufruf der Funktion via Pointer wird kein *-Operator benötigt. Die
Schreibweise (*AnzFkt)(...) wäre äquivalent, nicht jedoch *AnzFkt(...)!
Funktionszeiger: Bemerkungen
Bei Zuweisungen mit Funktionspointern ist unbedingt darauf zu achten, dass die
Signaturen von Pointer und Funktion übereinstimmen. Alles andere führt zu
unkontrollierbarem Programmverhalten! Vor Fehlern dieser Art warnt der
Compiler bestenfalls.
Deklarationen von Zeigern auf Funktionen mit langer Parameterliste werden leicht
unleserlich (vor allem bei Funktionen mit Funktionszeigern als Parameter):
void funktion(int a, double b,
double *(*f)(double, int, int, double *, double *));
int main(void)
{
double *(*fp)(double, int, int, double *, double *);
fp = testfkt; // testfkt sei passend deklariert
funktion(42, 3.14, fp);
return 0;
}
Ein typedef schafft Abhilfe:
typedef double *(*MeineFkt)(double, int, int, double *, double *);
void funktion(int a, double b, MeineFkt f);
Deklaration der Funktionspointer-Variable in main: MeineFkt fp;
Rangfolge von Operatoren (Überblick)
Priorität
Priorität 1
Operator
()
[]
. , ->
Priorität 2
!
++ , −−
−,+
Assoziativität
linksassoziativ
Array/Index-Operator
Member-Zugriff
Logische Negation
Unäres Plus, unäres Minus
Adress-Operator
*
Dereferenzierung
*,/
%
rechtsassoziativ
Inkrement, Dekrement
&
(type)
Priorität 3
Bedeutung
Funktionenaufruf
Cast
Multiplikation, Division
linksassoziativ
Modulo
Priorität 4
+,−
Priorität 6
<, <=, >, >=
Priorität 7
== , ! =
gleich, ungleich
linksassoziativ
Priorität 11
&&
logisches UND
linksassoziativ
Priorität 12
||
logisches ODER
linksassoziativ
Plus, Minus
linksassoziativ
kleiner, ...
linksassoziativ
Merkregeln und lesen von Deklarationen
Merke:
Rechtsassoziativ sind lediglich: Zuweisungsoperatoren, Bedingungsoperator (? :)
und unäre Operatoren.
Sinnvolle Klammerungen können die Lesbarkeit von Code deutlich erhöhen!
int * (*Funkzeiger)();
↑ ↑ ↑
↑
4
2
3
1
Resultat:
Funkzeiger ist ein Zeiger (1) auf eine Funktion (2) mit leerer Parameterliste, die einen
Zeiger (3) auf Integer (4) zurückgibt.
Publikumsfrage
Was deklarieren die folgenden Statements?
double *f(double *, int);
; Funktion, die als Parameter einen double * und einen int nimmt und einen
double * zurückgibt.
double (*f)(double *, int);
; Zeiger auf eine Funktion, die als Parameter einen double * und einen int nimmt
und einen double zurückgibt.
double *(*f)(double *, int);
; Zeiger auf eine Funktion, die als Parameter einen double * und einen int nimmt
und einen double * zurückgibt.
double *g[20];
; g ist Feld mit 20 Einträgen vom Typ double *.
Funktionszeiger: Beispiel Sortierverfahren
In stdlib.h ist die folgende Sortierfunktion deklariert („quicksort“):
void qsort(void *base, size_t nmemb, size_t size,
int(*compar)(const void *, const void *));
Parameter:
base Zu sortierendes Feld eines (noch) nicht festgelegten Datentyps
nmemb Anzahl der Feldelemente
size Größe eines Feldelements in Byte
compar Zeiger auf eine (Vergleichs-)Funktion, die zwei void *-Zeiger auf zu
vergleichende Elemente als Parameter nimmt und einen int
zurückgibt.
Interpretation: compar repräsentiert eine mathematische
Ordnungsrelation „“ auf einer Menge M, d. h. für zwei beliebige
Werte a, b ∈ M gilt entweder a b, b a oder beides. Die zu
vergleichenden Elemente von base stammen aus M.
Der Rückgabewert von compar ist -1 (a b), 0 (Gleichheit) oder +1
(b a), wobei a dem ersten und b dem zweiten Parameter von
compar entspricht.
Funktionszeiger: Beispiel Sortierverfahren
Anwendung:
1
2
#include <stdio.h>
#include <stdlib.h>
3
4
int unsign_qsumme_kleiner(const void *pa, const void *pb);
5
6
7
8
9
int main(void)
{
unsigned z[5] = {23, 511, 10100, 8, 333};
qsort(z, 5, sizeof(unsigned), unsign_qsumme_kleiner);
10
11
12
13
14
}
printf("Sortiertes Feld:\n");
printf("%d %d %d %d %d\n", z[0], z[1], z[2], z[3], z[4]);
return 0;
15
16
17
18
19
int unsign_qsumme_kleiner(const void *pa, const void *pb)
{
unsigned a = *((unsigned *) pa), b = *((unsigned *) pb);
unsigned qs_a = a % 10, qs_b = b % 10;
20
while (a /= 10)
qs_a += a % 10;
21
22
23
while (b /= 10)
qs_b += b % 10;
24
25
26
27
28
}
return (qs_a < qs_b)? -1 : (qs_b < qs_a)? +1 : 0;
Funktionszeiger: Beispiel Sortierverfahren – Codeanalyse
Deklaration und Definition der Sortierfunktion
unsign_qsumme_kleiner soll die Quersumme von unsigned-Werten vergleichen.
Die Signatur muss int fkt(const void *, const void *) sein.
Im Funktionsrumpf werden die Parameter pa und pb zunächst als unsigned *
gecastet, anschließend dereferenziert und die Werte in unsigned-Variablen a
und b geschrieben. Exemplarisch für pa:
unsigned a = *( (unsigned *) pa );
In den Schleifen werden die Quersummen von a und b berechnet und in qs_a
bzw. qs_b gespeichert.
Beachte: while (a /= 10) führt zuerst Ganzzahl-Division durch und prüft
anschließend, ob das Ergebnis ungleich 0 ist (in diesem Fall besteht a noch nicht
aus einer einzigen Dezimalziffer).
Die return-Zeile verwendet die verkürzte Fallunterscheidung
(Bedingung)? Wert_Falls_Ja : Wert_Falls_Nein;
Beispiel:
absx = (x > 0)? x : -x;
speichert genau wie
if (x > 0) absx = x; else absx = -x;
den Betrag von x in absx.
Funktionszeiger: Beispiel Sortierverfahren – Codeanalyse
Hauptprogramm
Das Feld z mit 5 unsigned-Einträgen soll aufsteigend bzgl. der Quersumme
sortiert werden.
Dazu wird qsort mit den Parametern z (base), 5 (nmemb), sizeof(unsigned)
(size) und unsign_qsumme_kleiner (compar) aufgerufen:
qsort(z, 5, sizeof(unsigned), unsign_qsumme_kleiner);
Ausgabe:
Sortiertes Feld:
10100 23 511 8 333
Fazit: Wie qsort genau funktioniert, ist hier völlig unerheblich. Entscheidend ist,
dass die Funktion ein Feld anhand einer gegebenen Vergleichsroutine sortiert.
Kommandozeilenargumente
Bisher:
$ gcc -o ProgName C_Code.c
$ ./ProgName
.
.
.
Neu: Komandozeilenargumente
Die main-Funktion lässt sich auch mit zwei Parametern aufrufen.
Vollständige Deklaration von main:
int main(int argc, char *argv[])
argc argument counter: Anzahl der beim Programmaufruf übergebenen Argumente,
einschließlich des Programmnamens. Erst wenn argc > 1 ist, werden tatsächlich
Parameter übergeben.
argv argument vector: Feld von Strings
Bemerkung: Die Namen argc und argv (auch üblich: args) sind Konvention und
nicht zwingend festgelegt. Möglich (aber wenig sinnvoll) wäre auch
int main(bla, blubb)
Kommandozeilenargumente
Beispiel: argumente.c
1
#include <stdio.h>
2
3
4
5
int main(int argc, char *argv[])
{
int i;
6
printf("Anzahl der Argumente: %d\n\n", argc);
7
8
for (i=0; i<=argc; i++)
printf("Argument %d: %s\n", i, argv[i]);
9
10
11
12
13
}
return 0;
Ausgabe: Der Aufruf ./argumente arg1 arg2 ––help +~ 125.777 -99 liefert
Anzahl der Argumente: 7
Argument
Argument
Argument
Argument
0:
1:
2:
3:
./argumente
arg1
arg2
--help
Argument
Argument
Argument
Argument
4:
5:
6:
7:
+~
125.777
-99
(null)
Kommandozeilenargumente
Beachte:
Parameter werden durch Leerzeichen getrennt. Ausdrücke wie -o Ausgabe
werden als zwei separate Argumente aufgefasst.
Jedes Argument wird als Zeichenkette in argv gespeichert. Die Reihenfolge der
Strings in argv entspricht dabei der Reihenfolge auf der Kommandozeile.
Werden Zahlen als Argumente übergeben, müssen diese mit Hilfe der
entsprechenden Umwandlungsfunktionen (z. B. atoi oder atof) in ein
Zahlenformat konvertiert werden.
Mit String-Vergleichsfunktionen (z. B. strncmp) lässt sich beispielsweise prüfen,
ob ein Programm mit einem bestimmten Optionsparameter aufgerufen wurde.
Da jedoch die Reihenfolge der Optionsargumente festgelegt ist, benötigen
Programme mit vielen Optionen einen flexibleren Ansatz zur Auswertung der
Kommandozeile (→ Parser).
Kommandozeilenargumente: Beispiel
1
2
#include <stdio.h>
#include <stdlib.h>
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main(int argc, char *argv[]){
double x, y, z;
if (argc < 4){
// Es fehlen Argumente
printf("\nKorrekter Aufruf: ");
printf("%s zahl1 op zahl2\n", argv[0]);
return 1;
}
x = atof(argv[1]);
y = atof(argv[3]);
switch (argv[2][0]) {
case '+':
z = x + y; break;
case '-':
z = x - y; break;
case 'x':
case '*':
z = x * y; break;
case '/':
z = x / y; break;
default:
printf("\nFalscher Operator! ABBRUCH!\n");
return -1;
}
27
28
29
30
}
printf("\n%s %s %s = %lf", argv[1], argv[2], argv[3], z);
return 0;
Kommandozeilenargumente
$ gcc -o berechne taschenrechner.c
$ ./berechne 2 + 5
2 + 5 = 7.000000
$ ./berechne 2 x 5
2 x 5 = 10.000000
$ ./berechne 2 / 5
2 / 5 = 0.400000
$ ./berechne 2 /
Korrekter Aufruf: ./berechne zahl1 op zahl2
Programmierung für Mathematiker
Prof. Dr. Thomas Schuster
M.Sc. Dipl.-Phys. Anne Wald
07.06.2017
Fortgeschrittene Ein- und Ausgabe
Bisher: Ein- und Ausgabe nur über die Kommandozeile
Erweiterung: Konzept des Datenstroms (engl. data stream)
Bezeichnet allgemein eine Folge von Datensätzen gleichen Typs, deren Ende nicht
im Voraus bekannt ist.
Lässt sich nicht als Ganzes, sondern nur sequentiell verarbeiten.
Kann „vor- und zurückgespult“ werden.
Ein spezieller „End-of-file“-Indikator dient zur Markierung des Endes.
In C wird auf Datenströme mit Variablen vom Typ FILE * zugegriffen. FILE ist
eine Struktur und enthält u.a. einen Zeiger auf die aktuell zu verarbeitende
Position im Datenstrom.
Standard-Datenströme
#include <stdio.h>
stdin (standard input): Einlesen von Daten über die Tastatur
stdout (standard output): (gepufferte) Ausgabe von Daten auf der Kommandozeile
stderr (standard error): Ausgabe auf der Kommandozeile zur Fehlerbehandlung
Dateiströme: Zeiger vom Typ FILE * können mit Hilfe spezieller Funktionen mit
Dateien verbunden werden.
Fortgeschrittene Ein- und Ausgabe: Funktionen
Öffnen einer Datei
FILE *fopen(const char *path, const char *mode);
Erzeugt einen Datenstrom aus der Datei, die unter dem Pfad path liegt.
Rückgabewert ist ein FILE *, der entweder an den Anfang oder das Ende des
neuen Dateistroms zeigt. Bei Scheitern - z. B. Öffnen einer nicht vorhandenen
Datei oder fehlende Dateirechte - liefert die Funktion NULL zurück.
Der String path gibt den Pfad zur Datei im Dateisystem an, z. B.
“../Daten/werte.dat” (relativer Pfad) oder “/usr/include/stdio.h”
(absoluter Pfad).
Der Modus mode gibt an, in welche Richtung der Strom fließen kann. Mögliche
Modi sind “r”, “w”, “a”, “r+”, “w+” und “a+”:
Modus
“r”
“w”
“a”
“r+”
“w+”
“a+”
Bedeutung
read only
write only
append only
read/write
read/write
append/write
Zeigerposition
Dateianfang
Dateianfang
Dateiende
Dateianfang
Dateianfang
Dateiende
Dateiinhalt
unverändert
wird gelöscht
unverändert
unverändert∗
wird gelöscht
unverändert
∗ Bezieht sich nur auf den Zeitpunkt des Öffnens
Beispiel:
FILE *strom = fopen(“Dokument.txt”, “r”);
Datei existiert
Rückgabewert
Neue Datei
Rückgabewert
Rückgabewert
Neue Datei
Neue Datei
nicht
NULL
NULL
NULL
Fortgeschrittene Ein- und Ausgabe: Funktionen
Schließen eines Datenstroms
int *fclose(FILE *stream);
Schließt den Datenstrom stream und gibt 0 zurück bei Erfolg, andernfalls die
Konstante EOF (End Of File).
Wichtig: Zu jedem fopen() gehört ein fclose()!
Position herausfinden
long ftell(FILE *stream);
Gibt die aktuelle Position des Zeigers in stream als Adressabstand vom Anfang des
Stromes (offset) in Byte an.
Vor- und Zurückspulen
int fseek(FILE *stream, long offset, int whence);
Versetzt die Position des Zeigers in stream um offset Byte. Je nach Wert von
whence wird der Versatz relativ zum Anfang (whence = SEEK_SET), zum Ende
(whence = SEEK_END) oder zur aktuellen Position (whence = SEEK_CUR) gerechnet.
Der Rückgabewert ist 0 bei Erfolg, andernfalls ungleich 0.
void rewind(FILE *stream);
Spult den Strom stream an den Anfang zurück. Der Aufruf rewind(stream); ist
äquivalent zu fseek(stream, 0, SEEK_SET); (bis auf interne Feinheiten).
Fortgeschrittene Ein- und Ausgabe: Funktionen
Aus einem Datenstrom lesen (Text)
int fscanf(FILE *stream, const char *format, ...);
Genau wie scanf, wobei statt aus stdin aus dem Datenstrom stream gelesen wird.
(Deshalb ist scanf(...) äquivalent zu fscanf(stdin, ...).) Rückgabewert ist die
Anzahl der gelesenen Zeichen.
int fgetc(FILE *stream);
Liest ein Zeichen aus stream, das als int gecastet zurückgegeben wird. Im Fehlerfall
oder bei Dateiende ist der Rückgabewert EOF.
char *fgets(char *s, int n, FILE *stream);
Liest aus stream und schreibt das Ergebnis in den Puffer, auf den s zeigt. Das Lesen
wird abgebrochen, wenn entweder das Zeilenende \n oder das Dateiende erreicht ist
oder n − 1 Zeichen gelesen wurden. Im Fehlerfall oder bei Dateiende ist der
Rückgabewert NULL. Beachte: Nach dem Aufruf einer dieser Funktionen zeigt
stream auf die erste Position im Anschluss an den bearbeiteten Bereich.
Fortgeschrittene Ein- und Ausgabe: Funktionen
In einen Datenstrom schreiben (Text)
int fprintf(FILE *stream, const char *format, ...);
Arbeitet wie printf, wobei in den Datenstrom stream geschrieben wird. (Deshalb ist
printf(...) äquivalent zu fprintf(stdout, ...).) Rückgabewert ist die Anzahl der
geschriebenen Zeichen.
int fputc(int c, FILE *stream);
Schreibt das als unsigned char gecastete Zeichen c in den Strom stream. Das
Zeichen wird als int zurückgegeben, im Fehlerfall EOF.
int *fputs(const char *s, FILE *stream);
Schreibt den String, auf den s zeigt, ohne das Nullzeichen \0 in den Strom stream.
Im Fehlerfall wird EOF zurückgegeben.
Beachte: Nach dem Aufruf einer dieser Funktionen zeigt stream auf die erste Position
im Anschluss an den bearbeiteten Bereich.
Fortgeschrittene Ein- und Ausgabe: Beispiel
1
#include <stdio.h>
2
3
4
5
int main(void)
{
FILE *datenstrom;
6
datenstrom = fopen("Testdatei.txt", "r");
if (datenstrom == NULL)
{
fprintf(stderr, "Fehler: Testdatei.txt konnte nicht geoeffnet werden!\n");
return -1;
}
7
8
9
10
11
12
13
fseek(datenstrom, 0, SEEK_END); // Ans Ende spulen
fprintf(stdout, "Offset am Ende: %lu\n", ftell(datenstrom));
14
15
16
fseek(datenstrom, -5, SEEK_CUR); // 5 Bytes zurueck
fprintf(stdout, "
nach Zurueckspulen: %lu\n", ftell(datenstrom));
17
18
19
rewind(datenstrom);
fprintf(stdout, "
20
21
22
fclose(datenstrom);
23
24
25
26
}
return 0;
// Zurueckspulen
am Anfang: %lu\n", ftell(datenstrom));
Fortgeschrittene Ein- und Ausgabe: Beispiel
Datei Testdatei.txt:
Hallo Welt!
Die Datei enthält genau eine Zeile „Hallo Welt!“ (11 Characters = 11 Byte).
Ausgabe des Programms:
Offset am Ende: 11
nach Zurueckspulen: 6
am Anfang: 0
Beispiel: Matrix aus Datei einlesen (Funktion)
1
2
3
4
double **neueMatrixAusDatei(FILE *strom, int *n_zeilen, int *n_spalten)
{
int i, j;
double **matrix;
5
while (fgetc(strom) == '#') // Kommentarzeichen->Zeile ueberspringen
while (fgetc(strom) != '\n');
fseek(strom, -1, SEEK_CUR); // Eine Position zurueck
6
7
8
9
fscanf(strom, " n = %d, m = %d\n", n_zeilen, n_spalten);
matrix = neueMatrix(*n_zeilen, *n_spalten);
10
11
12
13
14
15
16
17
18
19
}
for (i=0; i<*n_zeilen; i++)
{
for (j=0; j<*n_spalten; j++)
fscanf(strom, "%lf", &matrix[i][j]);
}
return matrix;
Jeder Aufruf von fgetc erhöht die Position von strom um 1. Deshalb zeigt strom
nach der (leeren) Schleife in Zeile 7 auf den Anfang der nächsten Zeile.
Insgesamt überspringen die Zeilen 6 und 7 Dateizeilen, die mit ’#’ beginnen.
Zeile 10 erwartet einen Text wie n = 6, m = 1. Die gelesenen Zahlen werden in
n_zeilen und n_spalten (beides Zeiger auf int-Variablen) gespeichert.
Für fscanf gilt: Leerzeichen und Zeilenumbrüche innerhalb der Platzhalter
werden automatisch übersprungen. Ein Leerzeichen im Formatstring steht für
eine beliebige Anzahl (auch 0) von tatsächlich zu lesenden Leerzeichen.
Beispiel: Matrix aus Datei einlesen (Hauptprogramm)
1
2
#include <stdio.h>
#include <stdlib.h>
3
4
5
6
void zeigeMatrix(double **matrix, int n_zeilen, int n_spalten);
void loescheMatrix(double **matrix, int n_zeilen);
double **neueMatrixAusDatei(FILE *strom, int *n_zeilen, int *n_spalten);
7
8
9
10
11
12
13
int main(void)
{ int n, m;
FILE *fp;
if ( (fp = fopen("Matrix.dat", "r")) == NULL )
return 1;
double **H = neueMatrixAusDatei(fp, &n, &m);
14
15
16
17
18
19
}
zeigeMatrix(H, n, m);
loescheMatrix(H, n);
fclose(fp);
return 0;
20
21
// Funktionsdefinitionen ...
Ausgabe:
1.0000
0.5000
0.3333
0.2500
0.5000
0.3333
0.2500
0.2000
0.3333
0.2500
0.2000
0.1667
0.2500
0.2000
0.1667
0.1429
0.2000
0.1667
0.1429
0.1250
0.1667
0.1429
0.1250
0.1111
0.1429
0.1250
0.1111
0.1000
Beispiel: Matrix aus Datei einlesen (Matrix.dat)
Version 1
n=4, m=7
1.0
0.5
0.333333
0.25
0.500000
0.333333
0.25
0.2
0.333333
0.25
0.2
0.166667
0.250000
0.2
0.166667
0.142857
0.2
0.166667
0.142857
0.125
0.166667
0.142857
0.125
0.111111
0.142857
0.125
0.111111
0.1
Version 2
# Das ist die Hilbertmatrix H_ij = 1/(i+j-1)
#
# Hoffentlich werden diese Zeilen uebersprungen...
n = 4, m = 7
1.0
0.5
0.333333
0.25
0.500000
0.333333
0.25
0.2
0.333333
0.25
0.2
0.166667
0.250000
0.2
0.166667
0.142857
0.2
0.166667
0.142857
0.125
; Beide Versionen werden korrekt eingelesen!
0.166667
0.142857
0.125
0.111111
0.142857
0.125
0.111111
0.1
Beispiel: Matrix in Datei schreiben (Funktion)
1
2
3
4
5
6
void schreibeMatrixInDatei(FILE *strom, double **matrix,
int n_zeilen, int n_spalten)
{
int i, j;
fprintf(strom, "# Automatisch generiert mit schreibeMatrixInDatei\n\n");
fprintf(strom, "n = %d, m = %d\n\n", n_zeilen, n_spalten);
7
for (i=0; i<n_zeilen; i++)
{
for (j=0; j<n_spalten; j++)
fprintf(strom, "%.6lf ", matrix[i][j]);
8
9
10
11
12
13
}
14
fprintf(strom, "\n");
15
16
17
}
return;
Beispiel: Matrix in Datei schreiben (Hauptprogramm)
1
2
#include <stdio.h>
#include <stdlib.h>
3
4
5
6
7
void loescheMatrix(double **matrix, int n_zeilen);
double **neueMatrixAusDatei(FILE *strom, int *n_zeilen, int *n_spalten);
void schreibeMatrixInDatei(FILE *strom, double **matrix,
int n_zeilen, int n_spalten);
8
9
10
11
12
13
int main(void)
{
int n, m;
FILE *fp;
double **H;
14
fp = fopen("Matrix.dat", "r");
H = neueMatrixAusDatei(fp, &n, &m);
fclose(fp);
15
16
17
18
fp = fopen("Matrix_generiert.dat", "w");
schreibeMatrixInDatei(fp, H, n, m);
fclose(fp);
19
20
21
22
loescheMatrix(H, n);
23
24
25
26
}
return 0;
Beispiel: Matrix in Datei schreiben
Inhalt von Matrix_generiert.dat:
# Automatisch generiert mit schreibeMatrixInDatei
n = 4, m = 7
1.000000
0.500000
0.333333
0.250000
0.500000
0.333333
0.250000
0.200000
0.333333
0.250000
0.200000
0.166667
0.250000
0.200000
0.166667
0.142857
0.200000
0.166667
0.142857
0.125000
0.166667
0.142857
0.125000
0.111111
0.142857
0.125000
0.111111
0.100000
Speicherung als Text oder binär: Vergleich
Dateigröße Binär: Produkt aus Anzahl der Elemente und Größe eines Elements
in Byte.
Text: Variiert mit der textuellen Länge der gespeicherten Elemente.
Meistens größer als im Binärformat.
Präzision Binär: Entspricht der Genauigkeit des Datentyps.
Text: Der Genauigkeit sind prinzipiell keine Grenzen gesetzt, jedoch
kann der Speicherbedarf dadurch sehr groß werden.
Beispiel: Um die größtmögliche float-Zahl (3.4 · 1038 ) als Text zu
speichern, benötigt man 38 Zeichen =
ˆ 38 Byte, im Binärformat nur
sizeof(float) = 4 Byte!
Metadaten Binär: Üblicherweise ist die Datei unterteilt in einen Header und die
Daten. Es muss genau beschrieben werden, welche Bytes wofür
stehen.
Beispiel Matrix: Bytes 1-4 – unsigned – Länge des Headers in Byte,
5-8 – unsigned – Anzahl der Zeilen, 9-12 – unsigned – Anzahl der
Spalten, 13-Ende – double – Matrixelemente.
Text: Informationen über die Daten können in die Datei geschrieben
werden.
Vor- und Nachteile der binären Speicherung
Vorteile:
Allein der Datentyp bestimmt die Größe des benötigten Speicherplatzes.
Es wird meistens deutlich weniger Speicherplatz als für die Textvariante benötigt.
Maschinennahe Ein- und Ausgabe, daher deutlich schneller.
Nachteile:
Binäre Daten sind nicht direkt vom Menschen lesbar.
Binärformate sind vom System abhängig, d.h. Daten müssen unter Umständen
zuerst umgewandelt werden.
Fortgeschrittene Ein- und Ausgabe: Funktionen
Aus einem Datenstrom lesen (binär)
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
Liest nmemb Elemente der Größe size aus dem Datenstrom stream und speichert sie
(in der gleichen Reihenfolge) im Feld, auf das ptr zeigt. Rückgabewert ist die Anzahl
der erfolgreich gelesenen Elemente.
Beachte: Die Daten werden nicht als Text, sondern als Bitfolgen interpretiert.
In einen Datenstrom schreiben (binär)
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
Schreibt nmemb Elemente der Größe size aus dem Feld, auf das ptr zeigt, in den
Datenstrom stream. Rückgabewert ist die Anzahl der erfolgreich geschriebenen
Elemente.
Beispiel: binäres Lesen und Schreiben (Funktionen)
1
2
3
4
void schreibeMatrixInBinDatei(char *pfad, double **matrix,
int n_zeilen, int n_spalten)
{ int i;
FILE *fp;
5
if ( (fp = fopen(pfad, "wb")) == NULL ) return;
6
7
for (i=0; i<n_zeilen; i++)
fwrite(matrix[i], sizeof(double), n_spalten, fp);
8
9
10
11
12
13
}
fclose(fp);
return;
14
15
16
17
18
double **neueMatrixAusBinDatei(char *pfad, int n_zeilen, int n_spalten)
{ int i;
double **matrix;
FILE *fp;
19
if ( (fp = fopen(pfad, "rb")) == NULL ) return NULL;
20
21
matrix = neueMatrix(n_zeilen, n_spalten);
for (i=0; i<n_zeilen; i++)
fread(matrix[i], sizeof(double), n_spalten, fp);
22
23
24
25
26
27
28
}
fclose(fp);
return matrix;
Beispiel: binäres Lesen und Schreiben (Hauptprogramm)
1
2
3
double **neueMatrixAusBinDatei(char *pfad, int n_zeilen, int n_spalten);
void schreibeMatrixInBinDatei(char *pfad, double **matrix,
int n_zeilen, int n_spalten);
4
5
6
7
8
int main(void)
{
int n = 4, m = 7;
double **H;
9
H = neueMatrixAusBinDatei("Matrix.bin", n, m);
zeigeMatrix(H, n, m);
schreibeMatrixInBinDatei("Matrix_kopie.bin", H, n, m);
loescheMatrix(H, n);
10
11
12
13
14
15
16
}
return 0;
Beachte:
Die Funktionen werden (zur Abwechslung) nicht mit dem bereits geöffneten
Strom, sondern mit dem Pfad zum Dateinamen aufgerufen.
In den if-Statements wird zunächst der Datenstrom geöffnet und anschließend
auf NULL überprüft.
Da jede Matrixzeile als separates Feld gespeichert ist (zweistufige Speicherung
mit double **), muss auch zeilenweise in die Datei geschrieben werden.
C-Präprozessor
Wir haben bereits die Direktive #include <Headerdatei> verwendet, um
„fremde“ Funktionsdeklarationen zu importieren
Generell ist der Präprozessor dafür zuständig, Text durch anderen Text zu
ersetzen.
Der Präprozessor wird vor dem Compiler aufgerufen, deshalb muss der
verarbeitete Code syntaktisch korrekt sein.
Präprozessordirektiven beginnen stets mit einer Raute # und stehen im Code bis
auf wenige Ausnahmefälle ganz zu Beginn.
Die wichtigsten Direktiven sind Einfügungen, Makros und bedingte Ersetzungen.
C-Präprozessor: Einfügungen
Standard-Header
Syntax:
#include <systemheader.h>
Fügt den Text der Headerdatei systemheader.h an der Stelle im Code ein, an
welcher der #include-Befehl steht.
Die Datei wird im Standard-Header-Pfad des Systems gesucht. Bei
Unix-Systemen ist dies üblicherweise /usr/include.
Beispiel: #include <stdio.h> bindet die Datei /usr/include/stdio.h ein.
Bei Headern in Unterordnern muss dieser mit angegeben werden, z. B.
#include <sys/time.h> für den Header /usr/include/sys/time.h.
Lokale (eigene) Header
Syntax:
#include “lokaler_header.h”
Bindet die Datei lokaler_header.h ein, die im aktuellen Verzeichnis liegt.
Will man einen Header aus einem anderen Pfad inkludieren, muss dem Compiler
der Pfad mitgeteilt werden.
Beispiel: Zum Einbinden des Headers matrix.h im Unterordner meine_header
wird
im Quellcode die Zeile #include “matrix.h” eingefügt.
der Compiler mit der Option -Imeine_header aufgerufen.
Die Option -IOrdner macht die Header in Ordner für den Compiler sichtbar.
C-Präprozessor: Einfügungen – Beispiel matrix.h
Headerdatei matrix.h im Unterordner meine_header (Ausschnitt):
1
2
#include <stdio.h>
#include <stdlib.h>
3
4
5
6
// Legt Speicher fuer eine neue Matrix an und liefert einen Zeiger auf die
// erste Zeile; bei Misserfolg NULL
double **neueMatrix(int n_zeilen, int n_spalten);
7
8
9
// Gibt die Matrix am Bildschirm aus
void zeigeMatrix(double **matrix, int n_zeilen, int n_spalten);
Programmcode matrix_test.c:
1
#include "matrix.h"
2
3
4
5
6
7
8
int main(void)
{
int n = 4, m = 7;
double **H = neueMatrixAusBinDatei("Matrix.bin", n, m);
zeigeMatrix(H, n, m);
loescheMatrix(H, n);
9
10
11
return 0;
} // Hiernach die Funktionsdefinitionen!!
Compiler-Aufruf: gcc -o matrix_test matrix_test.c -Imeine_header
Programmierung für Mathematiker
Prof. Dr. Thomas Schuster
M.Sc. Dipl.-Phys. Anne Wald
14.06.2017
C-Präprozessor: Einfügungen
Standard-Header
Syntax:
#include <systemheader.h>
Fügt den Text der Headerdatei systemheader.h an der Stelle im Code ein, an
welcher der #include-Befehl steht.
Die Datei wird im Standard-Header-Pfad des Systems gesucht. Bei
Unix-Systemen ist dies üblicherweise /usr/include.
Beispiel: #include <math.h> bindet die Datei /usr/include/math.h ein.
Bei Headern in Unterordnern muss dieser mit angegeben werden, z. B.
#include <sys/time.h> für den Header /usr/include/sys/time.h.
Lokale (eigene) Header
Syntax:
#include “lokaler_header.h”
Bindet die Datei lokaler_header.h ein, die im aktuellen Verzeichnis liegt.
Will man einen Header aus einem anderen Pfad inkludieren, muss dem Compiler
der Pfad mitgeteilt werden.
Beispiel: Zum Einbinden des Headers matrix.h im Unterordner meine_header
wird
im Quellcode die Zeile #include “matrix.h” eingefügt.
der Compiler mit der Option -Imeine_header aufgerufen.
Die Option -IOrdner macht die Header in Ordner für den Compiler sichtbar.
C-Präprozessor: Einfügungen – Beispiel matrix.h
Headerdatei matrix.h im Unterordner meine_header (Ausschnitt):
1
2
#include <stdio.h>
#include <stdlib.h>
3
4
5
6
// Legt Speicher fuer eine neue Matrix an und liefert einen Zeiger
// auf die erste Zeile; bei Misserfolg NULL
double **neueMatrix(int n_zeilen, int n_spalten);
7
8
9
// Gibt die Matrix am Bildschirm aus
void zeigeMatrix(double **matrix, int n_zeilen, int n_spalten);
Programmcode matrix_test.c:
1
#include "matrix.h"
2
3
4
5
6
7
8
int main(void)
{
int n = 4, m = 7;
double **H = neueMatrixAusBinDatei("Matrix.bin", n, m);
zeigeMatrix(H, n, m);
loescheMatrix(H, n);
9
10
11
return 0;
} // Hiernach die Funktionsdefinitionen!!
Compiler-Aufruf: gcc -o matrix_test matrix_test.c -Imeine_header
C-Präprozessor: Makros ohne Parameter I
Syntax:
#define MAKRO_OHNE_PARAMETER
Definiert ein Makro mit dem Namen MAKRO_OHNE_PARAMETER
Namenskonvention: Makros werden immer durchgehend mit Großbuchtstaben
benannt.
Mit #ifdef MAKRO_OHNE_PARAMETER
bzw.
#ifndef MAKRO_OHNE_PARAMETER
kann abgefragt werden, ob das Makro definiert bzw. nicht definiert ist.
Syntax:
#ifdef MAKRO
// Weitere Direktiven, z. B. #include
#endif
Verwendungsbeispiel: Verhindern, dass ein Header mehrfach eingebunden wird.
#ifndef __MEIN_HEADER_H__
#define __MEIN_HEADER_H__
// Weitere Direktiven, Funktionsdeklarationen etc.
#endif
C-Präprozessor: Makros ohne Parameter II
Syntax:
#define MAKRO Ersetzungstext
Bewirkt, dass überall im Quellcode, wo MAKRO steht, der Ersetzungstext
eingesetzt wird.
Beispiele:
#define ANTWORT 42
Alle Vorkommen von ANTWORT im Quellcode wird textuell durch 42 ersetzt.
Auf diese Weise brauchen für Konstanten keine Variablen deklariert zu
werden.
#define ADRESSE long unsigned int
In der Folge kann ADRESSE wie ein Datentyp verwendet werden. So lassen
sich intuitive Namen für Datentypen vergeben.
Generelle Verwendung: Vergabe von sprechenden Namen für Konstanten,
Datentypen, . . . .
Vermeidung von „magische Zahlen“ (PUFFERGROESSE hat mehr Aussagekraft
als 1024).
Durch Änderung der Definition lassen sich alle Vorkommen der Konstanten
schnell und unkompliziert anpassen.
Folge: Besser lesbarer und leichter zu pflegender Code
C-Präprozessor: Makros mit Parametern
Syntax:
#define MAKRO(Parameterliste) Ersetzungstext
Im Code lässt sich MAKRO wie eine Funktion aufrufen.
Achtung: Es wird nicht überprüft, ob die Argumente passende Typen haben.
Generell umfassen Makros nur eine einzige Zeile. Lange Zeilen lassen sich mit
einem Backslash \ am Ende umbrechen.
Beispiel 1:
#define MAX(X,Y) X > Y ? X : Y
Berechnet das Maximum von zwei Argumenten
Beispiel 2:
#define TAUSCHE(X,Y) int z = X; X = Y; Y = z;
Vertauscht die Werte von zwei int-Argumenten.
C-Präprozessor: Makros mit variabler Anzahl von Parametern
Syntax:
#define MAKRO(Parameterliste,...) Ersetzungstext
Werden in Verbindung mit Funktionen verwendet, die mit einer variablen Anzahl
von Argumenten aufgerufen werden können, z. B. fprintf.
Das oben definierte Makro muss mit mindestens so vielen Parametern aufgerufen
werden wie die Parameterliste lang ist.
Auf die über diese Anzahl hinausgehenden Parameter, die anstelle der Punkte ...
übergeben werden, kann mit dem Makro __VA_ARGS__ zugegriffen werden.
Beispiel:
#define EPRINTF(_fmt_str,...) \
fprintf(stderr, _fmt_str, ##__VA_ARGS__)
Der Aufruf EPRINTF(“i = %d\n”, i); wird beispielsweise ersetzt durch
fprintf(stderr, “i = %d\n”, i);
Die doppelte Raute ## vor __VA_ARGS__ bewirkt hier speziell, dass das Komma
nach dem Formatstring wegfällt, falls __VA_ARGS__ leer ist. Andernfalls ergäbe
sich ein Syntaxfehler.
Beispiel: EPRINTF(“Hallo!\n”); wird hier ersetzt zum syntaktisch korrekten
fprintf(stderr, “Hallo!\n”); Ohne die Doppelraute würde die Ersetzung
fprintf(stderr, “Hallo!\n”, ); lauten, was einen Syntaxfehler darstellt.
C-Präprozessor: Fallstricke
Argumente sollten im Ersetzungstext immer geklammert werden, da sonst
zusammengesetzte Ausdrücke als Argument eventuell falsch ausgewertet werden.
Beispiel:
#define ZWEIMAL(X) 2 * X
Der Aufruf ZWEIMAL(4 + 2) wird aufgelöst zu 2 * 4 + 2, im Ergebnis 10.
Das Makro #define ZWEIMAL(X) 2 * (X) liefert das korrekte Ergebnis:
2*(4 + 2)=12.
Formeln sollten im Ersetzungstext immer geklammert werden, da sonst
zusammengesetzte Ausdrücke als Argument eventuell falsch ausgewertet werden.
Beispiel:
#define MAX(X,Y) (X) > (Y) ? (X) : (Y)
Der Aufruf 2*MAX(3,5) wird aufgelöst zu 2 * (3) > (5) ? (3) : (5), im
Ergebnis 3. Das Makro #define MAX(X,Y) ((X) > (Y) ? (X) : (Y)) liefert
das korrekte Ergebnis: 2*((3) > (4) ? (3) : (5))=10.
C-Präprozessor: Fallstricke
Anweisungsfolgen sind in geschweifte Klammern zu setzen.
Beispiel 1:
#define ABBRUCH_VOID printf(“Abbruch!\n”); return;
Der Code if(fehler) ABBRUCH_VOID; resultiert im aufgelösten Code
if(fehler) printf(“Abbruch!\n”); return; Dadurch wird immer das
return-Statement ausgeführt!
Mit #define ABBRUCH_VOID {printf(“Abbruch!\n”); return;}
tritt dieses Problem nicht auf.
Beispiel 2:
#define TAUSCHE(X,Y) int z = X; X = Y; Y = z;
Falls eine Variable z bereits deklariert wurde, beschwert sich der Compiler!
Die Definition #define TAUSCHE(X,Y) {int z = X; X = Y; Y = z;} behebt
diesen Konflikt, da die Deklaration im neuen Anweisungsblock übergeordnete
Variablen bis zum Ende des Blocks überdecken kann.
C-Präprozessor: Fallstricke
Ausdrücke mit Nebeneffekten oder Aufrufe von rechenzeitintensiven Funktionen
sollten in Makros vermieden werden, da sie eventuell mehrfach ausgewertet
werden.
Beispiel 1:
char c = MAX(fgetc(stdin), ’a’);
wird aufgelöst zu
char c = ((fgetc(stdin)) > (’a’)) ? (fgetc(stdin)) : ’a’;
Falls das erste eingelesene Zeichen (als Zahl) größer ist als ’a’, so wird ein
zweites Zeichen eingelesen.
Beispiel 2:
int i = 5, j = MAX(i++, 2);
wird aufgelöst zu
int i = 5, j = ((i++) > (2)) ? (i++) : (2);
Danach gilt nicht wie erwartet i = und j = , sondern i =
Beispiel 3:
und j =
.
double x = 1.0; double y = MAX(viel_zu_rechnen(x), 0.0);
Hier wird unter Umständen die Funktion viel_zu_rechnen zweimal ausgewertet,
was zu einer doppelt so langen Laufzeit führt!
C-Präprozessor: Vor- und Nachteile von Makros gegenüber Funktionen
Vorteile:
Makros werden zur Compilezeit ausgewertet und sind in vielen Fällen schneller.
Viele Makros können universell eingesetzt werden (z. B. MAX für alle
vergleichbaren Datentypen).
Nachteile:
Zu viele oder zu lange Makros lassen den Codeumfang anwachsen und ergeben
unter Umständen große und langsame Programme. Zudem ist der Speicherbedarf
größer als bei Funktionen.
Mehrfache Auswertung von Ausdrücken kann unerwünschte Konsequenzen nach
sich ziehen.
Es gibt keine Möglichkeit zu prüfen, ob sinnvolle Datentypen als Parameter
verwendet werden.
Die Fehlerquellen im Umgang mit Makros sind vielfältig.
Der Versuch, Funktionspointer als Zeiger auf ein Makro zu verwenden, scheitert
an einem Syntaxfehler. Dies ist vor allem dann problematisch, wenn nicht klar ist,
ob ein Name für eine Funktion oder ein Makro steht.
Fazit: Makros mit Parametern als Ersatz für Funktionen eignen sich für einfache
Aufgaben in geschwindigkeitskritischen Bereichen.
C-Präprozessor: bedingte Ersetzung
Konditionale Direktiven:
#if Bedingung1
// Direktiven / Code
#elif Bedingung
// Weitere Direktiven / Code
#else
// Alternative Direktiven / Code
#endif
Prinzipiell funktioniert dieses Konstrukt wie C-Statements mit if - else if - else
(#elif ist eine Kurzform für „else if“). Als Bedingungen können konstante Zahlen,
andere Makros sowie arithmetische und logische Ausdrücke verwendet werden.
#ifdef MAKRO // bzw. #ifndef
// Weitere Direktiven
#endif
Direktiven werden ausgeführt, falls MAKRO (nicht) definiert wurde.
Wichtig: #if und #ifdef müssen immer durch ein #endif abgeschlossen werden.
Definition rückgängig machen:
#undef MAKRO
C-Präprozessor: bedingte Ersetzung – Beispiel
Header debug.h:
1
2
#ifndef __DEBUG_H__
#define __DEBUG_H__
3
4
#include <stdio.h>
5
6
7
8
9
#ifdef DEBUG_AN
#define DEBUG_AUSGABE(_fmt_string, ...) \
fprintf(stderr, "[Datei %s, Zeile %d] " _fmt_string, \
__FILE__, __LINE__, ##__VA_ARGS__)
10
11
12
#else
#define DEBUG_AUSGABE(_fmt_string, ...) // definiert als "nichts"
13
14
15
#endif // DEBUG_AN
#endif // __DEBUG_H__
Programmcode debug_test.c:
1
#include "debug.h"
2
3
4
5
6
7
int main(void)
{
int i = 42;
DEBUG_AUSGABE("Hallo Welt!\n");
DEBUG_AUSGABE("i = %d\n", i);
8
return 0;
9
10
}
C-Präprozessor: bedingte Ersetzung – Beispiel
Codeanalyse Header:
#ifdef DEBUG_AN
#define DEBUG_AUSGABE(_fmt_string, ...) \
fprintf(stderr, "[Datei %s, Zeile %d]: " _fmt_string, \
__FILE__, __LINE__, ##__VA_ARGS__)
#else
#define DEBUG_AUSGABE(_fmt_string, ...) // definiert als "nichts"
#endif // DEBUG_AN
Falls DEBUG_AN definiert wurde, nimmt der Präprozessor die obere Version des
Makros DEBUG_AUSGABE, andernfalls die untere leere Version.
Die hintereinander geschriebenen Strings “[Datei %s, Zeile %d]: ” und
_fmt_string werden automatisch zu einem zusammengefügt (konkateniert). Dies
gilt generell für konstante Strings in C.
__FILE__ und __LINE__ sind vordefinierte Makros, die vom Präprozessor durch
den Dateinamen (als String) und die Codezeile (als Ganzzahlkonstante) ersetzt
werden. Aus diesem Grund sind dafür die Platzhalter %s bzw. %d vorgesehen.
Wurde DEBUG_AN nicht definiert, so ersetzt der Präprozessor alle Aufrufe von
DEBUG_AUSGABE durch nichts. In diesem Fall wäre beispielsweise
DEBUG_AUSGABE(“Hallo!\n”); ein leeres Statement (;).
C-Präprozessor: bedingte Ersetzung – Beispiel
Codeanalyse Hauptprogramm:
int i = 42;
DEBUG_AUSGABE("Hallo Welt!\n");
DEBUG_AUSGABE("i = %d\n", i);
Compileraufruf 1: gcc -o debug_test debug_test.c
Das Makro DEBUG_AN ist nicht definiert, daher erfolgt keine Ausgabe.
Compileraufruf 2: gcc -o debug_test debug_test.c -DDEBUG_AN
Durch die Option -DDEBUG_AN wird das Makro DEBUG_AN definiert, und der
Präprozessor verwendet die „gehaltvolle“ Version von DEBUG_AUSGABE.
Das zweite DEBUG_AUSGABE-Statement in obigem Code beispielsweise wird wie
erwartet durch fprintf(stderr, “[Datei %s, Zeile %d] ” “i = %d\n”, i);
ersetzt.
Generell wirkt die Compileroption -DMAKRO[=Wert] wie eine Zeile
#define MAKRO [Wert] im Code.
Achtung: Bei Stringkonstanten müssen Zeichen, die normalerweise von der
Kommandozeile interpretiert werden, mit einem Backslash „escaped“ werden.
Dazu gehören u. a. Anführungszeichen, Hochkommata, Backslash, Dollar, Raute,
Klammern, . . .
Beispiel: Um ein Makro NACHRICHT mit dem Wert “Im Westen nichts Neues.”
zu definieren, muss die Compileroption
-DNACHRICHT=\“Im Westen nichts Neues.\” lauten.
C-Präprozessor: mehrstufige Ersetzungen und Zugriff auf Variablennamen
Es ist möglich, Makros zu definieren, die von anderen Makros abhängen.
Beispiel: Kurzform eines langen Konstantennamens
#define ERDBESCHLEUNIGUNG 9.81
#define G ERDBESCHLEUNIGUNG
Generell durchläuft der Präprozessor den Code so lange, bis alle Ersetzungen erfolgt
sind.
Beim ersten Durchlauf werden alle Vorkommen von G durch ERDBESCHLEUNIGUNG
ersetzt. Im zweiten Durchgang erfolgt die endgültige Auflösung zur Konstanten 9.81.
Es ist möglich, mit Makros auf den Namen von Variablen zuzugreifen.
Beispiel: #define PRINTDOUBLEWITHNAME(X) printf("%s = %.3lf ",#X,X)
Im Programm führt der Aufruf
double x=1.121,y=2.3; PRINTDOUBLEWITHNAME(x); PRINTDOUBLEWITHNAME(y);
dann zu folgender Ausgabe: x = 1.121 y = 2.300.
Programmierung für Mathematiker
Prof. Dr. Thomas Schuster
M.Sc. Dipl.-Phys. Anne Wald
21.06.2017
Programmierprojekte mit mehreren Dateien
Die Präprozessordirektive #include “eigener_header.h” erlaubt es,
Funktionsdeklarationen, typedefs, Definitionen von Makros usw. in einen
externen Header auszulagern.
Nach wie vor müssen jedoch alle Funktionsdefinitionen im Hauptprogrammcode
enthalten sein.
Diese Tatsache führt zu langen und unübersichtlichen Codedateien und verhindert
eine einfache Wiederverwendung der programmierten Funktionen (einzige
Möglichkeit: copy&paste in andere Projekte).
Wünschenswert wäre eine Möglichkeit, Funktionsdefinitionen (gruppiert nach
Aufgabengebiet) in externe Quellcode-Dateien auszulagern.
Mit Hilfe von Objektdateien, einem Zwischenprodukt beim Compilieren von
Code, lässt sich diese Möglichkeit realisieren.
Programmierprojekte mit mehreren Dateien: Prinzip
main.c
modul_1.c
...
modul_N.c
gcc -c -o <ausgabe>.o <code>.c
main.o
modul_1.o
...
modul_N.o
Bibliotheken
gcc -o programm main.o modul_1.o [...] modul_N.o -lBibliotheksname
programm
Programmierprojekte mit mehreren Dateien: Prinzip
Der Quellcode ist verteilt auf mehrere Module - getrennt nach Funktionalität und das Hauptprogramm.
Jede Codedatei wird separat zu Objektcode compiliert mit dem Aufruf
gcc -c -o Datei.o Datei.c
Mit gcc -o programm Liste_von_Objektdateien werden die Objektdateien zu
einem ausführbaren Programm „zusammengebunden“ (gelinkt).
Erinnerung:
Der Compiler braucht alle nötigen Deklarationen und gibt als Resultat
Objektcode aus.
Erst der Linker muss die Definitionen zur Verfügung haben, sonst liefert er
einen undefined reference-Fehler. Das Ergebnis des Link-Vorgangs ist
schließlich das ausführbare Programm.
Programmierprojekte mit mehreren Dateien: Beispiel – matrix.h
1
2
#ifndef __MATRIX_H__
#define __MATRIX_H__
3
4
5
6
// Legt Speicher fuer eine neue Matrix an und liefert einen Zeiger auf die
// erste Zeile; bei Misserfolg NULL
double **neueMatrix(int n_zeilen, int n_spalten);
7
8
9
// Gibt die Matrix am Bildschirm aus
void zeigeMatrix(double **matrix, int n_zeilen, int n_spalten);
10
11
12
// Gibt den Speicherplatz der Matrix frei
void loescheMatrix(double **matrix, int n_zeilen);
13
14
15
16
// Reserviert Speicher fuer eine Matrix und liest die Elemente aus der
// TEXTdatei, die unter dem Pfad zu finden ist; bei Misserfolg NULL
double **neueMatrixAusDatei(char *pfad, int *n_zeilen, int *n_spalten);
17
18
19
20
// Schreibt die Matrix im Textmodus in die Datei im angegebenen Pfad
void schreibeMatrixInDatei(char *pfad, double **matrix,
int n_zeilen, int n_spalten);
21
22
23
24
// Reserviert Speicher fuer eine Matrix und liest die Elemente aus der
// BINAERdatei, die unter dem Pfad zu finden ist; bei Misserfolg NULL
double **neueMatrixAusBinDatei(char *pfad, int n_zeilen, int n_spalten);
25
26
27
28
29
// Schreibt die Matrix im Binaermodus in die Datei im angegebenen Pfad
void schreibeMatrixInBinDatei(char *pfad, double **matrix,
int n_zeilen, int n_spalten);
#endif
Programmierprojekte mit mehreren Dateien: Beispiel –
matrix_funktionen.c
1
2
#include <stdio.h>
#include <stdlib.h>
3
4
5
6
7
double **neueMatrix(int n_zeilen, int n_spalten)
{
int i, j;
double **matrix;
8
matrix = (double **)malloc(n_zeilen * sizeof(double *));
if (matrix == NULL)
return NULL;
9
10
11
12
for (i = 0; i < n_zeilen; i++)
{
matrix[i] = (double *)malloc(n_spalten * sizeof(double));
if (matrix[i] == NULL)
{
for (j = 0; j < i; j++)
free(matrix[j]);
free(matrix);
return NULL;
}
}
return matrix;
13
14
15
16
17
18
19
20
21
22
23
24
25
}
26
27
// Und alle weiteren Funktionsdefinitionen
Programmierprojekte mit mehreren Dateien: Beispiel –
matrix_test_main.c
1
#include "matrix.h"
2
3
4
5
6
int main(void)
{
int n, m;
double **G, **H;
7
G = neueMatrixAusDatei("Matrix_kommentiert.dat", &n, &m);
H = neueMatrixAusBinDatei("Matrix.bin", n, m);
8
9
10
zeigeMatrix(G, n, m);
zeigeMatrix(H, n, m);
11
12
13
loescheMatrix(G, n);
loescheMatrix(H, n);
14
15
16
return 0;
17
18
}
Kommandos:
gcc -c -o matrix_test_main.o matrix_test_main.c (Compiler)
gcc -c -o matrix_funktionen.o matrix_funktionen.c (Compiler)
gcc -o matrix_test matrix_test_main.o matrix_funktionen.o (Linker)
Programmierprojekte mit mehreren Dateien
Anmerkungen:
Das Modul matrix_funktionen.c benötigt die Systemheader stdio.h und
stdlib.h, damit die dort deklarierten Funktionen (z. B. printf oder malloc)
verwendet werden können. Sind sie nicht eingebunden, liefert der Compiler
(gcc -c) einen Fehler.
Der Header matrix.h braucht hingegen nicht inkludiert zu werden.
Im Hauptprogramm genügt es, matrix.h einzufügen. Gäbe es Statements, die
beispielsweise printf enthalten, so müsste zusätzlich stdio.h inkludiert werden.
Merkregel: Jede Codedatei benötigt genau diejenigen Header, die nötig sind,
damit alle Variablen, Konstanten, Funktionen, . . . innerhalb dieser Datei korrekt
deklariert sind.
Wird der Code eines Moduls geändert, so müssen alle Objekte und Programme
neu compiliert und gelinkt werden, die von diesem Modul abhängen.
In diesem Beispiel müssen nach einer Änderung in matrix_funktionen.c alle
Schritte von neuem durchgeführt werden!
Bei größeren Projekten mit vielen Dateien wird es zunehmend schwierig
nachzuvollziehen, welche Aktionen nach welcher Änderung vollzogen werden
müssen. Hierfür gibt es die elegante Lösung der Makefiles.
Mehrdateiprojekte mit make
make ist das mit Abstand wichtigste Entwicklungs-Tool bei Softwareprojekten.
Wir betrachten hier die am weitesten verbreitete Implementierung GNU Make
(www.gnu.org/software/make/)
Aufgabe: Neuübersetzung genau derjenigen Programmteile, die von einer
Änderung am Code betroffen sind.
Hintergrund: Die „Brechstangen“-Methode, bei jeder Änderung alles neu zu
übersetzen, ist aus zwei Gründen nicht praktikabel:
1 Umfangreiche Projekte benötigen Minuten bis Stunden zur
Komplettübersetzung
2
Für jede einzelne Datei gcc aufzurufen (mit individuellen Optionen) ist
extrem mühsam.
Funktionsweise: In einer Steuerungsdatei mit dem Namen Makefile gibt man
alle Abhängigkeiten zwischen den Dateien in Form von Erstellungsregeln (engl.
make rules) an.
Mehrdateiprojekte mit make: Erstellungsregeln
Aufbau:
Ziel:
Abhaengigkeiten \
weitere Abhaengigkeiten
−−−−−→Befehl1
−−−−−→...
−−−−−→BefehlN
Ziel bezeichnet „das, was getan werden soll“, also entweder einen Dateinamen
(z. B. matrix_funktionen.o) oder eine abstrakte Aktion (z. B. clean).
Als Abhängigkeiten werden sämtliche Dateien angegeben, von deren Änderung
das Ziel abhängt bzw. abhängen soll. Lange Zeilen können mit einem Backslash
am Ende umgebrochen werden.
Die Befehle definieren, was make unternehmen soll, um das Ziel zu erstellen.
Wichtig: Befehlszeilen müssen immer mit einem Tabulator (TAB-Taste)
beginnen, sonst meldet make einen Fehler.
Mehrdateiprojekte mit make: Beispiel Matrix-Projekt
Bisheriger Inhalt: matrix.h, matrix_funktionen.c und matrix_test_main.c
Jede Quellcodedatei .c wird zu einer Objektdatei .o kompiliert. Diese Objekte werden
schließlich zu einem ausführbaren Programm zusammengebunden.
Abhängigkeiten:
matrix_funktionen.o: Entsteht aus matrix_funktionen.c
matrix_test_main.o: Entsteht aus matrix_test_main.c und inkludiert
matrix.h
matrix_test: Entsteht aus matrix_test_main.o und matrix_funktionen.o
Daraus ergeben sich folgende Erstellungsregeln:
1
2
matrix_test: matrix_test_main.o matrix_funktionen.o
−−−−−→gcc -o matrix_test matrix_test_main.o matrix_funktionen.o
3
4
5
matrix_test_main.o: matrix_test_main.c matrix.h
−−−−−→gcc -c -o matrix_test_main.o matrix_test_main.c
6
7
8
matrix_funktionen.o: matrix_funktionen.c
−−−−−→gcc -c -o matrix_funktionen.o matrix_funktionen.c
Von nun an genügt es, nach jeder Änderung auf der Kommandozeile make aufzurufen:
$ make
gcc -c -o matrix_test_main.o matrix_test_main.c
gcc -c -o matrix_funktionen.o matrix_funktionen.c
gcc -o matrix_test matrix_test_main.o matrix_funktionen.o
Auflösung der Abhängigkeiten: Erster Aufruf von make mit Ziel „Z“
Z
= Ziel im Makefile
= Abhängigkeit neuer als Ziel
= nicht vorhanden
A
B
= erstellt und aktuell
C
= kein Ziel
a
D
b
A
c
E
c
d
Auflösung der Abhängigkeiten: Erster Aufruf von make mit Ziel „Z“
Z
= Ziel im Makefile
= Abhängigkeit neuer als Ziel
= nicht vorhanden
A
B
= erstellt und aktuell
C
= kein Ziel
a
D
b
A
c
E
c
d
Auflösung der Abhängigkeiten: Erster Aufruf von make mit Ziel „Z“
Z
= Ziel im Makefile
= Abhängigkeit neuer als Ziel
= nicht vorhanden
A
B
= erstellt und aktuell
C
= kein Ziel
a
D
b
A
c
E
c
d
Auflösung der Abhängigkeiten: Erster Aufruf von make mit Ziel „Z“
Z
= Ziel im Makefile
= Abhängigkeit neuer als Ziel
= nicht vorhanden
A
B
= erstellt und aktuell
C
= kein Ziel
a
D
b
A
c
E
c
d
Auflösung der Abhängigkeiten: Nach Aktualisierung der Datei „a“
Z
= Ziel im Makefile
= Abhängigkeit neuer als Ziel
= nicht vorhanden
A
B
= erstellt und aktuell
C
= kein Ziel
a
D
b
A
c
E
c
d
Auflösung der Abhängigkeiten: Nach Aktualisierung der Datei „a“
Z
= Ziel im Makefile
= Abhängigkeit neuer als Ziel
= nicht vorhanden
A
B
= erstellt und aktuell
C
= kein Ziel
a
D
b
A
c
E
c
d
Auflösung der Abhängigkeiten: Nach Aktualisierung der Datei „a“
Z
= Ziel im Makefile
= Abhängigkeit neuer als Ziel
= nicht vorhanden
A
B
= erstellt und aktuell
C
= kein Ziel
a
D
b
A
c
E
c
d
Mehrdateiprojekte mit make
Bemerkungen:
Das Kommando make Ziel erstellt Ziel anhand der Abhängigkeiten in der Datei
Makefile. Der Befehl make ohne Argument erstellt das erste Ziel im Makefile.
Eine Erstellungsregel muss nicht unbedingt Abhängigkeiten besitzen.
Beispiel: Regel zum Aufräumen
clean:
−−−−−→rm *.o matrix_test -f
Mit make clean können nun alle Sicherungs- und Objektdateien sowie das
Programm gelöscht werden.
Auch Befehle müssen nicht zwingend in einer Regel enthalten sein.
Beispiel: Alles erstellen
all: matrix_test matrix_test_main.o matrix_funktionen.o
Durch die Auflistung sämtlicher Objekt- und Programmdateien als Abhängigkeiten
von all lässt sich mit dem Aufruf make all das gesamte Projekt aktualisieren.
Mehrdateiprojekte mit make: implizite Regeln und Variablen
GNU Make verfügt über eine große Menge von impliziten Regeln, die im Makefile
nicht neu definiert werden müssen.
Wichtigstes Beispiel:
%.o: %.c
−−−−−→$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
In Worten:
Jedes Objekt Datei.o hängt vom entsprechenden Code Datei.c ab.
Zur Erstellung eines solchen Objekts ist folgender Befehl aufzurufen:
Compiler -c Compileroptionen Präprozessoroptionen Datei.c -o Datei.o
Zusätzliche Abhängigkeiten können als Regel ohne Befehl angegeben werden.
Beispiel:
matrix_test_main.o: matrix.h
Mit Hilfe der Variablen CC, CFLAGS und CPPFLAGS lassen sich Compilername und
Optionen anpassen.
Beispiel:
CC = gcc
CFLAGS = -Wall
CPPFLAGS = -DDEBUG_AN
resultiert im generellen Compilerbefehl
gcc -c -Wall -DDEBUG_AN Datei.c -o Datei.o
Mehrdateiprojekte mit make: Vereinfachtes Matrix-Makefile
Ursprüngliches explizites Makefile:
1
2
matrix_test: matrix_test_main.o matrix_funktionen.o
−−−−−→gcc -Wall -o matrix_test matrix_test_main.o matrix_funktionen.o
3
4
5
matrix_test_main.o: matrix_test_main.c matrix.h
−−−−−→gcc -c -Wall -o matrix_test_main.o matrix_test_main.c
6
7
8
matrix_funktionen.o: matrix_funktionen.c
−−−−−→gcc -c -Wall -o matrix_funktionen.o matrix_funktionen.c
Angepasstes Makefile:
1
2
CC = gcc
CFLAGS = -Wall
3
4
5
matrix_test: matrix_test_main.o matrix_funktionen.o
−−−−−→$(CC) $(CFLAGS) $^ -o $@
6
7
matrix_test_main.o: matrix.h
Mehrdateiprojekte mit make: Vereinfachtes Matrix-Makefile
Bemerkung:
Wichtige vordefinierte Variablen sind beispielsweise:
$@
Name des Ziels
$ˆ
Liste aller Abhängigkeiten ohne Wiederholungen
$+
Liste aller Abhängigkeiten
$<
erste Abhängigkeit
Resultat des angepassten Makefiles:
$ make
gcc -Wall -c -o matrix_test_main.o matrix_test_main.c
gcc -Wall -c -o matrix_funktionen.o matrix_funktionen.c
gcc -Wall matrix_test_main.o matrix_funktionen.o -o matrix_test
Programmierung für Mathematiker
Prof. Dr. Thomas Schuster
M.Sc. Dipl.-Phys. Anne Wald
28.06.2017 und 05.07.2017
Die kommerzielle Software MATLAB von MathWorks
MathWorks TAH Campuslizenz
Die Software darf von allen Studierenden der Universität des Saarlandes genutzt
werden. Das umfasst einerseits beliebig viele Installationen auf dem Campus und
andererseits auch die Nutzung auf privaten Computern.
Notwendig: Registrierung bei der Firma ASKnet AG. Weitere Informationen:
https://unisb.asknet.de/cgi-bin/program/S1552
Weitere Informationen:
http://www.hiz-saarland.de/informationen/arbeitsplatz/sw-lizenzen/
Herstellerseite:
http://www.mathworks.com
http://www.mathworks.de
Einleitung
Matlab
Matlab (Matrix Laboratory ) ist ein interaktives Matrix-orientiertes Softwaresystem zur
Berechnung und Lösung numerischer Probleme.
Warum Matlab?
benutzerfreundliche Syntax
umfangreiche Sammlung mathematischer Algorithmen
vielfältige und einfach realisierbare Datenausgabe / Visualisierung
kurze Entwicklungszeiten, in der eingebauten Programmiersprache lassen sich
Algorithmen schnell und leicht realisieren (Interpretersprache)
einfaches Debugging
hohe Absturzsicherheit
Nachteile:
etwas höhere Anforderungen an den Rechner (v.a. Speicherbedarf)
langsamer als kompilierter Code, insbesondere bei for-Schleifen
Aber: Programme anderer Sprachen, z.B. C, lassen sich leicht einbinden.
Literatur
Anne Angermann et al.: Matlab-Simulink-Stateflow: Grundlagen, Toolboxen,
Beispiele, Oldenbourg Verlag, 2011
Wolfgang Schweizer: Matlab kompakt, Oldenbourg Verlag, 2009
Cleve Moler: Numerical Computing with Matlab, SIAM, 2010
Stormy Attaway: MATLAB: A Practical Introduction to Programming and
Problem Solving, Butterworth Heinemann, 2011
Günter Gramlich: Eine Einführung in Matlab aus der Sicht eines Mathematikers
www.hs-ulm.de//users/gramlich/EinfMATLAB.pdf
Susanne Teschl: MATLAB - Eine Einführung
staff.technikum-wien.at/~teschl/MatlabSkriptum.pdf
weitere Informationen:
Homepage von Matlab:
www.mathworks.de bzw. www.mathworks.com
Informationen zur Campuslizenz:
www.hiz-saarland.de/informationen/arbeitsplatz/sw-lizenzen/mathworks-tah-campuslizenz/
unisb.asknet.de/cgi-bin/program/S1552
Matlab-Oberfläche
aktuelles Release: R2012b
Matlab-Oberfläche
Starten mit $ matlab &
Elemente der Matlab-Oberfläche:
Command Window:
direkte Eingabe von Matlab-Befehlen
Command History:
Historie der im Command Window eingegebenen Befehle
Workspace Browser:
Anzeige der Variablen des Base-Speicherbereichs
Current Folder:
Auflistung der Dateien des aktuellen Verzeichnisses
Editor:
Bearbeitung von Matlab-Skripten und Funktionen
(mit Syntax-Highlighting, Tab-Vervollständigung, ...)
erste Schritte in Matlab
Eingabe direkt im Command Window
Ausgabe erscheint beim Weglassen des Semikolons am Ende der Befehlszeile.
Es gilt „Punktrechnung vor Strichrechnung“.
Es wird zwischen Klein- und Großbuchstaben unterschieden.
Strings werden durch Hochkommata erzeugt.
Beispiele:
>> 2.5+1.5
ans = 4
>> c=a^2+b^2
c = 25
>> a=3;
>> b=4;
>> a/b
ans = 0.75
>> d=a+b*c
d = 103
>> s = 'Hallo'
Unterschied zu C: keine Deklaration der Variablen nötig!
Variablen werden (intern) automatisch als double behandelt und belegen 8 Bytes
Speicherplatz.
; kein cast notwendig!
Ganzzahlige Ergebnisse werden als ganze Zahl ausgegeben.
Script-Files
Skripte / Script-Files
Ein Skript ist eine Folge von Anweisungen, die im Editor eingegeben und mit der
Endung „.m“ abgespeichert werden. Es wird durch Eingabe des Dateinamens im
Command Window oder durch den Button „Run“ im Editor (alternativ: F5)
ausgeführt, d.h. alle Anweisungen werden der Reihe nach abgearbeitet.
Beispiel:
Das Skript testskript.m
a=3;
b=4;
c=a^2+b^2
führt nach Ausführung zu der Ausgabe c=25 auf dem Bildschirm. Zudem sind die
Variablen a=3, b=4 und c=25 im Workspace enthalten (und können bei Bedarf für
weitere Berechnungen verwendet werden).
Operatoren
Vergleichende und logische Operatoren wie in C
Ausnahme:
Notation in Matlab
Notation in C
a < b
a >= b
a == b
A && B
A || B
a ∼= b
∼A
a < b
a >= b
a == b
A && B
A || B
a != b
!A
Unterschied zu C: In Matlab dürfen a und b Matrizen gleicher Dimension sein. In
diesem Fall wird elementweise verglichen und eine Matrix gleicher Dimension mit
Nullen und Einsen zurückgegeben.
keine Inkrement- und Dekrementoperatoren a++, a−−
keine arithmetischen Zuweisungsoperatoren
a+=2
Ausgewählte Variablen und Konstanten
ans
wird dem Ergebnis kein Variablenname zugeordnet, so wird
automatisch die Variable ans erzeugt
inf
liegt das Ergebnis nicht im Intervall [-realmax; realmax]
erhält dieses automatisch den Wert inf
NaN
Ergebnis nicht definierter arithmetischer Operationen,
z.B. 0/0, inf/inf, 0*inf, sind vom Typ NaN (Not a Number)
pi
=π
realmax
größte positive Zahl (PC:1.797710308 )
realmin
kleinste positive Zahl (PC: 2.225110−308 )
In Matlab ist es möglich mit komplexen Zahlen zu rechnen:
i,j
conj(Z)
real(Z),imag(Z)
angle(Z), abs(Z)
imaginäre Einheit (i 2 = −1)
konjugiert komplexe Zahl zu Z
Real- bzw. Imaginärteil von Z
Polardarstellung von Z
Nützliche Befehle bzw. Funktionen
clc
clear [mod]
ctrl + c
help [Name]
löscht die Oberfläche des Command Window, aber nicht die Variablen selbst
löscht alle Variablen aus dem Workspace
mod = var: nur die Variable var wird gelöscht
mod = all: alle Variablen werden gelöscht (Achtung: auch globale!)
„Notbremse“, bricht die aktuelle Berechnung ab
listet alle bzw. die zu Name gehörigen Hilfe-Themen auf, bequemer:
GUI (Grafische Benutzeroberfläche) benutzen (z. B. mit F1)
isinf(var)
liefert 1, falls var vom Typ inf
isnan(var)
liefert 1, falls var vom Typ NaN
who
whos
listet die im Workspace vorhandenen Variablen auf
liefert eine detailierte Liste der Variablen (Typ, Wert)
Erzeugen von Matrizen
Eingabe in eckigen Klammern:
einzelne Komponenten in einer Zeile werden durch Leerzeichen oder Komma getrennt
neue Zeilen werden durch ein Semikolon gekennzeichnet
x = [1 2 3]
A = [1 2 3; 4 5 6]
erzeugt den Zeilenvektor x = (1, 2, 3) sowie die Matrix A =
1
4
2
5
3
.
6
Merke: Zeilenvektoren werden intern als 1 × N-Matrizen behandelt.
Eingabe durch Steuerung der Schrittweite: y= a:h:b
erzeugt einen Vektor von a bis b mit Schrittweite h
y= 0:2:6
erzeugt den Zeilenvektor y = (0, 2, 4, 6)
Eingabe mit der Funktion linspace(a,b,N)
erzeugt einen Vektor von a bis b mit N Komponenten
y=linspace(0,6,4)
erzeugt ebenfalls den Zeilenvektor y = (0, 2, 4, 6).
Bemerkung: Bei großen Dimensionen ist die Eingabe mittels a:h:b etwas
schneller als bei der Verwendung von linspace.
Arbeiten mit Matrizen
Die Indizierung von Matrizen beginnt in Matlab bei 1!
Reservierung von Speicherplatz erfolgt automatisch.
Initialisierung trotzdem sinnvoll, führt in der Regel zu schnelleren Programmen.
Wir betrachten die Matrix A =
1
4
2
5
3
6
=
a11
a21
a12
a22
a13
.
a23
einzelnes Matrizenelement :
A(i,j) liefert das Element ai,j
>> A(2, 3)
ans = 6
i-te Zeile einer Matrix: A(i,:)
>> A(1,:)
ans = 1 2 3
j-te Spalte einer Matrix: A(:,j)
>> A(:,2)
ans = 2
5
Teilmatrix: A(p:q,[r,s])
liefert die Zeilen p bis q und die
Spalten r und s der Matrix
>> A(1:2,[1,3])
ans = 1
3
4
6
Arbeiten mit Matrizen
Wir betrachten zusätzlich die Matrix B =
1
0
2
.
3
B=[1,2;0,3]
Addition, Subtraktion und Multiplikation definiert wie in Linearer Algebra
>> B*A
ans = 9
12
12
15
15
18
>> B+3 % oder 3+B
ans = 4
5
3
6
elementweiser Zugriff auf Matrizen mit Punktoperator:
>> B.^2 % oder B.*B
ans = 1 4
0 9
Transponierte Matrix:
>> B'
ans = 1 0
2 3
>> B^2 % oder B*B
ans = 1 8
0 9
Matrix-Funktionen
det(B)
Determinante von B
eig(B)
Eigenwerte und (normierte) Eigenvektoren von B
lambda=eig(B) liefert λ =
1
3
[v,lambda]=eig(B) liefert v =
rank(B)
norm(B,1)
norm(B,’fro’)
inv(B)
1
0
√ −1 2
1
√ −1 und λ =
0
2
Rang der Matrix B
1-Norm (Spaltensummenorm)
der Matrix B, entspricht dem
Pn
|b |, hier kBk1 = 5.
Ausdruck max1≤j≤n
i=1 ij
Frobeniusnorm
Matrix B, entspricht dem Ausdruck
qP derP
√
m
n
kBkF =
|b |2 , hier kBkF = 14.
i=1
j=1 ij
Inverse der Matrix B, Ergebnis: B −1 =
1
3
3
0
−2
−1
Hinweis: Die explizite Berechnung einer inversen Matrix, um ein lineares Gleichungssystem zu lösen, ist in der Praxis (fast immer) zu aufwendig!
Ausweg: Vorlesung Praktische Mathematik (QR-Zerlegung, . . . )
Matlab: linsolve
0
3
Matrix-Funktionen
end
maximaler Indexwert von Matrizen,
z. B. liefert y(end) den letzten Eintrag des Vektors y .
length
höchste Dimension einer Matrix
size
beide Dimensionen einer Matrix
zeros(M,N)
M × N-Matrix, deren Einträge 0 sind
ones(M,N)
M × N-Matrix, deren Einträge 1 sind
eyes(M)
rand(M,N)
A(:)
M × M-Einheitsmatrix
M × N-Zufallsmatrix, wobei jeder Matrixeintrag eine Realisierung
einer auf [0,1)-gleichverteilten Zufallsvariablen ist.
Vektor, der die Spalten der Matrix A hintereinandergereit enthält. Mit der
Matrix A der vorigen Folie entspricht A(:) dem Vektor [1; 4; 2; 5; 3; 6].
A(A>2)
Vektor, der die Spalten der Matrix A hintereinandergereit enthält, wobei
nur Matrixelemente größer zwei berücksichtigt werden. Mit der Matrix A
der vorigen Folie entspricht A(A>2) dem Vektor [4; 5; 3; 6].
max(v)
kleinster Eintrag des Vektors v
min(v)
größter Eintrag des Vektors v
sum(v)
Summe der Vektorelemente
Mathematische Funktionen
Trigonometrische Funktionen: cos, sin, tan, asin, acos, atan, atan2
Soll die Berechnung in Grad durchgeführt werden, so müssen die Funktionen mit
’d’ ergänzt werden.
Beispiel:
>>sin(pi/4)
ans = 0.7071
>> sind(45)
ans = 0.7071
Exponential- und logarithmische Funktionen: exp, log, log10
Wurzeln: sqrt, nthroot(x,n)
Rundungsfunktionen: ceil, floor, round (runden zum nächsten integer-Wert)
Betragsfunktion: abs
Hinweis: Diese Funktionen akzeptieren als Argumente Matrizen, die dann elementweise
ausgewertet werden. Dies führt in der Regel zu schnellerem Code.
if-Anweisung
Syntax in Matlab
if Bedingung_1
Anweisungsblock_1
elseif Bedingung_2
Anweisungsblock_2
.
.
.
elseif Bedingung_N
Anweisungsblock_N
else
Anweisungsblock
end
Syntax in C
if (Bedingung_1)
Anweisungsblock_1
else if (Bedingung_2)
Anweisungsblock_2
.
.
.
else if (Bedingung_N)
Anweisungsblock_N
else
Anweisungsblock
switch-Anweisung
Syntax in Matlab
switch Variable
case Wert_1
Anweisungsblock_1
case Wert_2
Anweisungsblock_2
Syntax in C
switch (Variable)
{
case Wert_1:
Anweisungsblock_1
break;
// optional
case Wert_2:
Anweisungsblock_2
.
.
.
case Wert_N
Anweisungsblock_N
otherwise
Anweisungsblock
end
.
.
.
}
case Wert_N:
Anweisungsblock_N
default:
// optional
Anweisungsblock // optional
Unterschied zu C: In Matlab ist kein break notwendig, es wird immer nur der zum
case gehörige Anweisungsblock ausgeführt.
for- und while-Schleifen
Syntax in Matlab
Syntax in C
for Variable = Vektor
Anweisungsblock
end
for (Initialisierung; Bedingung; Update)
Anweisungsblock
while Bedingung
Anweisungsblock;
end
while (Bedingung)
Anweisungsblock
Do-while-Schleifen existieren in Matlab nicht! (Ausweg: while 1 und break)
Beispiel:
x=1:1:10;
summe=0;
for k=1:10
summe=summe+x(k);
end
% Ergebnis entspricht sum(x)
m=0;
Erg=0;
while m<=10
Erg=Erg+2;
m=m+1;
end
Ein- und Ausgabe am Bildschirm
Eingabe:
variable=input(string)
variable=input(string,’s’)
Ausgabe von string auf dem Bildschirm,
die Eingabe (Zahl, Variablenname) wird ausgewertet
und variable zugewiesen.
Ausgabe von string auf dem Bildschirm,
die Eingabe wird nicht ausgewertet sondern
als String variable zugewiesen.
Ausgabe:
disp(string)
disp(Variable)
fprintf(String, Parliste)
Ausgabe von string auf dem Bildschirm.
Ausgabe der Werte von variable auf dem Bildschirm.
formatierte Ausgabe auf dem Bildschirm. Die Syntax
entspricht im Wesentlichen der von printf in C.
Matlab als Programmiersprache
Neben den Script-Files gibt es noch Function-Files, welche auch in Dateien mit
der Endung .m gespeichert werden.
Sie beginnen mit dem Schlüsselwort function.
function [ Rueckgabewerte ] = Funktionsname( Parameter )
% Beschreibung als Kommentar
Anweisungsblock
end
Unterschiede zu C:
beliebige Anzahl von Rückgabewerten
Matrizen können übergeben werden
Übergabemethode: shared-data-copy, d.h. intern werden Variablen, die nicht verändert
werden, als Zeiger übergeben. Wird eine Variable in der Funktion verändert, so wird
eine Kopie im Speicherbereich der Funktion erstellt.
Funktionenvariablen sind lokale Variablen
Function-Files unter Funktionsname.m abspeichern
Aufruf anderer m-Files in m-Files möglich, sofern die Verzeichnisse in denen die
Dateien liegen unter „Set Path“ eingetragen sind oder mit dem „Current Folder“
übereinstimmen.
Nützliches
tic-toc
pause
pause(n)
nargin
nargout
(primitive) Zeitmessung: Mit tic wird die Stoppuhr auf null gesetzt,
toc gibt die vergangene Zeit aus.
wartet bis zu einem Tastendruck auf der Tastatur
Pause für n Sekunden
Anzahl der Funktionsparameter
Anzahl der Rückgabewerte
Beispiel:
function [ erg ] = testfunktion(a, b)
if nargin==1
erg=a;
else
erg=a+b;
end %if
end
testfunktion(2) liefert das Ergebnis 2.
testfunktion(2,3) liefert das Ergebnis 5.
testfunktion(2,3,2) führt zu der Fehlermeldung: Too many input arguments
Beispiel: Fakultät
function [ erg ] = fakul(n)
% berechnet n! = n*(n-1)*...*1 rekursiv
if(n==0 || n==1)
erg = 1;
elseif n<0
disp('Bitte positive Zahl eingeben!')
else
erg = n*fak(n-1);
end
end
function [ erg ] = fakul_iter(n)
% berechnet n! = n*(n-1)*...*1 iterativ
erg=1;
for k=1:n
erg=erg*k;
end
% Alternative zur for-Schleife: erg=prod(1:n);
end
Beispiel: Fakultät
Startscript:
% Vergleich Rekursion und Iteration
k=20;
% rekursiv
tic
n1=fakul(k);
zeit1=toc;
fprintf('\n Zeitverbrauch rekursiv: %f',zeit1);
% iterativ
tic
n2=fakul_iter(k);
zeit2=toc;
fprintf('\n Zeitverbrauch iterativ: %f',zeit2);
fprintf('\n Ergebnis rekursiv: %d, iterativ: %d \n',n1,n2);
Ergebnis:
Zeitverbrauch rekursiv: 0.000186
Zeitverbrauch iterativ: 0.000023
Ergebnis rekursiv: 2432902008176640000, iterativ: 2432902008176640000
Arbeiten mit Dateien
spezielles Matlab-Binärformat (Endung: .mat)
save(Dateiname, [Var1,...,VarN])
speichert die Variablen Var1 bis VarN (bzw. alle im
Workspace enthaltenen) in die Datei Dateiname.mat
load(Dateiname, [Var1,...,VarN])
lädt die Variablen Var1 bis VarN (bzw. alle enthaltenen)
aus der Datei Dateiname.mat in den Workspace
Textdateien
fopen, fprintf, fscanf, fclose
(nahezu) analog zu C, siehe Matlabhilfe für Details.
Binärdateien
fopen, fread, fwrite, fclose
(nahezu) analog zu C, siehe Matlabhilfe für Details.
verschiedene Dateiformate, z.B. xls, bmp, png, jpg, . . . und (einfache) Textdateien
importdata(Dateiname,[Delimiter],[Kopfzeilen]) importiert zahlreiche Formate, wobei
Delimiter das Spaltentrennzeichen
festlegt und die Kopfzeilen beim
Einlesen übersprungen werden
Fallstricke
Achtung: In Matlab ist fast alles erlaubt!
Führt man das folgende Skript in Matlab aus, so erhält man keine Fehlermeldung und
auch der Code Analyzer gibt keine Warnungen aus!
sum = 0;
for i = 1:10
sum = sum + i;
end
pi = 1;
Folgende Probleme treten anschließend auf:
1
2
3
»sum([1 2])
Index exceeds matrix dimensions.
Merke: Funktionen werden durch Variablen mit gleichem Namen überdeckt!
»z=1+2*i
z=21.
Merke: Möchte man mit komplexen Zahlen arbeiten, so muss man auf i oder j
als Laufindex für Schleifen verzichten!
»sin(pi)
ans=0.8415.
Merke: Selbst Konstanten wie π können durch beliebige Werte überschrieben
werden!
Matlab-Profiler und Code Analyzer
Code Analyzer
Während der Eingabe wird eine Syntax-Prüfung des Codes durchgeführt, auffällige
Stellen werden unterschlängelt. Zudem werden Syntax-Fehler mit roten und
Warnungen durch orangefarbene Balken in der Scrollleiste markiert.
mlintrpt
mlintrpt(’Dateiname’)
Anzeige des Code Analyzer Reports für alle Dateien
im current folder. (alternativ Klick auf „Analyze Code“)
Anzeige des Code Analyzer Reports für die Datei Dateiname.m.
(alternativ „Show Code Analyzer Report“ im Editor)
Matlab-Profiler
Suche nach Optimierungspotential, meist ist nur ein kleiner Teil des Programms
für lange Rechenzeiten verantwortlich.
Debugging des Matlab-Codes
profile on
profile viewer
Profiling starten.
Profile Summary anzeigen.
(alternativ: „Run and Time“ im Editor)
geschicktes Programmieren in Matlab: Binomialkoeffizient
langsame Variante:
schnellere Variante:
function [ binom ] = binom_l( n,k )
function [ binom ] = binom_s( n,k )
if (n < 0 || k < 0)
disp(['Bitte zwei positive,'...
'Zahlen eingeben']);
binom = NaN;
elseif n >= k
if k > n/2, k = n-k; end
zaehler = 1;
nenner = 1;
for l = n-k+1:n
zaehler = zaehler * l;
end
if (n < 0 || k < 0)
disp(['Bitte zwei positive,'...
'Zahlen eingeben']);
binom = NaN;
elseif n >= k
if k > n/2, k = n-k; end
binom = prod(((n-k+1):n)./(1:k));
else
binom = 0;
end
for l = 1:k
nenner = nenner * l;
end
binom = zaehler / nenner;
else
binom = 0;
end
end
end
Bemerkung: Die schnellere Variante ist zudem
stabiler, da für große k die Variablen zaehler
und nenner nicht über alle Schranken wachsen.
geschicktes Programmieren in Matlab: Funktionsauswertungen
langsame Variante:
function [x,y] = funkauswert_l( t )
x = zeros(1,length(t));
y = zeros(1,length(t));
for i = 1:length(t)
x(i) = t(i)^2;
y(i) = log2(t(i));
end
end
schnelle Variante:
function [x,y] = funkauswert_s( t )
x = t.^2;
y = log2(t);
end
geschicktes Programmieren in Matlab: Vandermonde-Matrix

1
1
1
V := 
.
 ..
1
x1
x2
x3
..
.
xm
x12
x22
x32
..
.
2
xm
...
...
...
..
.
...

x1n−1
x2n−1 

x3n−1 
.. 
. 
n−1
xm
häufige Anwendung: Beschreibung einer Polynom-Interpolation.
langsame Variante:
schnelle Variante:
function [ V ] = vander_l( x )
function [ V ] = vander_s( x )
for l = 1:length(x)
for m = 1:length(x)
V(l,m) = x(l)^(m-1);
end
end
n =
x =
V =
for
end
end
length(x);
x(:); %x ist nun Spaltenvektor
ones(n);
l = 1:n-1
V(:,l+1) = x.*V(:,l);
end
Bemerkung: Die Matlab-Funktion vander berechnet eine modifizierte Version der
Vandermonde-Matrix.
geschicktes Programmieren in Matlab
Um die Geschwindigkeit der Funktionen zu testen, wird mit dem Matlab-Profiler das
folgende Skript analysiert:
B1 = ones(100);
B2 = ones(100);
for n = 1:100
for k = 1:n
B1(n,k) = binom_l(n, k);
B2(n,k) = binom_s(n, k);
end
end
t = 0:0.00001:1;
[f1,f2] = funkauswert_s(t);
[g1,g2] = funkauswert_l(t);
x = 0.1:0.001:1;
v1 = vander_l(x);
v2 = vander_s(x);
geschicktes Programmieren in Matlab: Ergebnis des Profilers
allgemeine Tipps:
(Große) Matrizen sollten vor ihrer Verwendung mit der maximal benötigten
Größe vorbelegt werden (z.B. mit zeros(M,N)).
For-Schleifen sollten (falls möglich) durch Vektorisierung des Codes vermieden
werden. Dies führt zu effizienterem und übersichtlicherem Code.
Ausgaben im Command Window sowie grafische Ausgaben benötigen viel Zeit
und sollten daher nur mit Bedacht eingesetzt werden.
Löschen nicht mehr benötigter Variablen (mit clear Name) gibt dadurch belegten
Speicher wieder frei.
Jeder Aufruf von Skripten oder Funktionen, die in separaten Dateien gespeichert
sind, benötigt zusätzlich Zeit. Wird eine Funktion in einer for-Schleife häufig
aufgerufen, so erhält man schnelleren Code, indem man den Inhalt der Funktion
(entsprechend angepasst) in die Schleife kopiert.
Achtung: Dadurch verschlechtert sich die Lesbarkeit sowie die Wartung des
Codes, daher nur in geschwindigkeitskritischen Stellen verwenden!
Function Handles (@), welche im Wesentlichen mit dem Konzept von
Funktionszeigern in C übereinstimmen, können oft gewinnbringend eingesetzt
werden.
Graphiken mit Matlab
Graphiken können direkt in Matlab erstellt werden!
figure
hold on
hold all
legend(String1,String2,...)
axis equal
set(...) / get(...)
gcf / gca
erzeugt ein neues Figure
schützt ein Fenster vor Überschreiben
schützt ein Fenster vor Überschreiben, die
Linienfarbe wird automatisch gewechselt
fügt der Grafik eine Legende hinzu
Achseneinheit in alle Richtungen gleich lang
Eigenschaften setzen/anzeigen lassen
aktuelles Figure- bw. Achsen-Handle
2D-Plots:
plot(x,y)
plot(y)
subplot(m, n, zaehler)
plottet den Vektor y gegen den Vektor x
plottet den Vektor y gegen dessen Indizes
erstellt ein Figure für m × n - Subplots, 1 ≤ zaehler ≤ m · n
bezeichnet das aktuelle Fenster, wobei zeilenweise gezählt wird.
Allgemeine Syntax:
plot(x,y,’FMS’,...,’Eigenschaft’, ’Wert’)
8
7
6
5
FMS = Farbe Marker Linientyp
z.B. ’r*:’ zeichnet rote gepunktete Linie mit *-Marker
Eigenschaft = ’LineWidth’, ’MarkerSize’,...
4
3
2
1
0
0.2
0.4
0.6
0.8
1
1.2
1.4
1.6
1.8
2
2D-Plot: Beispiel
1
2
3
theta = linspace(-pi,pi,25);
sinus = sin(theta);
kosinus = cos(theta);
4
5
6
7
8
%figure
hold all;
plot(theta,sinus);
plot(theta,kosinus,'-o','MarkerSize',8);
9
10
11
12
13
14
15
16
17
18
19
20
set(gca,'XTick',-pi:pi/2:pi);
set(gca,'XTickLabel',{'-pi','-pi/2','0','pi/2','pi'});
xlabel('-\pi \leq \Theta \leq \pi');
set(gca,'YTick',-1:0.5:1);
title('Plot von sin(\Theta) und cos(\Theta)');
text(-pi/4,sin(-pi/4),'\leftarrow sin(-\pi/4)',...
'HorizontalAlignment','left') ;
legend('Sinus','Kosinus');
axis([-pi pi -1 1]);
axis equal;
box;
2D-Plot: Beispiel
Plot von sin(Θ) und cos(Θ)
Sinus
Kosinus
1
0.5
0
0.5
← sin( π/4)
1
pi
pi/2
0
π≤ Θ ≤ π
pi/2
pi
Der Plot Editor bietet zahlreiche Möglichkeiten der Nachbearbeitung.
Bei Speicherung als .fig ist eine Nachbearbeitung auch später noch möglich.
Export in alle Standardformate möglich: PNG, JPG, EPS, PDF, ...
3D Grafiken: Beispiele
Beispiel: Leastsquare
Gegeben: N Messwerte (x1 , y1 ), . . . , (xN , yN ), die als N × 2-Matrix in einer Textdatei
(ohne Kommentare) gespeichert sind.
Aufgabenstellung: Berechne die zugehörige Bestgerade mit Hilfe der Methode der
kleinsten Quadrate (vergleiche Aufgabe 2 auf Übungsblatt 6).
Ziele: Schreibe eine Funktion, die,
falls nur ein Rückgabewert angefordert wird, nur die Steigung der Geraden
berechnet und diese zurückgibt,
falls zwei Rückgabewerte angefordert werden, die Steigung und den
y -Achsenabschnitt der Geraden zurückgibt,
die Punktewolke und die berechnete Gerade plottet, sofern der Parameter
Plotschalter den Wert 1 besitzt.
Beispiel: Leastsquare
1
2
3
4
5
6
7
8
function [ a,b ] = leastsquare( Dateiname, Plotschalter )
daten = importdata(strcat(Dateiname,'.dat'));
[N,M] = size(daten);
if M ~= 2 disp('Falsche Dimension!'); end;
tquer = sum(daten(:,1))/N;
yquer = sum(daten(:,2))/N;
%alterantiv: s = sum(daten)/N; tquer2 = s(1); yquer2 = s(2);
a = (daten(:,1)-tquer)'*(daten(:,2)-yquer)/sum((daten(:,1)-tquer).^2);
9
10
11
12
13
14
15
16
17
18
19
20
if nargout == 2 || (nargin == 2 && Plotschalter == 1)
b = yquer-a*tquer;
if nargin == 2 && Plotschalter == 1
figure('Name','leastsquare')
hold all
plot(daten(:,1),daten(:,2),'x','MarkerSize',10)
tmin = min(daten(:,1));
tmax = max(daten(:,1));
plot([tmin;tmax],[a*tmin+b;a*tmax+b],'r','LineWidth',2);
end
end
21
22
end
Beispiel: Leastsquare
Datensatz: leastsquare.dat
(siehe Homepage, Übung)
leastsquare(’leastsquare’,1);
weiterer Datensatz
leastsquare(’testdaten’,1);
Erstellen von Filmen mit Matlab
F = getframe(H)
movie(F,n)
Obj = VideoWriter(Dateiname);
open(Obj);
writeVideo(Obj,F)
close(Obj);
Kreieren eine Bildabfolge, Figure H wird in
eine spezielle Struktur F (Frame) gespeichert.
n-maliges Abspielen des Frames F
Erstellung des Objekts Obj, um Videos in eine
.avi-Datei zu schreiben
Öffnen der Datei, welche Obj zugewiesen ist.
Schreibt den Frame in die Obj zugewiesenen Datei.
Schließen der Datei, welche Obj zugewiesen ist.
Beispiel: (primitive) Simulation eines Aktienkurses
Teil 1 des Skriptes: Parameter setzen und Werte berechnen.
1
2
3
4
5
6
%% Parameter
startwert = 100;
tage = 30;
anzahl = 20;
max_schwank = 0.2*startwert;
tendenz = 0.01;
7
8
9
10
11
12
13
14
%% Berechnung
wert = zeros(anzahl,tage+1);
wert(:,1) = startwert;
for k = 1:tage
wert(:,k+1) = wert(:,k)+2*(rand(anzahl,1)-0.5+tendenz)*max_schwank;
end
disp(['geschätzter Endwert: ', num2str(mean(wert(:,tage+1)),'%.2f')]);
Beispiel: (primitive) Simulation eines Aktienkurses
Teil 2 des Skriptes: Movie erzeugen und als aktie.avi speichern.
1
2
3
4
5
6
%% Figure und Movie
figure;
hold on;
axis([1 tage startwert-5*max_schwank startwert+10*max_schwank])
t = linspace(1,tage+1,tage+1);
col = lines(anzahl);
7
8
9
10
11
12
13
14
15
16
mov(anzahl*(tage-1)) = struct('cdata',[],'colormap',[]);
hilf = 0;
for l = 1:anzahl
for k = 2:tage
plot(t(k-1:k),wert(l,k-1:k),'LineWidth',2,'color',col(l,:));
hilf = hilf + 1;
mov(hilf) = getframe(gcf);
end
end
17
18
19
20
21
writerObj = VideoWriter('aktie.avi');
open(writerObj);
writeVideo(writerObj,mov);
close(writerObj);
Beispiel: (primitive) Simulation eines Aktienkurses
Simulation von 30 Aktienkursen
300
250
Wert der Aktie
200
150
100
50
0
5
10
15
Tag
20
25
30
Zugehörige Unterlagen
Herunterladen