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 -