Vorlesungsskript zur Lehrveranstaltung
Informatik I
Stand: 19.11.2015
© Klaus Rittmeier 2015
Die Verwendung dieser Lehrunterlagen ist ausschließlich für die Lehrveranstaltung des Autors gestattet.
Links und Literatur:
Vorlesungsskripte, Beispiele, Software-Downloads:
http://www.iks.hs-merseburg.de/~rittmeie
C++ Referenz (Bibliotheksfunktionen): http://www.cplusplus.com/ref
IDE wxDev-C++: http://wxdsgn.sourceforge.net/
IDE Codeblocks: http://www.codeblocks.org/
Bücher:
Dietrich May: Grundkurs Softwareentwicklung mit C++ - Vieweg + Teubner
Peter Prinz, Ulla Kirch-Prinz: C++ Lernen und professionell anwenden - MITP-Verlag
Dietmar Herrmann: Grundkurs C++ in Beispielen - Vieweg + Teubner
Harald Nahrstedt, C++ für Ingenieure- Effizient Programmierten erlernen- Vieweg + Teubner
Doina Logofatu, Algorithmen und Problemlösungen mit C++, Vieweg + Teubner
Helmke/ Isernhagen: Softwaretechnik in C und C++ - Das Lehrbuch - Hanser-Verlag
Inhalt der Lehrveranstaltung
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
Einführung, Arbeitsweise von Computern, Binärdarstellung
Vom Problem zum Programm
Die Programmiersprache C++
Grundbegriffe, Schlüsselwörter und Bezeichner
Datentypen und Variable, Ein- und Ausgabe
Ausdrücke, Anweisungen und Operatoren
Kontrollstrukturen
Programmierprinzipien und Algorithmen (1)
Funktionen
Programmierprinzipien und Algorithmen (2)
Arrays
Algorithmen mit Containern
Einführung in die Objektorientierte Programmierung, Strukturen & Klassen
Die C++-Standardbibliothek
Benutzerdefinierte Klassen
Beziehungen zwischen Klassen und Objekten, Vererbung
-1-
1. Einführung in die Arbeitsweise von Computern
Eingabegerät
Eingabeinformation
Zentralspeicher
Zentralprozessor
Ausgabegerät
Ausgabeinformation
Peripheriesteuerung
Adressbus
Datenbus
Steuerbus
Zentraleinheit
Massenspeicher
Merkmale eines Von-Neumann-Rechners
•
•
•
•
•
•
•
•
•
Es handelt sich um einen Universalrechner, der durch Austausch eines Programmes an die Problemstellung
angepasst werden kann.
Wesentliche Bestandteile eines Von-Neumann-Rechners sind: Zentralprozessor (CPU), Zentralspeicher
(Hauptspeicher), I/O-Einheit und Bussystem.
Der Zentralspeicher besteht aus Zellen gleicher Größe. Jede Zelle besitzt eine eindeutige Adresse.
Der Von-Neumann-Rechner unterscheidet sich von anderen Architekturen unter anderem dadurch, dass der
Zentralspeicher sowohl Programmcode (Befehle) als auch Daten enthält und beides nicht voneinander
getrennt gespeichert wird.
Daten und Befehle werden binär codiert. Der Code ist nicht selbstidentifizierend.
Die CPU übernimmt die Ablaufsteuerung. Sie decodiert den Programmcode und unterscheidet nach der
Decodierung eines Befehles anhand des Kontextes zwischen Programmcode und Daten.
Busse dienen dem Transport von Daten (Datenbus) und Adressen (Adressbus). Bei der praktischen
Realisierung bedarf es auch eines Steuerbusses, der der Funktionssteuerung dient, zum Beispiel die
Umschaltung zwischen Lesen aus dem Speicher und Schreiben in den Speicher.
Die Verbindung zur „Außenwelt“ erfolgt über die I/O-Einheit (Peripheriesteuerung).
Arbeitsprinzip: Single Instruction - Single Data (SISD) - ein Befehl bearbeitet ein Datum.
Moderne Computer realisieren neben dem Von-Neumann-Konzept auch weitere Konzepte, zum Beispiel
Befehlssätze, die mit einem Befehl mehrere Daten bearbeiten (SIMD) und in Form der Multi-CoreArchitekturen die Fähigkeit, mehrere Befehle gleichzeitig zu bearbeiten (MIMD).
-2-
Interne Darstellung/ Codierung
Hauptspeicherinhalt
Befehle
Daten
numerisch
Ganze Zahlen
ohne Vorzeichen
logisch
alphanumerisch
Zeichen
Gleitkommazahlen
Zeichenketten
mit Vorzeichen
einfache Genauigkeit
höhere Genauigkeit
Daten werden nach der Art der zu speichernden Information in unterschiedliche Datentypen eingeteilt:
Numerische, alphanumerische, logische, u.s.w..
Der Datentyp bestimmt die Codierung und die möglichen Operationen
Stellenwertsysteme
Kennzeichen eines Stellenwertsystems (Basis-n-Zahlensystem, g-adisches System):
- Zahlenbasis
- Alphabet (Ziffernvorrat)
Basis Alphabet
Dualsystem
2
0, 1
Oktalsystem
8
0, 1, 2, 3, 4, 5, 6, 7
Dezimalsystem
10
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Hexadezimalsystem
16
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
Die Zahlen 0...15 in den unterschiedlichen Zahlensystemen:
Binär
0000
0001
0010
0011
0100
0101
0110
0111
1000
1001
1010
1011
1100
1101
1110
1111
oktal
0
1
2
3
4
5
6
7
10
11
12
13
14
15
16
17
dezimal
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
hexadezimal
0
1
2
3
4
5
6
7
8
9
A
B
C
D
E
F
--> 01_00_AlleZahlenVonBis
-3-
Merke: Mit n Binärstellen können 2n Zahlen codiert werden.
g-adische Darstellung einer natürlichen Zahl
Beispiel:
• Dezimal (g=10):
57410 = 5*102 + 7*101 + 4*100
• Dual (g=2):
•
•
10001111102 = 1*29 + 1*25 + 1*24 + 1*23 + 1*22 + 1*21
Oktal (g=8):
10768 = 1*83 + 7*81 + 6*80
Hexadezimal (g=16):
23E16 = 2*162 + 3*161 + 14*160
--> 01_01_Zahlenbasisumwandlung
Merke: Ganz egal, in welchem Zahlensystem eine Zahl formuliert wird, die Zahl ist immer dieselbe, nur die
Schreibweise ist unterschiedlich.
Stellenwertsysteme im Vergleich
Dualsystem
+ Zahlen sind direktes Abbild des Speicherinhaltes
- lange Ziffernfolgen (schlecht aufzuschreiben, fehlerträchtig)
Dezimalsystem
+ für den Menschen leicht verständlich
- Umwandlung in‘s Dualsystem „mühsam“
Oktalsystem
+ leicht in‘s Dualsystem konvertierbar
- Zifferngrenzen nicht auf Byte-Grenzen
Hexadezimalsystem
+ leicht in‘s Dualsystem konvertierbar
+ kurze Zahlen
+ Ziffern auf Bytegrenzen
- Anzeige für Buchstaben erforderlich
Kilobyte versus Kibibyte
•
1 Byte = 8 bit [1]
Präfixe: SI-Präfixe zur Basis 10, IEC-Präfixe zur Basis 2
SI-Name
Kilobyte (kB)
Megabyte (MB)
Gigabyte (GB)
Terabyte (TB)
Petabyte (PB)
Exabyte (EB)
Zettabyte (ZB)
Bedeutung
103 Byte
106 Byte
109 Byte
1012 Byte
1015 Byte
1018 Byte
1021 Byte
IEC-Name
Kibibyte
Mebibyte
Gibibyte
Tebibyte
Pebibyte
Exbibyte
Zebibyte
Bedeutung
210 Byte = 1024
220 Byte = 1.048.576
230 Byte = 1.073.741.824
240 Byte = 1.099.511.627.776
250 Byte = 1.125.899.906.842.624
260 Byte = 1,153...* 1018
270 Byte = 1,181... * 1021
Differenz
2,4%
4,9%
7,4%
10%
12,6%
15,3%
18,1%
1. bit ist ein "Kofferwort" aus binary und digit. Dessen Bestandteile lassen sich auf die lateinischen Worte digitus (Finger) und binarius
(zweifach) zurück führen.
Das Wort Byte ist künstlich und stammt vom englischen bit (ein bisschen) und bite (Bissen). Verwendet wurde es, um eine Speichermenge
zu kennzeichnen, die ausreicht, um ein Zeichen darzustellen. Bite wurde zu Byte, um Verwechslungen mit bit zu vermeiden.
-4-
Codierung alphanumerischer Daten
Grundprinzip: Jedem Zeichen wird eine Zahl zugeordnet.
Dezimal:
Hexadezimal:
Oktal:
Dual:
A
S
C
I
I
65
41
101
1000001
83
53
123
1010011
67
43
103
1000011
73
49
111
1001001
73
49
111
1001001
ASCII ist eine Codierungsvorschrift für Zeichen. Dazu wird entsprechend einer standardisierten Tabelle jedem
Zeichen eine Zahl zugeordnet. ASCII verwendet für die Codierung eines Zeichens 7 bit.
Mit 7 bit können 27 Zeichen codiert werden.
Eine eindeutige Codierung aller existierenden Zeichen erfolgt mittels UNICODE. UNICODE codiert Zeichen
mit mit 8 bit, 16 bit, 24 bit oder 32 bit.
Codepage 437, oktal:
bs
ht nl
cr
be
l
ASCII
Erweiterter
ASCII
-->01_03_ASCII-ZeichensatzOktal
-5-
Aufgaben
1.1.
1.2.
1.3.
1.4.
1.5.
1.6.
1.7.
1.8.
1.9.
1.10.
1.11.
1.12.
1.13.
1.14.
1.15.
Welches Merkmal eines Von-Neumann-Rechners unterscheidet ihn von anderen Rechnerkonzepten?
Welche Merkmale kennzeichnen ein Basis-N-Zahlensystem?
Welche Bedeutung hat das Hexadezimalsystem in der Informatik?
Wieviel Zahlen können mit 8 Bit codiert werden?
Wieviel Zahlen können mit 10 Bit codiert werden?
Welcher Unterschied besteht zwischen 1kB und 1KiB?
Welche Codierungsmöglichkeit für alphanumerische Daten kennen Sie?
Welche Probleme gibt es bei der Codierung von Sonderzeichen (z.B. Umlaute) mittels ASCII?
Wieviel Bit sind für die Codierung von 52 Zeichen (26 Groß- und 26 Kleinbuchstaben) erforderlich?
Welchen Standard zur Codierung aller existierenden alphanumerischen Zeichen kennen Sie?
Ordnen Sie die Ergebnisse folgender Problemstellungen den Datenkategorien „numerisch“, „logisch“ und
„alphanumerisch“ zu:
- Suchen des Namens eines Kunden mit der Kundennummer 0815 in einer Datenbank
- Berechnung des Volumens eines geometrischen Körpers
- Umrechnung von Celsius in Fahrenheit
- Ermitteln, ob ein vom Nutzer eingegebenes Jahr ein Schaltjahr ist
- Nullstellenbestimmung für eine Funktion y=f(x)
- Einen Index (Schlagwortverzeichnis) für ein eBook erzeugen
- Ermitteln der aktuellen Studierendenzahl an der FH
- Prüfen, ob ein Student alle Prüfungsvoraussetzungen erfüllt hat
- Notendurchschnitt eines Studierenden ermitteln
Nennen Sie je fünf weitere Beispiele für Problemstellungen, deren Ergebnis typischerweise
numerisch,alphanumerisch oder logisch sind.
Betrachten Sie die numerischen Probleme aus Aufgabe 1.11. und 1.12. Welche dieser Problemstellungen
haben typischerweise ein ganzzahliges und welche ein reelles Ergebnis?
Was bedeutet die hexadezimale ASCII-Folge: 43 2b 2b 20 50 72 6f 67 72 61 6d 6d ?
Wandeln Sie den Code (ohne Zuhilfenahme von Hilfsmitteln) in das Binärformat um.
Codieren Sie den folgenden Satz oktal (Codepage 437): „Wüsste ich das, wäre ich schlauer.“
-6-
2. Vom Problem zum Programm
Mit einem Computer sind nur solche Probleme lösbar, zu denen ein Algorithmus existiert.
Ein Algorithmus ist eine Folge von Anweisungen zur Lösung eines Problemes.
Diese Folge muss folgende Bedingungen erfüllen:
• Allgemeingültigkeit (Die Anweisungen besitzen Gültigkeit für die Lösung einer ganzen Problemklasse, nicht
nur für ein Einzelproblem)
• Ausführbarkeit (Die Anweisungen müssen für den Befehlsempfänger (Mensch oder Maschine) verständlich
formuliert sein und für diesen ausführbar sein.
• Eindeutigkeit (An jeder Stelle muss der Ablauf der Anweisungen eindeutig sein).
• Endlichkeit (Die Beschreibung der Anweisungsfolge muss in einem endlichen Text möglich sein.)
• Terminiertheit (Nach endlich vielen Schritten liefert die Anweisungsfolge eine Lösung des gestellten
Problems.)
Ein Algorithmus, der in einer für den Computer verständlichen Sprache formuliert ist, ist ein Programm.
Da ein Algorithmus allgemeingültig ist, muss er für einen konkreten Fall parametrisiert werden. Parameter
definieren die Eingangsgrößen für einen Algorithmus
Beispiele:
- Berechnung der n-ten Potenz einer Zahl.
Parameter: Basis, Exponent
- Berechnung der Oberfläche eines Zylinders
Parameter: Radius, Höhe
- Übersetzung eines Textes in eine andere Sprache
Parameter: Quelltext, Quellsprache, Zielsprache
Beispiel: Euklidscher [1] Algorithmus zur Berechnung des größten gemeinsamen Teilers zweier natürlicher
Zahlen x und y. Die Parameter sind x und y:
Solange x ungleich y ist, wiederhole folgende Anweisungen:
Wenn x größer y ist, dann
ziehe y von x ab und weise das Ergebnis x zu
anderenfalls
ziehe x von y ab und weise das Ergebnis y zu
Wenn x gleich y ist, dann
ist x (bzw. y) der gesuchte größte gemeinsame Teiler
Ein Algorithmus besteht aus Elementarschritten. Das sind Anweisungen, die sich nicht in weitere Anweisungen
zerlegen lassen, oder die im konkreten Fall nicht weiter zerlegt werden müssen.
Das A und O bei der Formulierung eines Algorithmus ist die Wahl geeigneter Variablen.
Trace-Tabelle
In einer Trace-Tabelle wird für jeden Schritt eines Algorithmus das Ergebnis erfasst.
Beispiel: Verfolgung der Variablen für die Startwerte x=15 und y=6
Verarbeitungsschritt
Ergebnis
Bedingung
x
x <- 15 (Initialisierung)
15
y <- 6 (Initialisierung)
x≠y
wahr
x>y
wahr
x <- x-y
9
x≠y
wahr
x>y
wahr
x <- x-y
3
x≠y
wahr
x>y
falsch
y <- y-x
x≠y
falsch
1. Euklid von Alexandria: Griechischer Mathematiker, 360 v. Chr. bis 280 v. Chr.
-7-
y
6
3
Das Zeichen <- soll eine Zuweisung symbolisieren. Programmiersprachen verwenden dafür unterschiedliche
Symbole, C verwendet das Zeichen = (z.B. x=x-y)
Eine Zuweisung ändert den Wert der Variablen, die auf der linken Seite steht.
Beschreibung sequenzieller Abläufe
Die Abarbeitungsreihenfolge von Anweisungen wird als Kontrollfluss bezeichnet.
Unter einer Kontrollstruktur (Steuerstruktur) versteht man eine Anweisung, welche den Kontrollfluss bestimmt
bzw. beeinflusst.
Es gibt folgende Kontrollstrukturen:
• Sequenz (Abfolge)
• Selektion (Fallunterscheidung)
• Iteration (Wiederholung)
Häufig werden bei der Formulierung eines Algorithmus bestimmte Wörter verwendet. Solche Schlüsselwörter
sind charakteristisch für die Kontrollstrukturen.
Selektion:
Wenn … dann …
Wenn … dann … anderenfalls (ansonsten)
Iteration:
Solange (während) … wiederhole …
Wiederhole … bis …
Für alle … von … bis …
Kontrollstrukturen im Euklidschen Algorithmus:
Die Farben bedeuten:
Iteration
Selektion
Weise x und y die Startwerte zu
Solange x ungleich y ist, wiederhole folgende Anweisungen:
Wenn x größer y ist, dann
ziehe y von x ab und weise das Ergebnis x zu
anderenfalls
ziehe x von y ab und weise das Ergebnis y zu
Zeige den größten gemeinsamen Teiler x an
Kontrollstruktur und Struktogramm
In einem Struktogramm (NASSI-SCHNEIDERMANN-Diagramm)[1] werden Kontrollstrukturen als genormte
Symbole dargestellt. Dadurch werden (sprachliche) Mehrdeutigkeiten vermieden.
Euklid‘scher
initialisiere x und y
x≠y
Iteration
x<y
ja
y=y-x
nei
n
x=x-y
Selektion
x ist größter gemeinsamer Teiler
1. Im Rahmen dieses Lehrmoduls geht es nicht um das Zeichnen von Struktogrammen. Sie sollen lediglich einer „bildlichen“
Veranschaulichung der Algorithmen dienen. Der Studierende soll einfache Struktogramme verstehen lernen.
-8-
Struktogramm und Quelltext
// Euklidscher Alg.
Euklidscher Algorithmus
Eingabe: x, y
x≠y
x<y
ja
nein
y=y-x
x=x-y
Ausgabe: „Größter gemeinsamer Teiler: ", x
cin >> x;
cin >> y;
while(x != y)
{
if(x < y)
y = y-x;
else
x = x-y;
}
cout "ggt: " << x;
Werden ausschließlich die Kontrollstrukturen Sequenz, Selektion und Iteration verwendet, so spricht man von
strukturierter Programmierung und der Entwurf kann 1:1 in ein Programm umgesetzt werden.
--> 02_00_GGT
Beispiel: Wurzelberechnung nach Heron [1]
√a lässt sich nach dem griechischen Mathematiker Heron nach folgendem Algorithmus berechnen:
Setze x=a.
x+
Berechne:
x=
a
x
2
solange sich x innerhalb der gewünschten Genauigkeit noch verändert.
Der Begriff Iteration beschreibt in der Programmierung die wiederholte Ausführung bestimmter Anweisungen.
Der Mathematiker hat eine strengere Definition der Iteration: Er versteht darunter die Wiederholung von
Operationen zur schrittweisen Verbesserung eines Ergebnisses.
Trace-Tabelle für den Algorithmus nach HERON
Startwerte: a=2, geforderte Genauigkeit: 4 Nachkommastellen
Schritt
A
X
X=a
2
2
X=(x+a/x)/2
2
1,5
x=(x+a/x)/2
2
1,4166
x=(x+a/x)/2
2
1,4141
x=(x+a/x)/2
2
1,4142
Änderung?
ja
ja
ja
Nein
Probleme bei der Umsetzung in ein Programm:
Problem 1: Wenn x neu berechnet worden ist, ist das Ergebnis aus dem Schritt davor
nicht mehr verfügbar. Um das Ende des Algorithmus bestimmen zu können, muss aber das letzte mit dem
vorletzten Ergebnis verglichen werden.
Lösung: x aus der vorletzten Zeile in einer Variablen (xalt) merken, bevor x neu berechnet wird.
Problem 2: Wie wird die Änderung einer bestimmten Nachkommastelle festgestellt?
Lösung: Es wird die Differenz zweier aufeinanderfolgender Ergebnisse (x - xalt) gebildet.
Deren Betrag wird mit einem bestimmten Schwellwert (z.B. 0,0001) verglichen:
x − xalt ≥ ε
1. Heron von Alexandria: Griechischer Mathematiker und Ingenieur, vermutl. 1. Jh.
-9-
Struktogramm und Quelltext
ε = 10-4
x=a
epsilon=1e-4;
x=a;
xalt = x
a
x+
x
x=
2
do
{
xalt=x;
x=(x+a/x)/2;
}
while(fabs(x-xalt)>=epsilon);
|x-xalt| >= ε
--> 02_01_Heron
Aufgaben
2.1.
2.2.
2.3.
2.4.
2.5.
2.6.
2.7.
2.8.
Was verstehen Sie unter einem Algorithmus?
Nennen Sie Beispiele für Algorithmen und geben Sie deren Parameter an.
Gegeben sei folgende Aufgabenstellung:
In dem Buch „Grundlagen der Programmierung“ von August Klammer soll die Häufigkeit des Wortes
„Schleife“ bestimmt werden.
Parametrisieren Sie einen möglichen Algorithmus so, dass er möglichst allgemeingültig ist.
Gegeben sei ein Algorithmus zur Bestimmung der Nullstelle einer Funktion f(x) im Intervall (a,b).
Nennen Sie mögliche Parameter für diesen Algorithmus.
Schreiben Sie eine Trace-Tabelle für den Algorithmus „Größter gemeinsamer Teiler“ (siehe Vorlesung)
mit der Anfangsbelegung x=56 und y=77 für die beiden Variablen.
Es seien a und b natürliche Zahlen, und sei c der größte gemeinsame Teiler von a und b, oder in anderen
Worten c = ggT(a,b). Dann gilt: (Kreuzen Sie die richtige Lösung an)
( )
Es gibt natürliche Zahlen x und y, so dass a=cx und b=cy gelten.
( )
c ist die größte natürliche Zahl mit c ≤ a/b.
( )
Für alle natürlichen Zahlen z gilt c ≤ z.
( )
Der Bruch a/b hat keine Nachkommastellen.
( )
Die Ziffern von c stimmen mit den Ziffern von a und b bis auf die Reihenfolge überein.
( )
ggT(a,b) = ggT(b, a mod b) für b>0
( )
für alle natürlichen Zahlen n (n≠0) gilt: ggT(a,b) = ggT(na, nb)
Welchen Wert hat i am Ende des folgenden Algorithmus:
i=1
solange i < 10 wiederhole
i = 2i + 1
Welche Werte haben die Variablen A, B und C am Ende der Ausführung folgender Algorithmen?
a)
A = 1; B = 1; C = 1
solange C < 8 wiederhole
A=A+B
B=A–B
C=C+1
b)
A = 1; B = 1; C = 1
wiederhole:
C=C+1
A = 2A - 3B
solange C <= 10
c)
A = 1; B = 1
Für alle i von 1 bis 7 wiederhole
A=A+B
B=A-B
- 10 -
2.9.
2.10.
2.11.
Zeigen Sie, dass der folgende Algorithmus nicht immer zu Ende geht:
x=n
solange x > 1 wiederhole
ist x gerade dann
x = x/2
sonst
x = 5x + 1
Hinweis: Es reicht, einen Fall zu finden, bei dem der Algorithmus nicht zu Ende geht, dann hat man
gezeigt, dass er nicht immer zu Ende geht.
Der folgende Algorithmus ermittelt den Wert einer Funktion f(x) für ein gegebenes Argument x:
x als Parameter übergeben
1 hinzuaddieren
das Ergebnis zur dritten Potenz nehmen
1 subtrahieren
durch x-1 dividieren
Ergebnis zurückgeben
a) Formulieren Sie die Funktion f(x).
b) Für welche x ist sie definiert?
c) Wie kann man den Algorithmus modifizieren, damit verhindert wird, dass durch 0 dividiert wird?
Wie groß ist E, wenn der folgende Algorithmus terminiert?
E=1
A=2
C=0
E <= 20
A=2*A
C=A+E
E=A*C
2.12.
Beschreiben Sie für jede der folgenden Aufgaben einen Algorithmus, der die Aufgabe löst.
Beschreiben Sie explizit die primitiven Operationen, die Sie benutzen.
a)
n Zahlen aufaddieren
b)
n Zahlen miteinander multiplizieren
Da n beliebig groß sein kann, ist es nicht praktikabel, für jede Zahl eine einzelne Variable zu benutzen
(etwa a, b, c, d, e, ...). Orientieren Sie sich deshalb bei der Variablenbezeichung an der Symbolik der
Matematik: x1, x2, x3, ... bzw. allgemein: xi
- 11 -
3. Die Programmiersprache C++
COBOL
...
PL/1
...
Modula
Simula
BASIC
C ++
C
...
Prolog
...
ADA
PASCAL
ALGOL
FORTRAN
1960
1970
J2EE
Servlets
SmallTalk
1980
ASP.net
...
.net
...
Python
C#
Java
1990
1990
2010
2000
- 12 -
Objective-C
C++11
Eigenschaften von C++
Wenn im Folgenden über C++ gesprochen wird, schließt das im Allgemeinen auch die Sprache C mit ein.
Lediglich die Programmierparadigmen objektorientiert und generisch werden von C nicht unterstützt.
Insofern kann man C als eine Untermenge zu C++ betrachten.
•
•
•
•
•
C++ ist eine höhere Programmiersprache, weil sie maschinenunabhängig ist.
Trotzdem unterstützt C++ auch maschinennahe Programmierung, indem z.B. Bitmanipulationen und Zeiger
(Adressen) unterstützt werden.
C++ ist eine universelle Programmiersprache (general purpose language), weil sie nicht auf spezielle
Anwendungsfälle zugeschnitten ist. (Gegenteil: domain specific language)
C++ ist eine imperative Programmiersprache, d.h. es wird zwischen Befehlen und Daten unterschieden.
(Gegenteil: Deklarative Sprache)
C++ ist eine typisierte Sprache, weil für unterschiedliche Arten von Daten (Informationen) verschiedene
Datentypen zur Verfügung stehen. Darüber hinaus können eigene Datentypen definiert werden.
C++ ist eine Multiparadigmen-Sprache, die dem Programmierer sehr viele Freiheiten lässt. C++ unterstützt
die folgenden Programmierprinzipien (Paradigmen):
- Prozedurale Programmierung
- Modulare Programmierung
- Strukturierte Programmierung
- Programmierung mit abstrakten Datentypen
- Objektorientierte Programmierung
- Generische Programmierung
Hochsprache und Maschinensprache
C/C++
Summe = Summe + 5;
Compiler
MOV AX, Summe
Assemblersprache
ADD
AX, 05h
MOV Summe, AX
Maschinensprache
1010000
1
0000000
0
1. Befehl
•
•
•
0100000
1
0000010
1
0000010
1
2. Befehl
0000000
0
1010001
1
0000000
0
3. Befehl
0100000
1
Maschinensprache ist der spezielle Binärcode für einen bestimmten Prozessor.
Assemblersprache ist Maschinensprache in besser lesbarer Form (Mnemonics).Eine
Hochsprache (z.B. C++) abstrahiert von einer konkreten Maschine. Die Übersetzung des Quelltextes in
Maschinensprache erledigt ein Compiler.
- 13 -
Vom Quelltext zum ausführbaren Programm
Start
Editor
Compiler
Eingabe
*.cp p
* .h
* .c p p
j
a
Syntaxfehl
nei
Linker
j
a
Bindefehl
B i b lio th e k
*.obj
S p e ic h e r u n g
*.obj *.lib
*.exe
*.exe
nei
Run-Executor
j
Integrated
Development
Environment
Laufzeitfehl
nei
Fertig
Struktur eines C++-Programmes
/* Das Programm gibt einen Text auf dem Bildschirm aus
und fordert den Benutzer zu einer Eingabe auf */
Kommentare
#include#include <cstdlib>
Anweisung
#include <iostream>
main()
using namespace std;
Hauptproint main()
gramm
{
Zeichenkette
string text;
cout << "Das ist ein erstes C-Programm." << endl;
cout << "Was denken Sie darüber? ";
Block
cin >> text;
cout << "Aha, also " << text << endl;
system("pause");
// warten auf Taste
return EXIT_SUCCESS;
Semikolon
}
--> 03_00_ErstesProgramm
•
Ein Programm wird schrittweise in der Reihenfolge der Notation ausgeführt.
- 14 -
Struktogramm und Programmablauf
int main()
{
int jahr;
double umsatz;
eroeffnung();
do
{
jahr=eingabe();
umsatz=schaetzung(jahr);
ausgabe(jahr,umsatz);
}
while(nochmal());
return 0;
}
eroeffnung()
jahr = eingabe()
umsatz = schaetzung(jahr)
ausgabe(jahr,umsatz)
nochmal()
--> 03_01_Prognose_01
Merke: Der Programmablauf kann durch Anweisungen beeinflusst werden.
Was in diesem Lehrmodul von der Sprache C/C++ behandelt wird und was nicht:
Bei diesem Lehrmodul handelt es sich nicht um eine Programmiersprachenausbildung.
Die Sprache C/C++ wird als Werkzeug benutzt, um die Grundprinzipien der Algorithmik und der
Programmierung zu vermitteln. Deshalb werden nur solche Sprachelemente behandelt, die unverzichtbar sind,
um einen Algorithmus zu implementieren und allgemeingültige Prinzipien verschiedenster Programmiersprachen
repräsentieren.
Nicht behandelt werden deshalb:
• Bitoperationen, weil sie im Wesentlichen der maschinennahen Programmierung dienen,
• Zeiger, weil es sich um eine C-Spezialität handelt,
• Spezielle Steuerstrukturen (Bedingungsoperator, switch-Anweisung), weil man ohne sie auskommt,
• Ein-/ Ausgabe in ANSI-C, weil die Ein-/ Ausgabe in C++ einfacher ist,
• ANSI-C-Zeichenketten und deren Verarbeitung, weil die C++-Klasse string besser ist,
• bestimmte Konzepte wie z.B. Exception Handling, Polymorphie, generische Programmierung, weil sie
den Rahmen dieses Moduls sprengen und Bestandteil weiterführender Module sind
• GUI-Programmierung (grafische Benutzeroberfächen), weil die grundlegenden Prinzipien ohne GUI
auskommen und beherrscht werden müssen, bevor es an die GUI-Programmierung geht.
• Die Programmierung eigener Klassen wird nur angerissen. Der Studierende soll zunächst in die Lage
versetzt werden, existierende Klassen zu verwenden. Eigene Klassen zu programmieren ist Bestandteil
eines weiterführenden Moduls.
- 15 -
Aufgaben
3.1.
3.2.
3.3.
3.4.
3.5.
3.6.
3.7.
3.8.
3.9.
Warum ist C++ eine „höhere Programmiersprache“?
Warum ist C++ eine „typisierte Sprache“?
Nennen Sie die Stufen der Programmerstellung von der Eingabe des Quelltextes bis zum ausführbaren
Programm!
Welche Aufgabe hat ein Compiler?
Was verstehen Sie unter einer IDE?
Was ist der prinzipielle Unterschied zwischen einer Assemblersprache und einer Hochsprache?
Legen Sie im Verzeichnis D:\USER ein Unterverzeichnis mit Ihrem Namen an. Bitte keine
Sonderzeichen oder Leerzeichen im Verzeichnisnamen verwenden!
Beispiel: D:\USER\KMUELLER
Kopieren Sie das Verzeichnis „03_00_ErstesProgramm“ samt aller darin enthaltenen Dateien aus dem
Beispielverzeichnis der Vorlesung in dieses Verzeichnis.
Laden Sie das Projekt in die IDE durch Doppelklick auf die Datei ErstesProgramm.dev (für wxDevC++) bzw. ErstesProgramm.cbp (für CodeBlocks)
Übersetzen und starten Sie das Programm.
Nehmen Sie Variationen vor: Ändern Sie die Zeichenketten, die vom Programm ausgegeben werden.
Duplizieren Sie einzelne Zeilen im Quelltext oder ändern Sie deren Reihenfolge und testen Sie die
Auswirkung Ihrer Änderungen.
Machen Sie sich mit den wichtigsten Hot-Keys der IDE vertraut und testen Sie sie. Notieren Sie sich
diese und prägen Sie sie sich ein:
- Rückgängig (Undo)
- Ausschneiden
- Kopieren
- Einfügen
- Kompilieren
- Kompilieren und Ausführen
Kopieren Sie das Projekt 03_02_Wurf aus dem Beispielverzeichnis in Ihr lokales Arbeitsverzeichnis
(D:\User\IhrName).
Sinnvollerweise legen Sie sich noch eine Sicherungskopie davon an, damit Sie ggf. nach missglückten
Änderungen den Ausgangszustand wieder herstellen können.
Übersetzen und starten Sie das Projekt.
Versuchen Sie nun, das Programm soweit zu verstehen, dass Ihnen der Sinn der einzelnen Anweisungen
klar wird.
Lösen Sie durch Modifizierungen im Quelltext des Programms folgende Aufgabe:
Das Programm soll eine Umrechnung von US$ in Euro vornehmen. Der Nutzer soll nach Aufforderung
den Umrechnungskurs und den US$-Betrag eingeben und das Programm gibt den entsprechenden EuroBetrag aus. Anschließend wird der Nutzer gefragt, ob er das Programm beenden möchte. Wenn er das
nicht möchte, kann er eine weitere Umrechnung vornehmen, wobei der Kurs nicht neu eingegeben
werden muss.
Hinweise:
Achtung: In C-Programmen steht anstatt des Dezimalkommas der Dezimalpunkt. Auch bei der Eingabe
von Zahlen muss der Nutzer den Dezimalpunkt verwenden!
Passen Sie zunächst die Texte (in Gänsefüßchen stehende Zeichenketten) im Programm an die
geänderte Aufgabenstellung an.
Versuchen Sie nun, die Formel für die Umrechnung im Programm zu finden und entsprechend der
geänderten Aufgabenstellung zu ändern.
- 16 -
4. Grundbegriffe
Syntax beschreibt die zulässigen Folgen der lexikalischen Elemente (Zeichen, Wörter), die der Compiler versteht
und übersetzen kann. Sie ist also das Regelwerk für die korrekte Schreibweise von Anweisungen.
Der Compiler erkennt und meldet Syntaxfehler.
Semantik beschreibt die Bedeutung der Anweisungen, welche Aktionen das Programm ausführt, wie die
eingegebenen Daten verarbeitet werden etc.
Bei semantischen Fehlern kann der Compiler bestenfalls warnen.
Schlüsselwörter sind reserviert und können vom Programmierer nur in der festgelegten Bedeutung benutzt
werden. Sie bilden sozusagen den Grundwortschatz der Programmiersprache.
Die folgenden Schlüsselwörter werden im Rahmen dieses Moduls verwendet:
bool
char
class
const
do
double
else
false
for
if
int
private
public
return
struct
true
void
while
Schlüsselwörter werden in der IDE besonders hervorgehoben (Syntax-Highlight).
Eine Programmiersprache kann aus nur wenigen Schlüsselwörtern bestehen und trotzdem mächtig sein. (C++
besitzt insgesamt 65 Schlüsselwörter.)
Alle anderen Wörter (Nicht-Schlüsselwörter) sind vom Programmierer definierte Bezeichner. Dies sind vom
Programmierer definierte, eindeutige Namen für alle Objekte eines Programmes (Typen, Variable, Funktionen).
Bezeichner sind eine Folge von Buchstaben und Ziffern. Das erste Zeichen muss ein Buchstabe sein. Der
Tiefstrich zählt als Buchstabe.
main, i, flaeche, tag_2, _temp, zinsProJahr
Groß- und Kleinschreibung ist signifikant. (C++ hat eine case-sensitive Syntax.)
Temperatur und temperatur sind zwei verschiedene Bezeichner.
Sonderzeichen (erweiterter ASCII) und Interpunktionszeichen sind unzulässig.
Falsch: Schätzung, code-page
Ratschläge:
- Mischen Sie nicht wahllos Groß- und Kleinschreibung. Schreiben Sie am besten alles klein.
- Bezeichner sollten selbsterklärend sein: radius, umfang, flaeche
- Gewöhnen Sie sich für Bezeichner, die mehrere Wörter enthalten eine einheitliche Schreibweise an.
Gebräuchlich sind die folgenden Schreibweisen:
zins_pro_jahr, anzahl_studenten
alternativ: zinsProJahr, anzahlStudenten
Ein Kommentar ist erläuternder Text, den der Programmierer seinem Programm als Verständnishilfe mitgibt. Ein
Kommentar wird vom Compiler ignoriert.
Kommentare
•
geben Erläuterungen zur Bedeutung von Variablen und Funktionen,
•
erklären schwer verständliche Programmteile,
•
beschreiben Schnittstellen von Funktionen,
•
gliedern längere Programme optisch.
Kommentare in ANSI-C werden durch /* und */ eingeschlossen.
/*
ANSI-C-Kommentar
über mehrere Zeilen
*/
C++-Kommentare beginnen mit // und enden am Zeilenende.
// ein C++-Kommentar
- 17 -
Verschachtelte C-Kommentare
/* ...
/* ... */
... */
sind nicht bei jedem Compiler zulässig.
Allerdings können problemlos C++-Kommentare in von C-Kommentaren eingeschlossen werden:
/*
...
//
...
//
*/
Aufgaben
4.1.
4.2.
4.3.
4.4.
4.5.
4.6.
4.7.
4.8.
4.9.
4.10.
Was verstehen Sie unter Syntax und Semantik einer Programmiersprache?
Welche Bedeutung hat der doppelte Schrägstrich (//) in einem C++-Programm?
Wie wird in C/C++ ein mehrzeiliger Kommentar gekennzeichnet?
Was ist ein Schlüsselwort? Nennen Sie fünf Beispiele für Schlüsselwörter!
In welcher der folgenden Zeilen stehen ausschließlich Schlüsselwörter:
(A)
else
while
return
(B)
main
if
double
(C)
int
class
system
(D)
include
for
pointer
Nach welchen Regeln werden Bezeichner gebildet?
Welche der folgenden Bezeichner sind korrekt, welche nicht? Begründen Sie, warum Bezeichner nicht
korrekt sind.
Anfangsbuchstabe
standard-abweichung
möglichkeit
_entweder_oder
zweiZuEins
Darf ich eine Variable for nennen? (Begründung)
Laden Sie den Quelltext zum Programm 03_01_Prognose_01 in den normalen Windows-Texteditor (zu
finden im Startmenü unter Zubehör).
Schreiben Sie hinter jede Quelltextzeile einen Kommentar mit den Schlüsselwörtern, die in der
jeweiligen Zeile enthalten sind. Beispiel:
double a0=2.214;
// double
Überprüfen Sie Ihre Lösung, indem Sie das Programm in der IDE öffnen. Dort werden die
Schlüsselwörter farblich hervorgehoben.
Welche der folgenden Bezeichner sind legal, welche sind illegal? (Begründung)
einsplus
wahr
true
eins-minus
x123
nullachtfünfzehn
0815
Übertrag
NeuesGuthaben
x_12
_12
- 18 -
5. Datentypen und Variablen, Ein- und Ausgabe
Variablen [1] (auch Variable ist korrekt) sind benannte Hauptspeicherplätze für Daten. Es sind Platzhalter für
Werte, die zur Programmausführung verwendet werden und verändert werden können.
Eine Variable besitzt einen Datentyp, einen Namen (Bezeichner), einen Wert und eine Adresse.
Es gibt unterschiedliche Datentypen für Variablen. Der Datentyp richtet sich nach der Art der Information, die in
der Variablen gespeichert werden soll.
Mit dem Datentyp sind der Speicherplatzumfang, die Codierung und die mit den Daten möglichen Operationen
verbunden.
Beispielsweise sind mit dem Typ int (ganze Zahl) andere Operationen möglich als mit dem Typ double
(reelle Zahl).
Hauptspeicherinhalt
Befehle
Daten
logisch
bool
numerisch
Ganze Zahlen
ohne Vorzeichen
unsigned int
Gleitkommazahlen
Zeichenketten
string
Zeichen
char
mit Vorzeichen
int
einfache Genauigkeit
float
Datentyp
(Schlüsselwort)
bool
char
int
float
double
alphanumerisch
Speicherplatz
(bit)
1
8
32
32
64
höhere Genauigkeit
double
Wertevorrat
von
bis
0
1
000000002
111111112
-2 147 483 648 2 147 483 647
3.4*1038
-3.4*1038
308
-1.79*10
1.79*10308
Genauigkeit
(Mantissenstellen)
7
14
Der Datentyp int ist das so genannte Maschinenwort. Deshalb ist der Wertebereich unter anderem von der
Plattform (Maschine) abhängig, für die das Programm übersetzt wurde.
int wird zwar häufig, jedoch nicht ausschließlich mit 32 Bit kodiert.
Da der Typ float für reelle Zahlen nur eine Genauigkeit von 7 Mantissenstellen hat, sollte der Typ double
bevorzugt werden. Im Übrigen rechnen alle mathematischen Bibliotheksfunktionen mit double-Genauigkeit.
1. Mathematik: Unbekannte, Unbestimmte, Veränderliche
Grammatik: die/eine Variable; der/einer Variablen oder Variable, die Variablen/zwei Variable oder Variablen
- 19 -
Variablendefinition [1]
Jede Variable muss genau einmal definiert werden. Die Definition reserviert Speicherplatz für die Variable.
Es sind Datentyp und Bezeichner (Name der Variablen) festzulegen:
int
anzahl;
double
radius;
char
buchstabe;
Die Definition einer Variablen sollte normalerweise am Beginn einer Funktion erfolgen (nach der geschweiften
Klammer auf). Es handelt sich dann um eine lokale [2] Variable.
2. Es gibt auch globale (im gesamten Programm gültige) Variablen. Sie werden außerhalb der Funktionen definiert. Deren Verwendung ist
jedoch fehlerträchtig und schlechter Programmierstil und deshalb tunlichst zu vermeiden.
Mehrere Variablen vom gleichen Typ können mit einer Anweisung definiert werden.
int main()
{
int zahl, nummer;
double radius, umfang, flaeche;
...
}
Der Gültigkeitsbereich einer lokalen Variablen erstreckt sich von der Definition bis zum Ende des Blockes
(geschweifte Klammer zu), in dem sie definiert wurde.
Ein- und Ausgabe
Die Anzeige im Konsolenfenster ist die sogenannte Standardausgabe, die Tastatur ist die Standardeingabe.
Damit Standardausgabe und -eingabe verwendet werden können, wird folgender Code am Beginn des
Programmes eingefügt:
#include <iostream>
#include <iomanip>
// nur für Manipulatoren (s.u.)
using namespace std;
Die Standardeingabe heißt in C++ cin (console input).
Mittels des Eingabeoperators >> wird von cin gelesen.
Rechts des Operators >> muss eine Variable stehen.
cin >> x;
Die Standardausgabe heißt in C++ cout (console output).
Mittels das Ausgabeoperators << wird auf cout geschrieben.
Rechts des Operators << kann ein beliebiger Ausdruck stehen (siehe Kap. 6).
cout << "x=";
cout << x;
Vor jeder Eingabe sollte eine Eingabeaufforderung (Prompt) an den Nutzer ergehen.
cout << "Radius: ";
cin >> r;
Bei der Standardeingabe handelt es sich um eine gepufferte Eingabe, d.h. alle Eingabedaten landen zunächst in
einem Pufferspeicher und werden erst nach Drücken der Enter-Taste in die Variablen geschrieben.
Es werden bei einer Eingabe immer nur so viele Zeichen aus dem Eingabepuffer entnommen, wie für den jeweils
einzulesenden Datentyp gelesen werden können. Werden mehr Zeichen eingegeben, so verbleiben die restlichen
Zeichen im Eingabepuffer.
1. Man unterscheidet in der Programmierung zwischen Deklaration und Definition.
Eine Deklaration ist eine Festlegung (Bekanntmachung für den Compiler), die es erlaubt, diese Variable (oder Funktion) im Programm zu
verwenden.
Die Definition (einer Variablen oder einer Funktion) ist ein Sonderfall der Deklaration.
Man spricht von Definition, wenn der Compiler Code erzeugt, der Speicherplatz belegt.
- 20 -
Kommt im Eingabepuffer ein unpassendes Zeichen vor, so wird die Eingabe an dieser Stelle beendet.
Beispiel: Ein häufiger Fehler beim Eingeben einer Gleitkommazahl ist folgender:
Statt Dezimalpunkt (12.34) gibt der Nutzer ein Komma ein (12,34). Folge: Es wird nur die Zahl vor dem Komma
(12) eingelesen, der Rest (,34) verbleibt im Puffer.
Führende white spaces (Leerzeichen, Tabulatoren) werden ignoriert, white spaces mitten in einer Eingabe
verbleiben im Puffer, einschließlich aller darauf folgenden Zeichen.
Der Eingabepuffer kann mit folgender Anweisungsfolge geleert werden:
cin.clear();
// evtl. Fehler löschen
cin.sync();
// Puffer leeren
Auf die Standardausgabe wird jeweils der Wert eines Ausdruckes in einem voreingestellten Format ausgegeben.
double u = 223.0265735;
cout << u;
// 223.03
Ausgaben auf cout können verkettet werden.
cout << "Spannung: " << u << " mV" << endl;
Mittels so genannter Manipulatoren, die auf cout ausgegeben werden, kann die Ausgabe, insbesondere das
Ausgabeformat, verändert werden.
endl ist ein solcher Manipulator. Er bewirkt, dass der Textcursor an den Anfang einer neuen Zeile gesetzt wird.
Ein- und Ausgabe können niemals gemeinsam in einer Anweisung vorkommen.
Ausgabeformatierung mittels Manipulatoren
double x = 12.3456789;
cout << "x=" << x << endl; // default-Format
Die Ausgabebreite wird mittels setw gesetzt. Es wird mit Leerzeichen aufgefüllt.
setw beeinflusst immer nur die unmittelbar folgende Ausgabe.
cout << "x=" << setw(20) << x << endl;
Die Ausgabegenauigkeit (Anzahl der Mantissenziffern) wird mittels setprecision definiert.
setprecision wirkt bis auf Widerruf.
cout << "x=" << setprecision(10) << x << endl;
Man kann auch das Festkommaformat mittels fixed einstellen, Dann definiert man mit setprecision die
Anzahl der Nachkkommastellen. fixed wirkt bis auf Widerruf (resetiosflags).
cout << fixed << setprecision(2);
cout << "x=" << x << endl;
Vom Festkommaformat gelangt man zurück zum Fließkommaformat mittels
resetiosflags(ios::floatfield):
cout << resetiosflags(ios::floatfield) << setprecision(6);
cout << "x=" << x << endl;
--> 05_01_iomanip
Manipulator
setw(n)
setprecision(n)
Beschreibung
Setzt die minimale Größe der direkt folgenden Ausgabe
Gibt die Zahl der Mantissenstellen für Gleitkommadarstellung oder die
Zahl der Nachkommastellen für Festkommadarstellung an
left
Linksbündige Ausgabe
right
Rechtsbündige Ausgabe
fixed
Festkommaschreibweise
scientific
Exponentialschreibweise
resetiosflags(ios::floatfield) Zurück zur normalen Gleitkommadarstellung
boolalpha
Boolesche Werte werden als true bzw. false angezeigt (anstatt 1 oder 0)
endl
Zeile beenden und Ausgabepuffer leeren
- 21 -
Initialisierung von Variablen
Lokale Variablen werden nicht automatisch initialisiert. Sie enthalten nach der Definition willkürliche Werte.
Variablen können zum Zeitpunkt der Definition mit einem typgerechten Anfangswert belegt (initialisiert)
werden.
int beschaeftigte = 0;
bool ende = false;
const double pi = 3.141592654;
const string copyright = "\270 Klaus Rittmeier";
const ist ein so genannter Modifikator und bedeutet, dass die Variable zur Laufzeit nicht verändert werden
darf. Es handelt sich damit um eine Konstante.
Konstanten müssen initialisiert werden.
Literale und Konstante
Ein Literal ist ein konstanter Wert, der im Quelltext steht.
"Umsatzschaetzung", 0.417621
Literale sollten nur verwendet werden
- für einmalig auftretene konstante Werte,
- zur Initialisierungen von Variablen.
Konstanten sollten einen Bezeichner (Namen) bekommen
- für mehrfach auftretende konstante Werte,
- zur besseren Lesbarkeit des Quelltextes.
const double pi = 3.141592654;
In der folgenden Anweisung wird der Name anstatt des Literals verwendet:
umfang = pi * durchmesser;
Einige mathematische Konstanten sind bereits vordefiniert.
Dafür muss am Beginn des Programmes cmath inkludiert werden:
#include <cmath>
Beispiele sind:
M_PI
// Kreiskonstante Pi
M_E
// Eulersche Zahl
Ganze Zahlen
Datentyp: int (short int, long int, long long)
integer: engl. für ganze Zahl.
Ganze Zahlen können im Dezimalformat [1] mit und ohne Vorzeichen angegeben werden:
1357, -19097, +45
Rechnungen mit ausschließlich ganzen Zahlen ergeben immer ein ganzzahliges Ergebnis:
int x=10, y=6, q, r;
q = x/y; // q=1
r = x%y;
// r=4 (x mod y, Divisionsrest)
Reelle Zahlen
Datentyp: double (float, long double)
floating point: engl. für Fließkomma, double für doppelte Genauigkeit
Reelle Zahlen können im Gleitkommaformat (Fließkommaformat) mit oder ohne Exponent angegeben werden.
-75.0645, 0.456, 34.54E-9, -1.456e12
Man beachte den Dezimalpunkt an Stelle des (üblichen) Kommas. Auch bei der Eingabe über cin muss der der
Nutzer den Dezimalpunkt verwenden.
1. C++ akzeptiert auch ganze Zahlen im Oktal- und im Hexadezimalformat. Dies wird jedoch im Rahmen dieses Moduls nicht benötigt.
Literale im Oktalformat beginnen mit einer führenden ‚0‘, z.B. 0123
Literale im Hexadezimalformat beginnen mit einem führenden ‚0x‘, z.B. 0xF03A
Standardein- und ausgabe benutzen normalerweise das Dezimalformat. Dies kann mittels Manipulatoren geändert werden.
- 22 -
Eine Rechnung, in der mindestens ein Operand reell ist, ergibt ein reelles Ergebnis.
Allerdings gibt es dabei einige Fallstricke zu beachten:
int x=10, y=19;
double a=3.45;
x/y*a ergibt 0.0, weil x und y ganze Zahlen sind und der Ausdruck von links nach rechts abgearbeitet wird.
(siehe dazu Kapitel 6: Operatoren)
Überlauf und Unterlauf
Sowohl ganze als auch reelle Zahlen haben einen beschränkten Wertebereich.
Wird dieser Bereich als Ergebnis einer Rechnung überschritten, so spricht man von einem Überlauf.
int i = 2000000000;
i = 2*i;
Fließkommazahlen haben aufgrund der beschränkten Genauigkeit eine „Lücke“ um die 0.
Wird die kleinstmögliche darstellbare Zahl unterschritten, so spricht man von einem Unterlauf. Deshalb kann das
Ergebnis einer Rechnung 0 sein, obwohl es mathematisch ungleich 0 ist.
double f;
f = 1.0 / 1e309;
Da es sich um Laufzeitfehler handelt, kann ein Überlauf oder ein Unterlauf nicht vom Compiler erkannt und
gemeldet werden.
Ein Zahlenbereichsüberlauf kann nicht-vorhersehbare Folgen haben.
Logische Werte (Boolesche Werte)
Datentyp: bool (nur C++)
Eine Variable vom Typ bool kann die Werte true und false annehmen.
bool ende = false;
Alternativ zum Wert true kann der Wert 1 und alternativ zum Wert false kann der Wert 0 verwendet
werden.
bool ende = 0;
Die Konsolenausgabe (cout) gibt logische Werte standardmäßig als 0 bzw. 1 aus, die Konsoleneingabe (cin)
kann nur 0 oder 1 als logischen Wert einlesen.
Der Manipulator boolalpha erzwingt die alphanumerische Ausgabe eines booleschen Wertes.
Für die Konsoleneingabe gibt es jedoch keinen entsprechenden Manipulator.
cout << boolalpha << ende << endl;
Zeichen
Datentyp: char (unsigned char)
Alle darstellbaren Zeichen, Steuerzeichen und Sonderzeichen des erweiterten ASCII-Zeichensatzes (Codes 0 bis
255 bzw. 0x00 bis 0xFF).
Als Literale (also im Quelltext) werden Einzelzeichen in Hochkommas gesetzt.
'A'
Als Literal können Zeichen im Klartext angegeben werden, deren ASCII zwischen 32 (Leerzeichen) und 127
liegen.
Alle anderen Zeichen (z.B. Umlaute) sind Sonderzeichen und müssen als Literal in einer speziellen Art und
Weise angegeben werden.
- 23 -
Rechnen mit Zeichen
Da Zeichen als Zahl gespeichert werden (ASCII), kann mit Zeichen auch gerechnet werden.
32
40
48
56
64
72
80
88
96
104
112
120
33
41
49
57
65
73
81
89
97
105
113
121
(
0
8
@
H
P
X
`
h
p
x
'A'
'Z'
'b'
'a'
+
–
-
!
)
1
9
A
I
Q
Y
a
i
q
y
34
42
50
58
66
74
82
90
98
106
114
122
"
*
2
:
B
J
R
Z
b
j
r
z
35
43
51
59
67
75
83
91
99
107
115
123
#
+
3
;
C
K
S
[
c
k
s
{
36
44
52
60
68
76
84
92
100
108
116
124
$
,
4
<
D
L
T
\
d
l
t
|
37
45
53
61
69
77
85
93
101
109
117
125
%
5
=
E
M
U
]
e
m
u
}
38
46
54
62
70
78
86
94
102
110
118
126
&
.
6
>
F
N
V
^
f
n
v
~
39
47
55
63
71
79
87
95
103
111
119
127
'
/
7
?
G
O
W
_
g
o
w
1 ergibt 'B'
25 ergibt 'A'
'a' ergibt 1
'A' ergibt 32
Das macht man sich zunutze zur Umwandlung on Groß- in Kleinschreibung bzw. umgekehrt:
'K' + 32 ergibt 'k' (Umwandlung groß - klein)
's' - 32 ergibt 'S' (Umwandlung klein - groß)
Sonderzeichen
Sonderzeichen (Oktalcode ab 200) werden im Quelltext durch einen Backslash ‚\‘ eingeleitet und im Oktalcode
(immer dreistellig !) angegeben.
\204
ä
\224
ö
\201
ü
\216
A
\231
Ö
\232
Ü
\341
ß
\270
©
--> ASCII-Tabelle im Oktalformat siehe Seite 5
Zeichenketten in C++
Datentyp: string (nur in C++)
Zeichenketten beliebiger Länge
Der Datentyp string gehört nicht zu den elementaren Datentypen von C++. string ist eine Klasse und
Bestandteil der so genannten „C++ Standard Template Library“ (C++ STL, siehe Kapitel 15).
Als Literale werden Zeichenketten durch Gänsefüßchen begrenzt.
"Hallo"
"W\204hrung"
"Nr.\t\tDatum\t\tPreis"
Auch "A" ist eine Zeichenkette (nicht identisch mit 'A' und passt nicht in eine Variable vom Typ char.
Steuerzeichen und spezielle Zeichen
Steuerzeichen sind nicht darstellbare Zeichen mit Steuerfunktion.
Ein Steuerzeichen beginnt im Quelltext mit \ (Backslash):
\t
Tabulator
\n
Zeilenvorschub (new line)
\b
Backspace
\a
Alert
"Nr.\t\tName\t\tVorname"
- 24 -
Speziellen Zeichen muss im Quelltext ebenfalls ein Backslash (\) vorangestellt werden, weil sie der Compiler
ansonsten falsch interpretieren würde:
\\
der Backslash selbst
\“
Gänsefüßchen
\‘
Hochkomma
"D:\\user\\rittmeier"
Aufgaben
5.1.
5.2.
5.3.
5.4.
5.5.
5.6.
5.7.
5.8.
5.9.
5.10.
5.11.
5.12.
5.13.
5.14.
Welchen Datentyp würden Sie verwenden für:
Kalenderwoche
Wochentag
Matrikel-Nr.
PLZ
Wohnort
Kreisumfang
Gewicht
Divisionsrest
Zähler der richtigen Lösungen
Note
Nullstelle einer Funktion
Gemessene Temperaturen
Postleitzahlen
Zahl der Teilnehmer eines Wettbewerbes
Namen eines Studierenden
Anzahl der Studierenden in einem Matrikel
Matrikelnummer eines Studierenden
Welcher Unterschied besteht zwischen den Datentypen float und double?
Warum wird zwischen ganzen Zahlen und Gleitkommazahlen unterschieden?
Was ist eine Variable?
Was verstehen Sie unter Initialisierung einer Variablen?
Welche Bedeutung hat das Schlüsselwort const bei der Definition einer Variablen?
Welchen Speicherbedarf hat eine Variable vom Typ char?
Wann sollten Konstanten mit einem Bezeichner versehen werden?
Welche Werte kann eine Variable vom Typ bool annehmen?
Welchen Wert besitzt eine nicht initialisierte Variable nach der Definition?
Erzeugen Sie ein neues Projekt und schreiben Sie ein Programm, das die folgende Bildschirmausgabe
erzeugt. Die Ausgabe soll zwei Tabulatorpositionen eingerückt sein:
Übermut tut selten gut.
© (Ihr Name)
Erzeugen Sie ein neues Projekt. Definieren Sie fünf Variablen vom Typ double. Schreiben Sie ein
Programm, das den Nutzer auffordert, fünf beliebige Zahlen (ganze und gebrochene, kleine und große,
mit oder ohne Exponent) einzugeben. Die eingegebenen Zahlen sollen anschließend untereinander
ausgegeben werden.
Experimentieren Sie mit unterschiedlichen Ausgabeformatierungen, beispielsweise
unterschiedlichen Genauigkeiten, Festkommaformat, definierte Ausgabebreiten.
Schreiben Sie ein Programm, das vier Variablen der Datentypen int, double, char und string
über cin einliest und über cout ausgibt. Dabei soll der Nutzer jeweils zur Eingabe eines bestimmten
Typs aufgefordert werden. Der eingegebene Wert soll dann in einem Antwortsatz ausgegeben werden.
Beispiel für einen Bildschirmdialog:
Eine ganze Zahl: -5
Sie haben -5 eingegeben.
Eine reelle Zahl: 2.45e-5
Sie haben 2.45e-5 eingegeben.
Ein Zeichen: a
Sie haben a eingegeben.
Eine Zeichenkette: Hallo
Sie haben Hallo eingegeben.
Schreiben Sie ein Programm um folgende Problemstellung zu lösen: Der Nutzer soll einen
Kleinbuchstaben eingeben, das Programm gibt den entsprechenden Großbuchstaben aus.
- 25 -
6. Ausdrücke, Anweisungen, Operatoren
Ein Ausdruck ist eine Variable, Konstante oder Funktion oder eine Kombination von Konstanten, Variablen und
Funktionen mit Operatoren, die einen Wert repräsentiert.
Es wird zwischen arithmetischen (numerischen), logischen und alphanumerischen Ausdrücken unterschieden.
3.1415
// numerisch
pi * sqrt(x)
// numerisch
x < y
// logisch
c == 'q'
// logisch
"Hallo"
// alphanumerisch
Wird ein Ausdruck mit einem Semikolon abgeschlossen, so entsteht eine Anweisung an den Rechner.
int k;
// definiere die Variable k
k = 3;
// weise k den Wert 3 zu
ausgabe();
// rufe die Funktion ausgabe auf
cout << "k=" << k << endl;
// Konsolenausgabe
Mehrere Anweisungen können durch geschweifte Klammern { } zu einer Anweisung (einem Block)
zusammengefasst (geblockt) werden.
if(x>=0)
{
y=sqrt(x);
cout << "Wurzel aus " << x << " = " << y << endl;
}
Ein Operator führt eine Operation aus und liefert ein Ergebnis.
Unäre (einstellige) Operatoren wenden die Operation auf einen Operanden an, binäre (zweistellige) Operatoren
verknüpfen zwei Operanden mit einer Operation.
Nach Art des Ergebnisses wird zwischen arithmetischen (numerischen), logischen und Zuweisungsoperatoren
unterschieden.
Operatoren besitzen eine Rangfolge bei der Abarbeitung.
Runde Klammern erzwingen eine bestimmte Abarbeitungsreihenfolge.
Der in einem Ausdruck zuletzt (d.h. mit niedrigster Priorität) ausgeführte Operator bestimmt die Art des
Ergebnisses.
a / 4 > x + 5
// > logisch
a / (4 > x) + 5
// + numerisch
a / (4 > x + 5)
// / numerisch
(a / 4 > x) + 5
// + numerisch
Unäre (einstellige) Operatoren
Ein unärer Operator bezieht sich auf einen Operanden.
!
Arithmetische Negation (negatives Vorzeichen)
-x
Logische Negation (nicht, not)
!(a>b) // nicht(a>b)
++
Inkrement einer ganzzahligen Variablen: Die Variable wird um 1 erhöht.
als Präfix (++var) oder Postfix (var++)
++i // i erst erhöhen, dann verwenden
i++ // i erst verwenden, dann erhöhen
--
Dekrement einer ganzzahligen Variablen: Die Variable wird um 1 vermindert.
als Präfix (--var) oder Postfix (var--)
--i // i erst vermindern, dann verwenden
i-- // i erst verwenden, dann vermindern
Inkrement und Dekrement sind typische Ganzzahloperationen, können also nur auf Ganzzahldatentypen (int,
char) angewendet werden.
- 26 -
Einige Compiler erlauben zwar auch ein Inkrement/ Dekrement für reelle Zahlen, der Compiler wandelt dann
aber ein reelles x++ um in ein x=x+1.
Andererseits kann man natürlich anstatt x++; auch immer schreiben x=x+1;
Solange Inkrement oder Dekrement allein in einer Anweisung stehen, ist es egal, ob man Postfix- oder
Präfixnotation verwendet.
x++; ist dasselbe wie ++x;
Als Einsteiger sollte man Inkrement und Dekrement nicht unbedingt in komplexen Ausdrücken verwenden. Das
führt schnell zu Unklarheiten. Also lieber eine Zeile mehr schreiben:
x++;
cout << a*x+b << endl;
Binäre (zweistellige) Operatoren
Ein binärer Operator verknüpft zwei Operanden mit einer Operation.
Arithmetische Operatoren liefern ein arithmetisches Ergebnis
+ - * /
Addition, Subtraktion, Multiplikation, Division
%
modulo: Rest der ganzzahligen Division
x % 5
// lies: x modulo 5
Vergleichsoperatoren liefern ein logisches Ergebnis
> >= < <=
== (gleich)
!= (ungleich)
x >= 3
Logische Verknüpfungsoperatoren liefern ein logisches Ergebnis
&& (logisches UND)
|| (logisches ODER)
a!=b && x>0
Für den Einsteiger ist es eine gute Hilfe, wenn er sich die Bedeutung der logischen Operatoren mit Hilfe von
Beispielsätzen klar macht:
„Wenn der Studierende alle Vorleistungen erbracht hat und sich rechtzeitig zur Prüfung angemeldet hat, (darf er
an der Prüfung teilnehmen.)“
Das und bedeutet: Beide Teile der Aussage müssen wahr sein, damit die Gesamtaussage wahr wird.
Will man die Aussage negieren (Wann darf der Studierende nicht an der Prüfung teilnehmen?), so müssen
sowohl die Teilaussagen als auch die Verknüpfung negiert werden:
„Wenn der Studierende nicht alle Vorleistungen erbracht hat oder sich nicht rechtzeitig angemeldet hat, darf er
an der Prüfung nicht teilnehmen. Das oder bedeutet: Wenn eine der beiden Teilaussagen wahr ist, ist die
Gesamtaussage wahr.
Mann kann einen logischen Ausdruck auch negieren, indem man ihn klammert und eine Negation voranstellt:
„Es trifft nicht zu, dass der Studierende alle Vorleistungen erbracht hat und sich rechtzeitig zur Prüfung
angemeldet hat.“
!(a>b && c<=d) ist dasselbe wie a<=b || c>d
Wenn ein logischer Ausdruck kompliziert bzw. unübersichtlich ist, so ist es eine gute Strategie, Teilausdrücke zu
berechnen und die Ergebnisse der Teilausdrücke logischen Variablen zuzuweisen. Diese Variablen werden dann
zu einem Gesamtausdruck verknüpft:
bool durch4 = jahr%4==0;
bool durch100 = jahr%100==0;
bool durch400 = jahr%400==0;
bool schaltjahr = durch4 && !(durch100 && !durch400));
if(schaltjahr) ...
ist zwar etwas länger, aber wesentlich leichter lesbar als
if((jahr%4==0) && !((jahr%100==0) && !(jahr%400==0))) ...
- 27 -
Wertzuweisung
Variablen, die links vom Zuweisungsoperator (=) stehen, werden als lvalues (left values, modifizierbare
Linkswerte) bezeichnet.
Rechts des Zuweisungsoperators steht immer ein Ausdruck. Durch eine Zuweisung nimmt die Variable auf der
linken Seite (lvalue) den Wert des Ausdrucks auf der rechten Seite an.
y = sin(x);
poly = a*pow(x,3) + b*pow(x,2) + c*x + d;
sum = sum + x;
Der Zuweisungsoperator liefert ein Ergebnis vom Typ der linken Seite. Folglich können Zuweisungen auch
verkettet werden. Sie werden dann immer von rechts nach links abgearbeitet.
a = b = c;
Eine Konstante kann nicht als lvalue auftreten. Ihr kann kein Wert zugewiesen werden.
Verwechseln Sie das Zuweisungzeichen nicht mit dem mathematischen Gleichheitszeichen!
In C++ ist a=b nicht dasselbe wie b=a.
y=sin(x) ist o.k, während sin(x)=y ein Syntaxfehler ist.
Die wichtigsten C++-Operatoren [1]
Der Rang der Operatoren nimmt in der folgenden Tabelle von oben nach unten ab.
Operator
Auswertung von
()
links nach rechts
[]
rechts nach links
-
++
*
/
% (Multiplikation, Division, Modulo)
links nach rechts
+
-
(Addition, Subtraktion)
links nach rechts
(Ausgabe, Eingabe)
links nach rechts
<<
<
>>
<=
>
--
(logische/ arithmetische, Negation, Inkrement, Dekrement
!
>=
(ist kleiner, ist kleiner oder gleich, ...))
links nach rechts
links nach rechts
(ist gleich, ist ungleich)
==
!=
&&
(logisches UND)
links nach rechts
||
(logisches ODER)
links nach rechts
=
Rechts nach links
(Zuweisung)
Implizite Typumwandlung
Da die Typisierung in C++ nicht sehr streng ist, ist es zulässig, dass in einem Ausdruck
unterschiedliche Datentypen vorkommen.
Diese müssen jedoch zueinander kompatibel, d.h. ineinander konvertierbar sein.
Alle „Zahlentypen“ sind zueinander kompatibel.
Bei einer Zuweisung wird in den Typ der linken Seite (lvalue, Zieltyp) konvertiert.
double d;
int i;
d=i;
// double
i=d;
// int, Warnung: Kommastellen gehen verloren
1. Es gibt in C++ wesentlich mehr Operatoren. Die hier aufgeführten sind jedoch für dieses Lehrmodul notwendig, andere sind verzichtbar.
- 28 -
Wird ein ganzzahliger Ausdruck nach bool konvertiert, so wird der Wert 0 nach false und jeder Wert
ungleich 0 nach true konvertiert.
int a = 3;
bool wf = a;
// wf = true
Wird ein logischer Ausdruck in eine Zahl konvertiert, so wird false nach 0 und true im Allgemeinen nach 1
konvertiert.
int b = wf;
// b = 1
Sind in einem Ausdruck A operator B die beiden Operanden A und B von unterschiedlichem Typ, so wird in der
Regel so konvertiert, dass keine Information verloren geht.
char a;
int b;
double c;
a+b
// int
a+c
// double
b+c
// double
Inkompatible Datentypen sind nicht ineinander konvertierbar (weder implizit noch explizit):
char a;
string s="3.14";
float f=1.2345;
a=s
// Fehler
f=s;
// Fehler
s=f;
// Fehler
Explizite Typumwandlung
Ein Ausdruck kann explizit in einen kompatiblen Zieltyp konvertiert werden.
In C++ wird dazu der Zieltyp vor den geklammerten Ausdruck geschrieben.
int(x)
In ANSI-C erfolgt type casting durch Klammerung das Zieltyps:
(int)x
Explizit wird dann konvertiert, wenn vermieden werden soll, dass der Compiler bei impliziter Konvertierung
eine Warnung ausgibt,
double f = 54.76;
int i = f;
// Compiler-Warnung
i = int(f);
// keine Warnung
oder wenn es semantisch erforderlich ist, zum Beispiel das Abschneiden von Nachkommastellen oder die
Umwandlung eines Zeichens in eine Zahl (ASCII) oder umgekehrt.
w = int(sqrt(a));
// ganzzahliger Teil
cout << int('A');
// ASCII von 'A' ausgeben
cout << char(240);
// Das Zeichen mit ASCII=240
Der Programmierer muss sich über die Folgen eines type cast im Klaren sein. Er sagt dem Compiler damit: „Ich
weiß, was ich tue.“
- 29 -
Aufgaben
Gegeben sei:
int a=1, b=2, c=3;
Welchen Wert besitzen folgende Ausdrücke:
c / b
a * b / c
c - 1 / b
c || a
a && b
!c
a < b
a != b
c % b
b + c % a
6.2. Was ist an folgender Anweisungsfolge auszusetzen?
double x=6, y=3, z;
z = x % y;
6.3. Welcher der folgenden Operatoren besitzt den höchsten Rang?
( ) +
( ) *
( ) >
( ) &&
( ) ==
6.4. Welcher der folgenden Operatoren besitzt den niedrigsten Rang?
( ) =
( ) ||
( ) %
( ) !=
( ) ()
6.5. Wodurch wird ein Ausdruck zur Anweisung?
6.6. Was ist ein Ausgabemanipulator im Zusammenhang mit cout? Nennen Sie ein Beispiel.
6.7. Was muss bei der Ausgabe von Umlauten beachtet werden?
6.8. Die Variable x vom Typ double habe den Wert 43.2345678901.
Was wird in folgendem Beispiel auf den Bildschirm ausgegeben? (Es wird vorher kein anderer
Ausgabemanipulator verwendet.)
cout << setprecision(5) << x;
6.9. Schreiben Sie eine Trace-Tabelle mit den Belegungen der Variablen wert, nummer, zahl jeweils nach
Abarbeitung der folgenden Ausdrücke. Alle Variable sind ganze Zahlen:
wert = 2, nummer=3, zahl=1;
// Nr. 1
wert = - wert * nummer;
// Nr. 2
wert = wert * (nummer = zahl = 4);
// Nr. 3
wert = zahl != 0 || nummer == 0;
// Nr. 4
wert = ++nummer;
// Nr. 5
wert = 4, nummer=0, zahl=1;
// Nr. 6
wert = wert && !nummer;
// Nr. 7
wert = nummer = 1;
// Nr. 8
zahl = zahl + wert + nummer++;
// Nr. 9
wert = nummer = zahl = 18;
// Nr. 10
zahl = zahl + wert < nummer;
// Nr. 11
wert = zahl >= wert >= nummer;
// Nr. 12
wert = zahl >= wert && nummer >= wert;
// Nr. 13
wert = !wert;
// Nr. 14
wert = 5, nummer=2, zahl=1;
// Nr. 15
wert = wert / nummer;
// Nr. 16
wert = 3 * wert % nummer;
// Nr. 17
wert = wert * (nummer + zahl);
// Nr. 18
6.10. Erstellen Sie ein neues C++–Projekt. Vereinbaren Sie die erforderlichen Variablen und implementieren
Sie die Anweisungen aus Aufgabe 6.9.
Schreiben Sie hinter jede der o.a. Anweisungen eine Ausgabeanweisung, die Ihnen die Werte der drei
Variablen auf der Konsole anzeigt, z.B.
Nr.1: wert=2 nummer=3 zahl=1.
6.1.
- 30 -
6.11. Schreiben Sie hinter jeden der folgenden logischen Ausdrücke dessen negierte Form:
c > d
a <= b
!(c != d)
!c && !d
a == b || c > d
a < b && c != d
a && b && c
6.12. Die Werte für die Variablen seien a=3, b=2, c=1, d=0. Berechnen Sie den jeweiligen Wert der
Ausdrücke aus Aufgabe 6.11. und schreiben Sie die Ergebnisse auf.
6.13. Die Werte für die Variablen seien a=0, b=1, c=2, d=3. Berechnen Sie den jeweiligen Wert der
Ausdrücke aus Aufgabe 6.11. und schreiben Sie die Ergebnisse auf.
6.14. Implementieren Sie ein Testprogramm, um Ihre Ergebnisse aus den Aufgaben 6.12. und 6.13. zu
überprüfen. Das Programm soll den jeweiligen Ausdruck und dessen Ergebnis anzeigen.
6.15. Schreiben Sie ein Programm, das zu einer eingegebenen Celsius-Temperatur die Temperatur nach
Fahrenheit berechnet.
Die Formel lautet: fahrenheit = 1.8 * celsius + 32
6.16. Schreiben Sie ein Programm, das für einen Kreis Fläche und Umfang berechnet. Der Nutzer soll den
Radius eingeben.
Hinweis: flaeche=M_PI*radius*radius; umfang=2*M_PI*radius;
Die Kreiszahl Pi ist in C bereits als Konstante M_PI vordefiniert. Dazu muss im Programmkopf
#include <cmath>
eingebunden werden.
6.17. Schreiben Sie ein Programm, das bei den gegebenen Größen Startkapital, Zins (in %) und Laufzeit das
Endkapital berechnet. Der Nutzer soll Startkapital, Zins und Laufzeit eingeben.
Formel:
Endkapital = Startkapital * (1 + Zins/100)Laufzeit
Die Potenz wird mittels der Funktion pow berechnet. Beispiel:
potenz = pow(basis, exponent);
Also lautet dann die obige Formel für das Endkapital:
Endkapital = Startkapital * pow(1+Zins/100, Laufzeit);
Für die Verwendung der Funktion pow muss im Programmkopf
#include <cmath>
eingebunden werden.
- 31 -
7. Kontrollstrukturen
Die Abarbeitungsreihenfolge von Anweisungen wird als Kontrollfluss bezeichnet.
Unter einer Kontrollstruktur (auch Steuerstruktur genannt) versteht man eine Anweisung, die den Kontrollfluss
bestimmt bzw. beeinflusst.
Es gibt folgende Kontrollstrukturen:
Sequenz („normale“ Abfolge von oben nach unten)
Selektion (bedingte Verzweigung, Fallunterscheidung)
Iteration (Wiederholung, Schleife)
Werden ausschließlich diese Kontrollstrukturen verwendet und der Kontrollfluss nicht auf andere Art beeinflusst
(Sprünge), so spricht man von strukturierter Programmierung.
Vorteil der strukturierten Programmierung:
Aus der (statischen) Niederschrift ist der (dynamische) Steuerfluss ersichtlich.
Kontrollstruktur und Struktogramm
Euklidscher Algorithmus
Initialisiere x und y
x≠y
Iteration
x<y
tru
e
y=y-x
fals
e
x=x-y
Selektion
Ausgabe: x ist größter gemeinsamer Teiler
Die Selektion (Bedingte Verzweigung)
Beispiele für Selektionen:
• Wenn das Jahr ein Schaltjahr ist, hat der Februar 29 Tage, ansonsten hat er 28 Tage.
• Wenn der Nutzer die Taste ‚Q‘ drückt, wird das Programm beendet.
• Wenn die Jahreszahl durch 4 teilbar ist, so ist es ein Schaltjahr, außer es ist durch 100 und nicht durch 400
teilbar [1].
• Wenn y größer als x ist, subtrahiere x von y und weise das Ergebnis y zu, andernfalls subtrahiere y von x
und weise das Ergebnis x zu.
• Wenn das Argument n kleiner oder gleich 1 ist, ist die Fakultät 1, anderenfalls berechnet sich die Fakultät zu
n! = n*(n-1)!
• Wenn das reelle Argument negativ ist, ist dessen Wurzel nicht definiert.
• Wenn die Note 5 ist, gibt es keinen Credit-Point und die Prüfung muss wiederholt werden.
• Wenn die Datei nicht bereits existiert, wird sie erzeugt.
Die if-else-Anweisung [1]
Die Ausführung von Befehlsfolgen kann vom Wert eines logischen Ausdrucks abhängig gemacht werden. Ist der
Ausdruck wahr, wird eine bestimmte Anweisung (A1) abgearbeitet, wenn nicht, eine andere Anweisung (A2).
1. Es gibt in C++ noch eine zweite Selektionsanweisung- die switch-case-Anweisung (Schalteranweisung, Mehrfachalternative). Sie ist
jedoch nicht zwingend erforderlich, denn jede Selektion kann durch if-else-Konstrukte realisiert werden.
- 32 -
logischer
Ausdruck
true
A1
false
A2
if (x < y)
y = y-x;
else
x = x-y;
Der Bedingungsausdruck (logischer Ausdruck) wird in runde Klammern gesetzt und muss vom Typ bool sein
bzw. nach bool konvertierbar sein.
if(x)
// wenn x ungleich 0 ist
...
Im if- und im else-Zweig können jeweils mehrere Anweisungen notiert werden, dann ist aber eine
Klammerung der Anweisungen (Blockung) mit { } erforderlich. Bei nur einer Anweisung ist die Klammerung
nicht zwingend.
if(x>=0)
{
y=sqrt(x);
cout << "Wurzel(" << x << ")=" << y << endl;
}
else
cout << "Wurzel nicht definiert." <<endl;
Falls der else-Zweig nicht erforderlich ist, kann er (samt dem else) entfallen.
Bei Schachtelung von Verzweigungen gehört ein else-Zweig immer zum letzten (öffnenden) if.
if(i>0)
cout << i << " ist positiv."<<endl;
else
if(i < 0)
cout<< i << " ist negativ."<<endl;
else
cout<< "i ist 0." <<endl;
So sieht das Beispiel als Struktogramm aus:
i>0
true
false
i<0
true
false
Ausgabe
"0"
Ausgabe
Ausgabe
"positiv" "negativ"
--> 07_01_if
- 33 -
Häufige Fehler
•
ein Semikolon hinter der Bedingung:
if(x<0);
cout<<„Negativ“<<endl;
Merke: Das Semikolon beendet eine Anweisung (in diesem Fall die Selektion). Hier wird also in
Abhängigkeit von der Bedingung nichts gemacht. Die Ausgabe erfolgt danach in jedem Fall.
Richtig ist also:
if(x<0)
cout<<„Negativ“<<endl;
•
Geschweifte Klammern vergessen:
if(x>0)
y=sqrt(x);
cout<<y<<endl;
Merke: Das Einrücken ist für den Compiler irrelevant. Entscheidend ist, dass immer nur eine Anweisung als
Folge der Bedingung ausgeführt wird. In diesem Fall ist zwar die Wurzelberechnung von der Bedingung
abhängig, die Ausgabe erfolgt aber danach in jedem Fall.
Ein folgendes else würde der Compiler als „misplaced else“ melden.
Richtig ist also:
if(x>0)
{
y=sqrt(x);
cout<<y<<endl;
}
Deshalb ist es eine gute Strategie, immer zu klammern (auch bei nur einer Anweisung).
Auch, weil der Editor beim Öffnen einer geschweiften Klammer automatisch einrückt.
Wiederholungen (Schleifen)
Soll eine Anweisung bzw. Anweisungsfolge in Abhängigkeit von einer Bedingung wiederholt werden, so spricht
man von einer Schleife.
Beispiele:
• Solange x ungleich y ist, wiederhole die folgenden Anweisungen: …
• Wiederhole das Programm, bis der Nutzer die Escape-Taste drückt.
• Berechne x=(x+a/x)/2 und wiederhole die Berechnung bis sich x kaum noch ändert.
Nicht sofort offensichtlich sind Wiederholungen in folgenden Anweisungen:
• Summiere alle xi für i von 1 bis n.
• Bilde die Quersumme über die Ziffern einer Zahl.
• Warte, bis 5 Sekunden abgelaufen sind.
Allen Schleifen ist gemeinsam, dass es einen Bedingungsausdruck (logischen Ausdruck) gibt, der vor oder nach
jedem Durchlauf getestet wird.
Der Bedingungsausdruck muss vom Typ bool sein oder nach bool konvertierbar sein.
Solange der Bedingungsausdruck wahr ist, wird die Schleife wiederholt. Deshalb nennt man diese Bedingung
auch Wiederholungsbedingung oder Laufbedingung. [1]
Der Schleifendurchlauf muss den Wert des Bedingungsausdruckes beeinflussen, sonst entsteht eine
Endlosschleife.
Die Anweisungen, die mehrfach wiederholt werden sollen, nennt man Iterationsbereich oder auch
Schleifenkörper.
Besteht der Iterationsbereich aus mehreren Anweisungen, so ist eine Klammerung der Anweisungen (Blockung)
mit { } erforderlich. Besteht er aus nur einer Anweisung, so ist die Klammerung nicht zwingend.
Es ist jedoch eine gute Strategie, den Iterationsbereich in jedem Fall zu klammern.
1. In verschiedenen anderen Programmiersprachen (z.B. Pascal) gibt es auch eine so genannte Abbruchbedingung (until). In C/C++ gibt es
keine Schleife, die mit Abbruchbedingung arbeitet.
- 34 -
Die while-Schleife (kopfgeprüfte Schleife)
Eine while-Schleife wiederholt Anweisungen, solange eine Bedingung wahr ist.
Bedingung
Anweisungen
Zu Beginn jedes Durchlaufes wird der Bedingungsausdruck ausgewertet.
Der Iterationsbereich (Anweisungen) wird wiederholt ausgeführt, solange der vor jedem Durchlauf neu
berechnete Bedingungsausdruck true ist.
Ist der Wert des Bedingungsausdruckes bereits beim Eintritt in die Schleife false, werden die Anweisungen
nie ausgeführt.
Das Schlüsselwort while leitet eine kopfgeprüfte Schleife ein.
Der Bedingungsausdruck wird in runde Klammern gesetzt und muss vom Typ bool sein oder nach bool
konvertierbar sein. (char, int)
// Warteschleife
const int dauer = 5;
// kann angepasst werden
int tstart = time(0);
// aktuelle Zeit
cout << "Startzeit: " << tstart << endl;
while(time(0) < tstart + dauer)
;
// Leeranweisung (NOP)
cout << "Endzeit:
" << time(0) << endl;
--> 07_03_while
Die do-while-Schleife (fußgeprüfte Schleife)
Eine do-while-Schleife wiederholt Anweisungen, solange eine Bedingung wahr ist. Sie wird benutzt, wenn
der Iterationsbereich wenigstens einmal durchlaufen werden muss.
Anweisungen
Bedingung
Der Iterationsbereich (Anweisungen) zwischen do und while wird ausgeführt und danach der Wert vom
Bedingungsausdruck ermittelt. Ist dieser true, so wird der Vorgang wiederholt.
Das Schlüsselwort do leitet eine fußgeprüfte Schleife ein.
Der Bedingungsausdruck steht in runden Klammern und muss vom Typ bool sein oder nach bool
konvertierbar sein. (char, int)
// Zentrale Schleife für ein Programm,
// das wiederholt ausgeführt werden soll
do
{
// hier alle Anweisungen
}
while(!ende);
Achtung: Hinter der Schleife das Semikolon nicht vergessen. (Anweisungsende)
--> 07_04_do_while
- 35 -
Die for-Schleife
Die for-Schleife wird im Allgemeinen für Iterationen mit fester Anzahl verwendet. Es handelt sich um eine
kopfgeprüfte Schleife mit spezieller Syntax [1].
Initialisierungsanweisung
Bedingungsausdruck
A
Änderungsanweisung
Vor dem Eintritt in die Schleife wird einmalig die Initialisierungsanweisung (z.B. i=0) ausgeführt.
Dann wird eine kopfgeprüfte Schleife mit dem Bedingungsausdruck (z.B. i<n) ausgeführt.
Die letzte Anweisung des Schleifenkörpers ist die Änderungsanweisung. Bei der Änderungsanweisung handelt
es sich oft (jedoch nicht zwingend) um einen Zählschritt (z.B. i++), daher der Name Zählschleife.
Es gelten sinngemäß alle Erläuterungen zur kopfgeprüften Schleife.
Das Schlüsselwort for leitet eine Zählschleife ein.
Die drei Bestandteile Initialisierungsanweisung, Bedingungsausdruck und Änderungsanweisung stehen
gemeinsam in runden Klammern und werden jeweils durch ein Semikolon getrennt.
for(ascii='a'; ascii<='z'; ascii++)
{
cout << ascii << ' ';
}
Initialisierungsanweisung:
Bedingungsausdruck:
Änderungsanweisung (letzte Anweisung in der Schleife):
ascii='a'
ascii<='z'
ascii++
Jeder der drei Ausdrücke kann entfallen, die Semikolons müssen jedoch stehen bleiben. Bei fehlendem
Bedingungsausdruck wird true angenommen. Dadurch entsteht eine Endlosschleife.
for(;;)
{
// Anweisungen
}
--> 07_05_for
Vergleich von for- und while-Schleife
Prinzipiell kommt der Programmierer ohne for-Schleife aus. Jede for-Schleife lässt sich auch als whileSchleife formulieren. Die for-Schleife verbessert jedoch in vielen Fällen Lesbarkeit und Übersichtlichkeit
(Zählschleifen sind besser erkennbar) und gestattet eine Formulierung mit weniger Programmzeilen.
Das folgende Beispiel zeigt das Prinzip. Beide Versionen sind absolut äquivalent:
for(ascii='a'; ascii<='z'; ascii++)
cout << ascii << ' ';
Die while-Schleife braucht mehr Programmzeilen:
ascii='a';
while(ascii<='z')
{
cout << ascii << ' ';
ascii++;
}
1. Eigentlich gibt es für die Zählschleife ein spezielles Struktogramm-Symbol. Zugunsten einer besseren Verständlichkeit wird es hier jedoch
nicht verwendet.
- 36 -
Verwechseln Sie nicht bedingte Verzweigung (Selektion) und Wiederholung (Schleife, Iteration). Verwenden
Sie konsequenterweise das Wort „wenn“ nur für Selektionen und das Wort „während“ bzw. „solange wie“ für
Iterationen.
Selbst wenn es im Einzelfall mal vorkommt, dass in einer Schleife eine Anweisung nur einmal ausgeführt wird,
bleibt es eine Schleife.
Verwenden Sie bei der Formulierung niemals das Wort „bis“. Das wäre das genaue Gegenteil von „während“.
Wenn Ihnen die Syntax der for-Schleife zu kompliziert erscheint, formulieren Sie eine normale while-Schleife.
Prägen Sie sich die Syntax der while- und der do-while-Schleife ein und schreiben Sie am besten immer das
Grundgerüst in einem „Rutsch“:
while( )
do
{
{
}
}
while( );
Häufige Fehler
•
Semikolon hinter der Bedingung bei einer while-Schleife:
while(a!=b);
…
Das wird in diesem Fall zu einer Endlosschleife führen, falls die Bedingung wahr ist.
Achtung: Bei der do-while-Schleife muss als Abschluss ein Semikolon stehen.
•
Geschweifte Klammern vergessen:
while(i<10)
cout<<i<<endl;
i++;
Auch das führt zu einer Endlosschleife, falls die Bedingung von vornherein wahr ist. In der Schleife ändert
sich i ja nicht. Die Anweisung i++ würde erst nach der Schleife ausgeführt, in diesem Fall also niemals.
Aufgaben
7.1.
7.2.
7.3.
7.4.
7.5.
7.6.
7.7.
7.8.
7.9.
7.10.
7.11.
7.12.
7.13.
Was verstehen Sie unter „Steuerfluss“ eines Programms?
Was ist eine Kontrollstruktur?
Welche Kontrollstrukturen kennen Sie?
Welche Kontrollstruktur wird in C durch die if-Anweisung realisiert?
Welche Kontrollstruktur wird in C durch eine while-Anweisung realisiert?
Welche Kontrollstruktur wird in C durch die for-Schleife realisiert?
Welcher Unterschied besteht zwischen einer kopfgesteuerten und einer fußgesteuerten Schleife?
Geben Sie ein Beispiel (Code-Fragment) für eine bedingte Verzweigung.
Welche der folgenden Beispiele werden mittels Selektion implementiert:
( )
Wenn das Jahr ein Schaltjahr ist, hat der Februar 29 Tage, ansonsten hat er 28 Tage
( )
Solange x ungleich y ist, wiederhole die folgenden Anweisungen:…
( )
Bilde die Quersumme über die Ziffern einer Zahl
( )
Wenn y größer als x ist, subtrahiere x von y und weise das Ergebnis y zu,
andernfalls subtrahiere y von x und weise das Ergebnis x zu.
( )
Warte, bis 5 Sekunden abgelaufen sind
Was gibt das folgende Codefragment auf dem Bildschirm aus? (Schreiben Sie die Ausgabe auf.)
int i; char z;
for(i=0, z=’a’; i<10; i++)
cout << z << " ";
Eine Variable k soll von 1 bis 10 inkrementiert und in jedem Schritt angezeigt werden. Schreiben Sie
eine entsprechende Programmanweisung.
In einer for-Schleife mit der Anweisung for(i=0; i<dim; i++), wobei dim vorher definiert wurde,
kommt i nicht in einer weiteren Anweisung vor. Die Anzahl der Wiederholungen der Schleife beträgt:
(Kreuzen Sie die richtige Lösung an.)
() i
( ) dim
( ) dim+1
( ) i+1
( ) i++
Kreuzen Sie die richtige Lösung an: Bei einer while-Schleife mit der Anweisung while(2*k>1)
( )
beträgt die Anzahl der Wiederholungen k
( )
wird die Schleife nur einmal ausgeführt
( )
beträgt die Anzahl der Wiederholungen 2*k-1
( )
ist die Schleife eine Endlosschleife
( )
kann die Anzahl der Wiederholungen nicht mit diesen Angaben ermittelt werden
- 37 -
7.14.
7.15.
7.16.
7.17.
7.18.
7.19.
7.20.
7.21.
7.22.
7.23.
7.24.
7.25.
7.26.
7.27.
7.28.
7.29.
Schreiben Sie ein Programm, das folgende Aufgabe löst:
Der Nutzer soll eine beliebige Zahl eingeben. Das Programm ordnet die Zahl in drei Kategorien ein:
Kategorie A: kleiner als 1, Kategorie B: 1 bis 10, Kategorie C: größer als 10 und gibt die Kategorie aus.
Schreiben Sie ein Programm, das alle ganzen Zahlen von 1 bis 10 nebeneinander, jeweils durch ein
Leerzeichen getrennt und alle ganzen Zahlen von 11 bis 20 untereinander ausgibt.
Schreiben Sie ein Programm, das die Fakultät einer vom Nutzer einzugebenden Zahl berechnet und
anzeigt.
Schreiben Sie die Bildschirmausgabe des folgenden Algorithmus auf!
for(i=1; i<=3; i++)
{
for(j=1; j<=i; j++)
cout << '*';
cout << endl;
}
Schreiben Sie die Ausgabe des folgenden Algorithmus auf!
for(i=1; i<=3; i++)
cout << char('A'+i) << ' ';
Der Nutzer soll einen Großbuchstaben eingeben, das Programm gibt den entsprechenden
Kleinbuchstaben aus. Gibt der Nutzer keinen Großbuchstaben sondern ein anderes Zeichen ein, so gibt
das Programm das eingegebene Zeichen unverändert aus.
Der Nutzer gibt die Hörsaalnummer (1,2,4,5) ein. Das Programm gibt die Lage des Hörsaals aus (unten
links, unten rechts, oben links, oben rechts).
Der Nutzer gibt einen Monat ein (eine Zahl zwischen 1 und 12). Das Programm gibt die Anzahl der
Tage des Monats aus (beim Februar wird „28/29“ ausgegeben).
Bei einer Falscheingabe (Zahl nicht im Bereich 1..12) soll das Programm eine Fehlermeldung ausgeben.
Ein Auktionshaus verlangt von einem Verkäufer eine Verkaufsprovision lt. folgender Tabelle:
Verkaufspreis
Provision
1€ - 50€
8% des Verkaufspreises
50,01€ - 500€
4€ zzgl. 5% des Verkaufspreises über 50€
500,01 und höher
26,50€ zzgl. 2% des Verkaufspreises über 500€
Schreiben Sie ein Programm, das zu einem eingegebenen Verkaufspreis die zu zahlende
Verkaufsprovision anzeigt.
(do-while) Programm aus Aufgabe 7.21. wird dahingehend modifiziert, dass bei falscher Nutzereingabe
(Zahlen außerhalb des Bereiches 1..12) die Eingabe wiederholt werden muss.
Das Programm zeigt an, ob ein vom Nutzer eingegebenes Jahr ein Schaltjahr ist.
Regel: Ein Jahr ist ein Schaltjahr, wenn es durch 4 teilbar ist, mit Ausnahme des Falles, wenn es durch
100 und nicht durch 400 teilbar ist.
Man kann es auch umgekehrt ausdrücken: Ein Jahr ist kein Schaltjahr, wenn es nicht durch 4 teilbar ist
oder wenn es durch 100 und nicht durch 400 teilbar ist.
Beispiele: 2006 ist kein Schaltjahr, 2008 ist ein Schaltjahr, 2000 ist ein Schaltjahr, 1900 und 2100 sind
jedoch keine Schaltjahre (sind Ausnahmen).
Das Programm aus Aufgabe 7.23. wird mit Programm 7.24. verbunden und dahingehend modifiziert,
dass für den Februar die korrekte Tageszahl (28 oder 29) ausgegeben wird. Dazu muss beim Februar
auch das Jahr abgefragt werden.
Das Programm zeigt den Wochentag zu einem eingegebenen Datum an.
Hinweis:
Zunächst muss zu dem Datum eine Schlüsselzahl (code) berechnet werden:
code=int(30.6001*(1+monat+12*h))+int(365.25*(jahr-h))+tag;
Die Variable h besitzt für die Monate Januar und Februar den Wert 1, sonst den Wert 0.
Der Wochentag ergibt sich aus code mod 7. (Modulo-Operator: %)
0 entspricht Freitag
1 entspricht Samstag, u.s.w.
Lösen Sie folgende Aufgabe durch die while-Anweisung und alternativ durch die for-Anweisung:
Ausgabe aller ganzen Zahlen von 1 bis 10 nebeneinander, jeweils durch ein Leerzeichen getrennt und
Ausgabe aller ganzen Zahlen von 10 bis 20 untereinander.
Der Nutzer gibt zwei Buchstaben c1 und c2 ein. Das Programm gibt alle Zeichen von c1 bis c2 jeweils
durch ein Leerzeichen getrennt aus.
Der Nutzer gibt zwei ganze Zahlen i1 und i2 ein. Das Programm gibt tabellarisch alle Zahlen von i1 bis
i2, sowie deren Quadrate und (reelle) Quadratwurzeln aus. Beispiel i1=3, i2=7:
i
i2
Wurzel(i)
3
9
1.73205
4
16
2
5
25
2.23607
- 38 -
6
36
2.44949
7
49
2.64575
7.30.
Ausgabe einer Tabelle mit Kreisradius, Umfang und Fläche. Der Radius soll in Schritten zu 0.5 von 1
bis 5 laufen.
Als „Verschönerung“ kann die Anzeige mit 2 Nachkommastellen erfolgen:
Radius
Umfang
Fläche
1.00
6.28
3.14
1.50
9.42
7.07
2.00
12.57
12.57
u.s.w.
Erweiterung: Der Nutzer gibt Untergrenze und Obergrenze für den Radius ein.
7.31.
Ausgabe aller ganzen Zahlen von 1 bis 10 untereinander und neben jeder Zahl die bis dahin
aufgelaufene Summe.
1
1
2
3
3
6
4
10
u.s.w.
7.32.
Berechnung der Summe aller ganzen Zahlen von m bis n. m und n gibt der Nutzer ein.
7.33.
Berechnung der Fakultät einer Zahl m, die vom Nutzer eingegeben wird.
7.34.
Schreiben Sie ein Programm, das für zwei vom Nutzer eingegebene ganze Zahlen anzeigt, ob sie
teilerfremd sind.
Zwei Zahlen a und b sind teilerfremd, wenn es keine Zahl z≠1 gibt, durch die beide Zahlen teilbar sind.
7.35. Formulieren Sie einen Algorithmus und schreiben Sie ein Programm, um die natürlichen Zahlen
zwischen 1 und 100 zu ermitteln, deren Summe der Quadrate der Ziffern durch 7 teilbar ist.
Beispiel: Die Zahl sei 54, dann ist 52 + 42 = 41, ist nicht durch 7 teilbar.
Die Zahl sei Zahl 77, dann ist 72 + 72 = 98, ist durch 7 teilbar.
7.36.
7.37.
7.38.
7.39.
7.40.
Alle folgenden Aufgaben sollen jeweils durch zwei ineinander geschachtelte Schleifen realisiert
werden. Versuchen Sie sich an diesen Aufgaben nur, wenn Sie die einfachen Schleifen beherrschen.
Erzeugen Sie die folgende Bildschirmausgabe iterativ (d.h. durch Schleifen):
1
1 2
1 2 3
1 2 3 4
1 2 3 4 5
Erzeugen Sie die folgende Bildschirmausgabe iterativ:
A
A B
A B C
A B C D
A B C D E
Ein Programm soll folgende Bildschirmausgabe iterativ erzeugen. Die Anzahl der Zeilen soll vom
Nutzer vorgegeben werden. Der Abstand benachbarter Zeichen soll eine Tabulatorposition betragen:
Z
Z
Y
Z
Y
X
Z
Y
X
W
u.s.w.
Erzeugen Sie die folgende Bildschirmausgabe iterativ. Die höchste Ziffer wird vom Nutzer vorgegeben:
1
2
3
4
5
Erzeugen Sie die folgende Bildschirmausgabe iterativ die höchste Ziffer wird vom Nutzer vorgegeben:
5
4
3
2
1
- 39 -
8. Programmierprinzipien und Algorithmen (1)
Programmierparadigmen (Programmierprinzipien), eine Auswahl:
• Iteration (schrittweise Annäherung an eine Lösung)
• Interpolation / Extrapolation
• „teile und herrsche“
Der Problemumfang wird auf die Hälfte (oder einen anderen Bruchteil) zurückgeführt.
• Rekursion
Problemlösung greift auf sich selbst zurück.
• Simulation
Ein komplexer Vorgang (Experiment) wird am Rechner nachvollzogen
• Backtracking (Rückverfolgung)
Rekursives Verfahren zur Entwicklung einer intelligenten Suchtechnik
• Branch and bound (Verzweigungen und Beschränkungen)
Begrenzung der Anzahl der zu untersuchenden Lösungen durch Berechnung von Schranken für partielle
Lösungen
Diese Prinzipien schließen einander nicht aus, sondern kommen oft in Kombination miteinander vor. So lässt
sich beispielsweise das Prinzip „teile & herrsche“ sowohl iterativ als auch rekursiv implementieren.
Das Prinzip der Iteration
Beispiel: Wurzelberechnung nach HERON
Die Quadratwurzel aus einer reellen Zahl a lässt sich nach dem griechischen Mathematiker HERON iterativ nach
folgendem Algorithmus berechnen:
Setze x=a.
Wiederhole die Berechnung:
x=(x+a/x)/2
bis sich x kaum noch ändert.
Beispiel: a=2, Berechnung auf 4 Nachkommastellen genau:
x=2
x=(2+1)/2 = 1,5
x=(1,5+2/1,5)/2 = 1,4166
x=(1,4166+2/1,4166)/2 = 1,4142
x=(1,4142+2/1,4141)/2 = 1,4142 hier änderst sich x nicht mehr
Die Bedingung "bis sich x kaum noch ändert" ist für eine Algorithmus zu unpräzise.
Zunächst sollte die Bedingung nicht als Abbruchbedingung ("bis ..."), sondern als Wiederholungsbedingung
("solange ...") formuliert werden. Darüber hinaus ist die Formulierung "kaum noch ändert" keine mathematische
Bedingung. Was heißt "kaum"?
Eine Mathematisch korrekte Formulierung könnte lauten:
"solange |xneu - xalt| > e ist", wobei e in diesem Beispiel 0,0001 (4 Nachkommastellen) ist und xneu der aktuell
berechnete Wert sowie xalt der zuvor berechnete Wert ist.
--> 08_00_Heron
Beispiel: Iterative Berechnung von n2
n
n = ∑ (2i − 1)
2
i =1
Beispiel: 52 = 1+3+5+7+9 = 25
--> 08_01_Quadrat
- 40 -
Beispiel: Lösung der Gleichung x = cos(x)
Gegeben seien die beiden Funktionen f1(x)=x und f2(x)=cos(x).
Der Schnittpunkt der beiden Funktionen f1(x) und f2(x) ist die Lösung der Gleichung x=cos(x).
Ein möglicher Algorithmus berechnet abwechselnd die Funktionswerte der beiden Funktionen indem jeweils der
Funktionswert der einen Funktion als Argument der anderen Funktion benutzt wird:
Wiederhole folgende Berechnung:
x=y
y=cos(x)
solange sich y von Schritt zu Schritt innerhalb der gewünschten Genauigkeit noch ändert.
--> 08_02_Gleichung
Zufallszahlen
Zufall: Ereignis, dass unter bestimmten Bedingungen mit einer gewissen Wahrscheinlichkeit eintreten kann,
jedoch nicht notwendig eintreten muss.
Zufallszahlen: Zahlen für deren Entstehung es keine Zusammenhänge oder Gesetzmäßigkeiten zu geben scheint.
Da der Computer eine deterministische Maschine ist, kann er keine echten Zufallszahlen erzeugen. Er erzeugt
lediglich Pseudo-Zufallszahlen.
Folgende Kriterien kann man von einer guten Zufallszahlenfolge verlangen:
- zufälliges Aussehen (Beweisführung über statistische Testverfahren)
- nach Möglichkeit gleich verteilt
- große Periodenlänge
Ein geeignetes Verfahren, eine Folge von Pseudo-Zufallszahlen zu erzeugen, ist die Methode der linearen
Kongruenz (D. Lehmer 1951):
Xi = (a * Xi-1 + b) mod m
Beim allerersten Aufruf des Zufallszahlengenerators muss ein willkürlich gewählter Startwert, die Saat (engl.:
seed) verwendet werden.
Die C-Standardbibliothek stellt zur Erzeugung von Pseudo-Zufallszahlen die Funktion rand() zur
Verfügung. Diese liefert eine ganze Zahl im Bereich 0 bis RAND_MAX (0x7FFF, 32767).
Der Zahlenbereich kann vom Nutzer mittels Modulo-Operation eingeschränkt werden.
Damit der Zufallsgenerator nach dem Programmstart nicht immer dieselbe Zahlenfolge liefert, muss der
Zufallsgenerator zu Beginn einmalig mit einer willkürlichen Zahl (seed) initialisiert werden. Das geschieht
mittels der Funktion srand(), am einfachsten durch Initialisierung mit der Systemzeit.
#include <cstdlib>
#include <ctime>
…
srand(time(0));
for(i=0; i<10; i++)
cout << rand() % 6 + 1 << endl;
Die Funktion srand() darf nur ein einziges Mal am Beginn des Programmes aufgerufen werden.
--> 08_03_Zufallszahlen
Da die Funktion rand() eine ganze Zahl im Bereich 0 bis RAND_MAX (32767) liefert, muss der Programmierer
häufig die Zufallszahlen in einen anderen Bereich transformieren. Dafür gibt es zwei Möglichkeiten:
1. Wenn eine ganze Zahl aus einem bestimmten Bereich erzeugt werden soll, wird die Modulo-Operation
(Divisionsrest) verwendet. Dabei gilt folgende Formel:
zufallszahl = rand() % anzahl + offset;
anzahl ist die mögliche Anzahl von Zufallszahlen (beim Würfel 6), offset ist die kleinste Zahl (beim Würfel 1)
rand() % 6 + 1
// 1 ... 6
rand() % 201 – 100
// -100 ... +100
rand() % 100 + 100
// 100 ... 199
2. Wenn eine reelle Zufallszahl erzeugt werden soll, wird eine reelle Division oder Multiplikation ausgeführt.
Dazu muss mindestens einer der beteiligten Operanden reell sein. Das kann mit einem typecast (explizite
Typkonvertierung) erreicht werden:
- 41 -
double(rand()) / RAND_MAX
10.0 * rand() / RAND_MAX + 100
100.0 * rand() / RAND_MAX – 50
r * rand() / RAND_MAX
//
//
//
//
0..1 (reell)
100..110 (reell)
-50..50 (reell)
0..r (r reell)
Achtung:
1.0 / rand() liefert keine gleichverteilten Zufallszahlen (1.0, 0.5, 0.33, 0.25, 0.2, …). Die Verteilung wird
zu kleinen Werten hin immer „dichter“.
Beispiel: Integration nach der Monte Carlo Methode
Die Gleichverteilung von Zufallszahlen nutzt man, um Verhältnisse auszudrücken. So ist die Integration als
Verhältnis von bekannter zu unbekannter Fläche mittels Pseudozufallszahlen möglich. Dieses Verfahren wird als
Monte Carlo Methode bezeichnet.
Man kann mit zwei Intervallen von gleich verteilten Zufallszahlen eine Ebene aufspannen, auf der alle Punkte
dieser Intervalle in der Ebene ebenfalls gleich verteilt sind.
Die Fläche des Viertelkreises sei unbekannt, die Fläche des Quadrates ist A=a2. Das Verhältnis der Zufallspunkte
im Quadrat zu denen im Viertelkreis ist gleich dem Verhältnis der Flächen:
nK AK
=
nQ AQ
Ein Treffer im Viertelkreis liegt dann vor, wenn gilt:
z x2 + z y2 <= a 2
--> 08_04_MonteCarlo
- 42 -
Aufgaben
8.1.
8.2.
8.3.
Welche Programmierprinzipien bezüglich der Implementierung von Algorithmen kennen Sie?
Was versteht der Programmierer unter Iteration?
Die Lösung für welches Problem berechnet der folgende Algorithmus?
for(x=0; fabs(x-cos(x)) > 1e-4; )
x = cos(x);
cout << "L\224sung x=" << x << endl;
8.4.
Ein Zufallsgenerator muss vor dem ersten Aufruf mit einem „seed“ (Startwert) initialisiert werden.
Welcher Wert wäre für solch eine Initialisierung geeignet, damit die Zufallsfolge nicht vorhersagbar ist?
Die C-Standardbibliothek bietet die Funktion rand(), die eine Zufallszahl zwischen 0 und RAND_MAX
(32767) liefert.
Wie können mit Hilfe dieser Funktion reelle Zufallszahlen zwischen 0 und 1 erzeugt werden?
(Schreiben Sie eine Anweisung auf.)
8.5.
8.6.
8.7.
8.8.
Wie können Zufallszahlen im Bereich 1...6 erzeugt werden? Kreuzen Sie die richtige Lösung an!
( ) rand()/6
( ) rand()/6+1
( ) rand()%6
( ) rand()%6+1
( ) rand()+1%6
( ) rand()%(6+1)
Wie können Zufallszahlen im Bereich -100 .. +100 erzeugt werden?
( ) rand()%101-100
( ) rand()-100%101
( ) rand()%201-100
( ) rand()-200%100
( ) (rand()-100)%201
( ) rand()%200-101
Wie können gleichverteilte reelle Zufallszahlen im Bereich 0…1 erzeugt werden?
( ) rand()/RAND_MAX
( ) rand()%RAND_MAX
( ) double(rand()/RAND_MAX)
( ) double(RAND_MAX)%rand()
( ) double(rand())/RAND_MAX
( ) 1.0/rand()
8.9.
Schreiben Sie ein Programm (in Analogie zum Beispiel aus der Vorlesung) zur Lösung der Gleichung
x = cos(x)-x2.
8.10.
Schreiben Sie ein Programm, das folgende Aufgabe löst: Der Nutzer soll eine bestimmte Anzahl an
Messwerten (reelle Zahlen) eingeben. Die Anzahl legt der Nutzer selbst fest. Das Programm zeigt deren
Mittelwert an. Das Programm soll sich die Messwerte nicht merken.
8.11.
Erweitern Sie das Programm aus Aufgabe 8.12. dahingehend, dass das Programm bereits während der
Eingabe den kleinsten und den größten Wert bestimmt und am Ende anzeigt.
8.12.
Der Nutzer würfelt und gibt die gewürfelte Zahl (1..6) ein. Der Rechner „würfelt“ ebenfalls, d.h. er
erzeugt eine Zufallszahl zwischen 1 und 6. Es wird angezeigt, wer die höhere Zahl gewürfelt hat.
Falsche Nutzereingaben (Zahlen außerhalb des Bereiches 1..6) sollen nicht zugelassen werden.
8.13.
Schreiben Sie ein Programm, dass 20 Pseudozufallszahlen nach der Methode der linearen Kongruenz
(siehe Vorlesung) erzeugt. Experimentieren Sie mit unterschiedlichen Werten für die Parameter a, b und
m.
Hinweise zu den Parametern:
Startwert X0: willkürlich
Multiplikator a: Empfohlen wird ein Wert, der um eine Grössenordnung kleiner ist als m. Er soll also
eine Stelle weniger aufweisen. Für m = 64 ergibt diese Regel eine Zahl zwischen 1 und 9. Wenn m vier
oder mehr Stellen hat (also grösser 1000 ist), wird empfohlen, a mit den Ziffern 21 aufhören zu lassen.
Summand b: im einfachsten Fall = 1.
Divisor m: bestimmt den Wertebereich
- 43 -
8.14.
8.15.
8.16.
Schreiben Sie ein Programm, das 1000 Zufallszahlen im Bereich 1..100 erzeugt. Das Programm soll die
Häufigkeit der Zahlen in den Intervallen 1..20, 21..40, 41..60, 61..80, 81..100 anzeigen.
Schreiben Sie ein Programm, das die Fläche des folgenden Blechteiles nach der Monte-Carlo-Methode
ermittelt. (Sie können das Programm aus der Vorlesung modifizieren.)
Formulieren Sie einen Algorithmus und schreiben Sie ein Programm, um die Anzahl der Punkte in der
Ebene zu ermitteln, die ganzzahlige Koordinaten haben, und die innerhalb eines Kreises mit dem Radius
r liegen (r ist reell).
Hinweis: Denken Sie sich ein Quadrat um den Kreis (Mittelpunkt des Kreises im Koordinatenursprung)
und prüfen jedes ganzzahlige Koordinatenpaar in dem Quadrat, ob es im Kreis liegt (Satz des
Pythagoras). Wenn das so ist, dann wird der Punkt gezählt.
- 44 -
9. Funktionen
Ein C- bzw. C++-Programm besteht aus Funktionen.
Genau eine der Funktionen hat den Namen „main“.
Funktionen können vordefiniert sein (Bibliotheksfunktionen) oder vom Programmierer definiert werden
(benutzerdefinierte Funktionen).
Die „Benutzung“ einer Funktion heißt Funktionsaufruf:
system("pause");
z = rand();
y = sin(x);
p = pow(a,b);
srand(time(0));
Ein Datenaustausch mit Funktionen erfolgt über zwei Wege:
- Einer Funktion können Parameter (Argumente) übergeben werden.
- Eine Funktion kann einen Funktionswert liefern.
Der Begriff „Funktion“ hat in der Programmiersprache C eine etwas andere Bedeutung als in der Mathematik.
In der Mathematik besitzt eine Funktion einen oder mehrere Parameter und liefert einen (funktional abhängigen)
Funktionswert. In C muss eine Funktion weder Parameter noch einen Funktionswert haben. Selbst wenn es
beides gibt, muss es keine funktionale Abhängigkeit geben.
Es handelt sich aber in jedem Fall um eine logisch zusammengehörige Folge von Anweisungen.
Um den Unterschied zwischen mathematischen Funktionen und Funktionen in C++ etwas deutlicher zu machen,
seien einige Beispiele genannt:
y = pow(x, e);
Die Funktion berechnet xe. pow() ist eine Funktion im streng mathematischen Sinne: Sie besitzt Parameter und
liefert einen funktional abhängigen Funktionswert.
swap(x, y);
Die Funktion soll die Inhalte der beiden Variablen x und y vertauschen. Es gibt zwar zwei Parameter, jedoch
keinen Funktionswert. (Wenn man so will, gibt es auch zwei Funktionswerte: Ein Parameter ist vom jeweils
anderen Parameter abhängig.)
sortiere(array);
Es gibt zwar Parameter (das unsortierte Array) und (im weiteren Sinne) Funktionswerte (das sortierte Array),
jedoch besitzt nicht die Funktion sortiere() einen Funktionswert, sondern es stehen die Funktionswerte nach
Abarbeitung der Funktion in dem Parameter-Array und die Parameter sind überschrieben.
z = rand();
Die Funktion liefert eine Zufallszahl. Es gibt keinen Parameter und trotzdem einen Funktionswert.
y = eingabe(„Eine ganze Zahl:“);
Die Funktion besitzt zwar einen Parameter (hier eine Zeichenkette) und einen Funktionswert, es gibt jedoch
keine funktionale Abhängigkeit zwischen Parameter und Funktionswert. Tatsächlich ist der Funktionswert eine
Nutzereingabe.
Bedeutung von Funktionen
•
•
•
•
Funktionen ermöglichen eine Strukturierung des Programmtextes in relativ selbständige Einheiten. Das
Programm wird übersichtlicher, weniger fehleranfällig und einfacher änderbar.
Funktionen können von verschiedenen Stellen des Programmtextes aus mit unterschiedlichen Parametern
aufgerufen werden. Dadurch wird Programmcode gespart.
Funktionen ermöglichen die Wiederverwendbarkeit von früher Programmiertem. (auch über die
Verwendung von Funktionen aus der Literatur)
Funktionen ermöglichen Teamwork in der Programmierung.
Funktionen werden gebildet, wenn
• ein logisch geschlossener Sachverhalt vorliegt,
• eine Codereduzierung möglich ist,
• die Übersichtlichkeit besser wird
- 45 -
Je kürzer eine Funktion ist, umso besser. Es gibt auch „Einzeiler“.
Achtung: Funktionen „schwatzen“ nicht mit dem Benutzer, d.h., in Funktionen, die einen Algorithmus
implementieren, gehören normalerweise keine Ein- oder Ausgaben.
Abarbeitung von Funktionen
Wird eine Funktion aufgerufen, so wird vom laufenden Programm zum Beginn der Funktion verzweigt, die
Anweisungen in der Funktion ausgeführt und danach zum aufrufenden Programm zurückgekehrt.
void Eroeffnung ()
{
Funktionsaufruf
return;
Eroeffnung ()
}
Funktionsdefinition
Die Funktionsdefinition kann im selben Quelltext wie der Funktionsaufruf erfolgen oder in einem anderen
Projekt-Modul oder die Funktion kann bereits vordefiniert in einer Bibliothek vorliegen (Bibliotheksfunktionen).
Datenaustausch mit Funktionen kann in zwei Richtungen stattfinden:
1. Vom aufrufenden Programm an die Funktion: Übergabe von Daten an eine Funktion in Form von Parametern
(auch Argumente genannt).
2. Von der Funktion zurück an das aufrufende Programm: Rückgabe von Daten in Form eines Funktionswertes.
y = sin(x);
Funktionswert
Parameter
In Fällen, in denen eine Funktion mehrere Werte liefern muss, kann die Rückgabe auch über die
Funktionsparameter (sogenannte Referenzparameter) erfolgen.
- 46 -
Funktionsdefinition
Die Definition einer Funktion definiert den Typ des Funktionswertes, den Namen der Funktion, die Parameter
und den Funktionsrumpf (Funktionskörper).
Typ
Name
Parameter
double kehrwert(double zahl)
{
double k;
k = 1 / zahl;
return k;
}
Lokale Variable
Rumpf
Eine Variable, die innerhalb einer Funktion definiert wurde, ist eine lokale Variable und somit nur in dieser
Funktion gültig. Auch jeder formale Parameter ist eine lokale Variable.
Es gibt keine Namenskonflikte mit gleichnamigen Variablen in anderen Funktionen.
Funktionen werden nacheinander (nicht ineinander geschachtelt) definiert.
Besitzt eine Funktion keine Parameter, so bleiben die runden Klammern leer.
bool nochmal()
{
…
}
Wenn die Funktion keinen Funktionswert besitzt, so ist sie vom Typ void.
void anzeige(string text, double wert)
{
cout << text << " " << wert << endl;
}
Funktionsprototypen
Funktionen können im Programmtext vor oder hinter ihrem Aufruf definiert sein.
Bei der Definition hinter dem Aufruf oder in einem anderen Modul oder bei Bibliotheksfunktionen benötigt der
Compiler zur Prüfung der aktuellen Parameter vor dem Aufruf eine Funktionsdeklaration (Bekanntmachung),
die als Prototyp bezeichnet wird.
Der Prototyp umfaßt den Funktionskopf (Header) mit abschließendem Semikolon. Die Namen der formalen
Parameter sind irrelevant und können entfallen.
double sin(double);
Dateien mit Prototypen werden als Headerdateien bezeichnet und tragen die Dateiendung .h.
--> 09_00a_Progrnose
Einige Bibliotheksfunktionen
<cstdlib>
void system(char*);
int rand();
void srand(int);
<cmath>
int abs(int);
double fabs(double);
double ceil(double);
double floor(double);
double exp(double);
double pow(double, double);
double sqrt(double);
double log(double);
double log10(double);
// Konsolenbefehl absetzen
// Zufallszahl „würfeln“
// Zufallsgenerator initialisieren
//
//
//
//
//
//
//
//
//
Betrag von ganzen Zahlen
Betrag von reellen Zahlen
Aufrunden zur ganzen Zahl
Abrunden zur ganzen Zahl
e hoch x
Potenz Basis hoch Exponent
Quadratwurzel
natürlicher Logarithmus
dekadischer Logarithmus
- 47 -
<cctype>
char toupper(char)
char tolower(char)
// Umwandlung in einen Großbuchst.
// Umwandlung in einen Kleinbuchst.
Formale und aktuelle Parameter
Zum Datenaustausch deklariert eine Funktion formale Parameter im Funktionskopf.
Beim Aufruf der Funktion werden aktuelle Parameter übergeben. Die aktuellen Parameter treten an die Stelle
der formalen Parameter.
Aktueller Parameter kann jeder Ausdruck vom passenden Typ sein.
// Funktionsdefinition
void eroeffnung(string txt)
// txt ist formaler Parameter
{
...
}
int main()
{
// Funktionsaufruf
eroeffnung("Programm 9.1");
// "Programm 9.1" ist
// aktueller Parameter
}
Es werden zwei Arten von Parametern unterschieden: Wertparameter und Referenzparameter.
Wertparameter enthalten bei Aufruf die Werte der aktuellen Parameter. Die Werte werden in die Variablen der
formalen Parameter kopiert und die gerufene Funktion kann auf die Werte zugreifen. Änderungen wirken sich
nicht auf die in der aufrufenden Funktion benutzten Variablen aus. Datenaustausch ist also nur in Richtung der
gerufenen Funktion möglich, nicht umgekehrt.
Der Aufruf einer Funktion mit Übergabe eines Wertparameters heißt „call by value“.
Wenn Datenaustausch nur in Richtung der Funktion erforderlich ist, sind Wertparameter zu benutzen. Das ist
meistens der Fall.Als aktueller Wertparameter kann jeder passende Ausdruck auftreten. Es muss sich nicht
unbedingt um eine Variable handeln.
y = pow(5*x+1, 1/n);
void zaehlen(int x) // formaler Parameter x
{
x++;
cout << "Wert in der Funktion zaehlen:" << endl;
cout << "x++ = " << x << endl;
}
int main()
{
int j=5;
cout << "vorher: j=" << j << endl;
zaehlen(j); // j wird durch zaehlen(j) nicht verändert
cout << "nachher: j=" << j << endl;
system("pause");
return 0;
}
--> 09_02_ByValue
- 48 -
Referenzparameter enthalten einen Verweis auf Variablen.
Formaler und aktueller Parameter sind Alias-Namen für den selben Speicherplatz.
Es können die darauf befindlichen Daten übernommen und dort auch wieder Ergebnisse abgelegt werden.
Demnach ist Datenaustausch in beiden Richtungen möglich.
Der Aufruf einer Funktion mit Übergabe eines Referenzparameters heißt „call by reference“.
Wenn Datenaustausch aus der Funktion heraus in die aufrufende Funktion erforderlich ist, sind
Referenzparameter zu benutzen. Das ist häufig dann der Fall, wenn Funktionen mehrere Ergebnisse liefern
müssen. (vgl. auch Kapitel 11. - Arrays als Parameter).
Formale Referenzparameter werden durch ein '&' gekennzeichnet:
void tauschen(int& x, int& y)
// x und y sind formale Parameter
{
int hilf = x;
x = y;
y = hilf;
}
Als aktueller Referenzparameter muss eine Variable stehen. Dem aktuellen Parameter sieht man (leider)
nicht an, ob er by value oder by reference übergeben wird:
void tauschtest()
{
int i=3, j=5;
cout << "vorher: i=" << i << " j=" << j << endl;
tauschen (i, j);
// i und j sind aktuelle Parameter
cout << "nachher: i=" << i << " j=" << j << endl;
}
--> 09_03_byReference
Funktionswert
Funktionen, die nicht mit dem Typ void deklariert wurden, stellen über ihren Namen einen Funktionswert
bereit. (return-Wert, Rückgabewert).
Die Bereitstellung des Funktionswertes erfolgt mit der return-Anweisung.
// Funktionsdefinition
double quadrat(double arg)
{
return arg*arg;
}
...
// Funktionsaufruf:
y=quadrat(x);
// quadrat(x) ist ein Wert
Eine Funktion kann (von Bedingungen abhängig) mehrere return-Anweisungen haben, jedoch sollte immer
eine return-Anweisung am Ende der Funktion stehen.
Die Funktion wird bei der ersten abgearbeiteten return-Anweisung beendet.
Return-Ausdruck kann ein beliebiger Ausdruck vom Typ der Funktion sein.
int datumscode(int t, int m, int j)
{
return int(30.6001*(1+m+12*(m<3)))+int(365.25*(j-(m<3)))+t;
}
Beim Funktionstyp void wird kein Wert bereitgestellt und die return-Anweisung kann entfallen.
--> 09_04_Rueckgabe
- 49 -
Die Funktion main()
Ein C-Programm muss genau eine Funktion mit dem Namen main haben. Die main-Funktion wird nach dem
Programmstart vom Betriebssystem aufgerufen
int main(int argc, char* argv[])
{
//Anweisungen
return intAusdruck;
}
Über die Parameter argc und argv kommt das Programm an die eigenen Kommandozeilenparameter heran.
Die Parameter können ganz entfallen, falls keine Kommandozeilenparameter benutzt werden sollen.
int main()
Der Wert von intAusdruck wird bei Beendigung des Programmes an das Betriebssystem zurückgegeben und dort
im Allgemeinen in der Umgebungsvariablen ERRORLEVEL gespeichert.
Überladen von Funktionen
Wird eine Funktion mit gleichem Namen aber unterschiedlicher Parameterliste (unterschiedliche Signatur)
mehrfach definiert, so spricht man vom Überladen der Funktion.
int max(int x, int y);
double max(double x, double y);
double finance(double pv, double i, int n);
double finance(double pv, double i, double pmt, int n);
Der return-Typ ist beim Überladen irrelevant. Der Compiler entscheidet ausschließlich nach Anzahl und Typ
der aktuellen Parameter, welche Funktion benutzt wird.
Tipps und Hinweise
Wenn Sie eine Funktion definieren, gehen Sie am besten schrittweise vor:
Beantworten Sie sich selbst zunächst folgende Fragen:
1. Wie soll die Funktion heißen?
2. Hat die Funktion Parameter, wenn ja, welchen Datentyp haben sie?
3. Hat die Funktion einen Funktionswert, wenn ja, welchen Datentyp? Wenn nein, dann ist sie vom Typ void.
Formulieren Sie die Titelzeile der Funktion:
double kreisflaeche(double r)
Beginnen Sie mit der Formulierung des Funktionsrumpfes. Werden lokale Variablen benötigt? Wenn ja, werden
sie am Beginn definiert. Weil es einen Funktionswert gibt, muss es am Ende eine return-Anweisung geben.
double kreisflaeche(double r)
{
double f;
return
}
Nun kommt die Implementierung des eigentlichen Algorithmus‘ im Funktionskörper. Am Schluss muss der
Funktionswert in der return-Anweisung angegeben werden:
double kreisflaeche(double r)
{
double f;
f = M_PI * r * r;
return f;
}
Oft geht‘s auch kürzer und ohne lokale Variablen, denn die return-Anweisung kann einen beliebigen Ausdruck
enthalten:
double kreisflaeche(double r)
{
return M_PI * r * r;
}
- 50 -
Augaben
9.1.
9.2.
9.3.
9.4.
9.5.
9.6.
9.7.
9.8.
9.9.
9.10.
9.11.
9.12.
9.13.
9.14.
9.15.
9.16.
9.17.
9.18.
9.19.
9.20.
9.21.
9.22.
9.23.
Welche Bedeutung haben Funktionen?
Welche Funktion muss in jedem C-Programm vorhanden sein?
Was bewirkt die return-Anweisung in einer Funktion?
Was ist ein formaler Funktionsparameter (Beispiel)?
Geben Sie ein Beispiel für einen Funktions-Prototypen.
Was sind Header-Dateien?
Was bewirkt die #include-Direktive?
Welche Header-Datei muss für den Funktionsaufruf system("pause") inkludiert werden?
Welche Angaben können Sie dem folgenden Prototypen entnehmen:
long abs(long n);
Wie erfolgt der Datenaustausch mit Funktionen?
In welchem Zusammenhang tauchen die Begriffe „call by value“ und „call by reference“ auf?
Welcher Unterschied besteht zwischen der Parameterübergabe “by value” und der “by reference”?
Schreiben Sie eine Funktion quadrat, die das Quadrat einer Zahl, die als Parameter übergeben wird,
zurückgibt.
Schreiben Sie eine Funktion kehrwert, die den Kehrwert einer reellen Zahl, die by value übergeben wird,
zurückgibt.
Schreiben Sie eine Eingabefunktion für eine reelle Zahl. Die Funktion soll den Nutzer zur Eingabe
auffordern und die eingegebene Zahl zurückgeben.
Schreiben Sie eine Funktion, die eine Temperatur, die in Grad Fahrenheit übergeben wird in Grad Celsius
umrechnet und zurückgibt. Die Formel lautet: Celsius = 5/9*(Fahrenheit - 32).
Beachten Sie die Datentypen.
Welche Syntaxfehler sind in folgender Funktion?
void schaetzung(int j)
{
const float a0 = 21,214;
a1 = 429.417621;
return a0 + a1 * log10(X)
}
Die folgenden Funktionen können Sie alle in ein- und demselben Projekt implementieren und testen.
Schreiben Sie jeweils eine Funktion zur Berechnung
a) der Fläche eines Quadrates,
b)der Fläche eines Rechteckes,
c) der Oberfläche eines Quaders
d)des Volumens eines Quaders
e) der Oberfläche eines Zylinders
f) des Rauminhaltes eines Zylinders
g)zur Umrechnung von Grad Celsius in Fahrenheit (die Formel lautet : C=(5/9)*(F-32) )
Testen Sie die Funktionen in einem Programm. Der Benutzer soll die jeweilige Funktion über ein Menü
aufrufen. Das Programm soll über einen weiteren Menüpunkt beendet werden.
Schreiben Sie eine Funktion linie, die eine horizontale Line auf den Bildschirm zeichnet.
Die Linie soll aus einzelnen Zeichen mit dem ASCII 315 (oktal) zusammengesetzt werden. Die Länge der
Linie wird als Parameter übergeben. Am Ende der Linie soll ein Zeilenwechsel erfolgen.
Schreiben Sie eine Funktion quersumme, die die Quersumme einer ganzen Zahl bestimmt. Testen Sie
die Funktion in einem Programm. Der Nutzer soll eine Zahl eingeben, die Quersumme soll angezeigt
werden.
Schreiben Sie unter Verwendung der Funktion quersumme aus Aufgabe 9.3. ein Programm, das von
einer eingegebenen Zahl die Quersumme, von der Quersumme wieder die Quersumme, u.s.w berechnet,
bis die Quersumme nur noch einstellig ist. Das Ergebnis wird anzeigt.
Schreiben Sie eine Funktion wurzel, die den Algorithmus der Wurzelberechnung nach HERON (siehe
Vorlesung) implementiert und testen Sie diese Funktion.
Der Algorithmus für nach HERON lautet (a sei der Radiant, x sei die Wurzel):
Setze x=a
Berechne:
x=(x+a/x)/2
solange, bis sich x kaum noch ändert.
Schreiben Sie eine Funktion maximum, die die größere von zwei übergebenen ganzen Zahlen als
Ergebnis liefert und testen Sie diese Funktion mit Nutzereingaben.
9.24. Schreiben Sie eine Funktion ggt, die den größten gemeinsamen Teiler zweier Zahlen bestimmt
(Euklid’scher Algorithmus, siehe Vorlesung) und testen Sie diese Funktion mit Nutzereingaben.
- 51 -
9.25. Für die numerische Integration einer Funktion wird häufig die Trapezregel verwendet:
b
I = ∫ f ( x)dx ≈
a
b−a
( f (a) + f (b))
2
- Schreiben Sie eine Funktion f1 für
f ( x) =
x2 −1
ln( x)
- Schreiben Sie eine Funktion trapez, die das bestimmte Integral der Funktion f1 liefert. Die beiden
Grenzen a und b sollen der Funktion als Parameter übergeben werden.
- Testen Sie die Funktion in einem Programm, wobei der Nutzer die Integrationsgrenzen eingibt.
9.26. Schreiben Sie eine Funktion futureValue, die das künftige Kapital (FV) bei gegebenem Startkapital
(PV), Verzinsung (i in % p.a.) und Laufzeit (n in Jahren) ermittelt und testen Sie diese Funktion.
FV = PV * (1 +
i n
)
100
9.27. Schreiben Sie eine Funktion fakultaet, die die Fakultät einer natürlichen Zahl iterativ berechnet.
9.28. Schreiben Sie eine Funktion x_hoch_n, die die Potenz xn iterativ berechnet.
9.29. Die Exponentialfunktion ex lässt sich durch die folgende Reihe berechnen:
∞
x x 2 x3
xn
e = 1 + + + + ... = ∑
1! 2! 3!
n = 0 n!
x
Schreiben Sie eine Funktion e_hoch_x, die den o.a. Algorithmus verwendet und die Reihe bis zum 13.
Glied (x12/12!) berechnet. Verwenden Sie die beiden Funktionen fakultaet und x_hoch_n aus den
Aufgaben 9.28. und 9.29. Testen Sie die e-Funktion mit einem Programm, bei dem der Nutzer den
Exponenten x eingibt und vergleichen Sie das Ergebnis mit dem Ergebnis, das Sie bei Verwendung der
Potenzfunktion aus der Mathematikbibliothek pow(M_E, x) bekommen.
9.30. Der Binomialkoeffizient
 a  ist folgendermaßen definiert:
B =  
n
n<0: B undefiniert
n=0: B=1
n>0:
a a −1 a − 2
a − n +1
B=
1
*
*
2
3
* ... *
n
Schreiben Sie eine Funktion binkoeff und testen Sie diese in einem Programm.
9.31. Überladen Sie die Funktion futureValue aus Aufgabe 9.26., die das künftige Kapital (FV) bei
gegebenem Startkapital (PV), Verzinsung (i in % p.a.) und Laufzeit (n in Jahren) ermittelt mit einem
zusätzlichen Parameter für regelmäßig wiederkehrende Zahlungen (PMT, so genannte Annuitäten) und
testen Sie diese Funktion mit Nutzereingaben.
i −n
)
100 + FV * (1 + i ) − n = 0
i
100
100
1 − (1 +
PV + PMT *
Die Formel setzt voraus, dass Zahlungen, die man bekommt, positiv sind, solche, die man leistet, sind
negativ. Stellen Sie die Formel nach FV um.
9.32. Schreiben Sie eine Funktion monat, die zu einem Monat, der als Zahl (1..12) übergeben wird, den Monat
als string zurückgibt. Testen Sie die Funktion in einem Programm.
9.33. Überladen Sie die Funktion monat so, dass sie für einen Monat, der als string übergeben wird, die
Zahl (1..12) zurückgibt. Testen Sie die Funktion in einem Programm.
- 52 -
10. Programmierprinzipien und Algorithmen (2)
Das Prinzip der Rekursion - Rekursive Funktionen
Häufig werden mathematische Algorithmen rekursiv formuliert.
Beispiel: Fakultät
0! = 1
1! = 1
n! = n * (n-1)! für n>1
Eine Funktion kann sich selbst aufrufen. Ein solcher Selbstaufruf wird Rekursion genannt.
Eine rekursive Funktion muss immer ein Abbruchkriterium enthalten.
Das Abbruchkriterium muss die Rekursion verhindern, sonst entsteht eine unendliche Rekursion.
Die Funktion zur rekursiven Berechnung der Fakultät könnte folgendermaßen implementiert werden:
int fakultaet(int n)
{
if(n<2)
// Abbruchkriterium
return 1;
// keine Rekursion
return n * fakultaet(n-1); // Rekursion
}
void faktest()
{
cout << "6! = " << fakultaet(6) << endl;
}
--> 10_01_Fakultaet
Funktionsaufrufe beim Aufruf von fakultaet(6):
x = fakultaet(6)
6 * fakultaet(5)
5 * fakultaet(4)
4 * fakultaet(3)
3 * fakultaet(2)
2 * fakultaet(1)
return 1
return 2 * 1
return 3 * 2
return 4 * 6
return 5 * 24
return 6 * 120
x = 720
- 53 -
Beispiel: Umwandlung dezimal-dual
•
•
•
•
Teile die Zahl sukzessive durch 2 und notiere jedes Mal den Divisionsrest.
Der Algorithmus ist beendet, wenn der Quotient 0 ist.
Die Divisionsreste in umgekehrter Reihenfolge gelesen ergeben die Dualzahl.
Beispiel: x = 25
25 / 2 = 12 Rest 1
12 / 2 = 6 Rest 0
6 / 2 = 3 Rest 0
3 / 2 = 1 Rest 1
1 / 2 = 0 Rest 1
Die gesuchte Dualzahl lautet 11001.
Das Problem lässt sich rekursiv lösen:
Teile die Zahl durch 2 und merke dir sowohl den Quotienten als auch den Divisionsrest. Ist der Quotient
ungleich 0, so rufe den Algorithmus mit dem Quotienten als Zahl erneut auf. Schreibe den Divisionsrest auf
den Bildschirm.
--> 10_02_Decimal2Binary
Beispiel: Fibonacci-Zahlen [1]
Ein Kaninchenpaar bekommt ab seinem zweiten Lebensmonat jeden Monat ein neues Paar Kaninchen als
Kinder.
Wie viele Kaninchenpaare gibt es nach 12 Monaten?
Am Anfang
1
1 Monat
1
2 Monate
2
3 Monate
3
4 Monate
5
Die Reihe der Fibonacci-Zahlen ergibt sich, indem jede Zahl aus der Summe
der beiden Vorgänger berechnet wird:
F1 = F2 = 1
FN = FN-1 + FN-2 für N > 2
Die ersten Zahlen der Fibonacci-Reihe sind
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …
Die Berechnung der Fibonacci-Reihe kann rekursiv (rekursive Funktion) oder
iterativ erfolgen.
--> 10_03_Fibonacci
1. Leonardo von Pisa (gen. Fibonacci), ca. 1180 – 1241
- 54 -
Das Prinzip "Teile und Herrsche" (lat.: divide et impera [1]
Beispiel: Ein Ratespiel
Der Nutzer denkt sich eine Zahl zwischen 1 und 100. Der Computer soll die Zahl „erraten“.
Dazu gibt der Computer jeweils einen Tipp ab und der Nutzer teilt dem Computer mit, ob der Tipp zu hoch oder
zu niedrig ist, bzw. ob die Zahl erraten wurde.
Fragen:
- Mit welcher Ratestrategie kann der Computer die Zahl am schnellsten erraten?
- Wie viele Rateversuche werden höchstens benötigt?
Beispiel: Lösung der Gleichung x=cos(x) nach dem Prinzip "Teile und Herrsche"
1,20
1,00
0,80
0,60
0,40
0,20
0,00
0,00
1.
2.
3.
0,20
0,40
0,60
0,80
1,00
1,20
Teile das Intervall (a,b) durch einen Testwert x in zwei Teile und schaue, in welchem Intervall sich die
Lösung befindet.
Befindet sich die Lösung im unteren Intervall, so setze die Obergrenze b auf x herab, ansonsten setze die
Untergrenze a auf x herauf.
Fahre mit Schritt 1 fort, bis die Lösung x die gewünschte Genauigkeit erreicht hat.
Beispiel: Quadratwurzeliteration
r
x
2
Die Zahl x > 0 mit x = r heißt für einen Radianten r > 0 die Quadratwurzel von r.
1. „divide et impera“ - Stifte Unfrieden unter denen, die du beherrschen willst! (legendäres, sprichwörtlich gewordenes Prinzip der
altrömischen Außenpolitik)
Bereits Sunzi (um 500 v. Chr.) beschreibt sinngemäß „teile und herrsche“ als eine Strategie der chinesischen Kriegskunst. Es bedeutet, einen
Gegner in Untergruppen aufzuspalten, die leichter besiegt werden können.
Im problemlösenden Denken bezeichnet „Teile und herrsche“ zwei verschiedene Vorgehensweisen; zum einen die Strategie, das Ziel in
kleinere Einheiten zu zerteilen und diese nacheinander abzuarbeiten, zum anderen die Strategie, die eigenen Kräfte aufzuteilen, um das Ziel
aus mehreren Richtungen anzugehen.
- 55 -
Gesucht ist die rationale Zahl x*, die von der exakten Lösung s > 0 der Gleichung x2 = r nicht mehr als eine
vorgegebene feste Größe K > 0 abweicht, d.h. es gilt |x*-s| ≤ K.
Voraussetzung: Es sei ein beliebiges Paar (a,b) positiver rationaler Zahlen bekannt, so dass a2 < r < b2 gilt.
Der Grundschritt besteht darin, für die Einschachtelung a < √r < b durch Bildung des arithmetischen
Mittelwertes m = (a+b)/2 ein halb so langes Intervall zu finden, in dem der exakte Wert √r liegt. Wir müssen
dafür jeweils ausrechnen, ob m2>r ist. Falls ja, gilt a2<r<m2, ansonsten gilt m2<r<b2.
Setzen wir diese Halbierung lange genug fort, so können wir den Abstand (b-a) unter jede beliebige, aber
feste Fehlerschranke K>0 in endlich vielen Schritten drücken.
•
•
•
•
Beispiel: Bestimmung der Nullstelle einer Funktion - Bisektionsverfahren (Intervallhalbierungsmethode)
y=f(x)
f(b)
f(x1)
a
b
x1
f(a)
Die Funktion f(x) habe im Intervall (a,b) eine Nullstelle, dann gilt: f(a)*f(b) < 0.
1. Teile das Intervall (a,b) durch den Testwert x1 in zwei gleiche Hälften.
2. Ist f(a)*f(x1)<0, dann setze die Obergrenze b auf x1 herab, ansonsten setze die Untergrenze a auf x1 herauf.
3. Wiederhole das Ganze ab Schritt 1. solange, bis sich f(x1) dem Nullpunkt weit genug genähert hat.
--> 10_06_Bisektion
Beispiel: Bestimmung der Nullstelle einer Funktion - regula falsi
y=f(x)
f(b)
f(x1)
a
b
X
f(a)
1.
Berechne innerhalb des Intervalles (a,b) einen Schätzwert x1:
x1 = a − f (a )
2.
3.
b−a
f (b) − f (a )
Liegt die Nullstelle links von x1, so wird b auf x1 herabgesetzt, ansonsten wird a auf x1 heraufgesetzt.
Wiederhole das Ganze, bis sich f(x1) dem Nullpunkt weit genug genähert hat.
10_07_RegulaFalsi
- 56 -
Aufgaben
10.1. Was verstehen Sie unter einer rekursiven Funktion?
10.2. Warum muss eine rekursive Funktion ein Abbruchkriterium enthalten?
10.3. Schreiben Sie eine rekursive Formel für die Berechnung von xn auf. (xn = …) Vergessen Sie das
Abbruchkriterium für die Rekursion nicht.
10.4. Wie lautet die rekursive Definition der Fakultät einer natürlichen Zahl n?
10.5. Schreiben Sie eine rekursive Funktion zur Berechnung der Fakultät einer natürlichen Zahl.
10.6. In den folgenden Formeln bezeichnet Fn die n-te Fibonacci-Zahl, wobei n eine natürliche Zahl >= 3 ist,
und die Festlegungen F1=1 und F2=1 gelten.
I) Fn = Fn-2 + Fn-1
II)
Fn = Fn+2 – Fn+1
III)
Fn+1 = Fn+3 – Fn+2
IV)
1 = Fn+2 / Fn+1, wenn man den Bruch abrundet
Welche der obigen Formeln treffen für alle n zu? Kreuzen Sie die richtige Lösung an.
( )
Nur I
( )
Nur I und II
( )
Nur I und III
( )
Nur I, II und III
( )
Nur I, III und IV
( )
I, II, III und IV
10.7. Führen Sie die Fibonacci-Folge mit drei weiteren Zahlen fort: 1, 1, 2, 3, 5, 8, 13, 21
10.8. Welche Strategie verbirgt sich hinter dem Prinzip „Teile und Herrsche“?
10.9. Erläutern Sie das Prinzip „Teile und Herrsche“ am Beispiel der Nullstellenbestimmung einer Funktion.
10.10. Mit dem Bisektionsverfahren wird eine Nullstelle einer Funktion f(x) im Intervall (a,b) bestimmt.
a) Welche Programmierprinzipien liegen dem Bisektionsverfahren zu Grunde.
b) In der Mitte des Intervalls (a,b) wird ein Testwert xt definiert. Wie wird festgestellt, ob die Nullstelle
links oder rechts des Testwertes xt liegt?
10.11. regula falsi
( )
ist ein Programmierprinzip
( )
beschreibt den goldenen Schnitt
( )
ist ein Verfahren zur näherungsweisen Bestimmung der Nullstelle einer Funktion
( )
arbeitet rekursiv
( )
wird mit dem Programmierprinzip „Teile und Herrsche“ implementiert
10.12. Schreiben Sie eine Funktion fakultaet_rekursiv, die die Fakultät auf rekursiven Wege berechnet:
0! = 1
n! = n * (n-1)! für n>0
10.13. Schreiben Sie eine Funktion x_hoch_n_rekursiv, die die Potenz xn auf rekursiven Wege berechnet:
x0 = 1;
xn = x * xn-1 für n>0
10.14. Die rekursive Definition des größten gemeinsamen Teilers lautet:
a für b = 0

ggt (a, b) = 

 ggt (b, a mod b) für b > 0
Schreiben Sie eine Funktion ggt_rekursiv, die den größten gemeinsamen Teiler zweier ganzer
Zahlen rekursiv berechnet und testen Sie diese Funktion.
10.15. Schreiben Sie eine Funktion fibonacci_rekursiv, die die Fibonacci-Zahl rekursiv berechnet. Die
Definition für eine Fibonacci-Zahl lautet:
F1 = F2 = 1
Fn = Fn-1 + Fn-2 für n>2
10.16. Systematisches „Erraten“ einer natürlichen Zahl x zwischen den Grenzen a und b.
Der Nutzer denkt sich eine Zahl zwischen 1 und 100. Der Computer soll die Zahl „erraten“.
Dazu gibt der Computer jeweils einen Tipp ab und der Nutzer teilt dem Computer mit, ob der Tipp zu
hoch oder zu niedrig ist, bzw. ob die Zahl erraten wurde. (Prinzip: „Teile und Herrsche“)
Die Ratestrategie des Computers ist folgende:
1. Setze x1 gleich (a+b)/2
2. Gib Tipp x1 ab
3. Antwort des Nutzers einlesen (zu hoch, zu niedrig, richtig)
4. wenn x1 zu niedrig ist, dann a=x1 setzen und von 1. wiederholen
wenn x1 zu hoch ist, dann b=x1 setzen und von 1. wiederholen
- 57 -
10.17. Implementieren Sie einen Algorithmus zur iterativen Näherungsbestimmung der Quadratwurzel nach dem
Prinzip „Teile und Herrsche“. Der Nutzer gibt den Radiant ein, das Programm zeigt die Wurzel an. (siehe
Vorlesung)
10.18. Implementieren Sie einen Algorithmus zur Näherungsbestimmung des dekadischen Logarithmus nach
dem Prinzip „Teile und Herrsche“. Der Nutzer gibt das Argument ein, das Programm zeigt den
dekadischen Logarithmus an. Als Potenzfunktion verwenden Sie pow(basis, exponent).
10.19. Es sei f eine stetige Funktion im Intervall [a,b] und es sei f(a)*f(b)<0. Dann weiß man, dass f eine
Nullstelle im offenen Intervall (a,b) besitzen muss.
Ist x die einzige Nullstelle von f im Intervall und ist x1 ein Testwert, dann gilt x<x1, falls f(a)*f(x1)<0 gilt,
bzw. x>x1, falls f(a)*f(x1) > 0 gilt.
Implementieren Sie folgende mathematische Funktion als C-Funktion:
f(x) = px + q*sin(x) + r
x, p, q, r sind der Funktion als Parameter zu übergeben.
Schreiben Sie ein Programm zur Nullstellensuche der angegebenen Funktion f(x) nach dem
Bisektionsverfahren. Bei diesem Verfahren wird der Testwert x1 als Mitte zwischen den Grenzen a und b
ermittelt.
10.20. Modifizieren Sie den Algorithmus nach Aufgabe 10.19. entsrechend der „regula falsi“. Bei diesem
Verfahren wird der Testwert x1 nach folgender Formel ermittelt:
x1 = a − f (a )
b−a
f (b) − f (a )
10.21. Wenden Sie den Algorithmus nach dem Bisektionsverfahren auf die folgende Funktion zur Berechnung
des Zinssatzes i bei gegebenen Größen PV, FV, PMT und n an.
i −n
)
100 + FV * (1 + i ) − n = 0
i
100
100
1 − (1 +
PV + PMT *
Implementieren Sie die Formel als Funktion finance.
Anmerkung:
Die Formel setzt voraus, dass Zahlungen, die man bekommt, positiv sind, solche, die man leistet, sind
negativ.
PV – Startkapital
FV – künftiges Kapital (Endkapital)
i – Zinssatz (% pro Zahlungsperiode)
n – Anzahl der Zahlungsperioden
PMT – Annuitäten (regelmäßig wiederkehrende Zahlung je Zahlungsperiode)
Testen Sie das Programm mit folgenden Werten:
Sie haben einen Sparplan. Zu Beginn leisten Sie eine Einmalzahlung von 1000€. Weiterhin zahlen Sie
monatlich 100€ in den Sparplan ein und zwar 10 Jahre lang. Am Ende der Laufzeit bekommen Sie
20.000€ ausgezahlt. Wie hoch ist die Rendite (Verzinsung)?
10.22. Modifizieren Sie den Algorithmus nach Aufgabe 10.21. entsprechend der „regula falsi“.
- 58 -
11. Arrays
Ein Array ist eine Zusammenfassung mehrerer Variablen vom gleichen Typ unter einem gemeinsamen Namen,
für die ein zusammenhängender Speicherbereich reserviert wird. Auf die einzelnen Komponenten dieses
Bereiches kann durch den gemeinsamen Namen und einen Index zugegriffen werden.
Vergleich Mathematik: Vektorelemente xi, Matrixelemente mi,j
Die Indexierung beginnt stets beim Index 0.
Beispiel: Speicherabbild eines Arrays aus 7 reellen Zahlen:
Index
:
12.8
6.56
10.02
3.87
234
82.1
54.02
0
1
2
3
4
5
6
Definition von Arrays
Ein Array muss (wie jede Variable) vor der Verwendung definiert werden.
Dabei wird der Typ, der Name und die Dimension festgelegt.
Die Dimension (in eckigen Klammern) ist die Anzahl der Elemente.
double werte[100];
Der Indexbereich erstreckt sich von 0 bis Dimension-1. (im Beispiel: 0..99)
Die Arraydimension (im Beispiel: 100) muss eine Konstante sein.
Wenn die Dimension eines Arrays erst zur Laufzeit bekannt (also eine Variable) ist, kann das Array nicht
statisch vereinbart werden.
(Neuere C++-Compiler ermöglichen dennoch eine solche Konstruktion.)
double werte[dim]; // Fehler, wenn dim variabel ist
Ein Array kann mehrere (unbegrenzt viele) Dimensionen haben.
double matrix[10][10];
Die Arraydimension kann zur Laufzeit nicht geändert werden. (Deshalb sagt man: Das Array ist statisch.)
double werte[25];
char text[128];
//
//
//
//
25 Elemente vom Typ double.
Die Elemente sind werte[0]..werte[24].
Array mit 128 Elementen des Typs char.
Die Elemente sind text[0]..text[127].
double matrix[4][5];
// Mehrdimensionales Array mit 20 Elementen vom Typ double.
// Die Elemente sind:
// matrix[0][0],matrix[0][1],matrix[0][2],matrix[0][3],matrix[0][4]
// matrix[1][0],matrix[1][1],matrix[1][2],matrix[1][3],matrix[1][4]
// matrix[2][0],matrix[2][1],matrix[2][2],matrix[2][3],matrix[2][4]
// matrix[3][0],matrix[3][1],matrix[3][2],matrix[3][3],matrix[3][4]
Initialisierung von Arrays
Arrayelemente können über eine Initialisierungsliste mit Anfangswerten belegt werden.
Die Initialisierungsliste ist in geschweifte Klammern einzuschließen.
string woerter[3] = {"Stein", "Schere", "Papier"};
Werden Arrays unvollständig initialisiert, so werden die restlichen Elemente mit 0 initialisiert.
int zahlen[7] = {3, 2, 1}; // unvollständige Initialisierung
Werden Arrays vollständig initialisiert, so kann die Dimensionsangabe entfallen.
float noten[] = {1, 1.3, 1.7, 2, 2.3, 2.7, 3, 3.3, 3.7, 4, 5};
Bei zweidimensionalen Arrays wird je Zeile eine Initialisierungsliste angelegt:
- 59 -
double matrix[3][4]={{2.05, 3.06, 4.12, 3.95},
{5.34, 6,81, 2.00, 4.79},
{5.10, 8,02, 7.56, 6.22}};
Auf Array-Elemente wird generell über einen Index, bzw. bei mehrdimensionalen Arrays über mehrere Indexe
zugegriffen. Der Index kann ein beliebiger ganzzahliger Ausdruck sein. Häufig ist der Index eine Variable vom
Typ int.
const int zeilen=3, spalten=4;
double matrix[zeilen][spalten];
int z,s;
for(z=0; z<zeilen; z++)
for(s=0; s<spalten; s++)
{
cout << "Element" << z+1 << "," << s+1 << ": ";
cin >> matrix[z][s];
}
Es liegt in der Verantwortung des Programmierers, dass der Index im zulässigen Bereich liegt. Ein Zugriff über
das Array-Ende hinaus wird vom System nicht überwacht und kann zu undefiniertem Programmverhalten
führen.
Beispiel: Sieb des Ersatotenes
Der griechische Mathematiker ERASTOTENES (3. Jh. v. Chr.) hat einen Algorithmus gefunden, um alle
Primzahlen zwischen 1 und n zu bestimmen:
Dazu werden alle Produkte i * j‚ wobei gilt: i * j <= n, „durchgespielt“ und das Produkt i*j jeweils als NichtPrimzahl gekennzeichnet. Die nicht gekennzeichneten Zahlen sind Primzahlen.
Für die Primzahlen <= 100 werden also folgende Zahlen aussortiert:
2*2=4
3*2=6
4*2=8
2*3=6
3*3=9
4 * 3 = 12
2*4=8
3 * 4 = 12
4 * 4 = 16
…
...
...
2 * 49 = 98
3 * 32 = 96
4 * 24 = 96
2 * 50 = 100
3 * 33 = 99
4 * 25 = 100
32 * 2 = 64
32 * 3 = 96
33 * 2 = 66
...
49 * 2 = 98
50 * 2 = 100
Der Algorithmus lässt sich noch dadurch optimieren, dass Mehrfachberechnungen von Produkten vermieden
werden. (3 * 2 ist bereits als 2 * 3 berechnet worden, ...)
--> 11_01_Erastotenes
Arrays als Funktionsparameter
Soll einer Funktion ein Array übergeben werden, so ist der formale Parameter als Array ohne Dimension zu
definieren.
Die Funktion besitzt keine Information über die Dimension des Arrays, deshalb muss im Allgemeinen neben
dem Array selbst die Anzahl der Arrayelemente als weiterer Parameter übergeben werden.
double mittelwert(double werte[], int anzahl)
{
int i;
double summe = 0;
for (i=0; i < anzahl; i++)
summe = summe + werte[i];
return summe/anzahl;
}
Eine Ausnahme bilden sogenannte terminierte Arrays, das sind solche, die ein spezielles Ende-Kennzeichen
enthalten. (z.B. enthalten ANSI-C-Zeichenketten ein Null-Byte als Ende-Kennzeichen). Die Funktion kann dann
über das Array iterieren, bis das Ende-Kennzeichen erreicht ist.
Beim Aufruf der Funktion wird als aktueller Parameter der Name des Arrays übergeben:
double messwerte[10] = {2.3, 3.4, 2.98, 4.1, 3.5}, mw;
mw = mittelwert(messwerte, 5);
--> 11_02_ArraysAlsParameter
- 60 -
Tatsächlich wird nicht der gesamte Speicherinhalt kopiert, sondern nur die Adresse des Arrays an die Funktion
übergeben. Insofern handelt es sich bei Arrays immer um Referenzparameter. Somit hat die Funktion über die
formalen Parameter auch Schreibzugriff auf das aktuelle Array.
void sort(double werte[], int anzahl); // kann Arrayelemente sortieren
Eine Funktion kann über die return-Anweisung kein Array zurückgeben.
Häufige Fehler
•
Arrays, deren Dimension vom Nutzer definiert werden soll. Wenngleich neuere C++ Compiler die folgende
Konstruktion akzeptieren, sollte man sie meiden:
int n;
cin >> n;
double array[n];
Wie groß soll dass Array sein? Der Compiler muss bei der Übersetzung im Programm Speicherplatz für das
Array reservieren. Da er die Nutzereingabe (die ja erst zur Laufzeit erfolgt) nicht kennen kann, müsste er
Hellseher sein.
•
Verwechslung von Dimension und Index:
Die Dimension ist die Größe eines Arrays und damit die Anzahl der Elemente. Sie muss der Compiler bei
der Übersetzung kennen, um Speicher zu reservieren.
Der Index ist die Position eines einzelnen Elementes, das zur Laufzeit angesprochen werden soll. Der Index
beginnt mit der Zählung immer bei 0. Deshalb ist der größtmögliche Index Dimension-1.
•
Zu großer Index bzw. zu kleines Array:
const int dim=10;
double array[dim];
for(int i=0; i<= dim; i++)
cout<<array[i]<<endl;
Da der Index bei einer Dimension von 10 nur bis 9 laufen darf, ist der Fall i=dim unzulässig (hier werden
11 Elemente ausgegeben). Das kann sogar zum Programmabsturz führen. Richtig ist
for(i=0; i<dim; i++) ...
Kopieren und Vergleichen von Arrays
Arrays können nicht durch einfache Zuweisung kopiert werden:
int zahlen[3] = {3, 2, 1};
int kopie[3];
kopie = zahlen;
// Fehler
Arrays werden kopiert durch Kopieren der einzelnen Arrayelemente.
Das kann man mit der Funktion memcpy erledigen.
memcpy(kopie, zahlen, sizeof(zahlen));
memcpy() benötigt drei Parameter: Das Zielarray, das Quellarray und die Anzahl Bytes, die kopiert werden
sollen. sizeof(zahlen) liefert die Größe des Arrays in Bytes.
Der Inhalt von Arrays kann nicht durch Anwendung der Vergleichsoperatoren auf die Arraybezeichner
verglichen werden:
if(kopie == zahlen)
// Fehler
Arrays müssen elementweise verglichen werden.
Das kann man mit der Funktion memcmp() erledigen. memcmp() liefert bei Gleichheit zweier
Speicherbereiche das Ergebnis 0:
if(memcmp(zahlen, kopie, sizeof(zahlen)) == 0)
cout << "Arrayinhalt ist gleich" << endl;
- 61 -
Ein-/Ausgabeumlenkung
Große Datenmengen auf dem Bildschirm auszugeben ist oft unübersichtlich.
Große Datenmengen über die Tastatur einzugeben ist mühsam oder gar unmöglich.
Deshalb ist es gelegentlich erwünscht, dass die Ausgabe eines Programms nicht auf dem Bildschirm angezeigt
wird, sondern in einer Datei gespeichert wird oder dass die Eingabe nicht über die Tastatur erfolgen soll, sondern
aus einer Datei gelesen werden soll.
Wird die Standardausgabe cout in eine Datei umgelenkt, so spricht man von einer Ausgabeumlenkung. Diese
wird über die Kommandozeile in folgender Form vorgenommen:
Programm > Datei
Alle Ausgaben des Programmes auf cout werden in die Datei umgelenkt, wobei die Datei ggf. erzeugt wird.
Sofern die Datei bereits existiert, wird sie überschrieben. Die Form
Programm >> Datei
hängt alle Ausgaben an die bereits existierende Datei an.
Sollen Eingaben nicht über die Tastatur vorgenommen, sondern aus einer Datei gelesen werden, so kann die
Standardeingabe cin ebenfalls auf eine Datei umgelenkt werden.
Programm < Datei
Alle Eingaben des Programmes werden dann anstatt von der Tastatur aus der Datei gelesen.
Ein- und Ausgabeumlenkung können auch kombiniert werden:
Programm < Eingabedatei > Ausgabedatei
Ein- und Ausgabeumlenkung funktionieren nur in der Kommandozeile.
Hier einige Tipps für die ersten Schritte:
Schreiben Sie ein kleines Programm, dass 100 Zufallszahlen auf cout ausgibt. Hier ein Fragment daraus:
for(i=0;i<100;i++)
cout<<rand()<<endl;
Das Programm nennen Sie beispielsweise zufall.exe
Öffnen Sie die Windows-Eingabeaufforderung (Das Programm heißt cmd).
Wechseln Sie mittels des Befehls cd in das Verzeichnis, in dem sich das Programm zufall.exe befindet.
Geben Sie in der Eingabeaufforderung ein:
zufall
Die 100 Zufallszahlen werden auf dem Bildschirm angezeigt. Geben Sie nun ein:
zufall > test.txt
Sie sehen keine Programmausgabe auf dem Bildschirm, auch nicht „Drücken Sie eine beliebige Taste“ von
system(„pause“);. Eine Taste müssen Sie ggf. trotzdem drücken, um das Programm zu verlassen.
Geben Sie den Befehl dir ein. Sie sehen, dass die Datei test.txt erzeugt wurde.
Die Datei test.txt können Sie mit einem Texteditor öffnen, ansehen und ggf. editieren.
Sie können den Texteditor (notepad) über die Kommandozeile mit einem Dateinamen als Argument aufrufen:
notepad test.txt
- 62 -
Aufgaben
11.1.
11.2.
11.3.
11.4.
11.5.
11.6.
11.7.
11.8.
11.9.
11.10.
Definieren Sie eine Array-Variable für reelle Zahlen mit der Dimension 20.
Ein Array besitze die Dimension 20. Welchen Bereich besitzt ein Index auf dieses Array?
Geben Sie ein Beispiel für Definition und Initialisierung eines Arrays aus fünf ganzen Zahlen mit den
Initialwerten 0.
Was muss bei der Übergabe eines nicht-terminierten Arrays (also eines Arrays, dessen Ende nicht mit
einem speziellen Code gekennzeichnet ist) an eine Funktion beachtet werden?
Gegeben seien die Variablendefinitionen:
int i;
double werte[10];
Was gibt es an folgender Anweisung auszusetzen?
for (i=0; i<=10; i++)
cout << werte[i] << endl;
Praktikumsaufgeben 11.1. und 11.2. (Mittelwert)
Was verstehen Sie unter einer Ausgabeumlenkung auf der Konsole?
Auf der Konsole wird folgendes Kommando eingegeben:
sort.exe < daten.txt > out.txt
Erläutern Sie die beiden Operatoren < und >.
Schreiben Sie je eine Funktion maxindex und minindex, die den Index der größten bzw. der
kleinsten Zahl eines Arrays aus reellen Zahlen zurück geben und testen Sie diese mit geeigneten Daten.
Schreiben Sie je eine Funktion mittelwert und standardabweichung, die Mittelwert und
Standardabweichung von reellen Zahlen, die in einem Array übergeben werden, zurück geben und
testen Sie diese mit geeigneten Daten. Die Formeln lauten:
n
n
∑ xi
x=
11.11.
11.12.
11.13.
∑ (x − x)
2
i
i =1
s=
n
i =1
n
Erweitern Sie das Programm aus Aufgabe 11.10. dahingehend, dass Mittelwert und
Standardabweichung berechnet werden unter Auslassung des kleinsten und des größten Wertes.
Verwenden Sie die Funktionen aus Aufgabe 11.9.
Testen Sie das Programm 11.11., indem Sie die Eingabedaten mittels Eingabeumlenkung aus einer
Datei lesen (siehe Vorlesung).
Gegeben sei folgender (linearer) Zusammenhang zwischen einer unabhängigen Größe x und der
abhängigen Größe y: y = a + bx
Liegen ausreichend viele Wertepaare (xi, yi) vor, so kann der Regressionskoeffizient b (Anstieg der
Regressionsgeraden) folgendermaßen geschätzt werden:
bˆ =
∑ ( x − x )( y − y )
∑ (x − x)
i
i
2
i
Der Parameter a kann dann aus den Mittelwerten und dem Parameter b geschätzt werden:
aˆ = y − bˆx
Verwenden Sie als Ausgangsprojekt das Vorlesungsbeispiel 11_02_ArraysAlsParameter.
Schreiben Sie eine Funktion linreg, der zwei Arrays (die x-Werte in einem Array, die y-Werte in
einem zweiten Array) sowie die Anzahl der Wertepaare übergeben werden und die den
Regressionskoeffizienten b als Ergebnis liefert. Testen Sie die Funktion mit folgenden Wertepaaren und
geben Sie die Parameter a und b aus:
x
y
0.3, 1.34
0.9, 2.05
1.5, 2.65
2.2, 3.12
3.0, 3.97
3.8, 4.75
- 63 -
12. Algorithmen mit Containern
Ein Container enthält Daten gleichen Typs. Auch ein Array ist ein Container.
Häufig benötigte Algorithmen mit Containern sind:
• Einfügen
• Löschen
• Suchen
• Sortieren
Algorithmen, die mit große Datenmengen operieren, werden nach Ihrer Berechnungskomplexität (kurz:
Komplexität) beurteilt. Die Komplexität macht eine Aussage darüber, wie sich die Laufzeit des Algorithmus in
Abhängigkeit von der Menge der Daten verhält.
Je nach logischer Organisation der Daten werden unterschiedliche Datenstrukturen unterschieden, zum
Beispiel:
• Vektor: Unmittelbar (eindimensional) aufeinander folgende Daten. Jedes Datum kann direkt über einen
Index angesprochen werden.
Bei einem Array handelt es datenorganisatorisch um einen Vektor.
• Baum: Organisation in Form von „Eltern-Kind“-Beziehungen. Ein Vorgänger (Elter) kann mehrere
Nachfolger (Kinder) haben. Jeder Nachfolger kann seinerseits wieder Nachfolger haben. Ein Sonderfall ist
der binäre Baum, bei dem jeder Vorgänger zwei Nachfolger hat. Ein Indexzugriff ist nicht möglich.
• Liste: Sonderfall des Baumes mit nur einem Nachfolger je Element.
Datenstrukturen können auch nach Ihrer Funktionalität unterschieden werden, zum Beispiel:
• Set (Menge): Ungeordnete Ansammlung von Daten. Die Reihenfolge spielt keine Rolle. Es ist nur relevant,
ob ein Element in einer Menge ist oder nicht.
• Stack (Stapel, LIFO-Speicher): Daten können nur an einem Ende hinzugefügt oder entnommen werden
(Operationen: push, pop).
• Queue (Warteschlange, FIFO-Speicher): Daten werden an einem Ende hinzugefügt (push_front) und am
anderen Ende entnommen (pop_back).
Beispiel: Löschen eines Elementes aus einem Array
Gegeben sei ein Array arr[] mit der Dimension dim. Das Element an der Position pos soll gelöscht werden.
Dazu müssen alle Elemente rechts der Position pos um eine Stelle nach links verschoben werden.
Algorithmus:
Beginne mit dem Element rechts der Position pos und verschiebe es um eine Stelle nach links. Wiederhole diese
Operation mit dem nächsten Element und so weiter, solange, bis das letzte Element verschoben wurde.
void DeleteFromArray(float arr[], int dim, int pos)
{
int i;
for(i=pos; i<dim-1; i++)
arr[i] = arr[i+1];
}
Frage: Wie viele „Verschiebeschritte“ sind notwendig, um ein Element zu löschen?
Beispiel: Einfügen eines Elementes in ein Array
Gegeben: Ein Array arr[] mit der Dimension dim. Das Element neu, soll an der Position pos eingefügt
werden.
Um Platz für das einzufügende Element zu schaffen, müssen alle Elemente ab der Position pos um eine Stelle
nach rechts verschoben werden.
Algorithmus:
Beginne mit dem letzten Element und verschiebe es um eine Position nach rechts.
Wiederhole diese Operation mit dem vorletzten Element und so weiter, solange, bis das Element mit der Position
verschoben wurde, an der das neue Element eingefügt werden soll. Schreibe nun das neue Element auf die
entsprechende Position.
// Achtung: Falls das Array „randvoll“ ist,
// verschwindet das letzte Element.
void InsertIntoArray(float arr[], int dim, float neu, int pos)
{
int i;
- 64 -
for(i=dim-2; i>=pos; i--)
arr[i+1] = arr[i];
arr[pos] = neu;
}
Frage: Wie viele „Verschiebeschritte“ sind notwendig, um ein Element einzufügen?
Rechenzeit von Algorithmen
Bei einem Algorithmus A wächst die Rechenzeit t linear mit der Datenmenge n, beim Algorithmus B wächst die
Rechenzeit t quadratisch mit der Datenmenge n.
t
A
n
t
B
n
Frage: welcher Algorithmus ist bei großen Datenmengen schneller?
Antwort: Algorithmus A
Frage: Welcher Algorithmus ist bei sehr kleinen Datenmengen schneller?
Antwort: Das lässt sich nicht sagen. Bei kleinen Datenmengen kann Algorithmus A oder B schneller sein.
Jedoch nur bei kleinen Datenmengen kann Algorithmus B schneller sein.
Die O-Notation
Die O-Notation beschreibt die Berechnungskomplexität (auch Komplexität oder Laufzeitkomplexität genannt)
eines Algorithmus, d.h., wie sich die Laufzeit des Algorithmus in Abhängigkeit von der Menge der zu
bearbeitenden Daten verhält.
Wenn N die Anzahl der Daten ist, dann stellt f(N) die funktionale Abhängigkeit der Laufzeit des Algorithmus
von N dar. Die O-Notation ist eine verkürzte Darstellung der Funktion f(N).
Häufige Komplexitäten sind:
• O(1)
Laufzeit ist konstant, d.h. unabhängig von N.
• O(log N)
Laufzeit wächst logarithmisch mit N. Bei einer Verdopplung von N wächst die Laufzeit um
einen gewissen konstanten Wert.
• O(N1/2)
Laufzeit wächst mit Wurzel aus N. Bei 100-fachem N verzehnfacht sich die Laufzeit.
• O(N)
Laufzeit wächst linear mit N. Eine Verdopplung von N hat eine Verdopplung der Laufzeit zur
Folge.
• O(N log N) Laufzeit wächst „linear-logarithmisch“. Wenn N sich verdoppelt, wird die Laufzeit etwas mehr
als doppelt so groß.
• O(N2)
Laufzeit wächst quadratisch mit N. Bei einer Verdopplung von N vervierfacht sich die Laufzeit
• O(N3)
Laufzeit wächst kubisch: Bei einer Verdopplung von N erhöht sich die Laufzeit auf das
Achtfache
--> 12_00_Komplexität
- 65 -
Es ist nicht immer ganz einfach herauszufinden, welche Komplexität ein Algorithmus hat. Es gibt jedoch
Anhaltspunkte:
Wenn es eine Schleife gibt, deren Laufzeit proportional N ist, so ist die Komplexität O(N).
Gibt es zwei ineinander geschachtelte Schleifen, die beide proportional zu N sind, so ist die Komplexität O(N2).
Das lässt sich leicht veranschaulichen. Betrachten Sie folgenden Algorithmus:
for(i=0; i<n; i++)
{
for(j=0; j<n; j++)
cout<<'*';
cout<<endl;
}
Der Algorithmus besteht aus zwei ineinander geschachtelten Schleifen und beschreibt n Zeilen und n Spalten mit
dem Zeichen ‚*‘. Die beschriebene Fläche ist ein Quadrat mit der Seitenlänge n. Wie berechnet sich diese
Fläche: A=n2. Die Anzahl der ausgegebenen ‚*‘ ist also proportional zu n2, die Komplexität also O(N2).
Eine kleine Modifikation würde aus dem Quadrat ein Dreieck machen:
for(i=0; i<n; i++)
{
for(j=0; j<=i; j++)
cout<<'*';
cout<<endl;
}
Die Fläche bleibt proportional zu n2, auch wenn sie nur halb so groß ist. Auch hier ist die Komplexität O(N2).
Ein komplizierteres Beispiel: Sieb des Erastotenes
Bestimmung aller Primzahlen zwischen 1 und n.
Dazu werden alle Produkte i*j‚ wobei gilt: i*j <= n, „durchgespielt“ und das Produkt i*j jeweils als NichtPrimzahl gekennzeichnet. Die nicht gekennzeichneten Zahlen sind Primzahlen.
Für die Primzahlen <= 100 werden also folgende Zahlen aussortiert:
2*2=4
2*3= 6
2*4=8
2*4=10
2*6=12
2*7=14
2*8=16
2*9=18
2*10=20
…
2*39=78
2*40=80
2*41=82
2*42=84
2*43=86
2*44=88
2*45=90
2*46=92
2*47=94
2*48=96
2*49=98
2*50=100
3*3=9
3*4=12
3*5=15
3*6=18
3*7=21
3*8=24
3*9=27
3*10=30
...
3*25=75
3*26=78
3*27=81
3*28=84
3*28=87
3*30=90
3*31=93
3*32=96
3*33=99
4*4=16
4*5=20
4*6=24
4*7=28
4*8=32
4*9=36
4*10=40
...
4*18=72
4*19=76
4*20=80
4*21=84
4*22=88
4*23=92
4*24=96
4*25=100
5*5=25
5*6=30
5*7=35
5*8=40
5*9=45
5*10=50
...
5*14=70
5*15=75
5*16=80
5*17=85
5*18=90
5*19=95
5*20=100
6*6=36
6*7=42
6*8=48
6*9=54
6*10=60
6*11=66
6*12=72
6*13=78
6*14=84
6*15=90
6*16=96
7*7=49
7*8=56
7*9=63
7*10=70
7*11=77
7*12=84
7*13=91
7*14=98
8*8=64
8*9=72
8*10=80
8*11=88
8*12=96
9*9=81
9*10=90 10*10=100
9*11=99
Die Komplexität dieses Algorithmus ist O(N log N)
--> 11_01_Erastotenes
--> 12_00_Komplexität
- 66 -
Lineares Suchen
Vorteil: Daten müssen nicht sortiert sein.
Algorithmus:
Durchlaufe die Daten der Reihe nach und prüfe jedes Element, ob es dem Suchkriterium entspricht. Bei einer
Übereinstimmung ist die Suche beendet.
int linearSearch(int x[], int n, int toFind)
{
int i;
// Laufindex
for(i=0; i<n; i++)
if(toFind == x[i])
return i;
return -1;
// nichts gefunden
}
Komplexität: O(N)
Binäres Suchen
Voraussetzung: Sortierte Daten
Algorithmus (Prinzip: „Teile und Herrsche“):
1. Teile die Daten in zwei Hälften.
2. Stelle fest, in welcher Hälfte sich das gesuchte Element befindet.
Wiederhole ab 1. bis nur noch das gesuchte Element übrig bleibt oder es nichts mehr zu teilen gibt.
Die folgende Implementierung ist problematisch, wenn toFind in x[] nicht vorkommt:
int binarySearch(int x[], int dim, int toFind)
{
int h, low=0, high=dim-1;
// Indexe
do {
h=(low+high)/2;
// Hälfte-Index
if(toFind < x[h]) // untere Hälfte ?
high=h;
// Obergrenze herabsetzen
else
low=h;
// Untergrenze heraufsetzen
} while(toFind != x[h]);
return h;
}
Komplexität: O(log N)
Aufgabe: Den Algorithmus so modifizieren, dass keine Endlosschleife entstehen kann.
Sortieren
Je nach Sortierstrategie werden verschiedene Algorithmen unterschieden.
Wichtige Vertreter der elementaren Sortieralgorithmen sind:
– Selection Sort
– Insertion Sort
– Bubble Sort
Alle elementaren Sortieralgorithmen besitzen die Laufzeitkomplexität O(N2).
Komplexe Sortieralgorithmen besitzen eine bessere Laufzeitkomplexität als elementare Algorithmen (meist O(N
log N). Einige dieser Algorithmen arbeiten rekursiv.
Wichtige Vertreter der komplexen Sortieralgorithmen sind:
– Shell Sort
– Merge Sort
– Quick Sort
- 67 -
Beispiel: Selection Sort
Finde das kleinste Element und tausche es gegen das erste. Finde das zweitkleinste und tausche es gegen das
zweite. Fahre so fort, bis zum vorletzten Element.
39
76
45
83
63
53
34
34
76
45
83
63
53
39
34
39
45
83
63
53
76
34
39
45
83
63
53
76
34
39
45
53
63
83
76
34
39
45
53
63
83
76
34
39
45
53
63
76
83
Selection Sort benötigt ungefähr N2/2 Vergleiche und N Austauschoperationen.
Die Komplexität ist also O(N2).
--> 12_04_SelectionSort
Beispiel: Insertion Sort
Betrachte die Elemente nacheinander. Ein gerade betrachtetes Element wird an dem richtigen Platz zwischen die
bereits betrachteten Elemente eingefügt.
39
76
45
83
63
53
34
39
45
76
83
63
53
34
39
45
63
76
83
53
34
39
45
53
63
76
83
34
34
39
45
53
63
76
83
Insertion Sort benötigt im Durchschnitt ungefähr N2/4 Vergleiche und N2/8 Austauschoperationen, im
ungünstigsten Fall doppelt so viele.
Die Komplexität beträgt also O(N2).
Insertion Sort ist für „fast sortierte“ Dateien linear.
--> 12_03_InsertionSort
- 68 -
Beispiel: Bubble Sort
Durchlaufe immer wieder die Daten und vertausche jedes mal, wenn es notwendig ist, benachbarte Elemente.
Wenn kein Tausch mehr notwendig ist, ist Schluss.
1. Durchlauf:
2. Durchlauf:
3. Durchlauf:
4. Durchlauf:
5. Durchlauf:
6. Durchlauf:
Ende:
39
39
39
39
39
39
39
39
39
39
39
39
34
76
45
45
45
45
45
45
45
45
45
45
34
39
45
76
76
76
76
63
63
63
53
53
34
45
45
83
83
63
63
63
76
53
53
63
34
53
53
53
63
63
83
53
53
53
76
34
34
63
63
63
63
53
53
53
83
34
34
34
76
76
76
76
76
76
34
34
34
34
83
83
83
83
83
83
83
83
83
Bubble Sort benötigt im Durchschnitt und im ungünstigsten Fall N2/2 Vergleiche und N2/2
Austauschoperationen.
Die Komplexität beträgt also O(N2).
--> 12_05_BubbleSort
Aufgaben
12.1. Welcher Unterschied besteht zwischen einem Stack und einer Queue?
12.2. Welche Aussagen über einen Stack sind korrekt:
I) er enthält immer sortierte Daten
II) er arbeitet nach dem Prinzip first-in-first-out
III) er besitzt die Operationen push und pop
IV) er realisiert eine Warteschlange
12.3. Welche Voraussetzung muss erfüllt sein, damit ein Wert in einem Container nach dem Prinzip „Teile und
Herrsche“ gesucht werden kann?
12.4. Was verstehen Sie unter der Komplexität (bzw. Laufzeitkomplexität) eines Algorithmus?
12.5. Was verstehen Sie unter der O-Notation?
n
12.6. n2 lässt sich iterativ nach folgender Vorschrift berechnen: 2
n = ∑ (2i − 1)
i =1
Welche Berechnungskomplexität besitzt dieser Algorithmus bezüglich n? (O-Notation)
12.7. Gegeben sei folgender Algorithmus zur Bestimmung der Anzahl der Punkte mit ganzzahligen
Koordinaten in einem Kreis mit gegebenen reellem Radius r:
„Für jedes Koordinatenpaar (x,y) mit –r ≤ x ≤ r und –r ≤ y ≤ r und x,y ganzzahlig prüfe, ob der Punkt im
Kreis liegt. Wenn ja, inkrementiere den Punktzähler.“
Begründen Sie, welche Berechnungskomplexität dieser Algorithmus bezogen auf den Radius r hat.
12.8. Von zwei Sortieralgorithmen besitzt der eine die Komplexität O(N log N), der zweite die Komplexität
O(N2). Welcher sollte bevorzugt werden und warum?
12.9. Sortieralgorithmus A benötigt für die Sortierung von 10000 Datensätzen 20 ms und für 20000 Datensätze
80 ms. Algorithmus B benötigt für die Sortierung von 10000 Datensätzen 100 ms und für 20000
Datensätze 230 ms.
a) Welche Berechnungskomplexität vermuten Sie für die Algorithmen A und B (O-Notation)?
b) Welchen Algorithmus sollte man für sehr große Datenmengen (>100000 DS) bevorzugen?
c) Hat Algorithmus B bei sehr kleinen Datenmengen Vorteile? (Begründung)
12.10. Welche Berechnungskomplexität besitzt das Einfügen eines Elementes an beliebiger Position in ein Array
der Dimension n? Geben Sie eine kurze Begründung
12.11. Welche Berechnungskomplexität bezüglich der Dimension n eines unsortierten Containers besitzt das
lineare Suchen eines Elementes in diesem Container?
12.12. Ein primitiver Algorithmus zur Berechnung der Potenz xn (n ganzzahlig) lautet: „Multipliziere die
Mantisse x n-mal.“
Welcher der folgenden Graphen verdeutlicht die Laufzeit des Algorithmus in Abhängigkeit vom
Parameter n am besten? (Kreuzen Sie die richtige Lösung an.)
- 69 -
A
B
C
D
E
12.13. Es seien A und B zwei Algorithmen, jeweils mit dem Parameter n. Der Algorithmus A habe das
Zeitverhalten O(N log N), der Algorithmus B das Zeitverhalten O(N2).
Was kann man bei den Zeitabläufen von A und B erwarten? (Kreuzen Sie die richtige Lösung an)
( )
Derjenige Algorithmus, der auf dem schnelleren Computer läuft, läuft bei jedem vorgegebenen
Wert von n schneller.
( )
A läuft schneller als B für alle Werte von n.
( )
B läuft schneller als A für alle Werte von n.
( )
Nur für kleine Werte von n kann B schneller als A laufen.
( )
Nur für kleine Werte von n kann A schneller als B laufen
12.14. Beschreiben Sie mit möglichst wenigen Worten den Algorithmus „Bubble Sort“.
12.15. Gegeben sei die Beschreibung eines Sortieralgorithmus:
„Betrachte die Elemente nacheinander. Ein gerade betrachtetes Element wird an dem richtigen Platz
zwischen die bereits betrachteten Elemente eingefügt.“
a) Wie heißt dieser Algorithmus?
b) Welche Berechnungskomplexität besitzt er bezogen auf die Datenmenge N? (O-Notation)
12.16. Gegeben sei die folgende Beschreibung eines Sortieralgorithmus:
„Finde das kleinste Element und tausche es gegen das erste. Finde das zweitkleinste und tausche es gegen
das zweite. Fahre so fort, bis zum vorletzten Element.“
a) Welchen Namen trägt dieser Algorithmus?
b) Welche Berechnungskomplexität besitzt er bezogen auf die Datenmenge N? (O-Notation)
12.17. Begründen Sie, warum alle elementaren (primitiven) Sortieralgorithmen die Komplexität O(N2) haben!
12.18. Der Algorithmus BinSearch von Seite 67 ist fehlerhaft, weil er zu einer Endlosschleife führt, wenn das zu
suchende Element nicht vorkommt. Modifizieren Sie den Algorithmus so, dass keine Endlosschleife
entstehen kann.
12.19. Modifizieren Sie das Beispielprojekt 12_05_BubbleSort dahingehend, dass der Nutzer die Anzahl der
Daten und die Daten selbst eingeben kann.
12.20. Schreiben Sie ein kleines Programm, das eine vom Nutzer bestimmte Anzahl reeller Zufallszahlen im
Bereich 0..1 erzeugt und diese untereinander auf cout ausgibt. Bevor die Zufallszahlen ausgegeben
werden, soll deren Anzahl auf cout ausgegeben werden.
Starten Sie das Programm über die Eingabeaufforderung und leiten Sie die Ausgabe in eine Datei um,
beispielsweise:
Prog12_2 > zufallszahlen.txt
Testen Sie das Programm aus Aufgabe 12.1., indem Sie es über die Eingabeaufforderung starten und alle
Eingabedaten über eine Eingabeumlenkung aus der Datei lesen, z.B.:
Prog12_1 < zufallszahlen.txt
12.21. Kombinieren Sie die Beispielprojekte der Vorlesung 12_03_InsertionSort, 12_04_SelectionSort und
12_05_BubbleSort zu einem einzigen Programm. Der Nutzer soll die Zahlen eingeben (die Anzahl legt er
vorher fest) und er soll den Sortieralgorithmus auswählen können und ob die Daten aufsteigend oder
absteigend sortiert werden sollen.
- 70 -
13. Einführung in die Objektorientierte Programmierung
Objektorientierung wird nicht nur bei der Programmierung angewendet, sondern zum Beispiel auch
in der Datenbank- und GIS-Technologie,
beim Entwurf von Benutzeroberflächen,
bei der Analyse und dem Software-Design.
Die dahinter stehende Grundidee ist: Objekte der realen Welt in der Software abbilden.
Objekte sind „Bausteine“ für Programme.
Modellierung,
Abstraktion,
Ausschnitt
Realwelt
Modell
Problem beim klassischen prozeduralen Konzept: Daten und Funktionen bilden keine Einheit. Daraus resultieren
diverse Problemfelder: Fehlerhafte Zugriffe, nicht initalisierte Variable, ...
Beim objektorientierten Konzept bilden Attribute (Daten) und Methoden (Funktionen) eine Einheit. Objekte
stehen untereinander in Verbindung.
Objekt
1
Attribute
Methoden
Nachricht
Nachricht
Objekt
2
Attribute
Methoden
Vorteile:
• Technologisch: Kein Bruch zwischen Design und Implementierung
• Qualitativ: Höhere Softwaresicherheit
• Produktiv: Wiederverwendbarkeit von Code („Software-Bausteine“)
• ... (und noch viel mehr)
Datenabstraktion
Eine OO-Sprache muss das Prinzip der Datenabstraktion unterstützen. Das heißt, der Nutzer hat die Möglichkeit
eigene abstrakte Datentypen (benutzerdefinierte Datentypen), bestehend aus Attributen und Methoden, zu
definieren.
- 71 -
Objekt
e
Gebäude
(Datentyp)
Abstraktio
Reale Welt
Gebäude 3:
Attribute
•
•
Grundfläche
•
Einheitswert
...
Eigenschaften
Gebäude
2:
• Grundfläche:
Eigenschaften
Gebäude
1:
87qm
• Grundfläche:
• Anzahl Stockwerke:
Attribute
48qm 4
• Grundfläche:
• Anzahl Stockwerke:
105qm2 • Einheitswert:
21500
• Anzahl
Stockwerke:
• ...Einheitswert:
3
Methoden
21800
• ...Einheitswert:
Methoden
55900
...
Methoden
Anzahl
Stockwerke
Methoden
•
•
•
Errichten
Vermieten
Reinigen
Der erste Schritt: Strukturen
Eine Struktur ist ein benutzerdefinierter Datentyp.
Strukturen sind Zusammenfassungen mehrerer Komponenten unter einem gemeinsamen Namen. Die
Strukturkomponenten müssen nicht vom gleichen Typ sein. Die Komponenten können Vereinbarungen von
einfachen Datentypen, Arrays oder auch von Strukturen sein.
Die Strukturdefinition vereinbart einen Datentyp, keine Variable! Es wird kein Speicherplatz reserviert.
Eine Strukturdefinition sollte am Anfang des Programmes, noch vor den Funktionsdefinitionen erfolgen.
struct datum
{
int tag, monat, jahr;
};
// beachte das Semikolon!!!
datum ist ein Datentyp.
struct mitarbeiter
{
string name;
char geschlecht;
char familienstand;
double
lohn;
};
mitarbeiter ist ein Datentyp.
Da eine Struktur ein Datentyp ist, können von diesem Typ Variablen vereinbart werden:
Variablendefinition:
mitarbeiter betriebsleiter, angestellte[20];
betriebsleiter und angestellte sind Bezeichner.
Benutzerdefinierte Datentypen (Strukturen und Klassen) können „by value“ oder „by reference“ an Funktionen
übergeben werden.
void datumAusgabe(datum d);
// by value
void datumEingabe(datum &d); // by reference
Eine Funktion kann eine Struktur oder eine Klasse zurückgeben.
datum heute();
Da in einer Struktur ein Array gekapselt sein kann, ist es auf diese Weise auch möglich, dass eine Funktion ein
Array zurück gibt (was auf direktem Wege ja nicht möglich ist).
- 72 -
Der zweite Schritt: Klassen
Klassen stellen eine Erweiterung des Strukturbegriffes dar. Klassen bestehen aus Daten (Attributen) und
Funktionen (Methoden).
Zusammenfassung zusammengehörender Daten
+
Zugriffskontrolle
+
Operationen
+
weitere Spezialitäten
=
C++ Klassen
class bruch
{
private:
int zaehler;
int nenner;
public:
bruch();
void eingabe();
void ausgabe();
...
};
Kapselung
Eine OO-Sprache muss das Prinzip der Datenkapselung unterstützen. Das heißt, der Zugriff auf die Attribute von
Objekten wird ausschließlich gemäß der öffentlichen Schnittstelle gewährt.
Kapselung bedeutet, dass Daten und Implementierung nach außen hin unsichtbar sind.
Ziele:
• Verbergen möglicherweise komplizierter Einzelheiten
• Klare Definition der Schnittstelle
• Unabhängigkeit der Benutzung eines Objekts von seiner Implementierung im Detail – die Implementierung
ist austauschbar.
Den „Benutzer“ einer Klasse (d.h. den Programmierer, der eine fertige Klasse verwendet) interessiert
ausschließlich die öffentliche Schnittstelle, das heißt, die Methoden, die er aufrufen kann.
Beispiel: Kapselung bei der Klasse Bruch
Initialisieren
Eingabe
Ausgabe
Addieren
Zähler
Nenner
Multiplizieren
Subtrahieren
Dividieren
Kehrwert
Kürzen
- 73 -
Attribut und Methote
Ein Attribut ist eine benannte Eigenschaft eines Objekts einer Klasse.
Ein Attribut besitzt (wie eine Variable) einen Typ, einen Namen und einen Wert.
Eine Methode ist eine Funktion, die ein bestimmtes Verhalten eines Objekts einer Klasse definiert.
Attribute und Methoden können öffentlich (public) oder vor Zugriff geschützt (private) sein.
Nach außen stellt die Klasse über öffentliche Methoden eine Schnittstelle bereit.
class tank
{
private:
double volumen;
double inhalt;
public:
tank(double volumen);
double befuellen(double wieviel);
double entnehmen(double wieviel);
};
Klasse und Instanz
Eine Klasse ist ein abstrakter Datentyp. Sie beschreibt Objekte gleichen Typs, ist also eine Schablone für zu
erzeugende Variable.
Ein Objekt bzw. eine Instanz (Objekt und Instanz sind im Allgemeinen synonym) ist ein konkreter Repräsentant
seiner Klasse. Ein Objekt existiert im Speicher des Rechners. Es unterscheidet sich von anderen Objekten seiner
Klasse durch seine Identität (Adresse im Speicher) und die Werte der Attribute.
Wie bei Variablen üblich, erfolgt die Definition eines Objektes durch Angabe von Datentyp und Name.
tank t1;
Für die Daten des Objektes t1 wird Speicherplatz reserviert.
Eine Initialisierung erfolgt nur dann, wenn der Programmierer der Klasse dafür eine spezielle Methode, den so
genannten Konstruktor, geschrieben hat. (siehe Kap. 15)
tank t2(50);
Eine Zuweisung erfolgt datenelementweise auf ein anderes Objekt gleichen Typs.
Es wird eine 1:1-Kopie aller Attribute erzeugt.
bruch b1(3,4), b2;
b2 = b1;
Aufruf von Methoden
Methoden werden über ein zugehöriges Objekt (eine Instanz) aufgerufen.
Der Operator . (Punkt) verbindet Objekt und Methode.
void bruchtest()
{
bruch b1;
// Objekt definieren
cout<<"b1=";
b1.eingabe();
// Methode aufrufen
cout<<"b1=";
b1.ausgabe();
cout<<endl;
// Methode aufrufen
}
--> 13_01_Bruch
- 74 -
Arrays von Objekten
Voraussetzung für die Erzeugung eines Arrays von Objekten ist die Existenz eines Standardkonstruktors (siehe
Kapitel 15).
void arraytest()
{
int i;
bruch b[10];
// 10 Objekte vom Typ bruch
for(i=0; i < 10; i++)
b[i].ausgabe();
}
Aufgaben
13.1.
13.2.
13.3.
13.4.
13.5.
13.6.
13.7.
13.8.
13.9.
13.10.
13.11.
13.12.
13.13.
13.14.
13.15.
13.16.
13.17.
13.18.
Wie wird in C++ das Prinzip der Datenabstraktion unterstützt?
Definieren Sie eine Struktur bruch zur Abstraktion eines Bruches aus zwei ganzen Zahlen.
Schreiben Sie eine Funktion, die den Kehrwert eines Bruches, der als Parameter übergeben wird, liefert.
Definieren Sie eine Struktur zeit, bestehend aus Stunden, Minuten und Sekunden.
Definieren Sie eine Variable vom Typ zeit (siehe 4.).
Wie wird in C++ das Prinzip der Datenkapselung unterstützt?
Was ist eine Klasse?
Was ist eine Instanz?
Was ist ein Attribut?
Was ist eine Methode?
Warum werden Attribute private deklariert?
Erzeugen Sie von der Klasse time eine Instanz. (Anweisung aufschreiben)
Der Compiler gibt Ihnen die Fehlermeldung „’b1’ undeclared (first use this function)“ aus. Was haben
Sie verkehrt gemacht?
Schreiben Sie eine Anweisung auf, um von einer Klasse datum eine Instanz zu erzeugen.
Definieren Sie eine Struktur datum, bestehend aus den Komponenten Tag, Monat und Jahr.
Basierend auf diesem Datentyp datum schreiben Sie ein Programm, das den Wochentag zu einem
eingegebenen Datum anzeigt.
Hinweis: Zunächst muss zu dem Datum eine Schlüsselzahl (code) berechnet werden:
code = int(30.6001*(1+Monat+12*h)) + int(365.25*(Jahr-h)) + Tag;
Die Variable h besitzt für Januar und Februar den Wert 1, für die übrigen Monate den Wert 0.
Der Wochentag ergibt sich aus dem Divisionsrest von code/7 (Modulo-Operation).
0 entspricht Freitag, 1 entspricht Samstag, u.s.w.
Definieren Sie eine Struktur bruch (Echter Bruch).
Basierend auf diesem Datentyp bruch schreiben Sie ein Programm, bei dem der Nutzer zwei Brüche
eingibt und das Programm die Summe anzeigt. Außerdem soll das Ergebnis gekürzt werden.
Modifizieren Sie das Programm zu Aufgabe 13.16. dahingehend, dass Sie Funktionen schreiben für die
Eingabe, Ausgabe, Addition und das Kürzen von Brüchen und diese im Hauptprogramm verwenden:
bruch bruchEingabe() gestattet die Eingabe eines Bruches über cin und gibt diesen zurück.
void bruchAusgabe(bruch b) gibt den Bruch b auf cout aus,
bruch bruchSumme(bruch b1,bruch b2) gibt die Summe der Brüche b1 und b2 zurück.
bruch bruchGekuerzt(bruch b) gibt den Bruch b in gekürzter Form zurück.
Schreiben Sie basierend auf dem Programm 13.17. ein Programm, bei dem der Benutzer eine bestimmte
(vom Nutzer wählbare) Anzahl von Brüchen eingibt und das Programm deren Summe berechnet.
Darüber hinaus sollen alle Brüche zur Kontrolle noch einmal angezeigt werden.
- 75 -
14. Die C++-Standardbibliothek
Die C++ Standardbibliothek stellt häufig benötigte Datentypen (Klassen) und Funktionen (Algorithmen) bereit.
Da die Bibliothek standardisiert ist, stehen diese unter den unterschiedlichsten Plattformen und
Entwicklungsumgebungen zur Verfügung.
Ein integraler Bestandteil der C++ Standardbibliothek ist die C++ Standard Template Library (STL). Sie stellt
sogenannte Template-Klassen und Algorithmen bereit.
Eine Template-Klasse ist eine generische Klasse (generisch lat.: „die Gattung betreffend“). Eine solche muss
zum Zeitpunkt der Instanzierung parametrisiert werden.
list<int>
iliste; // Liste für ganze Zahlen
list<string> sliste; // Liste für Zeichenketten
list ist eine Template-Klasse zur Abstaktion einer verketteten Liste. Durch einen Parameter in spitzen
Klammern kann spezifiziert werden, welcher Datentyp in der Liste gespeichert werden soll.
Zu den Template-Klassen gehören zum Beispiel:
• String-Klassen (basic_string, string, wstring),
• Streams (ostream, istream, fstream, sstream, cin, cout),
(cin und cout sind Instanzen.)
• Mathematische Vektoren (valarray), komplexe Zahlen (complex),
• Container-Klassen (vector, stack, queue, list, map, set)
Zu den Algorithmen gehören Such- und Sortieralgorithmen sowie Mengenfunktionen.
Die Klasse string
// Beipiel für Verwendung der Klasse string
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
int main()
{
string s1, s2, s3;
cout << "Ein Wort eingeben: ";
cin >> s1;
cout << "noch ein Wort: ";
cin >> s2;
s3 = s1 + s2;
cout << s3 <<" ist "<< s3.length() <<" Zeichen lang"<<endl;
system("pause");
return 0;
}
Klasse: string
Instanzen: s1, s2, s3
Methode: length()
--> 14_01_string
Methoden der Klasse string (eine Auswahl):
length()
liefert die Länge
substr(i,l)
liefert den Teilstring an der Position i mit der Länge l
insert(i,t)
fügt den String t an der Stelle i ein
pos(t)
liefert die Position des Teilstrings t
c_str()
liefert eine Null-terminierte C-Zeichenkette
Folgende Operatoren sind definiert:
Zuweisung:
=
Vergleiche:
== != < > <= >=
Anhängen:
+ +=
Einzelzeichen:
[]
- 76 -
Komplexe Zahlen
complex ist eine Template-Klasse für komplexe Zahlen.
Voraussetzung:
#include <complex>
Bei der Instanzierung ist als Template-Parameter ein Datentyp für den Real- und den Imaginärteil anzugeben:
complex<float> c1;
complex<double>
c2;
Auf Real- und Imaginärteil kann über die beiden Methoden real() und imag() zugegriffen werden.
cout << "Realteil(c1): " << c1.real() << endl;
Die mathematischen Funktionen der Standardbibliothek funktionieren auch mit komplexen Zahlen:
cout << "Wurzel(c2): " << sqrt(c2) << endl;
--> 14_02_complex
Die Klasse vector- das bessere Array
vector ist eine Template-Klasse. Sie ist ein Container und ermöglicht die Speicherung von Daten gleichen
Typs sowie einen Indexzugriff auf einzelne Elemente. Insofern bestehen Parallelitäten zu einem Array.
Im Gegensatz zu einem Array lässt sich jedoch die Größe des Vektors zur Laufzeit ändern.
Voraussetzung:
#include <vector>
Bei der Instanzierung ist als Template-Parameter ein Datentyp anzugeben. Eine Dimensionsangabe (Kapazität)
in runden Klammern ist optional.
vector<double> vd;
// Kapazität ist zunächst 0
vector<string> vs(10); // Kapazität ist 10 Strings
Die Methode resize() ändert die Kapazität zur Laufzeit.
cin >> n;
vd.resize(n);
// Kapazität n double-Werte
Die aktuelle Kapazität lässt sich auch abfragen. Der Zugriff auf einzelne Elemente erfolgt analog zum Array
über einen Index. Die Indexierung beginnt bei 0.
for(i=0; i < vd.capacity(); i++)
cout<< vd[i] << endl;
Für mathematische Vektoren ist vector weniger geeignet. Dafür gibt es speziell die Klasse valarray, die
auch mathematische Vektoroperationen beinhaltet.
--> 14_03_vector
Containerklassen der STL
Variabel großer Vektor. Jedes Element kann über einen Index angesprochen werden.
Die Größe kann nachträglich geändert werden.
list<T>
Doppelt verkettete Liste. Es gibt eine definierte Reihenfolge der
Elemente, jedoch keinen Indexzugriff.
queue<T>
Queue (Warteschlange, fifo-Speicher)
dqueue<T>
Double ended queue. Diese Warteschlange kann von beiden
Seiten befüllt bzw. geleert werden.
priority_queue<T> Nach Wert sortierte Queue.
stack<T>
Stack (Stapel, lifo-Speicher)
set<T>
Ungeordnete Menge. Es kommt lediglich darauf an, ob ein
Element enthalten ist, oder nicht. (Beispiel: Skatblatt)
multiset<T>
Menge, in der ein Wert doppelt vorkommen kann.
map<key,wert>
Assoziatives Feld. Jedem eindeutigen Schlüsselelement wird ein
assoziiertes Element zugeordnet. (dictionary)
multimap<key,wert> Map, in der ein Schlüssel mehrfach vorkommen kann.
vector<T>
- 77 -
Methoden der Containerklassen
vector
[i]
front
back
push_back
pop_back
insert
erase
resize
capacity
queue
front
back
push
pop
deque
[i]
front
back
push_back
pop_back
push_front
pop_front
insert
erase
list
front
back
push_back
pop_back
push_front
pop_front
insert
erase
stack
top
push
pop
prority_queue
top
push
pop
set
find
insert
erase
multiset
find
insert
erase
map
[key]
find
insert
erase
multimap
find
insert
erase
--> 14_04_Container
Iteratoren
Ein Iterator abstrahiert einen Zeiger auf ein Objekt in einem STL-Container.
Zu jeder STL-Containerklasse gibt es eine passende Iteratorklasse.
list<int>
li;
list<int>::iterator
pi;
Jeder STL-Container besitzt die beiden Methoden begin() und end(), die jeweils einen Iterator auf den
Anfang und das Ende des Containers liefern.
begin()
end()
Mittels eines Iterators kann über alle Objekte in einem Container iteriert werden.
for(pi=li.begin(); pi!=li.end(); pi++)
cout << *pi << endl;
*pi ist das Objekt, auf das pi zeigt. Ein Inkrement von pi lässt pi auf das nächste Objekt zeigen.
Folgende Operationen mit Iteratoren sind möglich:
(x sei ein Objekt in einem Container, p und q seien Iteratoren und n sei eine ganze Zahl)
x=*p, *p=x, ++p, p++, --p, p--, p==q, p!=q
p[n], p+n, n+p, p-n, p+=n, p-=n, p-q, p<q, p==q, p>=q
Generische Algorithmen
Generische Algorithmen erkennen an den übergebenen Parametern, auf welche Klasse sie angewendet werden.
Die Parameter sind in der Regel Iteratoren auf Elemente im Container.
Viele der Algorithmen können auch auf Arrays angewendet werden.
Voraussetzung:
#include <algorithm>
Eine kleine Auswahl:
• find(von,bis,Suchwert) - sucht nach einem Objekt in einem Container; liefert einen Iterator auf das
gefundene Objekt oder end(), falls nicht gefunden.
Als Parameter von und bis werden begin() und end() übergeben, sofern der Algorithmus auf den
gesamten Container angewendet werden soll, anderenfalls sind es Iteratoren auf die betreffenden Positionen.
- 78 -
•
•
•
•
•
•
•
•
binary_search(von,bis,Suchwert) - binäre Suche in einem geordneten Container; liefert einen
Iterator auf das gefundene Objekt oder end(), falls nicht gefunden.
for_each(von,bis,Funktion) - wendet eine Funktion auf jedes Element an
copy(von,bis,Ziel) - kopiert Teile eines Containers; Ziel ist ein Iterator
count(von,bis,Wert,&Anzahl) - zählt das Vorkommen eines Elementes
replace(von,bis,&alterWert,&neuerWert) – ersetzt bestimmte Elemente
reverse(von,bis) – kehrt die Reihenfolge von Elementen in einem Container um
sort(von,bis) - Quicksort-Algorithmus, Komplexität: O(N log N)
merge, set_union, set_intersection, set_difference – Mengen-Funktionen
Die stream-Klassen der STL
Mit den stream-Klassen der STL kann die Dateiarbeit realisiert werden. Sie bilden eine Klassenhierarchie (siehe
Kapitel 16). Die Klassen ofstream (output file stream) und ifstream (input file stream) sind eine
Spezialisierung der Klasse fstream (file stream), welche wiederum eine Spezialisierung der Klasse stream
darstellt. Ebenso ist strstream (string stream) eine Spezialisierung von stream. (zu Spezialisierung vgl.
Kap. 16)
stream
strstream
fstream
ofstream
ifstream
Während fstream, ofstream und ifstream für reguläre Dateien verwendet werden, modelliert
strstream eine Datei im Arbeitsspeicher. Diese kann beschrieben oder gelesen werden, wird jedoch nicht
persistent auf den Massenspeicher geschrieben.
ifstream ist nur lesbar (analog zu cin), ofstream ist nur beschreibbar (analog zu cout). Bei fstream
und strstream kann zwischen lesen und schreiben gewechselt werden.
Die Klasse strstream
Die Klasse strstream abstrahiert eine Textdatei im Arbeitsspeicher (String-Stream).
Voraussetzung:
#include <strstream>
Ein String-Stream kann mit den üblichen Streamfunktionen und –operatoren beschrieben und gelesen werden
(analog zu cout/ cin).
Beim Wechsel zwischen Schreiben und Lesen sollte der Schreibpuffer durch Ausgabe des Manipulators ends
geleert werden.
strstream ss1;
string
st;
float
x = 61.5444585;
ss1 << setprecision(5) << x << ends; // wie cout <<
ss1 >> st;
// wie cin >>
Ein String-Stream kapselt einen String. Auf den gekapselten String kann mit der Methode str() zugegriffen
werden.
cout << ss1.str() << endl;
- 79 -
Dateiarbeit in C++
Mittels der beiden Klassen ifstream (input file stream) und ofstream (output file stream) kann die Dateiarbeit
relativ einfach realisiert werden. Die Arbeit mit fstream ist etwas komplizierter, da beim Wechsel zwischen
Lesen und Schreiben bestimmte Dinge beachtet werden müssen.
Alle fstream-Klassen werden eingebunden mittels:
#include <fstream>
Eine Datei muss vor der Benutzung geöffnet werden, dafür gibt es die Methode open, und sie sollte nach
Benutzung geschlossen werden, dafür gibt es die Methode close().
ofstream ofs;
// Instanz
string filename;
cout << "Dateiname: ";
cin >> filename;
ofs.open(filename.c_str());// Methode open
...
// hier Schreibaktionen
ofs.close();
// Datei schließen
Aus einer Instanz von ifstream kann (analog zu cin) mittels des Operators >> gelesen werden, auf eine
Instanz von ofstream kann (analog zu cout) mittels des Operators << geschrieben werden.
ofs << "Hallo" << endl;
// auf ofs schreiben
ifs >> x;
// von ifs lesen
--> 14_05_Streams
Die Methode open()
void open(char filename[]);
Wird open() mit nur einem Argument aufgerufen, so wird ein ifstream zum Lesen bzw. ein ofstream
zum Überschreiben/ Erzeugen im Textmodus geöffnet.
Es gibt auch eine Version von open() mit einem zweiten Argument.
void open(char filename[], openmode mode);
Der Modus openmode beschreibt die Art und Weise, wie die Datei behandelt wird.
openmode
ios::in
ios::out
ios::out|ios::app
Bedeutung
nur lesen
überschreiben/ erzeugen
schreiben und anhängen/ erzeugen
ios::in|ios::out
ios::in|ios::out|ios::trunc
ios::in|ios::out|ios::app
lesen/ schreiben (muss existieren)
lesen/ überschreiben/ erzeugen
lesen/ anhängen/ erzeugen
Zusätzlich kann der Modus noch mit |ios::binary bzw. |ios::text spezifiziert werden. (Öffnen im
Binär- bzw. Textmodus). Im Textmodus wird das Steuerzeichen „newline“ interpretiert, während im Binärmodus
keine Interpretation von Steuerzeichen statt-findet.
Fehlerbehandlung bei Dateioperationen
Jede Dateioperation kann „schiefgehen“. Mögliche Fehler sind:
- Fehler beim Öffnen, weil die Datei nicht existiert,
- Fehler beim Lesen, weil die Datei bereits vollständig gelesen wurde,
- Fehler beim Schreiben, weil zwischenzeitlich der Datenträger entfernt wurde.
Deshalb ist es erforderlich, nach jeder Dateioperation zu prüfen, ob ein Fehler aufgetreten ist, bzw. ob alles noch
in Ordnung ist.
Im folgenden sei dat ein Objekt von Typ fstream oder ifstream oder ofstream.
!dat liefert true, falls ein Fehler aufgetreten ist.
dat.good() liefert true, wenn noch alles in Ordnung ist.
dat.clear() sorgt dafür, dass die internen Fehlerflags gelöscht werden, bevor weitere Operationen
ausgeführt werden.
dat.eof() liefert true, wenn versucht wurde, über das Dateiende hinaus zu lesen.
- 80 -
Aufgaben
14.1.
14.2.
14.3.
14.4.
14.5.
14.6.
14.7.
14.8.
14.9.
14.10.
14.11.
Nennen Sie fünf STL-Containerklassen.
Welche STL-Containerklasse würden Sie verwenden für:
- eine Lotterietrommel
- einen Pufferspeicher für die gedrückten Tasten einer Computertastatur
- ein Telefonverzeichnis
- ein Englisch-Deutsch Wörterbuch
- einen Stichwortindex am Ende eines Buches?
Was verstehen Sie unter einer generischen Klasse?
Was ist ein Iterator?
Nennen Sie die Namen von drei Algorithmen der STL.
Welchen Parameter benötigt die Methode open() der fstream-Klasse mindestens?
Welchen Vorteil besitzt die Klasse vector gegenüber einem Array?
Beim Lesen aus einer Datei liefert der folgende Methodenaufruf true:
if( dat.eof())
Was ist die Ursache dafür?
Erweitern Sie das Beispielprogramm 14_01_String dahingehend, dass Sie jede der in der Vorlesung
angegebenen string-Methoden und Operatoren testen und das Ergebnis ausgeben
Geben Sie für jede der STL-Containerklassen, die in der Vorlesung genannt wurden, ein Beispiel für
deren Anwendung an.
Übersetzen und testen Sie die STL-Container-Beispiele der Vorlesung. Versuchen Sie die einzelnen
Operationen zu verstehen.
Modifizieren Sie die Beispiele so, dass in den Containern andere Datentypen gespeichert werden.
- 81 -
15. Benutzerdefinierte Klassen
Syntax der Klassendefinition:
class Klassenname
{
private:
Vereinbarungen
protected:
Vereinbarungen
public:
Vereinbarungen
};
Klassenname ist ein Bezeichner.
Vereinbarungen können Attribut- oder Methodenvereinbarungen sein.
private, protected und public spezifizieren Zugriffsrechte.
Wird kein Zugriffsspezifizierer angegeben, so gilt private.
Jede der drei Sektionen private, protected und public kann entfallen oder auch mehrfach vorhanden
sein.
Klassendefinition
class bruch
{
private:
int zaehler;
int nenner;
public:
void eingabe()
{
char c;
cin >> zaehler >> c >> nenner;
}
void ausgabe()
{
cout << zaehler << "/" << nenner;
}
// hier weitere Methoden
};
Konstruktor
Ein Konstruktor ist eine spezielle Methode mit dem Namen der Klasse, die automatisch vom System aufgerufen
wird, wenn ein Objekt erzeugt wird.
Ein Konstruktor wird zur Initialisierung von Attributen verwendet.
Ein Konstruktor besitzt keinen Typ (auch nicht void).
Konstruktoren können überladen werden, das heißt, es kann mehrere Konstruktoren mit unterschiedlicher
Argumentliste geben.
Ein Konstruktor ohne Argumente heißt Standardkonstruktor.
Schreibt der Programmierer keinen Konstruktor für eine Klasse, so generiert der Compiler einen leeren
Standardkonstruktor. Man nennt den vom Compiler generierten Standardkonstruktor auch Default-Konstruktor.
class bruch
{
int zaehler, nenner;
public:
bruch()
// Konstruktor 1: Standardkonstr.
{
zaehler=0; nenner=1;
}
bruch(int z)
// Konstruktor 2
{
- 82 -
zaehler=z; nenner=1;
}
bruch(int z, int n) // Konstruktor 3
{
zaehler=z; nenner=n;
}
// weitere Methoden
};
void konstruktortest()
{
bruch b1;
// K1
bruch b2(3);
// K2
bruch b3(4,3); // K3
cout<<"b1=";
b1.ausgabe();
cout<<endl;
cout<<"b2=";
b2.ausgabe();
cout<<endl;
...
}
Aufgaben
15.1.
15.2.
15.3.
15.4.
15.5.
15.6.
15.7.
Welche Aufgabe besitzt ein Konstruktor?
Wie lautet die Syntax zur Definition eines Konstruktors? Schreiben Sie ein Beispiel auf.
Warum braucht man unter Umständen mehrere Konstruktoren?
Deklarieren Sie eine mögliche Schnittstelle zu einer Klasse bruch. Beachten Sie: Die Schnittstelle
beinhaltet nur die Prototypen der öffentlichen Methoden.
Erweitern Sie die Klasse bruch (Vorlesungsbeispiel 15_01_Bruch).
Die Schnittstelle (öffentlicher Teil) soll um folgende Methoden erweitert werden:
void kuerzen(); die Instanz wird gekürzt.
Aufruf z.B.: b1.kuerzen();
bruch kehrwert(); der Kehrwert wird zurückgegeben, die Instanz wird nicht verändert.
Aufruf z.B.: b2 = b1.kehrwert();
bruch plus(bruch); gibt die Summe aus der Instanz und dem Parameter zurück.
Aufruf z.B.: b3 = b1.plus(b2);
bruch mal(bruch); gibt das Produkt aus der Instanz und dem Parameter zurück.
Aufruf z.B.: b3 = b1.mal(b2);
Schreiben Sie ein Testprogramm für die Klasse. Es soll etwa folgender Benutzerdialog realisiert
werden:
Bruchrechnung
Eingabe der Brüche in der Form Zähler/Nenner (z.B. 13/24)!
Erster Bruch: 12/34
Zweiter Bruch: 24/38
12/34 + 24/38 = 1272/1292
1272/1292 gekürzt = 318/323
Kehrwert von 318/323 = 323/318
12/34 * 24/38 = 288/1292
288/1292 gekürzt = 72/323
Kehrwert von 72/323 = 323/72
Schreiben Sie eine Klasse tank.
Die Schnittstelle soll aus folgenden Methoden bestehen:
tank(double); Konstruktor mit Parameter: Tankvolumen
double befuellen(double); Parameter: Volumen, das hinein soll. Rückgabe: Volumen, das
tatsächlich hinein gefüllt wurde.
double entnehmen(double); Parameter: Volumen, das entnommen werden soll. Rückgabe:
Volumen, das tatsächlich entnommen werden konnte.
double fuellstand(); Rückgabe: Füllstandsvolumen.
Schreiben Sie ein Testprogramm, um die Methoden zu testen.
Schreiben Sie eine Klasse complex zur Abstraktion einer komplexen Zahl mit den reellen Attributen
real und imaginaer. Die Klasse sollte folgende Schnittstelle besitzen:
- 83 -
15.8.
15.9.
15.10.
complex(); Standardkonstruktor: Initialisierung mit 0
complex(double,double); Konstruktor mit Argumenten: Real- und Imaginärteil
eingabe(); Eingabe durch den Benutzer im Format R+I i über cin
ausgabe(); Ausgabe auf cout im Format R+I i. Wenn der Imaginärteil 0 ist, wird nur der Realteil
ausgegeben.
complex plus(complex); gibt die Summe aus der Instanz und dem Parameter zurück.
Schreiben Sie ein Testprogramm. Es soll etwa folgender Benutzerdialog realisiert werden:
Addition komplexer Zahlen
Geben Sie komplexe Zahlen in folgender Form ein: z.B. 2.4 + 5.03i
Erste Zahl: 2.6+3i
Zweite Zahl: 8.34+7i
2.6+3i + 8.34+7i = 10.96 + 10i
Entwerfen und implementieren Sie eine Klasse konto.
Die Klasse soll folgende Attribute enthalten:
- Kontonummer
- Inhaber
- Dispolimit
- Saldo
- weitere Attribute nach Ihrer Wahl
Folgende Methoden sollen mindestens vorhanden sein:
- Konstruktor: Die Attribute sollen sinnvoll initialisiert werden.
- Eroeffnung: Die Stammdaten werden vom Nutzer eingegeben. Ein wiederholtes Eröffnen desselben
Kontos muss verhindert werden.
- Buchen: Beträge können positiv oder negativ sein. Ein Abbuchen über das Dispolimit hinaus wird
verhindert.
- Anzeige: Alle Daten werden angezeigt
- Schließen: Das Konto wird geschlossen. Buchungen sind nicht mehr möglich.
- weiter Methoden nach Ihrer Wahl.
Schreiben Sie ein menügesteuertes Hauptprogramm zu Aufgabe 15.8., in dem der Benutzer die
einzelnen Funktionen über jeweils eine Tasteneingabe aufrufen kann. Es soll lediglich ein einziges
Konto verwaltet werden.
Vorteilhaft wäre es, wenn sinnlose Funktionen (z.B. Buchen, bevor das Konto eröffnet ist) nicht
ausgewählt werden können.
Schreiben Sie eine Klasse wuerfel. Sie stellt folgende Schnittstelle zur Verfügung:
wuerfel()
// Standardkonsruktor
void werfen() // simuliert das Würfeln
int augen()
// liefert die zuletzt geworfene Augenzahl
void zeigen() // zeigt den Würfel symbolisch an
Die Anzeige des Würfels sollte symbolisch geschehen, z.B.
O
O
O
O
O
Schreiben Sie ein Testprogramm für die Klasse, mit dem das Würfeln simuliert wird. Das Programm
sollte eine vom Nutzer vorgegebene Anzahl von Würfen durchführen. Jeder Wurf wird angezeigt, der
Nutzer muss eine Taste drücken, um weiter zu würfeln. Am Ende wird die Häufigkeitsverteilung der
geworfenen Augen angezeigt, z.B.
13 mal die 1
15 mal die 2
12 mal die 3
10 mal die 4
12 mal die 5
13 mal die 6
- 84 -
16. Klassenbeziehungen, Vererbung
Es gibt prinzipiell zwei Arten von Beziehungen in denen Klassen bzw. deren Objekte zueinander stehen können:
1. Assoziation: Ein Objekt ist mit einem anderen über eine Komposition oder eine Aggregation verknüpft.
Aggregation: Ein Objekt ist über eine Beziehung mit einem anderen Objekt verknüft. Manchmal kann
man eine Aggregation als „benutzt“, manchmal als „hat“ formulieren.
Beispiele:
Ein Fahrer benutzt ein Auto.
Eine Hochschule hat Wohnheime.
Komposition: Die Komposition ist ein Spezialfall der Aggregation. Ein Objekt ist Teil eines anderen.
Man spricht deshalb auch von einer „part-of-Beziehung“.
Das Teil ist existenzabhängig vom Ganzen bzw. umgekehrt.
Beispiele:
Ein Motor ist Teil eines Autos.
Ein Fachbereich ist Teil der Hochschule.
2. Vererbung: Eine Klasse ist eine Unterkategorie einer übergeordneten Klasse.
Beispiel: Ein PKW gehört zur Klasse der Fahrzeuge. Man spricht auch von einer
„is-a-Beziehung“ (Ein PKW ist ein Fahrzeug). Stehen Klassen in einer Vererbungsbeziehung, so bilden sie eine
Klassenhierarchie. Untergeordnete Klassen stellen häufig Spezialisierungen Ihrer Basisklasse dar.
UML-Klassendiagramme
Zusammenführung der Ansätze von Grady Booch, Ivar Jacobsen, James Rumbauch, der drei Väter der
Modellierung objektorientierter Software zur Unified Modelling Language (UML).
Klassendiagramme beschreiben die Klassen und deren Beziehungen untereinander.
Je nach Zweck des Diagrammes (z.B. Grobentwurf oder Detaildarstellung) werden zu einer Klasse die
Komponenten unterschiedlich detailliert angegeben. Im einfachsten Fall findet man in einem Klassendiagramm
nur die Namen der beteiligten Klassen.
Klassenname
Attribute
Methoden
Beispiel:
stack
- data: int*
- size: int
- capacity: int
+ stack(size: int): stack
+ push(x: int): void
+ pop(): int
Zugriffsspezifizierer: + public, - private, # protected
- 85 -
Klassenbeziehungen in UML
Komposition
Aggregation
Auto
Vererbung
Auto
Fahrzeug
1
1
1..
*
Motor
0..
1
Fahrer
PKW
Realisierung von Objektbeziehungen in C++
Assoziationen werden durch Attribute einer Klasse realisiert.
Beispiel: Ein Attribut vom Typ Motor in einer Klasse PKW.
Aggregationen werden in C++ häufig über Zeiger als Attribut realisiert. [1]
Beispiel: Die Klasse Fachbereich enthält einen Verweis (Zeiger) auf den Dekan (der ja wechseln kann).
Vererbung wird durch abgeleitete Klassen realisiert. Eine Basisklasse vererbt ihre Attribute und Methoden an
abgeleitete Klassen.
Beispiel: Die Klasse Fahrzeug vererbt das Attribut Höchstgeschwindigkeit an die abgeleiteten Klassen PKW und
LKW.
Vererbung und Polymorphie
Eine OO-Sprache muss das Prinzip der Vererbung unterstützen:
Die Bildung von Klassenhierarchien ist möglich.
Attribute und Methoden werden aus einer Basisklasse an abgeleitete Klassen vererbt (weitergegeben).
Eine OO-Sprache muss das Prinzip der Polymorphie unterstützen:
Es kann in einer Klassenhierarchie mehrere Methoden mit gleicher Signatur geben. Zur Laufzeit wird
entschieden, welche Methode aufgerufen wird. (Stichworte: Späte Bindung, virtuelle Funktionen)
Wird von einer Superklasse (Basisklasse, Elternklasse) eine Subklasse (Unterklasse, Kindklasse) abgeleitet, so
erbt die Subklasse alle Attribute und Methoden der Basisklasse.
Der Subklasse können weitere Attribute und Methoden hinzugefügt werden, die sie dann von der Basisklasse
unterscheiden.
1. Im Rahmen dieser Lehrveranstaltung werden Zeiger nicht behandelt, da sie nicht zu den grundlegenden Konzepten jeder
Programmiersprache gehören. Sie sind eher eine „Spezialität“ von C/C++.
- 86 -
Fahrzeug
Geschwindigkeit
…
PKW
Sitzplätze
…
Ein PKW ist ein Auto. Es besitzt die Attribute Geschwindigkeit und Sitzplätze.
class container
{
double elemente[1000];
int anzahl;
public:
int getAnzahl();
void insert(int index, double element);
void remove(int index);
};
class stack:public container
{
int top;
public:
void push(double element);
double pop();
};
Aufgaben
16.1.
16.2.
16.3.
16.4.
Welche Prinzipien muss eine Programmiersprache unterstützen, damit Sie „objektorientiert“ genannt
werden darf?
In welchen Beziehungen können Klassen zueinander stehen?
Benennen Sie die Beziehungstypen folgender Datentypen zueinander:
ganze Zahl – natürliche Zahl
Person – Adresse
Säugetier – Primat
Fahrzeug – Rad
Professor – Fachbereich
Student – Studiengang
PKW - Fahrzeug
Was geschieht mit den Attributen und Methoden der Superklasse, wenn von ihr eine Subklasse
abgeleitet wird?
- 87 -