BBS III Mainz Objektorientierte Programmierung für IT-Berufe mit C++ von Gerold Mader D:\75807564.doc vom 13.10.99 BBS III Mainz OOP für IT-Berufe mit C++ Inhaltsverzeichnis 1 LITERATUR ZU C++ ............................................................................................................................... 1-1 2 ENTWICKLUNGSUMGEBUNG VON VISUAL C++ 6.0..................................................................... 2-2 3 GRUNDLEGENDE PROGRAMMSTRUKTUREN IN C++ ................................................................. 3-3 3.1 AUFBAU EINES PROGRAMMES IN C++ ................................................................................................... 3-3 3.1.1 Kommentare ................................................................................................................................. 3-3 3.1.2 Datentypen ................................................................................................................................... 3-4 3.1.3 Variablen ...................................................................................................................................... 3-5 3.1.4 Konstanten ................................................................................................................................... 3-6 3.1.5 Anweisungen ................................................................................................................................ 3-7 3.1.6 Wertzuweisung, Rechenoperationen ............................................................................................ 3-8 3.1.7 Implizite und explizite Typkonvertierung ..................................................................................... 3-9 3.1.8 Inkrement- und Dekrementoperatoren ...................................................................................... 3-10 3.2 FOLGE (SEQUENZ) ............................................................................................................................... 3-16 3.2.1 Grundlegende Übungsaufgaben ................................................................................................. 3-17 3.2.2 Anfangsprobleme und deren Lösungen ...................................................................................... 3-18 3.3 AUSWAHL (SELEKTION) ...................................................................................................................... 3-20 3.3.1 Einseitige Auswahl ..................................................................................................................... 3-20 3.3.2 Probleme und deren Lösung ...................................................................................................... 3-23 3.3.3 Zweiseitige Auswahl ................................................................................................................... 3-24 3.3.4 Mehrseitige Auswahl .................................................................................................................. 3-28 3.3.5 Probleme und deren Lösung ...................................................................................................... 3-35 3.4 WIEDERHOLUNG (REPETITION) ........................................................................................................... 3-37 3.4.1 Schleife mit Anfangsabfrage....................................................................................................... 3-37 3.4.2 Probleme und deren Lösung ...................................................................................................... 3-39 3.4.3 Schleife mit Endabfrage ............................................................................................................. 3-40 3.4.4 Zählschleife ................................................................................................................................ 3-42 3.4.5 Zählschleife mit variabler Schrittweite ...................................................................................... 3-43 3.4.6 Anweisungen für Schleifenabbruch und -wiederholung ............................................................ 3-45 3.5 MODUL (UNTERPROGRAMM) .............................................................................................................. 3-47 3.5.1 Funktionen ohne Parameter ....................................................................................................... 3-48 3.5.2 Funktionen mit Werteparametern .............................................................................................. 3-49 3.5.3 Funktionen mit Variablenparametern ........................................................................................ 3-51 3.5.4 Funktionen mit Rückgabetyp ...................................................................................................... 3-53 3.5.5 Sprachspezifische Besonderheiten bei Funktionen .................................................................... 3-55 3.6 VERBUNDE UND OBJEKTE: STRUKTUREN UND KLASSEN .................................................................... 3-63 3.6.1 Strukturen ................................................................................................................................... 3-63 3.6.2 Funktionen zum Zugriff auf Strukturelemente ............................................................................ 3-66 3.6.3 Kapselung von Struktur und Zugriffsfunktionen: Objekt ............................................................ 3-68 3.6.4 Objektklasse und Objektinstanz.................................................................................................. 3-70 4 OBJEKTORIENTIERTE PROGRAMMIERUNG .............................................................................. 4-72 4.1 PRINZIPIEN DER OBJEKTORIENTIERTEN PROGRAMMIERUNG ................................................................ 4-72 4.2 DAS OOP-PRINZIP KAPSELUNG .......................................................................................................... 4-73 4.2.1 Klassen ....................................................................................................................................... 4-73 4.2.2 Methoden .................................................................................................................................... 4-74 4.2.3 Der Bereichsoperator :: ............................................................................................................. 4-75 4.2.4 Konstruktoren ............................................................................................................................. 4-76 4.2.5 Destruktoren ............................................................................................................................... 4-78 4.2.6 Objekte als Rückgabewert von Funktionen ................................................................................ 4-79 4.2.7 Klassen, Objekte und Speicher ................................................................................................... 4-80 4.2.8 Operator-Überladung ................................................................................................................ 4-80 4.2.9 Übungsaufgaben: ....................................................................................................................... 4-85 D:\75807564.doc vom 13.10.99 BBS III Mainz OOP für IT-Berufe mit C++ 4.3 VERERBUNG........................................................................................................................................ 4-87 4.3.1 Das Prinzip der Vererbung ........................................................................................................ 4-87 4.3.2 Protected .................................................................................................................................... 4-93 4.3.3 Konstruktoren in abgeleiteten Klassen ....................................................................................... 4-93 4.3.4 Überschreiben von Methoden .................................................................................................... 4-94 4.3.5 Klassenhierarchie....................................................................................................................... 4-97 4.3.6 Vererbung von Zugriffsrechten ................................................................................................ 4-100 4.3.7 Mehrere Stufen der Vererbung ................................................................................................. 4-101 4.3.8 Mehrfachvererbung .................................................................................................................. 4-103 4.3.9 Übungsaufgaben ...................................................................................................................... 4-106 4.4 POLYMORPHIE................................................................................................................................... 4-107 4.4.1 Virtuelle Methoden/Funktionen................................................................................................ 4-107 4.4.2 Virtuelle Basisklassen .............................................................................................................. 4-113 4.4.3 Friend-Funktionen ................................................................................................................... 4-114 4.4.4 This-Zeiger ............................................................................................................................... 4-115 4.4.5 Übungsaufgaben ...................................................................................................................... 4-115 D:\75807564.doc vom 13.10.99 BBS III Mainz OOP für IT-Berufe mit C++ 1 Literatur zu C++ Herdt-Verlag: Objektor. Pro. mit C++ 30,-DM Arnush(SAMS) Borland C++ in 21 Tagen Breymann (Hanser): C++ Einführung 74 DM Beispiele auf hohem Niveau Louis (Markt+Technik): C/C++-Kompendium B. Stronstrup (Add.-Wesley): Die C++ Prog.sprache 100,- DM C++ Workshop (Add.-Wesley) Rororo Verlag OOP mit C++ 20,- DM Booch Objektorientierte Analyse und Design 90,- DM Chapman, David Teach Yourself Visual C++ 6 in 21 Days D:\75807564.doc vom 13.10.99 1-1 110,- DM BBS III Mainz OOP für IT-Berufe mit C++ 2 Entwicklungsumgebung von Visual C++ 6.0 1. DATEI - NEU: Leeren Arbeitsbereich ITSK97A anlegen. 2. DATEI - NEU - PROJEKTE - WIN32-Konsoleanwendung : Hinzufügen zu aktivem Arbeitsbereich (anklicken) NAME: TEIL1 3. Dateien dem Projekt TEIL1 hinzufügen 4. DATEI - NEU - PROJEKTE - WIN32-Konsoleanwendung : Hinzufügen zu aktivem Arbeitsbereich (anklicken) NAME: TEIL2 5. Dateien dem Projekt TEIL2 hinzufügen A. Teil1 aktivieren (Als aktives Projekt festlegen) B. Projekt Einstellungen - ALLGEMEIN Bis auf zu kompilierende Quelldatei alle anderen Quelldateien mit Haken versehen bei: DATEI VON ERSTELLUNG AUSSCHLIESSEN Mit F7 wird das Projekt erstellt: EXE-Datei wird dabei unter dem Projektnamen erzeugt. Einzelne Quelldateien können wie oben beschrieben von der Erstellung ausgeschlossen werden. Jedoch erhält die EXE-Datei die mit F7 erzeugt wird immer den Projektnamen (Projekt-Einstellungen - Projektnamen anklicken - DEBUG). D:\75807564.doc vom 13.10.99 2-2 BBS III Mainz OOP für IT-Berufe mit C++ 3 Grundlegende Programmstrukturen in C++ 3.1 Aufbau eines Programmes in C++ // Programm Erstprog.cpp // Einbinden einer Header-Datei // iostream.h ist notwendig, um Ein-/Ausgabe nutzen zu koennen #include <iostream.h> /* Definitionsteil */ // main() enthaelt das Hauptprogramm zwischen den geschweiften Klammern {} void main() { // Deklaration der Variablen float a,b,c; /* Anweisungsteil */ // Startmeldung cout << "Programm zum Addieren von zwei Zahlen." << endl; // Eingabeaufforderungen cout << "Geben Sie die erste Zahl ein: "; cin >> a; cout << "Geben Sie die zweite Zahl ein: "; cin >> b; // Berechnung der Summe, Ergebnis in c ablegen c=a+b; // Ergebnis ausgeben cout << "Die Summe von " << a << " und " << b << " ist " << c << "." << endl; } An diesem Beispiel soll der Aufbau eines Programmes in C++ erläutert werden. Ein Programm in C++ besteht zunächst aus 2 Teilen. 1. Einbinden evtl. benötigter „Headerdateien“ (Vorspanndateien). Durch diese werden neue Befehle in C++ zugänglich. Es gibt eine Reihe von Standard-Headerdateien, die bei jeder C++-Implementierung mitgeliefert werden. Eine davon ist die Datei iostream.h, durch die Befehle zur Eingabe und Ausgabe verfügbar werden. 2. Im Definitionsteil werden benötigte Variablen und Funktionen definiert. Im hier dargestellten einfachsten Fall wird nur die Funktion main() definiert, die das Hauptprogramm enthält. Funktionen und damit auch das Hauptprogramm main bestehen wiederum aus zwei Teilen. a) Im Variablendeklarationsteil werden die benötigten Variablen und Konstanten eingeführt (deklariert). b) Im Anweisungsteil wird der eigentliche Algorithmus in der Sprache C++ formuliert. Grobanalyse des Beispielprogrammes Erstprog.cpp Im Deklarationsteil werden 3 Variablen mit den Namen a, b und c deklariert. Diese können bestimmte Datentypen („Datentypen“ siehe einen der nächsten Abschnitte) speichern, hier „float“, also Fließkommazahlen. Im Anweisungsteil werden verschiedene Anweisungen zur Ausgabe (cout <<) und Eingabe (cin >>) verwendet. Auch eine Berechnungsvorschrift (c=a+b) ist zu finden. 3.1.1 Kommentare Um den Überblick über ein größeres Programm zu behalten ist es wichtig, Programmteile optisch voneinander trennen zu können und bestimmte Anmerkungen anbringen zu können. Dies geschieht durch Kommentare, die D:\75807564.doc vom 13.10.99 3-3 BBS III Mainz OOP für IT-Berufe mit C++ vom restlichen Text auf zwei Arten abgetrennt werden können. Durch // wird ein Kommentar eingeleitet, der bis zum Ende der Zeile geht. Kommentare können aber auch in /* ... */ eingeschlossen werden. Der Text in Kommentarklammern unterliegt keinen Einschränkungen und ist absolut beliebig. 3.1.2 Datentypen C++ kennt verschiedene Datentypen, die jeweils einen bestimmten Platzbedarf und verschiedene Wertebereiche haben. Typname Bereich Platzbedarf in Bytes bool true, false 1 char -128 bis 127 bzw. Zeichen nach ASCII-Tabelle, 1 z.B. ’A’ oder ’@’ unsigned char 0 bis 255 bzw. Zeichen nach ASCII-Tabelle, 1 z.B. ’A’ oder ’@’ int -32768 bis +32767 2 long -2 147 483 648 bis +2 147 483 647 4 unsigned int 0 bis 65535 2 unsigned long 0 bis 4.294.967.295 4 float 3,4*10-38 bis 3,4*10+38 4 double 1,7*10-308 bis 1,7*10+308 8 long double 3,4*10-4932 bis 1,1*10+4932 10 // Deklaration von Variablen float a,a2; // reelle Zahlen char b; // Zeichen unsigned long l; // lange vorzeichenlose Ganzzahl long double x; // sehr genaue relle Zahl mit großem Zahlenbereich Darüber hinaus können bei Bedarf weitere Typen definiert werden, z.B. Aufzählungstypen oder Verbunde. // Definition neuer Typen enum Wochentage {Mo,Di,Mi,Do,Fr,Sa,So}; // Aufzählungstyp struct Datum { int Tag,Monat,Jahr } ; // Verbund // darauf aufbauende Deklaration von Variablen Wochentage t; // Wert von Mo bis So Datum d; // Variable zusammengesetzt aus 3 Integer-Zahlen D:\75807564.doc vom 13.10.99 3-4 BBS III Mainz OOP für IT-Berufe mit C++ 3.1.3 Variablen In den Variablen eines Programms werden die zu verarbeitenden Daten, Ergebnisse und Zwischenergebnisse gespeichert. Variablen werden ebenso wie Typen über ihre symbolischen Namen angesprochen. Namen müssen mit einem Buchstaben beginnen und dürfen ansonsten Buchstaben, Ziffern und den Unterstrich „_“ enthalten; Groß- und Kleinschreibung wird beachtet, name, NaMe und NAME meinen also 3 verschiedene Variablen. Leerzeichen können in Variablennamen nicht vorkommen. Variablen können nur Daten eines bestimmten Typs speichern. Dieser Typ wird im Deklarationsteil festgelegt; er kann im Lauf des Programmes nicht verändert werden. Variablen bekommen einen Wert zugewiesen, indem man schreibt Variablenname=Ausdruck; a=1.2345; // Variable a erhält den Wert 1.2345 a2=a*5+7; // Variable a2 erhält den Wert, der sich aus der Formel berechnet b=’z’; // Variable b speichert das Zeichen ’z’ Konstante ASCII-Zeichen werden dabei in einfachen Apostrophen „’“ eingeschlossen. Bei reellen Zahlen (float oder double) wird ein Dezimal-Punkt, kein Dezimal-Komma verwendet. D:\75807564.doc vom 13.10.99 3-5 BBS III Mainz OOP für IT-Berufe mit C++ 3.1.4 Konstanten Konstanten können wie Variablen angesprochen werden, verändern aber während des Programmlaufs ihren Wert nicht. Wird dies trotzdem versucht, reagiert der Compiler mit einer Fehlermeldung. KonstantenBezeichner werden üblicherweise in BLOCKSCHRIFT deklariert. Beispiele: const double PI=3.1415; // Kreiszahl PI als double-Konstante const float E=2.7172; // Eulersche Zahl const unsigned int ZWEIUNDVIERZIG=42; // ob das sinnvoll ist? Zeichen und Zeichenketten (auch Literale genannt) werden in Apostrophen eingeschlossen. Einzelne Zeichen werden dabei in einfachen Apostrophen „’“und Zeichenketten werden in doppelten Apostrophen „"“ eingeschlossen; letzteres wird häufig bei Ausgaben verwendet (cout << "Hallo"). Bei reellen float- oder double-Zahlen wird ein Dezimal-Punkt, kein Dezimal-Komma verwendet. Bestimmte ASCII-Zeichen sind nicht direkt ausgebbar; wie lasse ich z.B. den doppelten Apostrophen ausgeben, wo der doch als Begrenzung für die auszugebende Zeichenkette dient? Deshalb werden diese Zeichen über besondere sogenannte Escape-Sequenzen dargestellt, die alle mit dem Backslash „\“ beginnen: Zeichen \t \v \r \n \a \b \\ \’ \" \xzz, \xZZ Bedeutung Tabulator Zeilensprung Zeilenrücklauf neue Zeile Signalton Backspace, löscht das Zeichen links von der Eingabemarke der Backslash selbst ’ einfacher Apostroph " doppelter Apostroph Zeichen aus Sedezimalziffern, z.B. \x40 ist der ’@’ D:\75807564.doc vom 13.10.99 3-6 BBS III Mainz OOP für IT-Berufe mit C++ 3.1.5 Anweisungen Über Anweisungen wird der Ablauf des Programmes gesteuert. Mit der Anweisung cout << ... << ... ; beispielsweise werden Zeichen, Zeichenketten und Zahlen auf dem Bildschirm ausgegeben. Mit cin >> ... >> ... ; werden Daten von der Tastatur in Variablen eingelesen. Anweisungen werden immer mit einem Strichpunkt ; abgeschlossen. Mehrere Anweisungen werden mit den geschweiften Klamern {...} zu einer Anweisungs-Gruppe zusammengefaßt. AnweisungA; { Anweisung1; Anweisung2; Anweisung3; } AnweisungC; Die Anweisungsgruppen werden bei den weiteren Strukturen benötigt; aber auch das Hauptprogramm main besteht aus einer Anweisungsgruppe. C++ stellt selbst bereits viele Anweisungen zur Verfügung. Eine besondere Art der Anweisung ist die Wertzuweisung, die meist mit der Ausführung von Rechenoperationen verbunden ist. D:\75807564.doc vom 13.10.99 3-7 BBS III Mainz OOP für IT-Berufe mit C++ 3.1.6 Wertzuweisung, Rechenoperationen Die Wertzuweisung an eine Variable erfolgt über den Operator „=“, z.B. a=5; ergebnis=x*faktor+abstand-12; Das erste Beispiel weist der Variablen a den Wert 5 zu. Im zweiten Beispiel wird eine Formel berechnet, das Ergebnis der Berechnung wird der Variablen mit Namen ergebnis zugewiesen. C++ bietet die Grundrechenarten Addition, Subtraktion, Multiplikation und Division an. Addition, Subtraktion, Multiplikation und Division werden durch die Operatoren +, -, * und / ausgeführt; der Ergebnistyp richtet sich dabei immer nach dem größten beteiligten Operandentyp. int+int ergibt z.B. int, aber long+float ergibt float. Ergänzend zur Ganzzahl-Division gibt es noch den Operator %, der den Rest der ganzzahligen Division errechnet. C++ beachtet die Punkt-vor-Strich-Regel, so daß Multiplikation und Division vor Addition und Subtraktion ausgeführt werden. Auch runde Klammern (...) können wie gewohnt eingesetzt werden. Formel 17+5 12*1.5 12/4.0 12/4 17/3 17/3.0 17%3 D:\75807564.doc vom 13.10.99 Ergebnis 22 18.0 3.0 3 5 5.66666666667 2 Ergebnistyp (int) (float) (float) (int) (int) (float) (int) 3-8 Bemerkung int*float 17:3=5 Rest 2 BBS III Mainz OOP für IT-Berufe mit C++ 3.1.7 Implizite und explizite Typkonvertierung Was passiert, wenn bei einer Zuweisung die Formel einen anderen Typ hat, als die Variable, der der Wert zugewiesen werden soll? Beispiel: long a,e; float b; e=a/b; Das Ergebnis der Formel a/b ist vom Typ float, weil b vom Typ float ist. Aber e ist eine long-Variable. Daher muß der Ergebnistyp dem Variablentyp von e angepaßt werden. Solche Anpassungen können implizit vorgenommen werden, wie in diesem Beispiel, oder sie können auch explizit vorgegeben werden. Bei geeignetem Programmzusammenhang erfolgt die Typumwandlung automatisch (implizit). Zusätzlich kann der Programmierer explizite Typ-Konvertierungen (engl. type casting) vornehmen. Beispiele: int i,j; bool a,b; char z; a i j b = = = = true; int(a); 7; bool(j); j i b z i = = = = = 0; a; j; ’Ç’; z; D:\75807564.doc vom 13.10.99 // // // // // // // // // // // keine Konvertierung explizit: int(true) ist 1, daher ist i gleich 1 keine Konvertierung explizit: bool(7) ist true, daher ist b gleich true Jede Ganzzahl <> 0 ergibt als bool-Wert true keine Konvertierung implizit: i wird auf 1 gesetzt implizit: b wird auf 0 gesetzt keine Konvertierung; Zeichen mit ASCII-Wert 128 implizit: Aufpassen: i auf -128 gesetzt, da i ein signed int ist 3-9 BBS III Mainz OOP für IT-Berufe mit C++ 3.1.8 Inkrement- und Dekrementoperatoren C++ erlaubt spezielle Schreibweisen für Operationen, die sich auf eine einzige Variable beziehen. Soll der Wert einer Variablen erhöht oder vermindert werden, kann man folgende Schreibweisen anwenden: int i=3,a=6; i+=15; // i i-=17*a; // i ++i; // i i++; // i --i; // i i--; // i i*=17; // i i/=2*a; // i i%=a; // i wird wird wird wird wird wird wird wird wird um 15 erhöht um 17*a vermindert um 1 erhöht. Inkrement-Operator (Pre-Inkrement) um 1 erhöht. Inkrement-Operator (Post-Inkrement) um 1 vermindert. Dekrement-Operator (Pre-Dekrement) um 1 vermindert. Dekrement-Operator (Post-Dekrement) mit 17 multipliziert durch 2*a dividiert der Divisionsrest von i/a zugewiesen Der Unterschied, ob man die Operatoren ++ bzw. – vor oder nach der Variablen plaziert, ist erst wichtig, wenn die Operatoren innerhalb von Ausdrücken verwendet werden: ++i: Zuerst wird i inkrementiert, dann wird mit diesem i im Ausdruck weitergerechnet (analog für --i) i++: Zuerst wird der Ausdruck mit dem momentanen Wert von i berechnet, danach wird i inkrementiert (analog für i--) Beispiele: int i=10, j=5, k; k = i*j++; // jetzt hat k hat den Wert 50, j hat den Wert 6 int i=10, j=5, k; k = i*++j; // jetzt hat k den Wert 60, j hat den Wert 6. D:\75807564.doc vom 13.10.99 3-10 BBS III Mainz OOP für IT-Berufe mit C++ Testen Sie dazu das folgende Programmbeispiel KurzOp.cpp. // PROGRAMM KurzOp.cpp // Inkrement-, Dekrement- und Zuweisungsoperationen testen. #include <iostream.h> void main() { int i, j; cout << "Ganze Zahl eingeben: "; cin >> j; i = 1000 * ++j; cout << "Inkrement-Operator vor der Variablen ergibt: " << i << endl; j--; // Erhöhung zurücknehmen i = 1000 * j++; cout << "Inkrement-Operator nach der Variablen ergibt: " << i << endl; j = 5; i += j; cout << "Addition um 5 ergibt: " << i << endl; i -= j; cout << "Subtraktion von 5 ergibt: " << i << endl; i *= j; cout << "Multiplikation mit 5 ergibt: " << i << endl; } Ganze Zahl eingeben: 3 Inkrement-Operator vor der Variablen ergibt: 4000 Inkrement-Operator nach der Variablen ergibt: 3000 Addition um 5 ergibt: 3005 Subtraktion von 5 ergibt: 3000 Multiplikation mit 5 ergibt: 15000 D:\75807564.doc vom 13.10.99 3-11 BBS III Mainz D:\75807564.doc vom 13.10.99 OOP für IT-Berufe mit C++ 3-12 BBS III Mainz D:\75807564.doc vom 13.10.99 OOP für IT-Berufe mit C++ 3-13 BBS III Mainz D:\75807564.doc vom 13.10.99 OOP für IT-Berufe mit C++ 3-14 BBS III Mainz D:\75807564.doc vom 13.10.99 OOP für IT-Berufe mit C++ 3-15 BBS III Mainz OOP für IT-Berufe mit C++ 3.2 Folge (Sequenz) Sollen für die Lösung einer Aufgabe einfach nur mehrere Anweisungen hintereinander ausgeführt werden, können sie im Algorithmus in der gleichen Reihenfolge beschrieben werden. Das Beispielprogramm Erstprog.cpp entspricht folgendem (ausführlichem) Struktogramm. Kommt es einem nur auf das Wesentliche an, kann man dieses Struktogramm auch wie folgt verkürzen. Die Verkürzungen, die hier möglich waren, beziehen sich auf das Thema „benutzerfreundliche Programmierung“. Wenn im zweiten Struktogramm steht „Eingabe a,b“, ist damit die benutzerfreundliche Eingabe von a und b gemeint, die die entsprechenden Ausgaben von Eingabeaufforderungen beinhalten. Durch diese Verkürzungen lenken im Struktogramm die benutzerfreundlichen Teile nicht von den wesentlichen Teilen der Programmierung ab. D:\75807564.doc vom 13.10.99 3-16 BBS III Mainz OOP für IT-Berufe mit C++ 3.2.1 Grundlegende Übungsaufgaben 1. (ErstProg2.cpp) Verändern Sie das erste Beispielprogramm, so daß es die Differenz der beiden eingegebenen Zahlen ausgibt. 2. (Benzinv.cpp) Schreiben Sie ein Programm, das nach Eingabe der gefahrenen Kilometer und der Menge des verbrauchten Kraftstoffes den Durchschnittsverbrauch auf 100km berechnet und ausgibt. Berechnung des Durchschnittsverbrauchs verbrauchter Kraftstoff in l: 32.3 gefahrene Strecke in km: 423.1 Der Durchschnittsverbrauch ist 7.63 l/100km Programmende. 3. (Ganzteil.cpp) Das ganzzahlige Ergebnis und der Rest der Division von zwei einzugebenden ganzen Zahlen sollen mit einem Programm berechnet werden. Ganzzahlige Division mit Rest Dividend: 23 Divisor: 4 Das Ergebnis der Division 23:4 ist 5 Rest 3 Programmende 4. (ParallelR.cpp) Der Gesamtwiderstand zweier parallelgeschalteter Widerstände soll nach der Eingabe der beiden Widerstandswerte berechnet werden. Rg R1 R2 R1 R2 Widerstands-Parallelschaltung R1 in Ohm: 12000 R2 in Ohm: 10000 Der Widerstand der Gesamtschaltung ist 5454.545 Ohm Programmende D:\75807564.doc vom 13.10.99 3-17 BBS III Mainz OOP für IT-Berufe mit C++ 3.2.2 Anfangsprobleme und deren Lösungen Bei den ersten Übungen entstehen normalerweise Probleme, von denen hier eine Auswahl mitsamt Lösungen angegeben sind. 3.2.2.1 Vergessene Strichpunkte (Syntaxfehler) Einzelne Anweisungen werden stets durch Strichpunkt getrennt. Wurde dieser hinter einer Anweisung vergessen, wird beim Übersetzen meist ein Fehler bei der folgenden Anweisung angemahnt. Beispiele (die zeigt die echte Fehlerstelle, die die vom Compiler angezeigte Fehlerstelle an): „Statement missing ;“ ist eine sehr klare Fehlermeldung in diesem Fall. Der Fehler wird aber leider hinter der Fehlerstelle angezeigt. cout << "Hallo" cin >> a; „Declaration syntax error“ kann seine Ursache auch in einem fehlenden Strichpunkt nach einer Variablendeklaration haben. Int a,b cout << "Hallo"; 3.2.2.2 Falsch geschriebene Worte (Syntaxfehler) „Undefined Symbol ‘XXX’“ bedeutet, daß der Compiler mit dem Wort XXX nichts anfangen kann. Eine mögliche Ursache ist ein Schreibfehler, wie im folgenden Fall mit XXX=Cout. Cout << "Hallo"; Eine weitere Ursache ist eine falsche Variablendeklaration, wie im folgenden Fall mit XXX=Summe. Float summe; cout << Summe; 3.2.2.3 Ausgabe verschwindet sofort nach Programmende Entweder die Entwicklungsumgebung bietet eine Möglichkeit, den Ausgabebildschirm noch einmal zu zeigen, oder man hält durch das Setzen einer Unterbrechung (Breakpoint) nach dem letzten Programmbefehl das Programm an dieser Stelle an. Es gibt noch eine weitere Möglichkeit, indem man als letzten Befehl das Programm auf eine Eingabe warten läßt. Dazu ist der Befehl getch(); aus der Bibliothek conio geeignet. #include <iostream.h> #include <conio.h> // Variablendeklarationen ... void main(); { // Ein sinnvolles Hauptprogramm ... getch(); } 3.2.2.4 Merkwürdiges Zahlenformat bei der Ausgabe Fließkommazahlen werden manchmal mit einer unerwünscht hohen Genauigkeit oder in wissenschaftlicher Notation (+1E+20 für 1020) angegeben. Auch die genaue Ausgabebreite liegt nicht fest, was bei Tabellen wichtig ist. In C++ kann man mit folgenden Anweisungen die Anzahl der Vor- und Nachkommastellen genau festlegen. D:\75807564.doc vom 13.10.99 3-18 BBS III Mainz cout.setf(ios::fixed); // cout.width(20); // cout.precision(3); // cout << f1 << f2; // nämlich für f1, f2 wird OOP für IT-Berufe mit C++ muß einmal am Anfang des Programmes stehen die Gesamtbreite der Ausgabe wird bestimmt Anzahl der Nachkommastellen Vorsicht! Die Formatierung mit width() gilt genau einmal, wieder mit variabler Gesamtbreite ausgegeben. Die Anweisung cout.setf(ios::fixed); legt fest, daß das Programm bei Ausgaben auf den Bildschirm nicht mit Fließkommadarstellung, sondern mit Festkommadarstellung arbeiten will. Deswegen legt dann die Anweisung cout.precision(3); fest, mit wievielen festen Nachkommastellen gearbeitet werden soll. Diese beiden Einstellungen haben ständige Gültigkeit, man kann damit am Anfang eines Programmes eine gewisse Ausgabegenauigkeit festlegen. Soll auch die Anzahl der Stellen vor dem Komma festliegen, muß mit cout.width(V+N+1); die Ausgabebreite der gesamten Zahl gesetzt werden. V ist die Anzahl der gewünschten Vorkommastellen, N die der Nachkommastellen. Leider muß die Ausgabebreite vor der Ausgabe jeder Variablen neu eingestellt werden. 3.2.2.5 Semantische Fehler Im Gegensatz zu syntaktischen Fehlern können semantische Fehler nicht vom Compiler festgestellt werden, da semantische Fehler durch einen falschen Sinn der Anweisungen entstehen, nicht durch feststellbar falsche Schreibung. summe=a-b; // Eigentlich soll nicht die Differenz berechnet werden cout << summe; Dieses Programm wird übersetzt und ausgeführt, liefert aber wegen eines falschen Rechenzeichens ein falsches Ergebnis. Es ist syntaktisch korrekt, aber semantisch falsch. D:\75807564.doc vom 13.10.99 3-19 BBS III Mainz OOP für IT-Berufe mit C++ 3.3 Auswahl (Selektion) In Abhängigkeit von einer Bedingung werden Anweisungen ausgeführt oder weggelassen. Diese Bedingungen sind in der Regel an Operatoren wie > (größer als), < (kleiner als), == (gleich) und != (ungleich) geknüpft. 3.3.1 Einseitige Auswahl Bei der einseitigen Auswahl wird eine Anweisung oder eine Gruppe von Anweisungen nur dann ausgeführt, wenn eine bestimmte Bedingung erfüllt ist. Die Syntax lautet in C++: if (Bedingung) Anweisung; Beispiel: if (SparkontoGuthaben >= AbbuchungsBetrag) SparkontoGuthaben -= AbbuchungsBetrag; Sollen mehrere Anweisungen in Abhängigkeit der Bedingung ausgeführt werden, muß diese Anweisungsgruppe mit {...} geklammert werden. if (SparkontoGuthaben>=UmbuchungsBetrag) { SparkontoGuthaben -= UmbuchungsBetrag; GirokontoGuthaben += UmbuchungsBetrag; }; D:\75807564.doc vom 13.10.99 3-20 BBS III Mainz OOP für IT-Berufe mit C++ Die angegebene Bedingung ist entweder wahr oder falsch, sie nimmt also die booleschen Werte true oder false an. Boolesche Werte können mit den Operatoren „&&“ für UND bzw. „||“ für ODER verknüpft werden. a b a && b false false false false true false true false false true true true a b a || b false false false false true true true false true true true true a&&b ist also nur dann wahr, wenn sowohl a als auch b wahr sind. Bei a||b genügt es, wenn mindestens eines der beiden a oder b wahr ist. Vergleichsoperationen werden vorrangig vor boolschen Operationen behandelt. Welchen Datentyp hat im folgenden Beispiel die Variable GuterKunde? if (GirokontoGuthaben >= AbbuchungsBetrag || GirokontoGuthaben+Dispositionskredit >= Abbuchungsbetrag || GuterKunde) GirokontoGuthaben -= AbbuchungsBetrag; D:\75807564.doc vom 13.10.99 3-21 BBS III Mainz OOP für IT-Berufe mit C++ Grundlegende Übungsaufgaben: 1. (Bestell.cpp) Eine Firma liefert bei einem Bestellwert ab 200,-DM porto- und verpackungsfrei. Für Aufträge unter 200.-DM beträgt die Versandpauschale 5,50DM. Ein Programm soll den Rechnungsbetrag in Abhängigkeit vom Bestellwert ausgeben. Berechnung des Rechnungsbetrages Bestellwert in DM: 150 Rechnungsbetrag: 155.5 DM Programmende. Berechnung des Rechnungsbetrages Bestellwert in DM: 250 Rechnungsbetrag: 250 DM Programmende. 2. (Dreieck.cpp) Ein Dreieck läßt sich aus den Seiten a, b und c konstruieren, wenn die Dreiecksungleichungen a+b>c, a+c>b, b+c>a gelten. Schreiben Sie ein Programm, das bei einzugebenden Seiten überprüft, ob sich das Dreieck konstruieren läßt. Konstruierbarkeit eines Dreiecks pruefen Seite a: 12.5 Seite b: 12.5 Seite c: 12.5 Das Dreieck ist konstruierbar. Programmende. Konstruierbarkeit eines Dreiecks pruefen Seite a: 12.5 Seite b: 21 Seite c: 7.5 Das Dreieck ist nicht konstruierbar. Programmende. Zusätzliche Aufgaben: 1. (LinGl.cpp) Die Gleichung ax+b=0 soll für einzulesende Werte a und b vom Computer gelöst werden. Denken Sie daran, daß sowohl a als auch b den Wert 0 annehmen können. D:\75807564.doc vom 13.10.99 3-22 BBS III Mainz OOP für IT-Berufe mit C++ 3.3.2 Probleme und deren Lösung In diesem Abschnitt sollen Sie auf ein paar typische Fehler im Zusammenhang mit der einfachen Auswahl aufmerksam werden. 3.3.2.1 Strichpunkt nach if (semantischer Fehler) Die Auswirkung dieses Fehlers ist meist, daß Programmteile immer ausgeführt werden, obwohl sie nur unter bestimmten Bedingungen ausgeführt werden sollen. Der „bedingte Teil“ des untenstehenden Programmfragmentes wird immer ausgeführt, weil für den Compiler die bedingte Anweisung die leere Anweisung vor dem Strichpunkt ist. Der Block in {} wird daher ohne Bedingung ausgeführt. if (D<0); { D=-D; // Vorzeichen umdrehen cout << "Vorzeichen umgedreht." << endl; } 3.3.2.2 Vergessene geschweifte Klammern (semantischer Fehler) Die Auswirkung dieses Fehlers ist meist, daß Programmteile immer ausgeführt werden, obwohl sie nur unter bestimmten Bedingungen ausgeführt werden sollen. Der „bedingte Teil“ des untenstehenden Programmfragmentes soll der eingerückte Teil sein. Da der Compiler Einrückungen aber nicht beachtet, wird nur die erste Anweisung nach dem if() bedingt ausgeführt, die Ausgabeanweisung dagegen immer. if (D<0) D=-D; // Vorzeichen umdrehen cout << "Vorzeichen umgedreht." << endl; Richtig wäre: if (D<0) { D=-D; // Vorzeichen umdrehen cout << "Vorzeichen umgedreht." << endl; } 3.3.2.3 Zuweisung statt Vergleich (semantischer Fehler) Verwechselt man den Vergleichsoperator == mit dem Zuweisungsoperator =, erhält man vom Compiler eine Warnung: „Possibly incorrect assignment.“ In der folgenden Zeile ist der Bedingungsausdruck niemals wahr, obwohl nach Ausführen der Anweisung garantiert D=0 ist; die Ausgabe wird nie getätigt. if (D=0) cout << "D ist 0!" << endl; D=0 ist eine Zuweisung an D, wonach D den Wert 0 erhält. Der Wert des gesamten Ausdrucks ist auch 0, was logisch „falsch“ entspricht. Damit wird die Ausgabeanweisung nie ausgeführt. richtig wäre hier: if (D==0) cout << "D ist 0!" << endl; D:\75807564.doc vom 13.10.99 3-23 BBS III Mainz OOP für IT-Berufe mit C++ 3.3.3 Zweiseitige Auswahl Bei der zweiseitigen Auswahl wird in Abhängigkeit davon, ob eine Bedingung erfüllt ist oder nicht, jeweils eine bestimmte Anweisung oder Anweisungsgruppe ausgeführt. Die Syntax lautet in C++: if (Bedingung) Anweisung1; else Anweisung2; Man kann die zweiseitige Auswahl also als eine erweiterung der einseitigen Auswahl um eine Alternative betrachten. Beachten Sie: die else-Anweisung bezieht sich immer auf das letzte, nicht durch else abgeschlossene if im gleichen Block. Beispiel: if (SparkontoGuthaben >= AbbuchungsBetrag) SparkontoGuthaben -= AbbuchungsBetrag; else AbbuchungsBetrag=0; Sollen mehrere Anweisungen in Abhängigkeit der Bedingung ausgeführt werden, muß diese Anweisungsgruppe wieder mit {...} geklammert werden. if (SparkontoGuthaben >= UmbuchungsBetrag) { SparkontoGuthaben -= UmbuchungsBetrag; GirokontoGuthaben += UmbuchungsBetrag; } else { UmbuchungsBetrag=SparkontoGuthaben; SparkontoGuthaben=0; GirokontoGuthaben+=UmbuchungsBetrag; } D:\75807564.doc vom 13.10.99 3-24 BBS III Mainz OOP für IT-Berufe mit C++ Zweiseite Auswahl mit Bedingungsoperator: Falls es nur darum geht, eine bedingte Zuweisung zu treffen, kann man eine ganz rasche zweiseite Auswahl mit dem Bedingungsoperator „?“ treffen: Ergebnis = Bedingung ? AusdruckBeiWahr : AusdruckBeiFalsch; Zuerst wird die Bedingung ausgewertet. Ist sie wahr, dann ist das Ergebnis gleich AusdruckBeiWahr, ansonsten AusdruckBeiFalsch. Beispiel: int Max; Max = a>b ? a:b; // if (a>b) Max = a; else Max = b; Im Struktogramm muß man dies ausführlich darstellen. D:\75807564.doc vom 13.10.99 3-25 BBS III Mainz OOP für IT-Berufe mit C++ Grundlegende Übungsaufgaben 1. (BenzinV2.cpp) Schreiben Sie ein Programm, das nach Eingabe der gefahrenen Kilometer und der Menge des verbrauchten Kraftstoffes den Durchschnittsverbrauch auf 100km berechnet und ausgibt. Die Berechnung darf aber nur erfolgen, wenn die gefahrenen Kilometer ungleich null sind, ansonsten soll eine (sinnvolle) Fehlermeldung ausgegeben werden. Berechnung des Durchschnittsverbrauchs verbrauchter Kraftstoff in l: 32.3 gefahrene Strecke in km: 0 Ungueltige Strecke eingegeben. Programmende. 2. (Bestell2.cpp) Eine Firma liefert bei einem Bestellwert ab 200,-DM porto- und verpackungsfrei. Für Aufträge unter 200.-DM beträgt die Versandpauschale 5,50DM. Ein Programm soll den Rechnungsbetrag in Abhängigkeit vom Bestellwert ausgeben. Berechnung des Rechnungsbetrages Bestellwert in DM: 150 Rechnungsbetrag: 155.5 DM Programmende. Berechnung des Rechnungsbetrages Bestellwert in DM: 250 Rechnungsbetrag: 250 DM Programmende. 3. (Bestell3.cpp) Die Elektronikfirma Kleinkram erhebt für Bestellungen unter 100,-DM einen Porto- und Verpackungsanteil von 5,50DM, von 100,-DM bis 200,-DM einen Betrag von 3,-DM, ab 200,-DM werden keine Spesen berechnet. Ein Programm soll bei gegebener Auftragssumme den Rechnungsbetrag ausgeben. Berechnung des Rechnungsbetrages Auftragssumme in DM: 150 Rechnungsbetrag: 153.00 DM Programmende. 4. (Widerstand.cpp) Wahlweise soll der Gesamtwiderstand zweier parallel oder in Reihe geschalteter Widerstände nach der Eingabe der beiden Widerstandswerte berechnet werden. Bei Parallelschaltung: Rg R1 R2 R1 R2 Widerstands-Schaltung (P)arallel- oder (R)eihenschaltung? P R1 in Ohm: 12000 R2 in Ohm: 10000 Der Widerstand der Parallelschaltung ist 5454.545 Ohm Programmende D:\75807564.doc vom 13.10.99 3-26 BBS III Mainz OOP für IT-Berufe mit C++ 5. (KinderG.cpp) Eine Familie in Kautschukistan erhält nach folgender einkommensabhängiger Tabelle Kindergeld. Ein Programm ist gesucht, das nach Eingabe der Kinderzahl und des Einkommens das zu zahlende Kindergeld berechnet. Einkommen < 45.000,- Talente ab 45.000,- Talente für das 1. Kind 70 Talente 70 Talente für das 2. Kind 130 Talente 70 Talente für das 3. Kind 220 Talente 140 Talente ab dem 4. Kind 240 Talente 140 Talente Zusätzliche Aufgaben 1. (Geraden.cpp) Der Schnittpunkt von 2 Geraden ist gesucht. Gerade 1 wird durch die Punkte A(ax,ay) und B(bx,by), Gerade 2 durch die Punkte C(cx,cy) und D(dx,dy) angegeben. Denken Sie daran, daß die Geraden auch parallel sein können, dies muß besonders beachtet werden. Es gilt: (bx - ax) y = (by - ay) x + (ay bx - ax by) und (dx - cx) y = (dy - cy) x + (cy dx - cx dy) Die Geraden sind parallel, wenn gilt: (by - ay) (dx - cx) - (dy - cy) (bx - ax) = 0 D:\75807564.doc vom 13.10.99 3-27 BBS III Mainz OOP für IT-Berufe mit C++ 3.3.4 Mehrseitige Auswahl Es gibt zwei Formen der mehrseitigen Auswahl, die sich darin unterscheiden, wie man sie programmieren kann. Die erste Art ist sehr speziell und beruht darauf, daß die einzelnen Fälle sich durch feste Einzelwerte bestimmen lassen. Die mehrseitige Auswahl stützt ihre Wahl auf einen abzählbaren Datentyp, den Selektor, der mehr als zwei Werte annehmen kann, z.B. int oder char. In Abhängigkeit des aktuellen Wertes wird dann eine von vielen Möglichkeiten ausgewählt und ausgeführt. Die Syntax lautet in C++: switch(c) { case m1: case m2: case m3: case m4: case m5: case m6: default: } Anweisung1; break; Anweisung2; break; Anweisung34; break; Anweisung56; break; AnweisungS; Der default-Teil kann auch weggelassen werden. Beispiel: switch (wochentag) { case Montag: case Dienstag: case Mittwoch: case Freitag: case Donnerstag: default: } D:\75807564.doc vom 13.10.99 cout << "Werktag"; break; cout << "Langer Werktag"; break; cout << "Wochenende"; 3-28 BBS III Mainz OOP für IT-Berufe mit C++ Auch mehrere Anweisungen in Abhängigkeit der Bedingung sind möglich, dabei kommt es aber auf das Beenden der Anweisungsfolge mit break an, die Anweisungen müssen nicht geklammert werden. switch (wochentag) { case Montag: case Dienstag: case Mittwoch: case Freitag: case Donnerstag: default: cout << "Werktag"; Werktag=true; break; cout << "Langer Werktag" Werktag=true; break; cout << "Wochenende"; Werktag=false; } D:\75807564.doc vom 13.10.99 3-29 BBS III Mainz OOP für IT-Berufe mit C++ Falls der Selektor kein abzählbarer Datentyp ist oder die Bedingungen für die einzelnen Fälle komplizierter sind, muß auf eine verschachtelte zweiseitige Auswahl zurückgegriffen werden. Dabei taucht die breakAnweisung aber nicht mehr auf, sondern mehrere Anweisungen werden wie bei der zweiseitigen Auswahl mit geschweiften Klammern geklammert. // a ist vom Typ float und damit nicht abzaehlbar if (a<1.7) b=1; else if (a<2.5) { b=2; cout << "Fall 2" << endl; } else if (a<4.0) b=3; else b=4; // komplexe Bedingungen if (a<5 && c>7) b=1; else if (a<5 && c<=7) b=2; else if (a>=5 && c>7) { b=3; cout << "Fall 3" << endl; } else b=4; Merke: Bei verschachtelter zweiseitiger Auswahl gehört das letzte else immer zum direkt vohergehenden if. if (Bedingung1) Anweis1; else if (Bedingung2) Anweis2; else Anweis3; // ist das gleiche wie: if (Bedingung1) Anweis1; else { if (Bedingung2) Anweis2; else Anweis3; } D:\75807564.doc vom 13.10.99 3-30 BBS III Mainz OOP für IT-Berufe mit C++ Grundlegende Übungsaufgaben I. (MonName.cpp) Schreiben Sie ein Programm, das die Monatsnamen „Januar“ bis „Dezember“ ausgibt, wenn eine der Zahlen von 1 bis 12 eingegeben wird. Ausgabe des Monatsnamens Nummer des Monats (1..12): 4 Der 4. Monat heisst April. Programmende. Ausgabe des Monatsnamens Nummer des Monats (1..12): 15 Es gibt nur 12 Monate! Programmende. 2. (Brief.cpp) Briefe werden nach ihrem Gewicht frankiert. Es gelten folgende Portobeträge: bis (g) 20 50 100 250 500 1000 Talente 1,- 1,70 2,40 3,20 4,00 4,80 Sendungen schwerer als 1kg werden als Päckchen oder Paket verschickt. Schreiben Sie ein Programm, das nach Eingabe des Gewichtes das Porto ausgibt. Portoberechnung Gewicht des Briefes in g: 75.6 Kosten: 2.40 Talente Programmende Portoberechnung Gewicht des Briefes in g: 1500 Das wird ein Paket! Programmende Zusätzliche Aufgaben 1. (QuadGl.cpp) Die quadratische Gleichung ax2 + bx + c = 0 ist zu lösen. Denken Sie daran, daß die Lösungen imaginär sein können. Hinweis: Wurzeln werden in C++ mit der Funktion sqrt(x) berechnet, die in der Headerdatei math.h deklariert wird. 2. (Volumen.cpp) Es sollen die Volumina und Oberflächen von Zylinder, Würfel, Quader, Kreiskegel oder Kugel berechnet werden. Erstellen Sie ein Programm, das die Eingabe der Körperform und der notwendigen Maße erlaubt und danach die Werte berechnet. Hinweis: Die Kreiszahl Pi ist in C++ in der Headerdatei math.h als Konstante M_PI vereinbart. D:\75807564.doc vom 13.10.99 3-31 BBS III Mainz OOP für IT-Berufe mit C++ I. Arbeiten Sie die Programmstrukturen hinsichtlich ihrer Darstellungsmöglichkeiten (=Struktogramm DIN 66 261 oder Programmablaufplan DIN 66 001) anhand des Handouts PROGRAMMSTRUKTUREN durch. II. Erstellen Sie für jede Programmstruktur (A bis F) 3 Beispielaufgaben: A. 3 Beispiele für eine Folgestruktur B. 3 Beispiele für einseitige Auswahlstruktur C. 3 Beispiele für zweiseitige Auswahlstruktur D. 3 Beispiele für Mehrfachauswahl (=verschachtelte zweiseitige Auswahlstruktur) Datentyp muß nicht abzählbar sein E. 3 Beispiele für Fallabfrage (=mehrseitige Auswahl) Datentyp muß abzählbar sein F. 3 Beispiele für jede Wiederholungsstruktur (Schleifen) 3 Beispiele für Repetition (kopfgesteuerte Schleife = WHILE .......) 3 Beispiele für Schleife mit Endabfrage (fußgesteuerte Schleife = DO ......WHILE) 3 Beispiele für Zählschleife (zählergesteuerte Schleife = FOR ......) D:\75807564.doc vom 13.10.99 3-32 BBS III Mainz OOP für IT-Berufe mit C++ Leichte Aufgaben der Programmstrukturen A bis E Erstellen Sie für jede Aufgabe ein Struktogramm und ermitteln Sie die Programmstruktur. Codieren Sie die Programme . 1. Erstellen Sie ein Programm, das 10% vom Umsatz eines Vertreters berechnet. (A) 2. Ein Vertreter erhält nur dann 10 % Provision von seinem Umsatz, wenn dieser größer als 10000 DM ist (B) 3. Ein Vertreter erhält bei einem Umsatz bis einschließlich 10000 DM 5 % Provision vom Umsatz. Ist der Umsatz häher, erhält er 10 % (C) 4. 4 Zahlen sollen eingegeben werden. Das Programm soll ermitteln, ob alle Zahlen gleich, mindestens zwei Zahlen gleich bzw. alle vier Zahlen verschieden sind. (B) 5. Einem Auskunftsprogramm namens Rabatt liegen folgende Konditionen zur Gewährung von Mengenrabatt zugrunde: 2% für Mengen unter 100 Stück, 10% bei Abnahme zwischen 100 und 200 Stück und 12% bei mehr als 200 Stück. (D) 6. Wie 5. aber zur Unterscheidung der drei Fälle soll eine Fallabfrage erfolgen. (D+E) 7. Bei der Eingabe der Noten 1 bis 6 sollen die Kommentare „prima“ (bei den Noten 1 und 2), „Akzeptable Leistung“ ( bei Note 3), „Leider nicht so gut (bei den Noten 4 und 5) sowie „Schweigen ist Gold!“ (bei Note 6) ausgegeben werden. (E) 8. Auf die Frage „Der wievielte Wochentag ist heute?“ soll der zugrhörige Wochentag (z.B. „Dienstag“) ausgegeben werden. Die Woche soll mit dem Montag = 1 beginnen. (E) 9. Zwei Spieler geben unabhängig voneinander gleichzeitig je eine nicht negative ganze Zahl an (etwa durch Ausstrecken von Fingern auf Kommando oder durch verdecktes Aufschreiben). Nennen beide Spieler die gleiche Zahl, so endet das Spiel unentschieden; andernfalls gewinnt, falls die Summe der genannten Zahlen gerade ist, der Spieler, der die kleinere Zahl genannt hat, und sonst (falls also die Summe ungerade ist) derjenige, der die größere Zahl genannt hat. (D) D:\75807564.doc vom 13.10.99 3-33 BBS III Mainz 10. OOP für IT-Berufe mit C++ Beim Verkauf von Waren muß der Endverbraucher 16 % bzw. 7 % Umsatzsteuer tragen. Schreiben Sie ein Programm, das es ermöglicht, die Umsatzsteuer und den Bruttopreis je nach Steuersatz (16 bzw. 7 %) zu errechnen und auszugeben, wenn man den Nettobetrag eingibt und die jeweilige Umsatzsteuerhöhe angibt. Es soll gelten: Umsatzsteuerziffer 1 bedeutet 7 % Nur Eingabetaste bedeutet 16 % (D) 11. 12. Die Mitarbeiter eines Betriebes erhalten einen Stundenlohn von 18,50 DM. Werden im Monat mehr als 170 Stunden gearbeitet, wird für die Mehrarbeitszeit ein Überstundenzuschlag von 20 % gezahlt. (C) Es sollen Noteneingaben (IHK-Prüfungen) nach untenstehendem Punkteschlüssel übertragen werden. Wie lautet das Programm mit CASE, wenn folgende Punktetabelle zugrundeliegt: Punkte 0 - 29 30 - 49 50 - 66 67 - 82 83 - 92 93 - 100 Note ungenügend mangelhaft ausreichend befriedigend gut sehr gut Bildschirmausgabe: Geben Sie die Punkte ein: ______ Die Prüfung wurde mit der Note ________ abgeschlossen. 13. (E) In einer Autovermietung werden – unter anderem – die Mietkosten anhand der gefahrenen km berechnet. Dabei wird wie folgt vorgegangen: 1. die ersten 200 km werden nicht berechnet, 2. für die nächsten 800 km werden 0,65 DM je km berechnet, 3. Darüber hinausgehende kmm werden mit 0,40 DM je km berechnet Schreiben Sie ein Programm, dass den km-Stand vor Abfahrt und den nach Rückkehr einliest und aus der Differenz nach den obigen Vorgaben die km-Kosten berechnet (D) 14. RATESPIEL Analysieren Sie das komplexe Struktogramm RATESPIEL hinsichtlich seiner Strukturen. D:\75807564.doc vom 13.10.99 3-34 BBS III Mainz OOP für IT-Berufe mit C++ 3.3.5 Probleme und deren Lösung Bei Verwendung der switch-Anweisung werden einige spezielle Fehler gerne gemacht. Auch bei verschachtelten if()-else()-Konstruktionen wird oft unbeabsichtigtes programmiert. 3.3.5.1 Selektor nicht abzählbar (Syntaxfehler) Tritt folgende Fehlermeldung beim Übersetzen auf: „Switch selection expression must be of integral type.“, wurde versucht, in der Switch-Anweisung einen nicht abzählbaren Datentypen zu verwenden, wie z.B. float. float i; cin >> i; switch (i) { case 1:... case 2:... ... } Verwenden Sie als Typen in der switch-Anweisung nur char, short, int oder long (unsigned oder signed) oder selbstdefinierte enum-Typen. Abhilfe 1 (Datentyp ändern): int i; cin >> switch { case case ... } i; (i) 1:... 2:... Abhilfe 2 (kein switch verwenden): float i; cin >> i; if (i>0.5 && i<=1.5) {...} else if (i>1.5 && i<=2.5) {...} else ... 3.3.5.2 Vergessene break-Anweisung (semantischer Fehler) Dieser Fehler äußert sich darin, daß plötzlich die Aktionen mehrerer switch-Fälle nacheinander ausgeführt werden. int i; cin >> i; switch (i) { case 1: cout case 2: cout case 3: cout default: cout } cout << endl; << << << << "Eins "; "Zwei "; "Drei "; "was anderes"; Bei der Eingabe von 4, 5, 6 usw. wird korrekt „was anderes“ ausgegeben. Bei der Eingabe 1 dagegen wird „Eins Zwei Drei was anderes“ ausgegeben, weil hinter der Ausgabeanweisung cout << "Eins "; kein abschließendes break; folgt, ebenso hinter den folgenden Ausgabeanweisungen. Abhilfe: D:\75807564.doc vom 13.10.99 3-35 BBS III Mainz int i; cin >> i; switch (i) { case 1: cout case 2: cout case 3: cout default: cout } cout << endl; OOP für IT-Berufe mit C++ << << << << "Eins "; break; "Zwei "; break; "Drei "; break; "was anderes"; 3.3.5.3 Falsche if()-else()-Konstruktionen (semantischer Fehler) Diese äußern sich in wirren, unbeabsichtigten Reaktionen des Programmes, genauer lassen sich die Folgen dieses Fehlers leider nicht beschreiben. Beachten Sie im folgenden Programmfragment die Einrückungen, die symbolisieren, was der Programmierer programmieren will. if (D>0) if (E>0) cout << "D und E sind beide positiv."; else if (E<=0) cout << "D und E sind beide nicht positiv."; Was der Programmierer tatsächlich programmiert hat, ist aber folgendes: if (D>0) if (E>0) cout << "D und E sind beide positiv."; else if (E<=0) cout << "D und E sind beide nicht positiv."; Begründung: Eine else-Anweisung bezieht sich immer auf das zuletzt noch nicht durch else abgeschlossene if. Abhilfe: if (D>0) { if (E>0) cout << "D und E sind beide positiv."; } else { // diese geschweifte Klammer ist nicht notwendig, aber konsequent if (E<=0) cout << "D und E sind beide nicht positiv."; } // diese geschweifte Klammer ist nicht notwendig, aber konsequent Dadurch muß die else-Anweisung sich auf die im Block vorhergehende if-Anweisung beziehen. D:\75807564.doc vom 13.10.99 3-36 BBS III Mainz OOP für IT-Berufe mit C++ 3.4 Wiederholung (Repetition) In Abhängigkeit einer Bedingung werden Anweisungen wiederholt. Dabei ist zu unterscheiden zwischen Bedingungen, die bereits am Schleifenanfang abgefragt werden und solchen, die erst beim Beenden der Schleife geprüft werden. 3.4.1 Schleife mit Anfangsabfrage Bei der Schleife mit Anfangsabfrage, auch kopfgesteuerte Schleife genannt, kann es vorkommen, daß der Schleifenkörper nie durchlaufen wird, weil die Schleifenbedingung schon zu Anfang nicht zutrifft. Solange die Bedingung wahr ist, wird die Schleife durchlaufen. Die Syntax in C++ lautet: while (Bedingung) Anweisung; Beispiel: cout << "Ja oder Nein? "; cin >> c; while (c!='j' && c!='n') { cout << (char)7; // (char)7=ASCII-Zeichen 7=BELL; erzeugt Warnton cin >>c; } D:\75807564.doc vom 13.10.99 3-37 BBS III Mainz OOP für IT-Berufe mit C++ Grundlegende Übungsaufgaben 1. (Primzahl.cpp) Lassen Sie prüfen, ob eine long-Zahl Primzahl ist. Dividieren Sie die zu prüfende Zahl durch i=2, 3, usw. bis i2>x ist und prüfen Sie, ob die Division x/i einen Rest läßt. 2. Lassen Sie den Rechner die Summe der folgenden Zahlen bis zu einem Grenzwert g berechnen und geben Sie die Anzahl der benötigten Glieder aus. a) (SumA.cpp) 1 + 2 + 3 + 4 + ...+ n +... b) (SumB.cpp) 1 - 2 + 3 - 4 + 5 ... + (2n+1) - 2n ... c) (SumC.cpp) 1 + ½ + 1/3 + ...+ 1/n +... d) (SumD.cpp) 1 - ½ + 1/3 ... + 1/(2n+1) - 1/(2n) ... e) (SumE.cpp) 1 + 4 + 9 + 16 + 25 +...+ n2 +... f) (SumF.cpp) 1 + 2 + 4 + 7 + 11 + 16 + ... + xn + (xn+n) + ... Berechnung der Summe 1+2+3+4+... bis zu einem Grenzwert Grenzwert: 16 Nach 6 Gliedern ist 16 erreicht. Die Summe ist 21. Berechnung der Summe 1+2+3+4+... bis zu einem Grenzwert Grenzwert: 28 Nach 7 Gliedern ist 28 erreicht. Die Summe ist 28. Zusätzliche Übungsaufgaben 1. (ProdAdd.cpp) Das Produkt zweier ganzer Zahlen ist mit Hilfe der Addition zu berechnen. Beachten Sie, daß die Zahlen auch 0 oder negativ sein können. Berechnung des Produktes ueber Addition Multiplikator: 3 Multiplikand : 5 Das Produkt ist 15. Berechnung des Produktes ueber Addition Multiplikator 1: 0 Multiplikator 2: 5 Das Produkt ist 0. 2. (QuotSub.cpp) Der ganzzahlige Quotient a/b zweier ganzer Zahlen a und b und der entstehende Rest sollen über die Subtraktion berechnet werden. Überlegen Sie, was es hier für Fehlerquellen geben kann und berücksichtigen Sie diese im Programm. Beispiel: 25 / 8 = 3 Rest 1 D:\75807564.doc vom 13.10.99 3-38 BBS III Mainz OOP für IT-Berufe mit C++ 3.4.2 Probleme und deren Lösung Die hier geschilderten Probleme ähneln den bei der einfachen Auswahl aufgezeigten. 3.4.2.1 Strichpunkt nach while (semantischer Fehler) Auswirkungen dieses Fehlers sind meist, daß der Schleifenkörper nur einmal ausgeführt wird, obwohl er öfter ausgeführt werden sollte. eine Endlosschleife entsteht, so daß es den Eindruck macht, das Programm „hänge“. Der „Schleifenkörper“ des untenstehenden Programmfragmentes wird immer einmal ausgeführt, wenn ein negatives e eingegeben wird, ansonsten entsteht eine Endlosschleife. Für den Compiler ist der Schleifenkörper die leere Anweisung vor dem Strichpunkt. Der Block in {} wird daher nicht als Schleife, sondern als Sequenz nach der Schleife ausgeführt. Bei negativem e wird dann die Schleife nie ausgeführt, weil sofort i>e ist. Bei nichtnegativem e ist i<=e sofort erfüllt, und da sich weder i noch e in der leeren Schleife ändern, entsteht eine Endlosschleife. cin >> e; i=0; s=0; while (i<=e); { s+=i; // Summieren cout << i++ << " " << endl; } 3.4.2.2 Vergessene geschweifte Klammern (semantischer Fehler) Auswirkungen dieses Fehlers sind meist, daß der Schleifenkörper nur einmal ausgeführt wird, obwohl er öfter ausgeführt werden sollte. eine Endlosschleife entsteht, so daß es den Eindruck macht, das Programm „hänge“. Die Ausgabe des untenstehenden Programmfragmentes wird immer einmal ausgeführt, wenn ein negatives e eingegeben wird, ansonsten entsteht eine Endlosschleife. Für den Compiler ist der Schleifenkörper die Anweisung s+=i. Die Ausgabe wird daher nicht in der Schleife, sondern als Sequenz hinter der Schleife ausgeführt. Bei negativem e wird dann die Schleife nie ausgeführt und stattdessen sofort die Ausgabe, weil sofort i>e ist. Bei nichtnegativem e ist i<=e sofort erfüllt, und da sich weder i noch e in der Schleife ändern, entsteht eine Endlosschleife. cin >> e; i=0; s=0; while (i<=e); s+=i; // Summieren cout << i++ << " " << endl; D:\75807564.doc vom 13.10.99 3-39 BBS III Mainz OOP für IT-Berufe mit C++ 3.4.3 Schleife mit Endabfrage Die Schleife mit Endabfrage wird mindestens einmal durchlaufen, sie wird auch als fußgesteuerte Schleife bezeichnet. Die Schleife wird solange wierholt, wie die Bedingung wahr ist. Die Syntax in C++ lautet: do Anweisung while Bedingung; Beispiel: cout << "Ja oder Nein? "; do { cin >> c; if (c!='J' && c!='j' && c!='N' && c!='n') cout << (char)7; } while (c!='J' && c!='j' && c!='N' && c!='n'); D:\75807564.doc vom 13.10.99 3-40 BBS III Mainz OOP für IT-Berufe mit C++ Grundlegende Übungsaufgaben 1. (Teiler.cpp) Lassen Sie alle Teiler einer einzulesenden ganzen Zahl ausgeben. Benutzen Sie auch hier eine Division mit ganzzahligem Ergebnis. Beispiel: 8 hat die Teiler 1, 2, 4, 8. 2. (Bremsen.cpp) Bei einem Bremsvorgang wird ein Auto mit 4,3m/sec2 gebremst. Ein Programm soll die Bremswege bis zu einem einzugebenden maximalen Bremsweg in Tabellenform ausgeben, wobei die Geschwindigkeit von 5km/h beginnend in Schritten von 5km/h zu steigern ist. Gleichzeitig soll die Bremszeit berechnet und ausgegeben werden. (s = v2 / (2 a); t = v / a) Zusätzliche Übungsaufgaben 1. (Suppe.cpp) Eine Schüssel Suppe kühlt pro Minute um 19% der Differenz aus Suppentemperatur und Umgebungstemperatur ab. Es soll die Temperatur der Suppe nach jeder Minute angegeben werden, wenn man als Anfangstemperatur 90oC und als Umgebungstemperatur 20oC annimmt. Schreiben Sie das Programm so, daß Suppen- und Umgebungstemperatur eingegeben werden können. 2. (Zins.cpp) Ein Kapital k wird mit Zinseszins verzinst. Lassen Sie ein Tabelle ausgeben, die den Betrag ausgibt, der am Ende jeden Jahres auf dem Konto steht. Der Zinsfuß p, das Kapital k und der zu erreichende Kontostand bmax sollen einzugeben sein. (b = k ( 1 + p / 100)n) D:\75807564.doc vom 13.10.99 3-41 BBS III Mainz OOP für IT-Berufe mit C++ 3.4.4 Zählschleife Die Zählschleife ist eine Struktur, bei der von Anfang an feststeht, wie viele Wiederholungen ausgeführt werden. Dabei wird noch ein Zähler (genannt Laufvariable) mitgeführt. Im Beispiel ändert sich die Laufvariable von Anfangswert bis Endwert. Die Syntax in C++ lautet: for (Laufvariable=Anfangswert; Laufvariable<=Endwert; Laufvariable++) Anweisung; Beispiele: // gibt 20 Sterne in einer Zeile aus for (i=1;i<=20;++i) cout << '*'; cout << endl; // addiert 5 einzugebende Werte auf cout << "5 Werte eingeben." << endl; summe=0; for (z=1; z<=5; z++) { cout << z << "-ter Wert: "; cin >> x; summe+=x; } Jede Zählschleife kann durch eine Schleife mit Anfangsabfrage ersetzt werden: for (Laufvariable=Anfangswert; Laufvariable<=Endwert; Laufvariable++) Anweisung; // ist das gleiche wie: Laufvariable=Anfangswert; while (Laufvariable<=Endwert) { Anweisung; Laufvariable++; } D:\75807564.doc vom 13.10.99 3-42 BBS III Mainz OOP für IT-Berufe mit C++ 3.4.5 Zählschleife mit variabler Schrittweite Ersetzt man die Inkrementierung der Laufvariablen Laufvariable++ in den obigen Ausdrücken durch einen anderen Inkrementierungsausdruck (z.B. Laufvariable+=5), dann ergibt sich eine Zählschleife mit variabler Schrittweite. Laufvariable=Anfangswert; while (Laufvariable<=Endwert) { Anweisung; Laufvariable+=Weite; } // ist das gleiche wie: for (Laufvariable=Anfangswert; Laufvariable<=Endwert; Laufvariable+=Weite) Anweisung; Die folgende Anweisung bildet z.B. eine Schleife mit der Schrittweite 3 und setzt die Laufvariable i nacheinander auf die Werte 1, 4, 7, 10: for (i=1; i<=10; i+=3) cout << "Wert der Laufvariable: " << i << endl; D:\75807564.doc vom 13.10.99 3-43 BBS III Mainz OOP für IT-Berufe mit C++ Grundlegende Übungsaufgaben 1. (ProdAdd2.cpp) Das Produkt zweier ganzer Zahlen ist mit Hilfe der Addition zu berechnen. Beachten Sie, daß die Zahlen auch 0 oder negativ sein können. 2. (Ungerade.cpp) Lassen Sie den Computer die ungerade Zahlen zwischen zwei einzugebenden Werten ausdrucken. 3. (Rechnen.cpp) Es sollen die Quadratzahlen, die Kubikzahlen und die Kehrwerte der Zahlen zwischen zwei einzugebenden Grenzen ausgegeben werden. 4. Schreiben Sie Programme, die nach Eingabe von n die folgenden Summen berechnen: a) (SumNa.cpp) 12 + 22 + 32 + 42 + ... + n2 b) (SumNb.cpp) 12 + 32 + 52 + ... + (2 n - 1)2 c) (SumNc.cpp) 22 + 42 + 62 + ... + (2 n)2 d) (SumNd.cpp) 1 + 1 / 2 + 1 / 3 + ... + 1 / n 5. (Primzahl2.cpp) Verändern Sie Primzahl.cpp, so daß es alle Primzahlen ab 2 bis zu einer einzugebenden Obergrenze berechnet. Zusätzliche Übungsaufgaben 1. (N3und5.cpp) Der Rechner soll alle natürlichen Zahlen zwischen einzugebenden Grenzen ausgeben, die sowohl durch 3 als auch durch 5 teilbar sind. 2. (N3oder5.cpp) Der Rechner soll alle natürlichen Zahlen zwischen einzugebenden Grenzen ausgeben, die durch 3 oder durch 5 teilbar sind. 3. (N4und7.cpp) Der Rechner soll alle natürlichen Zahlen zwischen einzugebenden Grenzen ausgeben, die sowohl durch 4 als auch durch 7 teilbar sind. 4. (N4oder7.cpp) Der Rechner soll alle natürlichen Zahlen zwischen einzugebenden Grenzen ausgeben, die durch 4 oder durch 7 teilbar sind. 5. (Kapital.cpp) Ein Kapital k wird n Jahre lang mit Zinseszins verzinst. Lassen Sie ein Tabelle ausgeben, die den Betrag ausgibt, der am Ende jeden Jahres auf dem Konto steht. Der Zinsfuß p, das Kapital k und die Anzahl der Jahre n sollen einzugeben sein. (b = k ( 1 + p / 100)n) 6. (Reis.cpp) Der indische König Schehram verlangte, daß Sessa, der Erfinder des Schachspiels, sich eine Belohnung wählen solle. Dieser erbat sich die Summe Weizenkörner, die sich ergibt, wenn für das 1. Feld des Schachbrettes 1 Korn, für das 2. Feld 2 Körner für das 3. Feld 4 Körner usw. gerechnet werden. Schreiben Sie ein Programm, das für alle 64 Felder die Gesamtsumme der Körner berechnet. Geben Sie auch das Gewicht der Körner an, wenn 200 Körner 1g wiegen. Wieviele Eisenbahnwaggons bräuchte man zum Transport des Reises und wie lange wäre der Zug, wenn ein Eisenbahnwaggon 30t faßt und 15m lang ist? D:\75807564.doc vom 13.10.99 3-44 BBS III Mainz OOP für IT-Berufe mit C++ 3.4.6 Anweisungen für Schleifenabbruch und -wiederholung Zwei zusätzliche Anweisungen können in einigen Fällen die Übersicht bei der Programmierung von Schleifen erhöhen, aber auch stark beeinträchtigen. Die Anweisungen sollten daher sparsam und nach reiflicher Überlegung eingesetzt werden; bei Vertretern der „reinen Lehre“ der strukturierten Programmierung gelten derartige Anweisungen als „unstrukturiert“. break; Die Schleife wird sofort verlassen (beendet). continue; Die Schleifenbedingung wird sofort erneut abgefragt. Die Anweisungen break und continue können bei allen Schleifenformen eingesetzt werden. Für break existiert eine eigene Darstellung im Struktogramm: // Verwendung von break: do { cout << "Ja oder Nein? "; cin >> c; if (c==’J’ || c==’j’ || c==’N’ || c==’n’) { break; } cout << "Ungültige Eingabe!" << endl; } while (true); D:\75807564.doc vom 13.10.99 3-45 BBS III Mainz OOP für IT-Berufe mit C++ // Verwendung von continue: bool ok; do { cout << "Ja oder Nein? "; cin >> c; ok = (c!='J' && c!='j' && c!='N' && c!='n'); if (!ok) continue; // Dann nochmal Ja oder Nein abfragen // Hier würde nun ein längerer Verarbeitungsteil folgen, // der nur abgearbeitet wird, wenn Ja oder Nein eingegeben wird. } while (false); Für die Verwendung der continue-Anweisung gibt es keine gesonderte Struktogrammdarstellung. Man kann jedoch die folgende Ersetzung verwenden: // Schleife mit continue // entspricht Schleife mit Auswahl while (Bedingung1) do { Anweisung1; if (Bedingung2) continue; Anweisung2; Anweisung3; // usw. } while (Bedingung1) do { Anweisung1; if (!Bedingung2) { Anweisung2; Anweisung3; // usw. } } Grundlegende Übungsaufgaben Entwerfen Sie diese Programme jeweils ohne Verwendung von break oder continue und dann verändern Sie sie, so daß break oder continue zum Einsatz kommen. 1. (AddBel.cpp) Schreiben Sie ein Programm, das eine beliebige Anzahl von einzugebenden Zahlen addiert, bis die Zahl 0 eingegeben wird. Addition von Zahlen Nach der letzten Zahl 0 eingeben 1. Zahl: 15.2 2. Zahl: 17.1 3. Zahl: 20 4. Zahl: 0 Die Summe der 3 eingegebenen Zahlen ist 52.3. Programmende. 2. (DivBel.cpp) Schreiben Sie ein Programm, das eine beliebige Anzahl von einzugebenden Zahlenpaaren dividiert, bis als erste Zahl 0 eingegeben wird. Wird als zweite Zahl 0 eingegeben, soll die Eingabe wiederholt werden. Division von Zahlen Nach der letzten Zahl 0 eingeben 1. Zahl: 15.2 2. Zahl: 17.1 15.2/17.1 ist 0.8888888888889. 1. Zahl: 20 2. Zahl: 0 Durch 0 kann nicht dividiert werden. 1. Zahl: 0 Programmende. D:\75807564.doc vom 13.10.99 3-46 BBS III Mainz OOP für IT-Berufe mit C++ 3.5 Modul (Unterprogramm) Wenn die zu bearbeitenden Algorithmen so groß werden, daß man sie durch ein übersichtliches Struktogramm nicht mehr darstellen kann, muß man die Aufgabenstellung modularisieren. Das bedeutet, ein Problem wird in Teilprobleme zerlegt, die einzeln für sich gelöst werden. Jede strukturierte Programmiersprache besitzt Sprachelemente zur Modularisierung. In C++ heißen die Module Funktionen. Bislang enthielten unsere C++-Programme schon ein einziges Modul, nämlich das Hauptprogramm main(). Bereits an void main() erkennen wir, daß eine Funktion einen Typ hat, einen Bezeichner und einen Anweisungsblock, der zwischen geschweiften Klammern „{“ und „}“ eingeschlossen wird. Allgemein wird eine Funktion deklariert als Rückgabetyp Funktionsname (Parameterliste) Also z.B. void bool int float Ausgabe(float f) IstPrimzahl(int n) Quadrat(int n) Kegelvolumen(float r,h) Der Rückgabetyp der Funktion main() war bislang void, das heißt, die Funktion main() gibt nach ihrem Ablauf keinen Wert an das Betriebssystem zurück. Als Rückgabetyp von Funktionen können beliebige Datentypen eingesetzt werden. Möchte man eine Funktion ohne Rückgabetyp verwenden, benutzt man dafür das Schlüsselwort void: void main() { /* Anweisungen */ } // Kein Rückgabewert an das // Betriebssystem. Diesen Spezialfall der typlosen Funktion bezeichnet man in anderen Programmiersprachen, z.B. Pascal, als Prozedur. Wir werden in den nächsten Kapiteln zunächst nur auf Funktionen ohne Rückgabewert eingehen, danach aber besprechen, wie Funktionen mit Rückgabewert programmmiert werden. D:\75807564.doc vom 13.10.99 3-47 BBS III Mainz OOP für IT-Berufe mit C++ 3.5.1 Funktionen ohne Parameter Module bieten den Vorteil, daß sie mehrmals in Programmen ausgeführt werden können, obwohl man sie nur einmal deklarieren und definieren muß. Am folgenden Programmbeispiel MAXIMUM1.CPP wird dies demonstriert. Die Funktion Max wird zweimal aufgerufen, um jeweils das Maximum von zwei Zahlen anzugeben. /* PROGRAMM Maximum1.cpp Maximum von zwei Zahlen. Typlose Funktion ohne Parameter */ #include <iostream> int a,b,x,y; void Max() // Deklaration der Funktion { if (a > b) cout << "Maximum = " << a; else cout << "Maximum = " << b; cout << endl; } // Definition der Funktion void main() { cout << "Zwei Zahlen: "; cin >> a >> b; Max(); // Erster Funktionsaufruf cout << "Zwei andere Zahlen: "; cin >> x >> y; a = x; b = y; Max(); // Zweiter Funktionsaufruf cout << "Programmende."; } Zwei Zahlen: 1000 2000 Maximum = 2000 Zwei andere Zahlen: 5 9 Maximum = 9 Programmende. Grundlegende Übungsaufgabe Schreiben Sie das früher bereits behandelte Programm BENZINV.CPP zu BENZIN2.CPP um. Im Hauptprogramm sollen nacheinander drei typlose Funktionen aufgerufen werden: main() { /* ... */ Eingabe(); Verarbeitung(); Ausgabe(); /* ... */ } D:\75807564.doc vom 13.10.99 3-48 BBS III Mainz OOP für IT-Berufe mit C++ 3.5.2 Funktionen mit Werteparametern Das Programm MAXIMUM1 arbeitet an einer Stelle sehr umständlich: Da innerhalb der Funktionsdefinition von Max() die Variablen a und b verglichen werden, müssen vor dem zweiten Funktionsaufruf die neuen Variablen x und y explizit „von Hand“ auf a und b zugewiesen werden. Hier wünscht man sich eine implizite Variablenzuweisung, die so arbeitet, daß in der Funktionsdefinition „Platzhaltervariablen“ angegeben werden können, in die dann beim Funktionsaufruf automatisch die tatsächlich benötigten Variablen eingesetzt werden. Das folgende Programm MAXIMUM2 erledigt diese „automatische“ Wertübergabe mittels Werteparametern. Die Parameter x und y werden innerhalb der Funktion Max() als „Platzhalter“ verarbeitet. Sie sind formale Parameter. Beim Funktionsaufruf werden an die Stelle der formalen Parameter x und y die aktuellen Parameter a und b bzw. c und d eingesetzt. Statt dem Begriff Parameter benutzt man auch die Bezeichung Argument. /* PROGRAMM Maximum2.cpp Maximum von zwei Zahlen. Typlose Funktion mit Werteparametern */ #include <iostream> int a,b,c,d; void Max(int x, int y) // Funktion mit formalen Werteparametern x und y { if (x > y) cout << "Maximum = " << x; else cout << "Maximum = " << y; cout << endl; } void main() { cout << "Zwei Zahlen: "; cin >> a >> b; Max(a,b); // Erster Funktionsaufruf cout << "Zwei andere Zahlen: "; cin >> c >> d; Max(c,d); // Zweiter Funktionsaufruf cout << "Programmende."; } Die Bildschirmausgabe von MAXIMUM2 ist identisch mit der von Maximum1. Globale und lokale Variablen Die Variablen a, b, c und d sind im globalen Programmteil als globale Variablen deklariert worden. Globale Variablen sind in allen Modulen bekannt, die ein Programm verwendet. Die Eingabeparameter x und y jedoch sind als lokale Variablen nur innerhalb der Funktion Max() gültig und bekannt. D:\75807564.doc vom 13.10.99 3-49 BBS III Mainz OOP für IT-Berufe mit C++ Werteparameter Ein Werteparameter kann innerhalb einer Funktion zwar verändert werden, der geänderte Wert kann jedoch nicht an den aktuellen Parameter des Hauptprogrammes zurückgegeben werden. Deshalb nennt man Werteparameter auch Eingabeparameter. Sie werden in das Modul nur hineingegeben, das Modul kann sie nicht (geändert) wieder ausgeben. Grundlegende Übungsaufgabe Schreiben Sie das Programm BENZINV.CPP um zu BENZIN3.CPP. Bei gleicher Bildschirmausgabe soll die folgende Funktion aufgerufen werden - innerhalb der Funktion sollen keinerlei Tastatureingaben oder Bildschirmausgaben getätigt werden: void DVerbrauch(float Verbrauch, float Strecke); D:\75807564.doc vom 13.10.99 3-50 BBS III Mainz OOP für IT-Berufe mit C++ 3.5.3 Funktionen mit Variablenparametern In den bisherigen Programmbeispielen war innerhalb des Moduls Max() eine Bildschirmausgabe enthalten. Das ist unschön, denn Programmodule sollen möglichst universell verwendbar sein. So wünscht man sich eine Funktion Max(), die den Maximalwert nur an das aufrufende Programm zurückgibt, anstatt ihn sofort auf dem Bildschirm auszugeben. Das aufrufende Programm kann dann den in der Funktion ermittelten Wert beliebig verwerten - ihn z.B. in arithmetischen Ausdrücken weiter verrechnen oder mit einem speziellen Format selbst auf dem Bildschirm ausgeben, usw. Das Zurückgeben von Werten, die in einem Modul geändert wurden, an das rufende Programm kann man mit Variablenparametern erreichen, auch Referenzparameter oder Ein-Ausgabeparameter genannt: /* PROGRAMM Maximum3.cpp Maximum von zwei Zahlen. Typlose Funktion mit Wert- und Variablenparameter */ #include <iostream> #include <stdlib> int a,b,Maximum; void Max(int x, int y, int& erg) // Funktion mit Wertparametern x und y und Referenzparameter erg { erg = x > y ? x:y ; } // Kurzform von { if (x > y) erg = x; else erg = y; } void main() { int i; for (i=0;i<2;i++) { if (!i) cout << "Zwei Zahlen: "; else cout << "Zwei andere Zahlen: "; cin >> a >> b; Max(a,b,Maximum); cout << "Maximum = " << Maximum << endl; } cout << "Programmende."; } Man erkennt, daß ein Variablenparameter durch das Symbol & direkt hinter der Typbezeichnung gekennzeichnet wird. Ein formaler Variablenparameter in einer Funktion zeigt immer auf die gleiche Speicheradresse wie der aktuelle Parameter im rufenden Programm. Daher verändert sich der aktuelle Parameter nach Ablauf der Funktion. D:\75807564.doc vom 13.10.99 3-51 BBS III Mainz OOP für IT-Berufe mit C++ Grundlegende Übungsaufgaben 1. Das folgende Programm Erhoehen.cpp ruft zweimal nacheinander eine Funktion namens Erhoehe auf. Innerhalb der Funktion wird der übergebene Variablenparameter um 10 erhöht und das Ergebnis wird an das Hauptprogramm zurückgegeben. Lassen Sie das Programm ablaufen und studieren Sie seine Wirkungsweise. /* PROGRAMM Erhoehen.cpp Erhoehen einer Zahl um 10. Typlose Funktion mit Variablenparameter */ #include <iostream> float a; void Erhoehe(float& par) { par += 10; } void main() { int i; for (i=0;i<2;i++) { if (!i) cout << "Eine Zahl eingeben: "; else cout << "Noch eine Zahl eingeben: "; cin >> a; Erhoehe(a); cout << "Die um 10 erhoehte Zahl lautet " << a << endl; } cout << "Programmende."; } Eine Zahl eingeben: 3.4 Die um 10 erhöhte Zahl lautet 13.4 Noch eine Zahl eingeben: 5.6 Die um 10 erhöhte Zahl lautet 15.6 Programmende. 2. Machen Sie den Variablenparameter par in Erhoehen.cpp zum Werteparameter, lassen Sie das Programm ablaufen und studieren Sie die Wirkung. Begründen Sie das Programmverhalten. 3. Schreiben Sie ein Programme TAUSCHE.CPP, in dem zwei Zahlen a und b getauscht werden. Verwenden Sie darin die folgende Funktion. Auch hier sollen innerhalb der Funktion weder Tastatureingaben noch Bildschirmausgaben stehen. void Tausch(/* Zwei Argumente */); D:\75807564.doc vom 13.10.99 3-52 BBS III Mainz OOP für IT-Berufe mit C++ 3.5.4 Funktionen mit Rückgabetyp Eine Funktion in C++ kann beliebig viele Wertparameter und/oder Variablenparameter mit sich tragen. Sehr oft hat einen Funktion jedoch den Zweck, genau einen einzigen, ganz bestimmten Wert zu berechnen. Dann gibt man der Funktion einen Rückgabetyp, dadurch wird der zu berechnende Wert im Funktionsnamen zurückgegeben. Diese Verwendung einer Funktion ist der Normalfall in C++: /* PROGRAMM Maximum4.cpp Maximum von zwei Zahlen mit Integerfunktion */ #include <iostream> int a,b,Erg; // Erg kann auch eingespart werden, siehe unten int Max(int x, int y) // Kein Variablenparameter mehr erforderlich { if (x > y) return x; else return y; } void main() { for (int i=0;i<2;i++) // i gleichzeit deklariert und verwendet! { if (!i) cout << "Zwei Zahlen: "; else cout << "Zwei andere Zahlen: "; cin >> a >> b; Erg = Max(a,b); cout << "Maximum = " << Erg << endl; // noch kürzer ohne Erg statt der oberen Zeile: // cout << "Maximum = " << Max(a,b) << endl; } cout << "Programmende."; } Man beachte die elegante Schreibweise, die mit typisierten Funktionen möglich ist: Da der Name der Funktion sowohl das Unterprogramm als auch die zurückgegebene Variable repräsentiert, kann der Funktionsaufruf syntaktisch genauso verwendet werden wie eine Variable. Die Anweisung return bewirkt Zweierlei: Der Rückgabewert der Funktion wird mit dem angegebenen Wert initialisiert. Die Funktion wird danach (sofort, ohne Ausführung eventuell in der Funktion folgender Befehle!) verlassen. Wenn man Funktionen benötigt, die mehr als einen Rückgabewert an das rufende Programm erfordern, muß man zusätzlich noch Variablenparameter verwenden. D:\75807564.doc vom 13.10.99 3-53 BBS III Mainz OOP für IT-Berufe mit C++ Grundlegende Übungsaufgabe (PotFunk.cpp) Schreiben Sie ein Programm mit einer C++-Funktion, die die folgende mathematische Funktion berechnet, wobei das Funktionsergebnis vom Typ float ist. Der Wert von x soll als Parameter an die Funktion übergeben werden. f(x) = 3x3 + x2 + 5x - 22 Eine Zahl eingeben: 1 Das Funktionsergebnis lautet -13 Noch eine Zahl eingeben: 2 Das Funktionsergebnis lautet 16 Programmende. Verschachtelter Funktionsaufruf Das nachfolgende Programm MAXIMUM5 demonstriert, wie man typisierte Funktionen geschachtelt aufruft. Damit können komplexe Aufgabenstellungen mit kompaktem Programmcode erledigt werden: /* PROGRAMM Maximum5.cpp Geschachtelte Aufrufe einer Integerfunktion */ #include <iostream> int a,b,c; int Max(int x, int y) { if (x > y) return x; else return y; } void main() { cout << "Drei Zahlen: "; cin >> a >> b >> c; cout << "Maximum = " << Max(Max(a,b),c) << endl; cout << "Programmende."; } Drei Zahlen: 4 8 6 Maximum = 8 Programmende. D:\75807564.doc vom 13.10.99 3-54 BBS III Mainz OOP für IT-Berufe mit C++ 3.5.5 Sprachspezifische Besonderheiten bei Funktionen Die im folgenden erläuterten Eigenschaften sind typisch für die Sprache C++. Bei anderen höheren Programmiersprachen sind ähnliche Konzepte definiert. 3.5.5.1 Gültigkeitsbereich und Sichtbarkeit von Bezeichnern An der Lösung von TAUSCHE.CPP ist zu ersehen, daß es außer globalen Variablen und (formalen) Parametern auch noch echte lokale Variablen innerhalb von Funktionen gibt: void Tausch( /* ... */) { int platz; // echte lokale Variable, ist nur in Funktion Tausch gültig /* ... */ } Das wirft die Frage auf, in welchen Programmteilen Variablen und andere Bezeichner gültig oder sichtbar sind. Allgemein gilt: Beliebige Namen sind nur nach der Deklaration und innerhalb des Blocks (eingeschlossen durch { und } ) gültig, in dem sie deklariert wurden. Bezeichner im aktuellen Block verdecken Bezeichner in übergeordneten Blöcken gleichen Namens - diese sind dann im aktuellen Block unsichtbar. In diesem Fall kann man globale Variablen trotzdem ansprechen, indem man einen doppelten Doppelpunkt :: voranstellt Beispiel: /* Programm test.cpp */ include<iostream> int n; float a, b, i; // a und b sind global - überall im Programm gültig double testfunc(int x, double& y) { char buchst; /* x, y, buchst sind nur in diesem Block gültig */ y = a; // das geht! c = x; // ergibt Compilerfehler, da c hier ungültig } void main() { int c; // nur innerhalb des main()-Blocks gültig for (int i=0;i<5;i++) // i wird hier deklariert !! { // i ist nur innerhalb dieses Blockes gültig !! // Das globale i wird hier verdeckt. cout << ::i; // Damit ist das globale i trotzdem sichtbar } testfunc(n,b); } D:\75807564.doc vom 13.10.99 3-55 BBS III Mainz OOP für IT-Berufe mit C++ 3.5.5.2 Funktionsdefinition und Funktionsdeklaration Wir unterscheiden in C++ Funktionsdeklaration und Funktionsdefinition. Eine Funktionsdeklaration erfordert lediglich den Funktionsnamen, den Rückgabetyp sowie Typ und Anzahl der übergebenen Argumente - die Namen der Argumente müssen bei einer Deklaration nicht angegeben werden! Eine Funktionsdeklaration wird auch als Funktionsprototyp bezeichnet. Demgegenüber muß eine Funktionsdefinition zusätzlich noch die Parameternamen und alle Anweisungen des Funktionsrumpfes enthalten. Beispiel: a) Deklaration (Prototyp): void AddiereWas(int, int); b) Definition: void AddiereWas(int p, int q) { p += 10; q +=20; } Zusätzliche Funktionsdeklarationen werden dann benötigt, wenn die Funktionsdefinition in einer anderen Quelldatei steht als der Funktionsaufruf. Unter Umständen kann der eigentliche Funktionsrumpf bereits compiliert in einer Bibliothek vorliegen. Dann macht man in der Quelldatei, die den Funktionsaufruf enthält, dem Compiler die Funktion lediglich durch Deklaration bekannt. extern void AddiereWas(int,int); // Funktion in anderer Quelldatei definiert Bei größeren Programmprojekten ist es üblich, alle benötigten Funktionsdeklarationen in sog. Header-Dateien (*.h) aufzulisten. Die Header.Dateien werden dann in allen benötigen cpp-Quelldateien mit der Direktive #include eingebunden. 3.5.5.3 Inline-Funktionen Eine Funktion kann als inline definiert werden, z.B. mit inline void AddiereWas(int p, int q); In diesem Fall versucht der Compiler, den gesamten Objektcode der Funktion bei jedem Funktionsaufruf immer wieder in die ablauffähige Datei einzusetzen. Das Zielprogramm wird dadurch größer, der Programmablauf wird jedoch schneller. 3.5.5.4 Initialisierung von lokalen Variablen, statische lokale Variablen Eine lokale Variable wird initialisiert (mit einem Anfangswert belegt), wenn der Programmablauf die Variablendefinition erreicht. Standardmäßig passiert das bei jedem Aufruf der Funktion. Jeder Funktionsaufruf verwaltet seine eigene Kopien von Wertparametern und lokalen Variablen. Demgegenüber ist zu beachten: Lokale, nichtstatische Variablen und dynamische Datenstrukturen (auf dem sog. Heap, werden in späteren Kapiteln noch besprochen) werden nicht mit Nullwert initialisiert, wenn sie nicht definiert werden - sie bleiben dann undefiniert. Man kann eine lokale Variable jedoch auch als static deklarieren. Dann wird im gesamten Programm bei allen Funktionsaufrufen ein einziges statisches Speicherobjekt für diese Variable benutzt. In diesem Fall wird die lokale Variable nur beim erstmaligen Erreichen der Variablendefinition initialisiert. Eine statische lokale Variable erzeugt für eine Funktion ein „Gedächtnis“, ohne daß man eine globale Variable verwenden muß, die von anderen Funktionen benutzt und verfälscht werden könnte. /* Programm StatBsp.cpp Demonstration von statischen lokalen Variablen */ #include <iostream> void staticfkt(int a) { while (a--) { static int n = 0; // Variable einmal mit 0 initialisert int x = 0; // bei jedem Funktionseintritt initialisiert cout << " n == " << n++ << ", x == " << x++ << endl; D:\75807564.doc vom 13.10.99 3-56 BBS III Mainz } } OOP für IT-Berufe mit C++ int main() { staticfkt(3); } n == 0, x == 0 n == 1, x == 0 n == 2, x == 0 3.5.5.5 Konstante Wert- und Referenzparameter Es kommt öfter vor, daß bei Funktionen Parameter übergeben werden, bei denen der Ersteller der Funktion schon von vornherein weiß, daß die Parameterwerte innerhalb der Funktion nicht geändert werden dürfen. In solchen Fällen werden die Parameter als konstant deklariert. Wenn die Werte von konstanten Parametern innerhalb von Funktionen geändert werden, kann das Programm nicht kompiliert werden - der Compiler liefert eine entsprechende Fehlermeldung. char Grossbuchstabe(const char buchst); // Konstanter Wertparameter { /* ... Hier darf die Variable buchst nicht geändert werden */ } char Grossbuchstabe(const char& buchst); // Konstanter Variablenparameter { /* .. Hier gilt das Gleiche. Damit stellt man sicher, daß sowohl der formale Parameter buchst als auch der jeweilige aktuelle Parameter beim Funktionsaufruf ihren Wert behalten. } Auf den ersten Blick erscheint es sinnlos, konstante Variablenparameter zu definieren. Normalerweise ist es ja gerade die Aufgabe eines Variablenparameters, Werte geändert aus einer Funktion zurückzugeben. Man muß jedoch bedenken, daß beim Funktionsaufruf für Wertparameter grundsätzlich eine lokale Kopie der Parametervariablen erzeugt wird. Das kostet Speicherplatz und Rechenzeit. Bei Variablenparametern wird der Funktion lediglich ein Verweis auf den aktuellen Parameter übergeben (auch Zeiger oder Adresse geannt - siehe spätere Kapitel), das ist ressourcenschonender und schneller. Konstane Variablenparameter verwendet man also, wenn man Ressourcen und Rechenzeit sparen und trotzdem sicherstellen will, daß sich ein Parameter nicht ändern kann. D:\75807564.doc vom 13.10.99 3-57 BBS III Mainz OOP für IT-Berufe mit C++ Grundlegende Übungsaufgabe: Kopieren Sie Progamm Maximum3.cpp nach Maximum6.cpp. Machen Sie in der Funktion Max von Maximum6.cpp den Variablenparameter erg konstant. Kompilieren Sie das Programm und beobachten Sie die Reaktion des Compilers. Kompilieren Sie nochmals, nachdem Sie den Wertparameter x ebenfalls konstant gemacht haben und ihn innerhalb von Max inkrementiert (d.h. um 1 erhöht) haben. D:\75807564.doc vom 13.10.99 3-58 BBS III Mainz OOP für IT-Berufe mit C++ 3.5.5.6 Referenz auf den Funktionsnamen Möchte man zusätzlich noch vermeiden, daß für den Rückgabewert des Funktionsnamens zusätzlicher Speicherplatz reserviert wird, kann man auch diesen Funktionsnamen als Referenz definieren: /* Programm Minvar1.cpp liefert das Minimum von drei Werten, Demonstration einer Referenz auf den Funktionsnamen */ #include <iostream> int& Min(int& x, int& y, int& z) { if (x<y) { if (x<z) return x; } else if (y<z) return y; return z; } int main() { int a=5, b=3, c=2; cout << "Minimum von a = " << a << ", b = " << b << ", c = " << c << " : " << Min(a,b,c); } Im Beispiel Minvar1.cpp wird für den Rückgabewert im Funktionsnamen Min keine zusätzlicher Speicherplatz reserviert. Stattdessen wird als Min ein Verweis auf die Variable zurückgeliefert, die nach dem Schlüsselwort return aufgelistet ist - je nach dem Funktionsergebnis ist das einer der aktuellen Variablenparameter a, b oder c. Wenn man (wie im obigen Beispiel) genau weiß, daß innerhalb der Funktion kein Variablenwert (hier x, y oder z) geändert wird, kann man auch vollständig konstante Variablenparameter verwenden. Der Compiler meldet dann einen Fehler, wenn der Programmierer trotzdem unabsichtlich diese als konstant definierten Werte verändern will: /* Programm Minvar2.cpp liefert das Minimum von drei Werten, Demonstration einer konstanten Referenz auf den Funktionsnamen */ #include <iostream> const int& Min(const int& x, const int& y, const int& z) { if (x<y) { if (x<z) return x; } else if (y<z) return y; return z; } int main() { int a=5, b=3, c=2; cout << "Minimum von a = " << a << ", b = " << b << ", c = " << c << " : " << Min(a,b,c); } D:\75807564.doc vom 13.10.99 3-59 BBS III Mainz OOP für IT-Berufe mit C++ 3.5.5.7 Überladene Funktionsnamen Manchmal schreibt man Funktionen, die die gleiche oder ähnliche Aufgaben für verschiedene Typen ausführen sollen. Dann kann man die Funktion mehrmals mit gleichem Namen definieren. int Max(int x, int y) // Kein Variablenparameter mehr erforderlich { cout << "Das ist die int-Version: " << endl; if (x > y) return x; else return y; } float Max(float x, float y) // Kein Variablenparameter mehr erforderlich { cout << "Das ist die int-Version: " << endl; if (x > y) return x; else return y; } Die Funktionen können bei jeder einzelnen Definition eine vollkommen unterschiedliche Anzahl von Argumenten und auch unterschiedliche Typen von Argumenten und Rückgabewerten besitzen. In Wirklichkeit existieren unterschiedliche Funktionen, die lediglich den gleichen Namen tragen. Welche der einzelnen Funktionen beim Aufruf tatsächlich zur Ausführung gelangt, wird aufgrund der Typen der Aktualparameter entschieden. Bei dieser Auswahl spielt der Typ des Rückgabewertes einer Funktion keine Rolle. Grundlegende Übungsaufgabe: Kopieren Sie das Programm Maximum4.cpp nach Maximum7.cpp und fügen Sie in Maximum7.cpp zwei zusätzliche globale float-Variablen c und d ein sowie eine zweite Funktion Max wie oben beschrieben. Lassen Sie das Programm ablaufen, geben Sie ganzzahlige und reelle Werte ein und beobachten Sie die Reaktion des Programmes. D:\75807564.doc vom 13.10.99 3-60 BBS III Mainz OOP für IT-Berufe mit C++ 3.5.5.8 Default-Argumente Eine allgemein gehaltene Funktion benötigt oft mehr Parameter, als für alle Funktionsaufrufe nötig ist - einige spezielle Parameter werden nur bei ganz bestimmten Funktionsaufrufen benötigt. Möchte man nicht auf das (relativ aufwendige) Konzept des Überladens zurückgreifen, kann man für Funktionen stattdessen DefaultArgumente angeben. Ein Default-Argument wird bei der Funktionsdeklaration typgeprüft und mit dem DefaultWert initialisiert, wenn kein Aktualparameter vorhanden ist. Beispiel: /* PROGRAMM Maximum8.cpp Maximum von zwei Zahlen, Funktion mit Default-Parameter */ #include <iostream> int a,b; int Max(int x, int y =5) // Vergleich mit Default-Wert 5 { if (x > y) return x; else return y; } void main() { cout << "Zwei Zahlen: "; cin >> a >> b; cout << "Maximum = " << Max(a,b) << endl; cout << "Nur eine Zahl (Vergleich mit 5): "; cin >> a; cout << "Maximum = " << Max(a) << endl; // Zweiter Aktualparameter fehlt! cout << "Programmende."; } Zwei Zahlen: 3 8 Maximum = 8 Nur eine Zahl (Vergleich mit 5): 4 Maximum = 5 Programmende. Dieses Konzept bietet einen weiteren Vorteil bei der Programmwartung. Stellt es sich heraus, daß in einer bereits entworfenen und vielfältig eingesetzten Funktion manchmal weitere Parameter notwendig sind, fügt man diese neuen Parameter mit Default-Argumenten hinten in der Parameterliste an, so daß alle bisher getätigten Funktionsaufrufe gültig bleiben und funktionieren. D:\75807564.doc vom 13.10.99 3-61 BBS III Mainz OOP für IT-Berufe mit C++ 3.5.5.9 Schablonen (Templates) für Funktionen Die dritte (und wohl eleganteste) Möglichkeit, Funktionen mit verschiedenen Typen aufzurufen, besteht darin, eine Schablone (engl. Template) zu definieren. Eine Schablone ist eine Definition, bei der die Datentypen noch nicht festgelegt werden. Es wird ein Platzhalter eingefügt, der später durch den tatsächlich benötigten Datentyp ersetzt wird. Die Syntax zur Deklaration einer Schablone lautet: template <class TypBezeichner> Funktionsdefinition Beispiel: /* PROGRAM Maximum9.cpp Maximum von zwei Zahlen mit Funktionsschablone */ #include <iostream> char float a,b; c,d; template<class TIrgendwas> TIrgendwas Max(TIrgendwas x, TIrgendwas y) { if (x > y) return x; else return y; } void main() { cout << "Zwei Buchstaben: "; cin >> a >> b; cout << "Maximum = " << Max(a,b) << endl; cout << endl << "Zwei Dezimalzahlen: "; cin >> c >> d; cout << "Maximum = " << Max(c,d) << endl; cout << "Programmende."; } Innerhalb von main() stellt der Compiler anhand des Funktionsaufrufs fest, für welchen Datentyp die Funktion benötigt wird und bildet die Definition mit Hilfe der Schablone. Für jeden bei Aufrufen benötigten Datentyp wird vom Compiler aus der Schablone eine Funktion erzeugt. D:\75807564.doc vom 13.10.99 3-62 BBS III Mainz OOP für IT-Berufe mit C++ 3.6 Verbunde und Objekte: Strukturen und Klassen Die Datentypen, welche bislang behandelt wurden (int, float, enum, usw.), nennt man auch einfache Datentypen. Nun werden komplexere Datenstrukturen behandelt. 3.6.1 Strukturen Sehr oft benötigt man in der Datenverarbeitung Variablen unterschiedlichen Typs, die im Rahmen eines Verbundes zusammengefaßt und unter einem Namen angesprochen werden können. (z.B. Person als Zusammenfassung von Personalnummer, Name, Vorname, Postleitzahl, usw.). In C++ wird ein solcher Verbund als Struktur bezeichnet. Eine Struktur wird folgendermaßen definiert: struct Typname Felddefinition(en) Variable(nliste); Beispiel: Ein metereologisches Institut startet dreimal am Tag einen Wetterballon, der Temperatur, Luftdruck, Relative Feuchte und Windstärke mißt sowie mittels eines Sensors anzeigt, ob Regen fällt oder nicht. Die Meßwerte sollen per Programm ausgewertet werden. Dazu wird eine Struktur Tballon definiert. // Zunächst einige Typdefinitionen für die Strukturelemente von Wetterballon typedef enum { morgens=1, mittags, abends } Tzeit; // Beobachtungstermin typedef short unsigned int Tfeuchte; // Für rel. Feuchte (0-100 %) struct Tballon { int Temperatur; float Luftdruck; Tfeuchte Feuchte; bool Regen; Tzeit Tageszeit; }; Tballon Ballon; // deklariert die Variablen Ballon mit dem Typ Tballon Die einzelnen Elemente des Verbundes werden über den Punktoperator . in der Form StrukturVariablenname.Elementname angesprochen. Diese Art des Elementzugriffs nennt man Qualifizierung: Ballon.Temperatur = 20; Auf die Elementvariablen können alle Operationen angewandt werden, die auch mit gewöhnlichen Variablen möglich sind, z.B. Vergleiche mit = = oder < =, usw. D:\75807564.doc vom 13.10.99 3-63 BBS III Mainz OOP für IT-Berufe mit C++ Beispiel: Im folgenden Programm Wetter1.cpp werden alle Elemente der Struktur mit Eingabewerten gefüllt, die über die Tastatur eingegeben werden. Danach werden alle Strukturelemente auf dem Bildschirm ausgegeben. Die Elemente Regen und Tageszeit können nicht direkt ein- oder ausgegeben werden, das ist über cin und cout sinnvoll nur mit den Typen Zahlen oder Zeichen bzw. Zeichenketten möglich. Daher wird eine boolsche Eingabefunktion für „Ja“ oder „Nein“ definiert und der Typ Tageszeit wird nach int konvertiert. D:\75807564.doc vom 13.10.99 3-64 BBS III Mainz /* Programm Wetter1.cpp Demonstration einer Struktur */ #include <iostream.h> #include <ctype.h> // wegen toupper OOP für IT-Berufe mit C++ typedef int Tzeit; // Beobachtungs-Tageszeit typedef short unsigned int Tfeuchte; // Für rel. Feuchte (0-100 %) struct Tballon { int Temperatur; float Luftdruck; Tfeuchte Feuchte; bool Regen; Tzeit Tageszeit; }; Tballon Ballon; bool JNEingabe() { char antw; do { cin >> antw; antw = toupper(antw); } while (antw != 'J' && antw != 'N'); return antw=='J'; } void main() { int Tageszeitzahl; cout cout cout cout cout cout << << << << << << "Eingabe eines Datensatzes von Messwerten:\n\n"; "Temperatur in Grad Celsius: "; cin >> Ballon.Temperatur; "Luftdruck in bar: "; cin >> Ballon.Luftdruck; "Relative Feuchte in %: "; cin >> Ballon.Feuchte; "Regen (J/N): "; Ballon.Regen = JNEingabe(); "Tageszeit (1-3): "; cin >> Tageszeitzahl; Ballon.Tageszeit = Tageszeitzahl; cout << "\nAusgabe des Datensatzes:\n\n"; cout << "Temperatur: " << Ballon.Temperatur << " Grad Celsius"<<endl; cout << "Luftdruck: " << Ballon.Luftdruck << " bar" << endl; cout << "Relative Feuchte: " << Ballon.Feuchte << " %" << endl; cout << "Regen: "; if (Ballon.Regen) cout << "Ja"; else cout << "Nein"; cout << endl; cout << "Tageszeit: " << Ballon.Tageszeit << endl; cout << "\nProgrammende."; } D:\75807564.doc vom 13.10.99 3-65 BBS III Mainz Eingabe eines Datensatzes von Messwerten: Temperatur in Grad Celsius: Luftdruck in bar: Relative Feuchte in %: Regen (J/N): Tageszeit (1-3): OOP für IT-Berufe mit C++ 20 1.04 40 j 2 Ausgabe des Datensatzes: Temperatur: Luftdruck: Relative Feuchte: Regen: Tageszeit: 20 Grad Celsius 1.04 bar 40 % Ja 2 Programmende. Strukturvariablen können auch direkt bei der Typdefinition definiert werden. Gleichzeitig können die Strukturelemente initialisiert werden: struct Tballon { int Temperatur; float Luftdruck; Tfeuchte Feuchte; bool Regen; Tzeit Tageszeit; } Ballon // Definition der Variablen = {20,1.04,70,true,morgens}; // Initialisierung der Elemente 3.6.2 Funktionen zum Zugriff auf Strukturelemente Da sich die Eingabe- und Ausgabeanweisungen im obigen Programmbeispiel direkt auf die Struktur beziehen, bietet sich eine Modularisierung mittels geeigneter Funktionen an. Dabei kann man den Funktionen die Strukturvariablen als Variablenparameter übergeben und auf diese Weise den langen Strukturbezeichner (hier: Ballon) abgekürzt verwenden (hier: b). Zusätzlich wurde im nachstehenden Beispiel Wetter2.cpp eine aussagekräftigere Ausgaberoutine für die Anzeige der Tageszeit eingebaut. /* Programm Wetter2.cpp Demonstration einer Struktur mit Ein- Ausgabezugriff über Funktionen */ #include <iostream.h> #include <ctype.h> typedef int Tzeit; typedef short unsigned int Tfeuchte; struct Tballon { int Temperatur; float Luftdruck; Tfeuchte Feuchte; bool Regen; Tzeit Tageszeit; }; Tballon Ballon; bool JNEingabe() { char antw; do { cin >> antw; antw = toupper(antw); } while (antw != 'J' && antw != 'N'); return antw=='J'; } void Eingabe(Tballon& b) { int Tageszeitzahl; D:\75807564.doc vom 13.10.99 3-66 BBS III Mainz OOP für IT-Berufe mit C++ cout << "Temperatur in Grad Celsius: cout << "Luftdruck in bar: cout << "Relative Feuchte in %: cout << "Regen (J/N): cout << "Tageszeit (1-3): b.Tageszeit = Tageszeitzahl; "; "; "; "; "; cin >> b.Temperatur; cin >> b.Luftdruck; cin >> b.Feuchte; b.Regen = JNEingabe(); cin >> Tageszeitzahl; } void AusgabeTageszeit(int z) { switch (z) { case 1 : cout << "morgens"; break; case 2 : cout << "mittags"; break; case 3 : cout << "abends"; break; } } void Ausgabe(Tballon& b) { cout << "Temperatur: " << cout << "Luftdruck: " << cout << "Relative Feuchte: " << cout << "Regen: "; if (b.Regen) cout << "Ja"; else cout << "Berichtsjzeit: "; AusgabeTageszeit(b.Tageszeit); cout << endl; } b.Temperatur << " Grad Celsius" << endl; b.Luftdruck << " bar" << endl; b.Feuchte << " %" << endl; cout << "Nein"; cout << endl; void main() { cout << "Eingabe eines Datensatzes von Messwerten:\n\n"; Eingabe(Ballon); cout << "\nAusgabe des Datensatzes:\n\n"; Ausgabe(Ballon); cout << "\nProgrammende."; } D:\75807564.doc vom 13.10.99 3-67 BBS III Mainz OOP für IT-Berufe mit C++ 3.6.3 Kapselung von Struktur und Zugriffsfunktionen: Objekt Im letzten Programmbeispiel ist offensichtlich, daß die Module Eingabe(Tballon&) und Ausgabe (Tballon&) nur mit der Struktur Tballon zusammen sinnvoll eingesetzt werden können. Ebenso kann die Struktur Tballon alleine nicht sinnvoll verwendet werden, wenn keine Zugriffsfunktionen für sie existieren. Bestimmte Datentypen erfordern immer dazu passende Algorithmen und umgekehrt. Daher bietet es sich an, die Struktur Tballon zusammen mit ihren Zugriffsfunktionen vollständig zu kapseln. Auch in größeren Programmen weiß man dann stets, welche Strukturen zu welchen Zugriffsfunktionen gehören. Damit wird die Struktur Ballon zum Objekt Ballon, welches sowohl Elementdaten enthält, auch Eigenschaften genannt, als auch zugehörige Funktionsmodule. In ein Objekt eingekapselte Funktionen nennt man Methoden (auch Elementfunktionen genannt): Ein Objekt besteht aus Eigenschaften und Methoden (auch Elementdaten und Elementfunktionen genannt) /* Programm Wetter3.cpp Demonstration einer Struktur mit eingekapselten Methoden (Objekt) */ #include <iostream> #include <ctype> struct Tballon { int Temperatur; // Eigenschaften von Tballon float Luftdruck; short unsigned int Feuchte; bool Regen; int Tageszeit; // Ende der Eigenschaften bool void void void JNEingabe(); Eingabe(); AusgabeTageszeit(); Ausgabe(); // Methoden von Tballon (Deklarationen) // Ende der Methoden-Deklarationen }; bool Tballon::JNEingabe() { // Beginn der Methoden-Definitionen von Tballon char antw; // :: ist der Zugriffsbereichsoperator! do { cin >> antw; antw = toupper(antw); } while (antw != 'J' && antw != 'N'); return antw=='J'; } void Tballon::Eingabe() { int Tageszeitzahl; cout << "Temperatur in Grad Celsius: cout << "Luftdruck in bar: cout << "Relative Feuchte in %: cout << "Regen (J/N): cout << "Tageszeit (1-3): Tageszeit = Tageszeitzahl; "; "; "; "; "; cin >> Temperatur; cin >> Luftdruck; cin >> Feuchte; Regen = JNEingabe(); cin >> Tageszeitzahl; } void Tballon::AusgabeTageszeit() { switch (Tageszeit) { case morgens : cout << "morgens"; break; case mittags : cout << "mittags"; break; D:\75807564.doc vom 13.10.99 3-68 BBS III Mainz case abends } } : cout << "abends"; break; OOP für IT-Berufe mit C++ void Tballon::Ausgabe() { cout << "Temperatur: " << Temperatur << " Grad Celsius" << endl; cout << "Luftdruck: " << Luftdruck << " bar" << endl; cout << "Relative Feuchte: " << Feuchte << " %" << endl; cout << "Regen: "; if (Regen) cout << "Ja"; else cout << "Nein"; cout << endl; cout << "Berichtsjzeit: "; AusgabeTageszeit(); cout << endl; } // Ende der Methoden-Definitionen main() { Tballon Ballon; cout << "Eingabe eines Datensatzes von Messwerten:\n\n"; Ballon.Eingabe(); // Ausführung einer Methode durch Qualifizierung mit . cout << "\nAusgabe des Datensatzes:\n\n"; Ballon.Ausgabe(); cout << "\nProgrammende."; } Das Konzept der Objektorientierung ist eine Fortentwicklung der strukturierten Programmiertechnik mit prozeduralen Programmiersprachen wie C oder Pascal. Wenn man Datentypen (Eigenschaften) und Elementfunktionen (Methoden) grundsätzlich zusammenfaßt, ist das resultierende Objekt viel leichter wartbar, speziell dann, wenn man - wie bei professionellen Programmierprojekten üblich - mit vielen Programmierern an einem einzigen Projekt arbeitet. D:\75807564.doc vom 13.10.99 3-69 BBS III Mainz OOP für IT-Berufe mit C++ 3.6.4 Objektklasse und Objektinstanz An der obigen Programmlösung ist noch unschön, daß die Datenelemente des Objekts Ballon durch Algorithmen außerhalb des Objekts geändert werden können. Variieren Sie z.B. Wetter3.cpp wie folgt: main() { Tballon Ballon; cout << "Eingabe eines Datensatzes von Messwerten:\n\n"; Ballon.Eingabe(); Ballon.Temperatur := -50; // Absichtlich verändern!! cout << "\nAusgabe des Datensatzes:\n\n"; Ballon.Ausgabe(); cout << "\nProgrammende."; } Nun wird das Programm immer eine Temperatur von -50 °C anzeigen, unabhängig davon, welchen Wert Sie mit der Objektmethode Tballon.Eingabe() eingegeben haben. Eine Beeinflussung von Objektdaten durch äußere Algorithmen ist in der Regel unerwünscht. Unter vollständiger Kapselung versteht man, daß die Eigenschaften eines Objektes nur über die zugeordneten Objektmethoden geändert werden können. Betrachtet man sich die Struktur Tballon genau, dann erscheint sinnvoll, daß alle Strukturelemente Temperatur bis Tageszeit sowie die Elementfunktionen Tballon::JNEingabe() und Tballon::AusgabeTageszeit() nur innerhalb des Objektes bekannt sind, denn außerhalb werden sie nicht benötigt. Wenn sie auch außerhalb des Objektes gültig sind, können sie auch von Programmierern verwendet werden, die das Objekt Tballon nicht selbst erstellt haben. Wenn nun der „Erfinder“ von Tballon solche „öffentlich zugänglichen“ Objektmethoden irgendwann einmal ändert, hat das mit Sicherheit ungünstige Auswirkungen auf fremde Programmteile. Lediglich die Elementfunktionen Tballon:Eingabe und Tballon::Ausgabe sollen öffentlich sein. Man erreicht das gewünschte Verhalten, indem man die Schlüsselwörter class (statt struct) und public verwendet: /* Programm Wetter4.cpp Demonstration eines vollständig gekapselten Objekts */ /* ... include-Direktiven wie bei Wetter3.cpp */ class Tballon { // Deklaration einer Objektklasse Tballon int Temperatur; // Alle Felder einer class sind standardmäßig private float Luftdruck; short unsigned int Feuchte; bool Regen; int Tageszeit; bool JNEingabe(); // Zwei private-Methoden void AusgabeTageszeit(); public: void Eingabe(); // Zwei öffentliche Zugriffsmethoden void Ausgabe(); }; /* ... Hier folgen die Methodendefinitionen wie bei Wetter3.cpp ... */ Tballon Ballon; // Deklaration einer Objektinstanz Ballon. Nun kann man vom Hauptprogramm aus Ballon.Temperatur nicht mehr verändern, denn diese Variable ist außerhalb der Objektklasse Tballon unbekannt, genauso wie alle übrigen Objekteigenschaften. Auch die privaten Elementfunktionen JNEingabe() und AusgabeTageszeit() kann man von außerhalb des Objektes nicht mehr ausführen - probieren Sie es aus, indem Sie das Programm Wetter4.cpp entsprechend variieren. Merken Sie sich auch die folgende Ausdrucksweisen: Wenn man den Typ eine Objektes deklariert (mit class typ { ...), dann spricht man von einer Objektklasse. Sobald man eine Variable mit dem Typ dieser Klasse deklariert (mit Tballon Ballon), redet man von einer Objektinstanz. Üblicherweise verwendet man das Schlüsselwort class, wenn man Objektklassen definiert. Bei einer class sind standardmäßig alle Elemente private, das heißt, sie sind außerhalb der Klasse ungültig. Bei einer struct ist es genau umgekehrt: Alle Elemente sind standardmäßig public, d.h. sie sind überall im Programm bekannt und können global verwendet werden. D:\75807564.doc vom 13.10.99 3-70 BBS III Mainz OOP für IT-Berufe mit C++ Die folgenden Definitionen von class und struct haben die gleiche Bedeutung: class Tetwas { /* Eigenschaften, Methoden */ public: /* Eigenschaften, Methoden */ } D:\75807564.doc vom 13.10.99 struct Tetwas { private: /* Eigenschaften, Methoden */ public: /* Eigenschaften, Methoden */ 3-71 BBS III Mainz OOP für IT-Berufe mit C++ 4 Objektorientierte Programmierung 4.1 Prinzipien der objektorientierten Programmierung Moderne Betriebssysteme und Anwenderprogramme sind grafikorientiert und ereignisgesteuert: Auf dem Bildschirm werden ständig eine Menge von “Objekten” zur Auswahl angeboten. Mit der Tastatur angewählt oder mit der Maus angeklickt, erwacht ein solches Objekt zum “Leben” und führt bestimmte Aktionen aus danach wartet das System auf weitere Maus- oder Tastaturereignisse. Die objektorientierte Programmierung (OOP) ist aus der modernen Softwareentwicklung nicht mehr wegzudenken. Die Betrachtungsweise der OOP ist optimal an die Funktionsweise moderner Programme und Betriebssysteme angepaßt. Bereits im Kapitel “Verbunde und Objekte: Strukturen und Klassen” haben wir gesehen, wie man Daten und Funktionen (Eigenschaften und Methoden) zu einer Objektklasse zusammenfaßt. In diesem Sinne versteht man unter einem Objekt eine Zusammenfassung von Datenstrukturen (auch Elementdaten oder Eigenschaften genannt) mit zugehörigen Programmodulen (auch Methoden oder Elementfunktionen genannt.). Die OOP verfährt nach drei Basisprinzipien: Kapselung Alle Daten und Methoden eines Objektes gehören fest zur jeweiligen Objektklasse. Daten und Methoden sind außerhalb von Klassen nicht definiert, sie können von außen nur über fest definierte Schnittstellen angesprochen werden. Daten und zugehöriger Programmcode einer Objektklasse sind gekapselt. Vererbung Eine Objektklasse ist in der Lage, eine Reihe bestimmter Programmanforderungen zu erfüllen. Kommen zusätzliche Anforderungen hinzu, wird eine neue Klasse als Nachkomme der alten Objektklasse erzeugt. Die neue Klasse erbt zunächst alle Eigenschaften und Methoden des Vorfahren. Sie kann erweitert und abgewandelt werden, indem neue Eigenschaften und Methoden hinzugefügt bzw. alte Elementdaten und Elementfunktionen überladen werden. Polymorphie Eine einmal definierte Methode ist für alle Nachkommen einer Objektklasse gültig. Methoden mit gleichem Namen können jedoch so implementiert werden, daß sie in verschiedenen Nachfolgeklassen unterschiedliche Aktionen auslösen. Diese Eigenschaft der Vielgestaltigkeit von Objektklassen bezeichnet man mit dem (griechischen) Ausdruck Polymorphie. Die Realisierung der drei Basisprinzipien der OOP in der Programmiersprache C++ wird nun näher untersucht. D:\75807564.doc vom 13.10.99 4-72 BBS III Mainz OOP für IT-Berufe mit C++ 4.2 Das OOP-Prinzip Kapselung 4.2.1 Klassen Jedes Objekt, auch in der realen Welt, ist mit bestimmten Eigenschaften und Verhaltensweisen versehen. Einzelne Objekte können zu (Objekt-)klassen zusammengefaßt werden. Diese wiederum können in Klassenhierarchien zueinander stehen und Eigenschaften voneinander übernehmen. Ein Beispiel: Das Urtier hatte eine Länge, Höhe und Breite und konnte fressen und laufen. Die von ihm abstammende Klasse der Flugsaurier konnte zusätzlich noch fliegen, während die Klasse der Schwimmsaurier schwimmen konnte, aber ansonsten die Eigenschaften und Fähigkeiten der Klasse Urtier vererbt bekam. Eine Klasse definiert eine Kategorie von Objekten, hat aber keinen Repräsentanten in der Realität. Jedes real existierende Objekt ist eine Instanz einer Klasse und hat daher die gleiche Beziehung zu einer Klasse wie eine Variable zu einem Datentyp. In C++ bezeichnet man die Eigenschaften als die Datenelemente der Klasse und die Fähigkeiten als Elementfunktionen oder Methoden der Klasse. Methoden können den Zustand eines Objekts durch Manipulation seiner Datenelemente verändern. // Programm Klasse1.cpp #include <iostream.h> #include <conio.h> class miniobjekte { private: int daten; public: void setzedaten(int d) { daten = d; } void zeigedaten() { cout << "\nDie Daten sind " << daten;} }; void main() { miniobjekte s1, s2; s1.setzedaten(1066); s2.setzedaten(1776); s1.zeigedaten(); s2.zeigedaten(); getch(); } Die Klasse Miniobjekte dieses Programms enthält ein Datenelement und zwei Methoden. Diese Funktionen bieten die einzige Möglichkeit, auf die Daten des Objekts zuzugreifen. Die Definition einer Klasse beginnt mit dem Schlüsselwort class, gefolgt vom jeweiligen Klassennamen. Wie bei einer Struktur ist der Rumpf einer Klasse von geschweiften Klammern eingeschlossen und wird mit einem Semikolon beendet. Der Rumpf der Klasse enthält zwei bisher unbekannte Schlüsselworte, private und public. Diese definieren Schutzmechnismen für die Daten einer Klasse. Auf private Daten oder Funktionen kann nur innerhalb der Klasse zugegriffen werden. Auf durch public gekennzeichnete Daten oder Funktionen kann man auch von außerhalb der Klasse zugreifen. Normalerweise sind Daten in einer Klasse private und Funktionen public, damit die Daten geschützt sind und die einmal definierten Funktionen mehrfach verwendet werden können. Wie man sieht, erfolgt der Zugriff auf Methoden analog zum Zugriff auf Elemente einer Struktur. Hier ein weiteres Beispiel mit mehr Datenelementen und Methoden, die auf die Datenelemente zugreifen. //Programm Klasse2.cpp #include <iostream.h> D:\75807564.doc vom 13.10.99 4-73 BBS III Mainz #include <conio.h> class teile { private: int modellnr; int teilnr; float kosten; OOP für IT-Berufe mit C++ public: void setzeteile(int mn, int tn, float k) { modellnr = mn; teilnr = tn; kosten = k; } void kostenaendern() { cout <<"\nDer Preis hat sich veraendert; geben Sie den neuen Preis ein: "; cin >> kosten; } void zeigeteile() { cout << "\nModell " << modellnr; cout << ", Teil " << teilnr; cout << ", Kosten " << kosten << " DM" ; } }; void main() { teile teil1; teil1.setzeteile(4711, 13,245.34); teil1.zeigeteile(); teil1.kostenaendern(); teil1.zeigeteile(); getch(); } 4.2.2 Methoden Eine Methode (oder auch Elementfunktion genannt) ist eine Funktion, die in eine Klassendefinition eingebettet ist. Sie wird im im Kontext einer Klasse aufgerufen und kann auf die Daten ihrer Klasse zugreifen. //Programm Englobj.cpp //Feet and inches #include <iostream.h> #include <conio.h> class laenge { private: int feet; float inches; public: void setzelaenge(int ft, float in) { feet = ft; inches = in; } void holelaenge() { cout << "\nBitte Feet eingeben: "; cin >> feet; cout << "\nBitte Inches eingeben: ";cin >> inches; } void zeigelaenge() //Länge ausgeben { cout << feet << "\'-" << inches << '\"'; } }; void main() { D:\75807564.doc vom 13.10.99 4-74 BBS III Mainz laenge l1,l2; l1.setzelaenge(14, 8.25); OOP für IT-Berufe mit C++ l2.holelaenge(); cout << "\nLaenge1 = "; l1.zeigelaenge(); cout << "\nLaenge2 = "; l2.zeigelaenge(); getch(); } Die Methoden setzelaenge(int ft, float in), holelaenge() und zeigelaenge() sind alle in der Klasse laenge deklariert und greifen auf die Datenelemente feet und inches zu. 4.2.3 Der Bereichsoperator :: In dem obigen Beispielprogramm ENGLOBJ.CPP sind alle Funktionen innnerhalb der Klasse definiert worden. Man kann jedoch auch Funktionen innerhalb der Klasse deklarieren und ausserhalb definieren. //Programm Englobj2.cpp //Feet and inches #include <iostream.h> #include <conio.h> class laenge { private: int feet; float inches; public: void setzelaenge(int ft, float in) { feet = ft; inches = in; } void holelaenge(); void zeigelaenge() //Länge ausgeben { cout << feet << "\'-" << inches << '\"'; } }; void laenge::holelaenge() { cout << "\nBitte Feet eingeben: "; cin >> feet; cout << "\nBitte Inches eingeben: ";cin >> inches; } void main() { laenge l1,l2; l1.setzelaenge(14, 8.25); l2.holelaenge(); cout << "\nLaenge1 = "; l1.zeigelaenge(); cout << "\nLaenge2 = "; l2.zeigelaenge(); getch(); } Der Funktion holelaenge() geht hier der Klassenname laenge voraus und das neue Symbol "::". Dieser sogenannte Bereichsoperator gibt an, zu welcher Klasse ein Funktion gehört. In diesem Beispiel bedeutet laenge::holelaenge(), dass die Funktion holelaenge() aus der Klasse laenge definiert werden soll. Diese Art der getrennten Deklaration und Definition verwendet man, wenn die Definition der Funktion umfangreich ist und dadurch die Übersichtlichkeit der Deklaration der Klasse leiden würde. D:\75807564.doc vom 13.10.99 4-75 BBS III Mainz OOP für IT-Berufe mit C++ 4.2.4 Konstruktoren 4.2.4.1 Standardkonstruktor Konstruktoren werden zur Initialisierung von Objekten benötigt. Wenn kein Konstruktor angegeben ist, wird das Objekt automatisch initialisiert, wie in den Beispielen Klasse1.cpp und Klasse2.cpp. In Klasse1.cpp wird durch miniobjekte s1, s2; erreicht, dass zwei Objekte des Typs miniobjekte erstellt werden. Dies geschieht durch den automatischen Aufruf des Konstruktors miniobjekte(). Ein weiteres Beispiel für Konstruktoren liefert Zaehler1.cpp: //Programm Zaehler1.cpp #include <iostream.h> #include <conio.h> class Zaehler { private: unsigned int count; public: Zaehler() { count = 0; } void inc_count() { count++; } int get_count() { return count; } }; void main() { Zaehler c1,c2; cout << "\nc1=" << c1.get_count(); cout << "\nc2=" << c2.get_count(); c1.inc_count(); c2.inc_count(); c2.inc_count(); cout << "\nc1=" << c1.get_count(); cout << "\nc2=" << c2.get_count(); getch(); } Hier ist die Funktion Zaehler() zum einen dafür verantwortlich, dass die beiden Objekte c1 und c2 initialisiert werden, zum anderen werden diese beiden auch gleich auf Null gesetzt. Die allgemeine Syntax für Konstruktoren lautet: class className { public: className(); //Standardkonstruktor className(<Parameterliste>); //anderer Konstruktor className(const className& c); //Kopierkonstruktor }; Einige Regeln für Konstruktoren: 1. Sie haben denselben Namen wie die Klasse, in der sie als Methode definiert sind. 2. Es gibt für Konstruktoren keine Rückgabewerte (auch nicht void), da sie automatisch vom System aufgerufen werden. 3. Eine Klasse kann eine beliebige Anzahl von Konstruktoren (also auch keinen) haben. 4. Standardkonstruktor ist derjenige Konstruktor, der entweder über keine Parameter verfügt oder in dessen Parameterliste für alle Parameter Vorgabeargumente verwendet werden. D:\75807564.doc vom 13.10.99 4-76 BBS III Mainz OOP für IT-Berufe mit C++ Ein Beispiel für 4): class punkt { private: double x,y; public: punkt(double xwert = 0, double ywert=0); }; 4.2.4.2 Überladener Konstruktor Wir betrachten das obige Beispiel Englobj.cpp. Es wäre schön, wenn wir einer Länge bereits beim Programmstart einen Wert zuordnen könnten, z.B. laenge weite(4, 3.25). Hier wird ein Objekt weite definiert und gleichzeitig seine Werte auf 4 für feet und 3.25 für inches gesetzt. Dies geschieht mit einem Konstruktor wie laenge(int ft, float in) { feet = ft; inches = in;} //Programm Englcon.cpp //Feet and inches #include <iostream.h> #include <conio.h> class laenge { private: int feet; float inches; public: laenge() {} //Konstruktor ohne Argument laenge(int ft, float in) //Konstruktor mit 2 Argumenten { feet = ft; inches = in; } void holelaenge() //Benutzer gibt Daten ein { cout << "\nBitte Feet eingeben: "; cin >> feet; cout << "\nBitte Inches eingeben: ";cin >> inches; } void zeigelaenge() //Länge ausgeben { cout << feet << "\'-" << inches << '\"'; } void add_laenge( laenge, laenge ); //Deklaration der Methode }; void laenge::add_laenge( laenge l2, laenge l3 ) //Definition { inches = l2.inches + l3.inches; feet = 0; if (inches >= 12.0) { inches-= 12.0; feet++; } feet += l2.feet + l3.feet; } void main() { laenge l1,l3; laenge l2(11, 6.25); l1.holelaenge(); D:\75807564.doc vom 13.10.99 4-77 BBS III Mainz l3.add_laenge(l1,l2); OOP für IT-Berufe mit C++ cout << "\nLaenge1 = "; l1.zeigelaenge(); cout << "\nLaenge2 = "; l2.zeigelaenge(); cout << "\nLaenge3 = "; l3.zeigelaenge(); getch(); } In diesem Programm werden verschiedene Elemente implementiert: Zum einen mehrere Konstruktoren, der Bereichsoperator und Objekte als Funktionsparameter. Die Längen l1 und l3 sind mit dem Konstruktor ohne Argumente erstellt worden, der sie auch nicht initialisiert. Die Länge l2 wiederum ist mit dem 2-ArgumentKonstruktor erstellt worden und wird mit den Werten initialisiert, die als Argumente übergeben werden. Mit l3.add_laenge(l1,l2); werden die beiden Längen addiert, wobei l1 und l2 Objekte sind. Die Syntax dafür ist dieselbe wie bei anderen einfachen Datentypen wie Integer. Da add_laenge eine Methode von laenge ist, kann sie auf die privaten Daten aller Objekte der Klasse laenge zugreifen. Da es jetzt mehr als einen Konstruktor in einer Klasse gibt, nennt man dies auch Überladung von Konstruktoren. Man kann beliebig viele überladenene Konstruktoren in einer Klasse deklarieren. Wenn man einen überladenen Konstruktor deklariert, muß man den ansonsten vom System bereitgestellten Standardkonstruktor ebenfalls deklarieren, z.B. laenge() {}. (Ohne die leeren geschweiften Klammern wäre dies eine Deklaration und die Definition müßte nachgeholt werden.) Wenn man mehrere Konstruktoren in einer Klasse deklariert hat, entscheidet das Programm zur Laufzeit anhand der übergebenen Argumente, welcher Konstruktor verwendet wird. 4.2.4.3 Kopierkonstruktor Man kann ein Objekt definieren und ihm zugleich die Werte eines anderen Objekts zuweisen: alpha a3(a2); alpha a3 = a2; Beide Schreibweisen rufen einen Kopierkonstruktor auf, d.h. einen Konstruktor, der seine Argumente in das neue Objekt kopiert. Der Standard-Kopierkonstruktor, den das System bereitstellt, kopiert alle Datenelemente in das neue Objekt. 4.2.5 Destruktoren Ähnlich wie Konstruktoren werden Destruktoren automatisch aufgerufen, wenn ein Objekt verschwinden soll. Ein Destruktor hat denselben Namen wie der Konstruktor (wie auch die Klasse), aber mit vorangestelltem Tilde-Zeichen. Bsp.: Class Abc { private: int daten; public: Abc() { daten = 0; } ~Abc() {} } Wie die Konstruktoren haben auch die Destruktoren keinen Rückgabewert. Außerdem ist ihre Parameterliste leer. Das Haupteinsatzgebiet von Destruktoren ist die Freigabe von Speicher, der für Objekte durch den Konstruktor beim Anlegen des Objektes oder andere Methoden während der Laufzeit reserviert wurde. Dazu sind jedoch Vorkenntnisse über Zeiger auf Objekte und dynamische Variablen notwendig. D:\75807564.doc vom 13.10.99 4-78 BBS III Mainz OOP für IT-Berufe mit C++ 4.2.6 Objekte als Rückgabewert von Funktionen //Programm Englret.cpp //Feet and inches #include <iostream.h> #include <conio.h> class laenge { private: int feet; float inches; public: laenge() {} //Konstruktor ohne Argument laenge(int ft, float in) //Konstruktor mit 2 Argumenten { feet = ft; inches = in; } void holelaenge() //Benutzer gibt Daten ein { cout << "\nBitte Feet eingeben: "; cin >> feet; cout << "\nBitte Inches eingeben: ";cin >> inches; } void zeigelaenge() //Länge ausgeben { cout << feet << "\'-" << inches << '\"'; } laenge add_laenge( laenge ); //Deklaration der Methode }; laenge laenge::add_laenge( laenge l2 ) //Definition der Methode { laenge temp; temp.inches = inches + l2.inches; if (temp.inches >= 12.0) { temp.inches-= 12.0; temp.feet=1; } temp.feet += feet + l2.feet; return temp; } void main() { laenge l1,l3; laenge l2(13, 4.25); l1.holelaenge(); l3 = l1.add_laenge(l2); cout << "\nLaenge1 = "; l1.zeigelaenge(); cout << "\nLaenge2 = "; l2.zeigelaenge(); cout << "\nLaenge3 = "; l3.zeigelaenge(); getch(); } Die Längen l1 und l3 werden mit dem Konstruktor ohne Argumente erstellt und daher nicht initialisiert. l2 wiederum ist mit dem Konstruktor mit 2 Argumenten erstellt worden und wird daher mit Werten initialisiert. Mit l3 = l1.add_laenge(l2); werden l1 und l2 addiert und das Ergebnis l3 zugewiesen. Der Objektname l2 wird der Funktion add_laenge als Argument übergeben. Das Resultat der Addition wird im Objekt temp gespeichert und dann an das Hauptprogramm mittels return temp übergeben. D:\75807564.doc vom 13.10.99 4-79 BBS III Mainz OOP für IT-Berufe mit C++ 4.2.7 Klassen, Objekte und Speicher Bisher konnte man den Eindruck gewinnen, dass jedes Objekt, das aus einer Klasse entstanden ist, eigene Kopien der Daten der Klasse und der Methoden besitzt. Die Analogie dazu ist ein Modell eines Autos und die realen Autos, die vom Band rollen. Dies ist jedoch nicht der Fall: Es ist richtig, dass jedes Objekt seine eigenen Daten beinhaltet, jedoch benutzen alle Objekte einer Klasse dieselben Methoden, da diese für jedes Objekt gleich sind, während die Daten verschieden sind. Die Daten werden daher mehrfach in den Speicher geladen, die Methoden nur einmal. Ausnahme: Wenn ein Datenelement einer Klasse als static definiert wurde, wird es für die gesamte Klasse nur einmal kreiert, egal, wieviele Instanzen der Klasse es gibt. Ein statisches Datenelement ist sinnnvoll, wenn alle Instanzen einer Klasse ein gemeinsames Element besitzen sollen. Ein Beispiel dafür ist ein Autorennen, bei dem ein Fahrer eines Autos wissen möchte, wieviele weitere Fahrer sich noch im Rennen befinden. In diesem Fall könnte man eine Variable zaehler einführen, auf die alle Objekte der Klasse zugreifen könnten und die für alle denselben Wert liefert. 4.2.8 Operator-Überladung Dies ist ein Feature, das dem C++-Programmierer das Leben erleichtern kann: Statt d3.addobj(d1,d2) kann man auch d3 = d1+d2 schreiben, wenn der Operator "+" entsprechend definiert ist. Stellen Sie sich vor, bei d1 und d2 handelt es sich um Matrizen, bei denen man “normalerweise” mit verschachtelten Schleifen arbeiten muss. Die allgemeine Syntax ist: ReturnDatentyp operator (Argumentliste) {Funktionscode} steht für eines der möglichen C++-Operatorsymbole. Ein selbstdefinierte Operator ist eine Funktion in einer syntaktisch anderen Verkleidung. Die richtige Bedeutung des Operators ermittelt der Compiler aus den Datentypen der Operanden. Der Funktionsname einer Operatorfunktion besteht aus dem Schlüsselwort operator und dem angehängten Operatorzeichen. Der Inkrement-Operator ++ kann sowohl in der Präfix- als auch in der Postfix-Notation auftreten. Der ++Operator in der Postfix-Notation (count++(int)) hätte ein zusätzliches Argument, das normalerweise mit 0 belegt wird und nicht verwendet wird. Wir verändern das Beispiel des Zählerprogramms so, dass nicht mehr c1.inc_count() aufgerufen wird, sondern man einfach ++c1 schreiben kann. //Programm zaehler2.cpp //Operator-Überladung #include <iostream.h> #include <conio.h> class Zaehler { private: unsigned int count; public: Zaehler() { count = 0; } int get_count() { return count; } void operator ++ () { ++count; } }; void main() { Zaehler c1,c2; cout << "\nc1=" << c1.get_count(); cout << "\nc2=" << c2.get_count(); ++c1; ++c2; D:\75807564.doc vom 13.10.99 4-80 BBS III Mainz ++c2; OOP für IT-Berufe mit C++ cout << "\nc1=" << c1.get_count(); cout << "\nc2=" << c2.get_count(); getch(); } Das Überladen des ++-Operators hat jedoch auch Nachteile, zum Beispiel geht c2=++c1; nicht, da die Operator-Funktion ++ keinen Rückgabewert hat. Überladen von Operatoren hat man bei Java übrigens nicht implementiert. Nun folgt ein längeres Beispiel über die Verwendung von Klassen und Methoden, das Ihnen zum einen einen Einblick über solche Porgramme geben soll, zum anderen auch als Anschauungsmaterial für die folgenden Übungen dienen soll. Mit dem Programm Airport1.cpp können Sie Befehle an drei Flugzeuge richten. Airplane.h enthält die HeaderDatei und danach folgt die Programmdatei. //Header-Datei airplane.h fuer das Airport-Programm //--------------------------------------------------------------------------#ifndef airplaneH #define airplaneH //--------------------------------------------------------------------------#define AIRLINER 0 #define COMMUTER 1 #define PRIVATE 2 #define TAKINGOFF 0 #define CRUISING 1 #define LANDING 2 #define ONRAMP 3 #define MSG_CHANGE 0 #define MSG_TAKEOFF 1 #define MSG_LAND 2 #define MSG_REPORT 3 class Airplane { public: Airplane(const char* _name, int _type = AIRLINER); ~Airplane(); virtual int GetStatus(char* statusString); int GetStatus() { return status; } int Speed() { return speed; } int Heading() { return heading; } int Altitude() { return altitude; } void ReportStatus(); bool SendMessage(int msg, char* response, int spd = -1, int dir = -1, int alt = -1); char* name; protected: virtual void TakeOff(int dir); virtual void Land(); private: int speed; int altitude; int heading; D:\75807564.doc vom 13.10.99 4-81 BBS III Mainz int status; int type; int ceiling; }; #endif OOP für IT-Berufe mit C++ //Programm Airport1.cpp #include <iostream.h> #include <conio.h> #include <stdio.h> #include "airplane.h" #pragma hdrstop // Constructor performs initialization // Airplane::Airplane(const char* _name, int _type) : type(_type), status(ONRAMP), speed(0), altitude(0), heading(0) { switch (type) { case AIRLINER : ceiling = 35000; break; case COMMUTER : ceiling = 20000; break; case PRIVATE : ceiling = 8000; } name = new char[50]; strcpy(name, _name); } // // Destructor performs cleanup. // Airplane::~Airplane() { delete[] name; } // // Gets a message from the user. // bool Airplane::SendMessage(int msg, char* response, int spd, int dir, int alt) { // // Check for bad commands. // if (spd > 500) { strcpy(response, "Speed cannot be more than 500."); return false; } if (dir > 360) { strcpy(response, "Heading cannot be over 360 degrees."); return false; } if (alt < 100 && alt != -1) { strcpy(response, "I'd crash, bonehead!"); return false; } if (alt > ceiling) { strcpy(response, "I can't go that high."); return false; } // // Do something base on which command was sent. // switch (msg) { case MSG_TAKEOFF : { D:\75807564.doc vom 13.10.99 4-82 BBS III Mainz OOP für IT-Berufe mit C++ // Can't take off if already in the air! if (status != ONRAMP) { strcpy(response, "I'm already in the air!"); return false; } TakeOff(dir); break; } case MSG_CHANGE : { // Can't change anything if on the ground. if (status == ONRAMP) { strcpy(response, "I'm on the ground"); return false; } // Only change if a non-negative value was passed. if (spd != -1) speed = spd; if (dir != -1) heading = dir; if (alt != -1) altitude = alt; status == CRUISING; break; } case MSG_LAND : { if (status == ONRAMP) { strcpy(response, "I'm already on the ground."); return false; } Land(); break; } case MSG_REPORT : ReportStatus(); } // // Standard reponse if all went well. // strcpy(response, "Roger."); return true; } // // Perform takeoff. // void Airplane::TakeOff(int dir) { heading = dir; status = TAKINGOFF; } // // Perform landing. // void Airplane::Land() { speed = heading = altitude = 0; status == ONRAMP; } // // Build a string to report the airplane's status. // int Airplane::GetStatus(char* statusString) { sprintf(statusString, "%s, Altitude: %d, Heading: %d, " "Speed: %d\n", name, altitude, heading, speed); return status; } // // Get the status string and output it to the screen. // void Airplane::ReportStatus() D:\75807564.doc vom 13.10.99 4-83 BBS III Mainz { char buff[100]; GetStatus(buff); cout << endl << buff << endl; } OOP für IT-Berufe mit C++ int getInput(int max); void getItems(int& speed, int& dir, int& alt); void main() { char returnMsg[100]; // // Set up an array of Airplanes and create // three Airplane objects. // Airplane* planes[3]; planes[0] = new Airplane("TWA 1040"); planes[1] = new Airplane("United Express 749", COMMUTER); planes[2] = new Airplane("Cessna 3238T", PRIVATE); // // Start the loop. // do { int plane, message, speed, altitude, direction; speed = altitude = direction = -1; // // Get a plane to whom a message will be sent. // List all of the planes and let the user pick one. // cout << endl << "Who do you want to send a message to?"; cout << endl << endl << "0. Quit" << endl; for (int i=0;i<3;i++) cout << (i + 1) << ". " << planes[i]->name << endl; // // Call the getInput() function to get the plane number. // plane = getInput(4); // // If the user chose item 0 then break out of the loop. // if (plane == -1) break; // // The plane acknowledges. // cout << endl << planes[plane]->name << ", roger."; cout << endl << endl; // // Allow the user to choose a message to send. // cout << "What message do you want to send?" << endl; cout << endl << "0. Quit" << endl;; cout << "1. State Change" << endl; cout << "2. Take Off" << endl; cout << "3. Land" << endl; cout << "4. Report Status" << endl; message = getInput(5); // // Break out of the loop if the user chose 0. // if (message == -1) break; // // If the user chose item 1 then we need to get input // for the new speed, direction, and altitude. Call // the getItems() function to do that. // if (message == 0) getItems(speed, direction, altitude); // // Send the plane the message. D:\75807564.doc vom 13.10.99 4-84 BBS III Mainz OOP für IT-Berufe mit C++ // bool goodMsg = planes[plane]->SendMessage( message, returnMsg, speed, direction, altitude); // // Something was wrong with the message // if (!goodMsg) cout << endl << "Unable to comply."; // // Display the plane's response. // cout << endl << returnMsg << endl; } while (1); // // Delete the Airplaine objects. // for (int i=0;i<3;i++) delete planes[i]; } int getInput(int max) { int choice; do { choice = getch(); choice -= 49; } while (choice < -1 || choice > max); return choice; } void getItems(int& speed, int& dir, int& alt) { cout << endl << "Enter new speed: "; getch(); cin >> speed; cout << "Enter new heading: "; cin >> dir; cout << "Enter new altitude: "; cin >> alt; cout << endl; } //--------------------------------------------------------------------------- Im Header finden Sie am Anfang der Datei #define-Anweisungen, mit denen Text (nach Konvention in Grossbuchstaben) durch Ziffern ersetzt wird. Danach erfolgt die Deklaration der Airplane-Klasse. In dieser gibt es eine überladene Funktion, GetStatus(); wird sie mit einem Zeiger auf ein char-Array als Argument aufgerufen, liefert sie eine Statusmeldung und den Wert des Datenelements status zurück, wird sie ohne Argument aufgerufen, wird nur das Datenelement status zurückgeliefert. Das Programm Airport1.cpp beginnt mit der Definition der Klasse Airplane. Der Konstruktor führt Initialisierungen durch und reserviert Speicherplatz für das char-Array, das den Namen des Flugzeugs enthält. Dieser Speicherplatz wird später vom Destruktor wieder freigegeben. In der Funktion SendMessage() überprüft eine switch-Anweisung die eingegangene Meldung und sorgt für eine entsprechende Reaktion. Im Hauptteil des Programms richtet das Programm ein Array von Airplane-Zeigern ein und bildet drei Instanzen der AirplaneKlasse. Danach beginnt eine Schleife, in der Sie den einzelnen Flugzeugen Nachrichten schicken können. Die while-Schleife wird endlos ausgeführt, kann jedoch bei Eingabe einer "0" mit break beendet werden. 4.2.9 Übungsaufgaben: 1. (Intclass.cpp) Erstellen Sie eine Klasse, die einen Teil der Funktionalität des Datentyps Integer imitiert und nennen Sie diese Int. Das einzigen Datenelement dieser Klasse ist eine Integervariable. Desweiteren sollen Elementfunktionen enthalten sein, die ein Objekt mit 0 initialisieren, die ein Objekt mit einem int-Wert initialisieren und die die Variable ausgeben. Außerdem sollen 2 Int-Objekte addiert werden. Schreiben Sie ein Programm Intclass.cpp, das diese Klasse ausführt, indem Sie zwei initialisierte und ein nichtinitialisiertes D:\75807564.doc vom 13.10.99 4-85 BBS III Mainz OOP für IT-Berufe mit C++ Int-Objekt erstellen, die beiden initialisierten addieren, das Ergebnis dem nichtinitialiserten zuweisen und dann ausgeben. 2. (Maut.cpp) Stellen Sie sich eine Mautstelle an einer Brücke vor. Jedes Fahrzeug kostet einen Euro, jedoch bezahlen nicht alle Fahrer. Eine elektronische Kasse zählt sowohl die Fahrzeuge als auch die eingenommene Geldmenge. Simulieren Sie diese Mautstelle mit einer Klasse Mautkasse. Die beiden Datenfelder sind vom Typ unsigned int für die Anzahl der Fahrzeuge und vom Typ double für die Geldmenge. Ein Konstruktor soll beide Datenfelder mit 0 initialiseren. Eine Methode Bezfahrzeug() erhöht die Anzahl der Fahrzeuge und addiert einen Euro zur Gesamteinnahme. Eine weitere Methode, Nbezfahrzeug() erhöht zwar die Anzahl der Fahrzeuge, nicht jedoch die Gesamteinnahme. Schließlich soll eine Methode Anzeige() die beiden Resultate ausgeben. Für das Hauptprogramm Maut.cpp lassen Sie den Benutzer eine Taste drücken für ein zahlendes Fahrzeug und eine andere für ein nichtbezahlendes Fahrzeug. Mit ESC gibt das Programm die Fahrzeuganzahl und die Gesamteinnahme aus und terminiert. D:\75807564.doc vom 13.10.99 4-86 BBS III Mainz OOP für IT-Berufe mit C++ 4.3 Vererbung 4.3.1 Das Prinzip der Vererbung Vererbung ist neben den Klassen das herausragende Merkmal der Objekt-orientierten Programmierung. Vererbung ist der Prozess der Entstehung neuer Klassen (abgeleiteter Klassen) aus bereits bestehenden Klassen (Oberklassen). Oberklasse Eigenschaft 1 Eigenschaft 2 Fähigkeit 1 Fähigkeit 2 Abgeleitete Klasse Eigenschaft 1 Eigenschaft 2 Eigenschaft 3 Fähigkeit 1 Fähigkeit 2 Fähigkeit 3 Der Pfeil bedeutet "ist abgeleitet von". Die abgeleitete Klasse erbt die Eigenschaften und Fähigkeiten der Oberklasse. Beachten Sie, daß der Pfeil entgegen der Vererbungsrichtung und damit entgegen der zeitlichen Entstehungsgeschichte der Klassen weist. Die oberste Klasse wird auch Basisklasse genannt. D:\75807564.doc vom 13.10.99 4-87 BBS III Mainz OOP für IT-Berufe mit C++ Beispiel: Publikationen haben die Eigenschaften Titel und Preis, die in der Methode holedaten() und zeigedaten() eingelesen und ausgegeben werden. Die Klasse Publikationen vererbt diese Eigenschaften weiter, wenn das Zugriffsrecht public gesetzt wird. Nachkommen der Klasse Publikationen könnten die abgeleiteten Klassen (=Nachkommen) Buch und CD sein. Buch und CD erben zunächst alle Eigenschaften und Methoden von Publikationen, wenn das Zugriffsrecht public gesetzt wird. Auch können Buch und CD erweitert und abgewandelt werden (bei Buch werden die Anzahl Seiten zusätzlich abgefragt/ bei CD wird die Spielzeit abgefragt). Buch erhält z.B. die neue Eigenschaft seitenzahl und CD die neue Eigenschaft spielzeit. Beide Eigenschaften werden in der gleichnamigen Methode holedaten() und zeigedaten() eingelesen und ausgegeben (=Erweiterung der gleichnamigen Methoden aus der Basisklasse Publikationen). Das Vererbungskonzept unterstützt die Wiederverwendbarkeit von Programmcode. Wenn eine Basisklasse entworfen und getestet wurde, kann sie in verschiedenen Situationen benutzt werden, ohne daß weitere Tests notwendig wären. Außerdem hilft das Vererbungskonzept beim Design eines komplexen Programms. Ein Resultat der Vererbung ist die Verbreitung von Klassenbibliotheken, die von anderen Programmierern oder Firmen erstellt wurden. D:\75807564.doc vom 13.10.99 4-88 BBS III Mainz OOP für IT-Berufe mit C++ // Programm Verlag1.cpp //Vererbung von Klassen und Übernahme von Methoden aus der //Basisklasse //Fachjargon: Abgeleitete Klassen und Überschreiben von Methoden #include <iostream.h> #include <conio.h> const int LEN = 80; class Publikation { private: char titel[LEN]; float preis; public: void holedaten() { cout << "\nBitte geben Sie den Titel ein: "; cin >> titel; cout << "\nBitte geben Sie den Preis ein: "; cin >> preis; } void zeigedaten() { cout << "\nDer Titel ist: "; cout << titel; cout << "\nDer Preis ist: "; cout << preis; } }; Die Klasse Buch wird von der Klasse Publikation abgeleitet class Buch : public Publikation { private: int seitenzahl; public: Definition der Methode „holedaten()“ void holedaten() der abgeleiteten Klasse Buch { Publikation::holedaten(); Die Methode „holedaten“ der Klasse Publikation wird aufgerufen Man sagt: DIE METHODE „holedaten() von Buch“ überschreibt die METHODE „holedaten() von Publikation“. cout << "\nBitte geben Sie die Anzahl der Buchseiten ein: "; cin >> seitenzahl; } void zeigedaten() { Publikation::zeigedaten(); cout << "\nDie Anzahl der Seiten beträgt: " << seitenzahl; } }; D:\75807564.doc vom 13.10.99 4-89 BBS III Mainz OOP für IT-Berufe mit C++ class CD : public Publikation { private: float spielzeit; 1 public: 2 void holedaten() { Publikation::holedaten(); cout << "\nBitte geben Sie die Spielzeit der CD ein: "; cin >> spielzeit; } void zeigedaten() { Publikation::zeigedaten(); 3 cout << "\nDie Spielzeit der CD beträgt: " << spielzeit; } }; void main() { Buch buch1; CD cd1; buch1.holedaten(); cd1.holedaten(); buch1.zeigedaten(); cd1.zeigedaten(); Vererbung von Zugriffsrechten class Buch : public Publikation Zugriff auf Methoden und Daten öffnen Public bedeutet, dass Funktionen und Daten der Oberklasse, die als public gekennzeichnet sind, auch als Funktionen und Daten der abgeleiteten Klasse zur Verfügung stehen und von ihren Objektinstanzen benutzt werden können. Zugriff auf Methoden und Daten sperren getch(); } Wenn anstelle von public private verwendet würde, können die Objektinstanzen der abgeleiteten Klasse nicht auf public-Funktionen und Daten der Basisklasse zugreifen. Da Objektinstanzen niemals auf private und public Daten einer Klasse zugreifen können, wäre das Ergebnis in diesem Fall, dass Objektinstanzen von abgeleiteten Klassen niemals auf Elemente der Basisklasse zugreifen könnten D:\75807564.doc vom 13.10.99 4-90 BBS III Mainz OOP für IT-Berufe mit C++ Merke: Eine abgeleitete Klasse kann nur auf public- und protected-Teile einer Oberklasse zugreifen. Die private-Teile der Oberklasse sind für die abgeleitete Klasse im Prinzip unsichtbar „Wäre holedaten() von Publikation mit dem Zugriffsrecht private ausgestattet, dann könnten die abgeleiteten Klassen Buch und Cd auf die Methode holedaten() nicht zugreifen“ Klassenhierarchie Entwickeln Sie ein Programm, dass die Vererbung bereits beim Design einsetzt (siehe VERLAG1). Es wird die Datenbank eines kleinen Unternehmens mit den drei Kategorien Manager, Wissenschaftler und Arbeiter, gefordert. Sie enthält einen Namen und eine Nummer für jeden Beschäftigten sowie bei Managern deren Titel und ihren jährlichen Golfklubbeitrag und bei Wissenschaftlern ihre Publikationsanzahl. Lösungsansätze Klasse Beschaeftigte Name Nummer Klasse Manager Klasse Wissenschaftler Klasse Arbeiter Titel Publikationen Golfklubbeitrag Im Hauptprogramm werden vier Objekte von verschiedenen Klassen deklariert: zwei Manager, ein Wissenschaftler und ein Arbeiter. Dann werden die jeweiligen Methoden (holedaten()- und zeigedaten()) aufgerufen, um Daten ein- und auszugeben. Hinweis Welchen Zweck hat die Basisklasse Beschaeftigte, wenn sie keine Objekte benötigt? D:\75807564.doc vom 13.10.99 4-91 BBS III Mainz OOP für IT-Berufe mit C++ Diese dient nur als Ausgangspunkt zur Erstellung weiterer abgeleiteteter Klassen. Eine solche Klasse nennt man abstrakte Klasse, in diesem Fall abstrakte Basisklasse. Das Vererbungskonzept unterstützt die Wiederverwendbarkeit von Programmcode. Wenn eine Basisklasse entworfen und getestet wurde, kann sie in verschiedenen Situationen benutzt werden, ohne daß weitere Tests notwendig wären. Außerdem hilft das Vererbungskonzept beim Design eines komplexen Programms. Ein Resultat der Vererbung ist die Verbreitung von Klassenbibliotheken, die von anderen Programmierern oder Firmen erstellt wurden. Wir betrachten als Beispiel die bei der Operator-Überladung schon behandelte Klasse Zaehler. Wir sind zufrieden mit ihrer Funktionalität, möchten aber noch eine Möglichkeit einfügen, um den Zaehler zu verringern. Angenommen, wir möchten die Kunden einer Bank zählen und erhöhen den Zähler, wenn ein Kunde die Bank betritt und verringern ihn wieder, wenn er sie verläßt, so daß der Zähler immer den aktuellen Kundenstand in der Bank zu einem beliebigen Zeitpunkt angibt. //Zaehler3.cpp //Vererbung mit Zähler-Klasse #include <iostream.h> #include <conio.h> class Zaehler { protected: unsigned int count; public: Zaehler() { count = 0; } Zaehler(int c) { count = c; } int hole_zaehler(){ return count; }// Zaehler operator ++ () { return Zaehler(++count); } }; class Zaehlerab : public Zaehler { public: Zaehler operator --() {return Zaehler(--count); } }; void main() { Zaehlerab c1; cout << "\nc1=" << c1.hole_zaehler(); ++c1; ++c1; ++c1; cout << "\nc1=" << c1.hole_zaehler(); --c1; --c1; --c1; cout << "\nc1=" << c1.hole_zaehler(); getch(); } Nach der Klasse Zaehler wird die abgeleitete Klasse Zaehlerab spezifiziert. Die Syntax class Zaehlerab : public Zaehler gibt an, dass die Klasse Zaehlerab von der Klasse Zaehler abgeleitet wurde. Die Bedeutung von public vor Zaehler erfahren Sie im Abschnitt „Private- und Public-Vererbung“. Die Klasse Zaehlerab enthält eine neue Funktion, operator--(), die den Zähler verringert. Das entscheidende bei der Vererbung ist nun, dass die abgeleitete Klasse alle Daten und Methoden der Basisklasse enthält, so daß diese nicht mehr aufgeführt werden, aber trotzdem zur Verfügung stehen. Der einfache Doppelpunkt beschreibt die Beziehung zwischen den Klassen. D:\75807564.doc vom 13.10.99 4-92 BBS III Mainz OOP für IT-Berufe mit C++ Ein wichtiger Punkt bei der Vererbung ist es, den Zugriff auf Methoden der Basisklasse genau zu kennen. Im Hauptprogramm des letzten Zaehler3-Beispiels wurde durch Zaehlerab c1 ein Objekt der Klasse Zaehlerab erstellt und mit 0 initialisiert. Wie war dies möglich, ohne dass ein Konstruktor in dieser Klasse definiert ist, der die Initialisierung hätte ausführen können? Unter bestimmten Umständen wird diese Aufgabe dann vom Konstruktor ohne Argument der Basisklasse übernommen. Für die Ausnahmen siehe „Konstruktoren in abgeleiteten Klassen“. Das Objekt c1 der Zaehlerab-Klasse benutzt auch den Operator operator++() und die Funktion hole_zaehler() aus der Klasse zaehler. Da der Compiler die Funktionen nicht in der Klasse von c1 findet, benutzt er diejenigen der Basisklasse. Vererbung funktioniert nicht in die Gegenrichtung; Oberklassen und deren Instanzen wissen nichts von den abgeleiteten Klassen und deren Objekten 4.3.2 Protected In dem Beispiel Zaehler3.cpp finden wir in der Zaehler-Klasse zum ersten Mal die Kennzeichnung protected vor dem Datenelement count, während bisher nur public oder private verwendet wurden. Bei der Betrachtung ohne Vererbung galt: Auf Klassenelemente, seien es Daten oder Methoden, kann immer von der eigenen Klasse zugegriffen werden, unabhängig davon, ob die Elemente als public oder private gekennzeichnet sind. Jedoch können Objekte einer Klasse, die außerhalb definiert wurde, auf die Elemente der betreffenden Klasse nur zugreifen, wenn diese als public gekennzeichnet sind. Die Frage im Fall der Vererbung ist, ob Methoden der abgeleiteten Klasse auf Daten der Oberklasse zugreifen können? Ja, wenn diese Daten public oder protected sind; nein, wenn sie private sind. Im obigen Beispiel sollte count nicht public sein, da sonst jede Funktion darauf Zugriff hat, was nicht erwünscht ist. Beachten Sie, dass zwar von Methoden der eigenen Klasse und von abgeleiteten Klassen auf ein mit protected gekennzeichnetes Datenfeld zugegriffen werden kann, nicht jedoch von außerhalb definierten Funktionen wie main(). Bezeichner Zugriff von der eigenen Klasse Zugriff von der abgeleiteten Klasse Zugriff von Objekten ausserhalb der Klassen Public Ja Ja Ja Protected Ja Ja Nein Private Ja Nein Nein 4.3.3 Konstruktoren in abgeleiteten Klassen Betrachten Sie das Zaehler3-Programm: Wie kann man ein Zaehlerab-Objekt mit einem Wert initialisieren? Kann man dies mit dem Konstruktor mit einem Argument aus Zaehler tun? Nein, da nur Konstruktoren ohne Argument vererbt werden. Daher muß man für die abgeleitete Klasse neue Konstruktoren definieren: //Zaehler4.cpp //Konstruktoren in der abgeleiteten Klasse #include <iostream.h> #include <conio.h> class Zaehler { D:\75807564.doc vom 13.10.99 4-93 BBS III Mainz protected: unsigned int count; public: Zaehler() { Zaehler(int c) { int hole_zaehler(){ Zaehler operator ++ }; OOP für IT-Berufe mit C++ count = 0; } count = c; } return count; }// () { return Zaehler(++count); } class Zaehlerab : public Zaehler { public: Zaehlerab() : Zaehler() {} Zaehlerab(int c) : Zaehler(c) {} Zaehlerab operator --() {return Zaehlerab(--count); } }; void main() { Zaehlerab c1; Zaehlerab c2(200); cout << "\nc1=" << c1.hole_zaehler(); cout << "\nc2=" << c2.hole_zaehler(); ++c1; ++c1; ++c1; cout << "\nc1=" << c1.hole_zaehler(); --c2; --c2; cout << "\nc2=" << c2.hole_zaehler(); Zaehlerab c3 = --c2; cout << "\nc3=" << c3.hole_zaehler(); getch(); } Um einen Konstruktor mit einem Argument in der abgeleiteten Klasse zu definieren, bedient man sich der folgenden Schreibweise: Zaehlerab(int c) : Zaehler(c) { } Der Doppelpunkt bedeutet, dass beim Aufruf des Ein-Argument-Konstruktors (z.B. durch Zaehlerab c2(200) ) der Konstruktor der Oberklasse mit dem Argument 200 aufgerufen wird. 4.3.4 Überschreiben von Methoden Dazu betrachten wir das folgende Beispiel, in dem ein Array als Datenfeld einer Klasse definiert ist. //stackar.cpp //Array als Datenfeld einer Klasse #include <iostream.h> #include <conio.h> const int MAX = 50; class Stack { private: int st[MAX]; int top; public: Stack() { top = 0; } void push(int var) { st[++top] = var; } int pop() D:\75807564.doc vom 13.10.99 4-94 BBS III Mainz { return st[top--]; } OOP für IT-Berufe mit C++ }; void main() { Stack s1; s1.push(111); s1.push(222); cout << "1: " << s1.pop() << endl; cout << "2: " << s1.pop() << endl; s1.push(333); s1.push(444); s1.push(555); s1.push(666); cout cout cout cout << << << << "3: "4: "5: "6: " " " " << << << << s1.pop() s1.pop() s1.pop() s1.pop() << << << << endl; endl; endl; endl; getch(); } Der Stack selbst besteht aus dem Feld st. Die Integer-Variable top kennzeichnet den Index des zuletzt auf dem Stack abgelegten Elements. Elemente werden mit push() abgelegt und mit pop() vom Stack geholt. Das obige Hauptprogramm legt zwei Datenelemente auf dem Stack ab, nimmt sie wieder herunter und gibt sie aus. Dann erfolgt das Gleiche mit 4 Datenelementen. Die Ausgabe ist: 1: 2: 3: 4: 5: 6: 222 111 666 555 444 333 Wie man sieht, werden die Datenelemente in der umgekehrten Reihenfolge ausgegeben. Dies nennt man auch LIFO: last in, first out. Durch st[++top] = var; wird in der Funktion push() zuerst top erhöht, so dass es auf das nächste freie Feldelement zeigt und danach erst der Wert der Variablen var zugewiesen. Der Befehl return st[top--]; gibt zuerst den Wert der Stackspitze an die aufrufende Funktion (hier main()) zurück und verringert dann top so, dass es auf das vorangegangene Element zeigt. Das Programm hat jedoch zwei Schwächen: wenn zu viele Items auf den Stack geschrieben werden, führt dies zum Programmabbruch. wenn zu viele Items gelesen werden, würde dies zu einem unsinnigen Resultat führen, da man Daten aus Speicherbereichen ausserhalb des Arrays lesen würde. Um diese Mängel zu beheben, wird eine neue Klasse Stack2 erstellt, die von Stack abgeleitet ist, aber in o.g. Fällen Warnungen ausgibt. //stack2.cpp //Überschreiben von Methoden #include <iostream.h> #include <process.h> //Für den Befehl exit() #include <conio.h> D:\75807564.doc vom 13.10.99 4-95 BBS III Mainz OOP für IT-Berufe mit C++ const int MAX = 50; class Stack { protected: int st[MAX]; int top; public: Stack() { top = 0; } void push(int var) { st[++top] = var; } int pop() { return st[top--]; } }; class Stack2 : public Stack { public: void push(int var) { if (top >=MAX) { cout << "\nFehler: Stack ist voll"; exit(1); } Stack::push(var); } int pop() { if (top <= 0) { cout << "\nFehler: Stack ist leer"; exit(1); } return Stack::pop(); } }; void main() { Stack2 s1; s1.push(111); s1.push(222); s1.push(333); cout << "1: " << s1.pop() << endl; cout << "2: " << s1.pop() << endl; cout << "3: " << s1.pop() << endl; getch(); cout << "4: " << s1.pop() << endl; //einmal zuviel } Die abgeleitete Klasse Stack2 enthält zwei Funktionen, push() und pop(). Diese Funktionen haben denselben Namen, dieselben Argumente und dieselben Rückgabewerte wie die Funktionen in der Oberklasse Stack. Wenn man eine solche Funktion in main() aufruft (z.B. s1.push()), stellt sich die Frage, woher der Compiler weiß, welche der beiden push-Funktionen verwendet werden soll? Die Regel lautet, dass in einem solchen Fall die Funktion der abgeleiteten Klasse ausgeführt wird. Man sagt, dass die abgeleitete Methode die Methode der Oberklasse überschreibt. In den obigen Beispiel wird also die push-Funktion von Stack2 ausgeführt, nicht die von Stack. Diese Funktion überprüft, ob der Stack voll ist, gibt gegebenenfalls eine Fehlermeldung aus und beendet das Programm. Falls noch Platz auf dem Stack ist, wird die push-Funktion von Stack aufgerufen. Analog funktioniert pop(). D:\75807564.doc vom 13.10.99 4-96 BBS III Mainz OOP für IT-Berufe mit C++ Auffällig ist noch die Kennzeichnung der Datenelemente von Stack mit protected, da bei Kennzeichnung als private Methoden von Stack2 nicht darauf zugreifen könnten. 4.3.5 Klassenhierarchie Im folgenden Beispiel wird Vererbung bereits beim Design des Programms eingesetzt. Es wird die Datenbank eines kleinen Unternehmens mit den drei Kategorien Manager, Wissenschaftler und Arbeiter, abgebildet. Sie enthält einen Namen und eine Nummer für jeden Beschäftigten sowie bei Managern deren Titel und ihren jährlichen Golfklubbeitrag und bei Wissenschaftlern ihre Publikationsanzahl. D:\75807564.doc vom 13.10.99 4-97 BBS III Mainz OOP für IT-Berufe mit C++ Klasse Beschaeftigte Name Nummer Klasse Manager Titel Klasse Wissenschaftler Klasse Arbeiter Publikationen Golfklubbeitrag //Programm besch.cpp //Vererbung anhand eines einfachen Firmenmodells #include <iostream.h> #include <conio.h> class Beschaeftigte { private: char name[80]; unsigned long nummer; public: void holedaten() { cout << "\n Bitte den Nachnamen eingeben: "; cin >> name; cout << "\n Bitte die Beschaeftigtennummer eingeben: "; cin >> nummer; } void zeigedaten() { cout << "\n Name: " <<name; cout << "\n Nummer: " << nummer; } }; class Manager : public Beschaeftigte { private: char titel[80]; double gbeitrag; public: void holedaten() { Beschaeftigte::holedaten(); cout << "\n Bitte den Titel eingeben: "; cin >> titel; cout << "\n Bitte den Golfbeitrag eingeben: "; cin >> gbeitrag; } void zeigedaten() { Beschaeftigte::zeigedaten(); cout << "\n Titel: " <<titel; cout << "\n Golfbeitrag: " << gbeitrag; } }; class Wissenschaftler : public Beschaeftigte { private: D:\75807564.doc vom 13.10.99 4-98 BBS III Mainz int publik; OOP für IT-Berufe mit C++ public: void holedaten() { Beschaeftigte::holedaten(); cout << "\n Bitte die Anzahl der Publikationen eingeben: "; cin >> publik; } void zeigedaten() { Beschaeftigte::zeigedaten(); cout << "\n Anzahl der Publikationen: " << publik; } }; class Arbeiter : public Beschaeftigte { }; void main() { Manager mana1, mana2; Wissenschaftler wiss1; Arbeiter arb1; cout << endl; cout << "\nBitte Daten fuer Manager 1 eingeben"; mana1.holedaten(); cout << "\nBitte Daten fuer Manager 2 eingeben"; mana2.holedaten(); cout <<endl; cout << "\nBitte Daten für Wissenschaftler 1 eingeben"; wiss1.holedaten(); cout <<endl; cout << "\nBitte Daten für Arbeiter 1 eingeben"; arb1.holedaten(); cout << "\n Daten von mana1.zeigedaten(); cout <<endl; cout << "\n Daten von mana2.zeigedaten(); cout <<endl; cout << "\n Daten von wiss1.zeigedaten(); cout <<endl; cout << "\n Daten von arb1.zeigedaten(); Manager 1"; Manager 2"; Wissenschaftler 1"; Arbeiter 1"; getch(); } Im Hauptprogramm werden vier Objekte von verschiedenen Klassen deklariert: zwei Manager, ein Wissenschaftler und ein Arbeiter. Dann werden die jeweiligen holedaten()- und zeigedaten()-Funktionen aufgerufen, um Daten ein- und auszugeben. Eine Verbesserung des Programms wäre z.B. die Speicherung der Daten in einem Feld oder einer Datei. Auffällig ist, dass es keine Objekte der Basisklasse Beschaeftigte gibt. Diese dient nur als Ausgangspunkt zur Erstellung weiterer abgeleiteteter Klassen. Eine solche Klasse nennt man abstrakte Klasse, in diesem Fall abstrakte Basisklasse. D:\75807564.doc vom 13.10.99 4-99 BBS III Mainz OOP für IT-Berufe mit C++ 4.3.6 Vererbung von Zugriffsrechten Ein weiterer Mechanismus des Zugriffsschutzes auf Klassen ist die Art ihrer Vererbung., z.B. class Manager : public Beschaeftigte wie im Beispiel Besch.cpp. Welche Bedeutung hat das Schlüsselwort public in diesem Zusammenhang? Es bedeutet, dass Funktionen und Daten der Oberklasse, die als public gekennzeichnet sind, auch als Funktionen und Daten der abgeleiteten Klasse zur Verfügung stehen und von ihren Objektinstanzen benutzt werden können. Wenn anstelle von public private verwendet würde, können die Objektinstanzen der abgeleiteten Klasse nicht auf public-Funktionen der Basisklasse zugreifen.. Da Objektinstanzen niemals auf private oder protected Daten einer Klasse zugreifen könen, wäre das Ergebnis in diesem Fall, dass Objektinstanzen von abgeleiteten Klassen niemals auf Elemente der Basisklasse zugreifen könnten. //Programm privpubl.cpp //Private und public Vererbung #include <iostream.h> class A { private: int privatdatenA; protected: int protdatenA; public: int pubdatenA; } class B : public A { public: void funktion() { int vari; vari = privatdatenA; vari = protdatenA; vari = pubdatenA; } }; class C : private A { public: void funktion() { int vari; vari = privatdatenA; vari = protdatenA; vari = pubdatenA; } //Fehler: kein Zugriff möglich // o.k. // o.k. //Fehler: kein Zugriff möglich // o.k. // o.k. void main() { int vari; B objB; vari = objB.privatdatenA; vari = objB.protdatenA; vari = objB.pubdatenA; C objC; vari = objC.privatdatenA; vari = objC.protdatenA; vari = objC.pubdatenA; //Fehler: kein Zugriff möglich //Fehler: kein Zugriff möglich // o.k. (B ist public vererbt) //Fehler: kein Zugriff möglich //Fehler: kein Zugriff möglich //Fehler: kein Zugriff möglich // (B ist privat vererbt) } D:\75807564.doc vom 13.10.99 4-100 BBS III Mainz OOP für IT-Berufe mit C++ In der folgenden Tabelle erkennt man, welche Zugriffsattribute in der abgeleiteten Klasse entstehen, je nachdem welche Ableitungsart gewählt und welches Attribut in der Oberklasse verwendet wurde: Oberklasse Attribut bei Ableitung private protected public private private (kein Zugriff) private (kein Zugriff) private (kein Zugriff) protected private protected protected public private protected public Eine abgeleitete Klasse kann somit nur auf die public- und protected-Teile einer Oberklasse zugreifen. Die private-Teile der Oberklasse sind für die abgeleitete Klasse im Prinzip unsichtbar. Public-Vererbung ist dann sinnvoll, wenn in der abgeleiteten Klasse die gleichen public-Deklarationen vorkommen sollen, während private-Vererbung dann eingesetzt werden sollte, wenn keine Teile der Oberklasse zur Schnittstelle (der public-Deklaration) der abgeleiteten Klasse gehören soll. Wenn kein Schlüsselwort bei der Vererbung verwendet wird, wird eine private-Vererbung angenommen. 4.3.7 Mehrere Stufen der Vererbung Klassen können von Klassen abgeleitet werden, die wiederum von anderen Klassen abgeleitet wurden. Bsp.: class A {}; class B : public A {}; class C : public B {}; In diesem Fall ist B abgeleitet von A und C wiederum von B. Diesen Prozeß kann man bis ins Unendliche weiterführen. Als konkretes Beispiel soll eine Erweiterung des Besch.cpp-Programms dienen: Angenommen, wir leiten von der Gruppe Arbeiter die Gruppe Meister ab. Diese werden nach der Qualität ihrer Ergebnisse (100% ist das Maximum) bewertet. //Programm besch2.cpp //Mehrere Ebenen der Vererbung #include <iostream.h> #include <conio.h> class Beschaeftigte { private: char name[80]; unsigned long nummer; public: void holedaten() { cout << "\n Bitte den Nachnamen eingeben: "; cin >> name; cout << "\n Bitte die Beschaeftigtennummer eingeben: "; cin >> nummer; } void zeigedaten() { cout << "\n Name: " <<name; cout << "\n Nummer: " << nummer; } }; D:\75807564.doc vom 13.10.99 4-101 BBS III Mainz OOP für IT-Berufe mit C++ class Manager : public Beschaeftigte { private: char titel[80]; double gbeitrag; public: void holedaten() { Beschaeftigte::holedaten(); cout << "\n Bitte den Titel eingeben: "; cin >> titel; cout << "\n Bitte den Golfbeitrag eingeben: "; cin >> gbeitrag; } void zeigedaten() { Beschaeftigte::zeigedaten(); cout << "\n Titel: " <<titel; cout << "\n Golfbeitrag: " << gbeitrag; } }; class Wissenschaftler : public Beschaeftigte { private: int publik; public: void holedaten() { Beschaeftigte::holedaten(); cout << "\n Bitte die Anzahl der Publikationen eingeben: "; cin >> publik; } void zeigedaten() { Beschaeftigte::zeigedaten(); cout << "\n Anzahl der Publikationen: " << publik; } }; class Arbeiter : public Beschaeftigte { }; class Meister : public Arbeiter { private: float quote; public: void holedaten() { Arbeiter::holedaten(); cout << "\n Bitte die Erfolgsquote in Prozent eingeben: "; cin >> quote; } void zeigedaten() { Arbeiter::zeigedaten(); cout << "\n Erfolgsquote: " << quote; } }; void main() { Manager mana1, mana2; Wissenschaftler wiss1; D:\75807564.doc vom 13.10.99 4-102 BBS III Mainz Arbeiter arb1; Meister meis1; OOP für IT-Berufe mit C++ cout << endl; cout << "\nBitte Daten fuer Manager 1 eingeben"; mana1.holedaten(); cout << "\nBitte Daten fuer Manager 2 eingeben"; mana2.holedaten(); cout <<endl; cout << "\nBitte Daten für Wissenschaftler 1 eingeben"; wiss1.holedaten(); cout <<endl; cout << "\nBitte Daten für Arbeiter 1 eingeben"; arb1.holedaten(); cout <<endl; cout << "\nBitte Daten für Meister 1 eingeben"; meis1.holedaten(); cout << "\n Daten von mana1.zeigedaten(); cout <<endl; cout << "\n Daten von mana2.zeigedaten(); cout <<endl; cout << "\n Daten von wiss1.zeigedaten(); cout <<endl; cout << "\n Daten von arb1.zeigedaten(); cout <<endl; cout << "\n Daten von meis1.zeigedaten(); getch(); } Manager 1"; Manager 2"; Wissenschaftler 1"; Arbeiter 1"; Meister 1"; Auffällig ist, dass eine Klassenhierarchie nicht unbedingt einem Organigramm entsprechen muß. Eine Klassenhierarchie entsteht durch Verallgemeinerung gemeinsamer Charakteristika. Je allgemeiner die Klasse, umso höher ist sie in der Hierarchie, z.B. steht Arbeiter über Meister, der ein spezielles Beispiel für einen Arbeiter ist. 4.3.8 Mehrfachvererbung Eine Klasse kann von mehr als einer Basisklasse abgeleitet werden. Dies nennt man Mehrfachvererbung. Die Syntax ist analog der bei der "normalen" Vererbung: class A {}; //Basisklasse A class B {}; //Basisklasse B class C : public A, public B {}; // C ist von A und B abgeleitet Angenommen, wir möchten in Besch.cpp die schulische Vorbildung der Beschäftigten integrieren. Weiterhin angenommen, wir hätten schon eine Klasse Student entwickelt, die die Vorbildung von Studenten enthält. Anstatt nun die Beschäftigten-Klasse zu modifizieren, damit sie die Vorbildungsdaten enthält, lösen wir das Problem mittels Mehrfachvererbung von der Klasse Student. Diese enthält den Namen der Schule oder Universität und den erreichten Abschluss. Zwei Funktionen getedu() und putedu() erwarten eine Eingabe und stellen sie am Bildschirm dar. Die Vorbildung soll nur bei Managern und Wissenschaftlern eine Rolle spielen. Angestellte D:\75807564.doc vom 13.10.99 Student 4-103 BBS III Mainz Arbeiter Wissenschaftler Manager OOP für IT-Berufe mit C++ Die Pfeile drücken wieder die Relation "ist abgeleitet von" aus. //Programm erbmehrf.cpp //Mehrfachvererbung #include <iostream.h> #include <conio.h> class Student { private: char schule[80]; char abschluss[80]; public: void getedu() { cout << "\nBitte geben Sie den Namen der Schule oder Universitaet ein: "; cin >> schule; cout << "\nBitte geben Sie Ihren hoechsten Abschluss ein: "; cin >> abschluss; } void putedu() { cout <<"\n Schule oder Uni: " <<schule; cout <<"\n Hoechster Abschluss: "<< abschluss; } }; class Beschaeftigte { private: char name[80]; unsigned long nummer; public: void holedaten() { cout << "\n Bitte den Nachnamen eingeben: "; cin >> name; cout << "\n Bitte die Beschaeftigtennummer eingeben: "; cin >> nummer; } void zeigedaten() { cout << "\n Name: " <<name; cout << "\n Nummer: " << nummer; } }; class Manager : private Beschaeftigte, private Student { private: char titel[80]; double gbeitrag; public: void holedaten() { Beschaeftigte::holedaten(); cout << "\nBitte den Titel eingeben: "; cin >> titel; cout << "\nBitte den Golfbeitrag eingeben: "; cin >> gbeitrag; Student::getedu(); } void zeigedaten() { Beschaeftigte::zeigedaten(); cout << "\nTitel: " <<titel; cout << "\nGolfbeitrag: " << gbeitrag; D:\75807564.doc vom 13.10.99 4-104 BBS III Mainz Student::putedu(); } }; OOP für IT-Berufe mit C++ class Wissenschaftler : private Beschaeftigte, private Student { private: int publik; public: void holedaten() { Beschaeftigte::holedaten(); cout << "\nBitte die Anzahl der Publikationen eingeben: "; cin >> publik; Student::getedu(); } void zeigedaten() { Beschaeftigte::zeigedaten(); cout << "\nAnzahl der Publikationen: " << publik; Student::putedu(); } }; class Arbeiter : public Beschaeftigte { }; void main() { Manager mana1; Wissenschaftler wiss1, wiss2; Arbeiter arb1; cout << endl; cout << "\nBitte Daten fuer Manager 1 eingeben"; mana1.holedaten(); cout << "\nBitte Daten für Wissenschaftler 1 eingeben"; wiss1.holedaten(); cout <<endl; cout << "\nBitte Daten für Wissenschaftler 2 eingeben"; wiss2.holedaten(); cout <<endl; cout << "\nBitte Daten für Arbeiter 1 eingeben"; arb1.holedaten(); cout << "\nDaten von mana1.zeigedaten(); cout <<endl; cout << "\nDaten von wiss1.zeigedaten(); cout <<endl; cout << "\nDaten von wiss1.zeigedaten(); cout <<endl; cout << "\nDaten von arb1.zeigedaten(); Manager 1"; Wissenschaftler 1"; Wissenschaftler 2"; Arbeiter 1"; getch(); } D:\75807564.doc vom 13.10.99 4-105 BBS III Mainz OOP für IT-Berufe mit C++ 4.3.9 Übungsaufgaben 1. (Verlag1.cpp) Stellen Sie sich einen Verlag vor, der sowohl Bücher als auch CDs vermarktet. Erstellen Sie eine Klasse Publikation, die den Titel (ein String) und den Preis (Typ float) einer Publikation beinhaltet. Von dieser Klasse werden zwei Klassen abgeleitet: Buch, die zusätzlich eine Seitenzahl (Typ int) enthält und CD, die eine Spielzeit in Minuten (Typ float) enthält. Alle diese Klassen sollen eine holedaten()-Funktion enthalten, die den Benutzer zur Dateneingabe auffordert und eine zeigedaten()-Funktion zur Anzeige der Daten. Schreiben Sie ein Programm Verlag.cpp zum Test dieser Klassen und lassen Sie Daten ein- und ausgeben. 2. (Verlag2.cpp) Fügen Sie zu obigem Beispiel eine Klasse Verkauf hinzu, die ein Feld mit drei float-Zahlen enthält, in dem die Verkäufe einer beliebigen Publikation in den letzten drei Monaten gespeichert werden. Die holedaten()-Funktion soll die Verkaufszahlen vom Benutzer erfragen und die zeigedaten()-Funktion diese anzeigen. Ändern Sie die Buch- und CD-Klassen so, dass sie sowohl von Publikation als auch von Verkauf abgeleitet werden. Eine Instanz der Klassen Buch oder CD sollte die Verkaufszahlen zusammen mit den anderen Daten ausgeben. D:\75807564.doc vom 13.10.99 4-106 BBS III Mainz OOP für IT-Berufe mit C++ 4.4 Polymorphie Unter Polymorphismus („Vielgestaltigkeit“) versteht man, dass mit der „gleichen“ Funktion für verschiedene Objekte unterschiedliche Resultate erzielt werden können. Stellen Sie sich vor, Sie haben die Klasse Fahrzeug entwickelt, wovon Sie die Klassen Auto und Motorrad abgeleitet haben. Jede dieser Klassen besitzt die Funktion fahren (gleicher Funktionsname, gleiche Parameterliste), die jedoch in allen Klassen unterschiedlich implementiert wird. Erst zur Laufzeit des Programms wird entschieden, welche Funktion ausgeführt werden soll. Dies wird durch sogenannte virtuelle Funktionen erreicht. 4.4.1 Virtuelle Methoden/Funktionen 4.4.1.1 Funktionen ohne Zeigerzugriff Betrachten Sie das folgende nicht-virtuelle Beispiel: //Programm nvirtoz.cpp #include <iostream.h> #include <conio.h> class X { public: double A(double x) { return x*x; } double B(double x) { return A(x)/2; } }; class Y : public X { public: double A(double x) { return x*x*x; } }; void main() { Y y; cout << y.B(3) << endl; getch(); } Die Klasse X umfaßt die Funktionen A und B, wobei die Funktion B die Funktion A aufruft. Die Klasse Y, die von der Klasse X abgeleitet ist, erbt die Funktion B, aber überschreibt die Funktion A. Die Ausgabe des Programms ist jedoch 4.5=3*3/2 und nicht, wie beabsichtigt, 13.5=3*3*3/2! Der Compiler benutzt zur Auflösung des Ausdrucks y.B(3) die geerbte Funktion X::B, die wiederum X::A aufruft. Um zu erreichen, dass zur Berechnung in Y.B auch Y.A aufgerufen wird, muß man das Schlüsselwort virtual verwenden, um klar zu machen, dass für die Funktion A Polymorphie erwünscht ist. //virtoz.cpp //Virtuelle Funktionen ohne Zeiger #include <iostream.h> #include <conio.h> class X D:\75807564.doc vom 13.10.99 4-107 BBS III Mainz { public: virtual double A(double x) { return x*x; } double B(double x) { return A(x)/2; } }; OOP für IT-Berufe mit C++ class Y : public X { public: virtual double A(double x) { return x*x*x; } }; void main() { Y y; cout << y.B(3) << endl; getch(); } Dieses Beispiel gibt nun 13.5 aus, da zur Laufzeit bei der Berechnung von B die richtige Funktion A, nämlich Y::A zugeordnet wird. Wenn man eine Funktion als virtual deklariert hat, kann man sie nur mit Funktionen in den abgeleiteten Klassen überschreiben, die die gleiche Parameterliste haben. Virtuelle Funktionen können nicht-virtuelle Funktionen in Basisklassen überschreiben. Eine Funktion, die in einer bestimmten Klasse als virtuell deklariert wurde, wird in allen abgeleiteten Klassen ebenfalls als virtuell angesehen, gleichgültig, ob das Schlüsselwort virtual verwendet wurde oder nicht. Daher gilt die Regel für die Deklaration virtueller Funktionen: „einmal virtuell, immer virtuell”. Wenn in der abgeleiteten Klasse nicht die gleiche Parameterliste verwendet wird, kann die abgeleitete Klasse (und keine von ihr abgeleitete Klasse) nicht auf die virtuelle Funktion der Basisklasse zugreifen. Man kann in C++ nicht-virtuelle, überladene Funktionen, die den gleichen Namen wie die virtuelle Funktion, aber eine andere Parameterliste, haben, deklarieren. Außerdem werden nicht-virtuelle Funktionen, die den gleichen Namen tragen wie virtuelle Funktionen, nicht vererbt. //Programm virtmi.cpp //Virtuelle und nicht-virtuelle Funktionen #include <iostream.h> #include <conio.h> class A { public: A() {} virtual void foo(char c) { cout << "virtual A::foo() returns " << c << endl; } }; class B : public A { public: B() {} void foo(const char* s) { cout << "B::foo() returns " << s << endl; } D:\75807564.doc vom 13.10.99 4-108 BBS III Mainz OOP für IT-Berufe mit C++ void foo(int i) { cout << "B::foo() returns " << i << endl; } virtual void foo(char c) { cout << "virtual B::foo() returns " << c << endl; } }; class C : public B { public: C() {} void foo(const char* s) { cout << "C::foo() returns " << s << endl; } void foo(double x) { cout << "C::foo() returns " << x << endl; } virtual void foo(char c) { cout << "virtual C::foo() returns " << c << endl; } }; void { A B C main() Aobj; Bobj; Cobj; Aobj.foo('A'); Bobj.foo('B'); Bobj.foo(10); Bobj.foo("Bobj"); Cobj.foo('C'); Cobj.foo(255.345); Cobj.foo("Cobj"); cin.peek(); } Die drei Klassen A, B und C bilden eine lineare Hierarchie. In A wird die Funktion foo(char) als virtuell deklariert, B deklariert ihre eigene Version davon und die nicht-virtuellen, überladenen Funktionen foo(const char*) und foo(int). Die von B abgeleitet Klasse C ist ähnlich definiert. Zu beachten ist, dass C die Funktion foo(const char*) deklarieren muß, wenn sie die Funktion benötigt, da sie die Funktion B::foo(const char*) nicht erben kann. 4.4.1.2 Zeiger auf Objekte Virtuelle Funktionen werden sehr häufig in Kombination mit Zeigern verwendet. Dies wird im folgenden verdeutlicht. Zur Erinnerung: element* bezeichnet den Zeiger auf ein Element. *elementzeiger dereferenziert einen solchen Zeiger und ist selbst wieder vom Typ element. elementzeiger = &element Elementzeiger zeigt auf die Adresse von element. D:\75807564.doc vom 13.10.99 4-109 BBS III Mainz OOP für IT-Berufe mit C++ Der new-Operator wird oft in Konstruktoren verwendet. Wir benutzen ihn in einem Beispiel mit einer selbstdefinierten String-Klasse. //Programm newstr.cpp //Der Befehl new zur Reservierung von Speicher #include <iostream.h> #include <conio.h> #include <string.h> class String { private: char* str; public: String(char* s) { int laenge = strlen(s); str = new char[laenge+1]; strcpy(str,s); } ~String() { delete str; } void anzeige() { cout << str; } }; void main() { String s1 = "Who knows nothing doubts nothing."; cout << endl << "s1="; s1.anzeige(); getch(); } Die Klasse String enthält nur ein Datenelement, einen Zeiger str auf char. Dieser zeigt auf einen String, der im String-Objekt selbst nicht enthalten ist. Der Konstruktor wird durch String s1 = "Who ..." aufgerufen. Eine andere Schreibweise wäre String s1("Who ....");. Der Konstruktor in diesem Beispiel hat einen String als Argument und reserviert mittels new Speicherplatz, um eine Kopie dieses Strings zu erstellen. Str zeigt auf diesen Speicherplatz. Mit Hilfe des Destruktors ~String() wird der reservierte Speicherplatz am Programmende wieder freigegeben. Objekte werden normalerweise "zerstört", wenn die Funktion, in der sie definiert sind, beendet wird. Der Destruktor indiesem Beispiel stellt sicher, dass der von einem String-Objekt benutzte Speicher dem System zurückgegeben wird. Wenn man zu Beginn eines Programms noch nicht weiß, wieviele Objekte erstellt werden sollen, benutzt man new, um Objekte zur Laufzeit zu erstellen. Im folgenden Beispiel werden beide Arten der Objekterstellung benutzt: //Programm Englptr.cpp //Feet und inches mit Pointern #include <iostream.h> #include <conio.h> class laenge { private: int feet; float inches; D:\75807564.doc vom 13.10.99 4-110 BBS III Mainz OOP für IT-Berufe mit C++ public: void holelaenge() //Benutzer gibt Daten ein { cout << "\nBitte Feet eingeben: "; cin >> feet; cout << "\nBitte Inches eingeben: ";cin >> inches; } void zeigelaenge() //Länge ausgeben { cout << feet << "\'-" << inches << '\"'; } }; void main() { laenge l1; l1.holelaenge(); l1.zeigelaenge(); laenge* laengenzeiger; laengenzeiger = new laenge; laengenzeiger->holelaenge(); laengenzeiger->zeigelaenge(); getch(); } In dem obigen Beispiel wird auf die Methoden des Objekts laenge mittels "->" - Operator zugegriffen, analog wie bei Zeigern auf Strukturen. Der "->"- Operator hat bei Zeigern auf Objekte dieselbe Funktion wie der "."Operator bei Objektinstanzen. 4.4.1.3 Funktionen mit Zeigern Im nächsten Beispiel werden Funktionen verwendet, die sowohl in der Basisklasse als auch in der abgeleiteten Klasse denselben Namen haben. //Programm Nichtvir.cpp //Funktionen mit Zeigerzugriff #include <iostream.h> class Basis { public: void zeige() { cout << "\nBasis"; } }; class Abgel1 : public Basis { public: void zeige() { cout << "\nAbgeleitete Klasse 1"; } }; class Abgel2 : public Basis { public: void zeige() { cout << "\nAbgeleitete Klasse 2"; } }; void main() { D:\75807564.doc vom 13.10.99 4-111 BBS III Mainz Abgel1 ab1; Abgel2 ab2; Basis* zeiger; OOP für IT-Berufe mit C++ zeiger = &ab1; zeiger->zeige(); zeiger = &ab2; zeiger->zeige(); } Die Klassen Abgel1 und Abgel2 sind von der Klasse Basis abgeleitet. Alle drei beinhalten eine Funktion zeige(). Im Hauptprogramm erstellen wir Funktionen der Klassen Abgel1 und Abgel2 und einen Zeiger aus Basis. Dann setzen wir den Zeiger zeiger auf die Adresse eines abgeleiteten Objekts. Dies funktioniert, da Zeiger auf Objekte einer abgeleiteten Klasse typkompatibel sind zu Zeigern auf Objekte der Oberklasse (jedoch nicht umgekehrt). Die Frage ist, welche zeige()-Funktion ausgeführt wird? Ist es Basis::zeige() oder ab1::zeige() bzw. ab2::zeige() ? Die Programmausgabe löst das Rätsel: Basis Basis Wie man sieht, wird die Funktion der Basisklasse zweimal ausgeführt. Der Compiler ignoriert den Typ des Inhalts des Zeigers zeiger und wählt diejenige Funktion aus, die mit dem Zeigertyp übereinstimmt. Manchmal ergibt dies einen Sinn, löst jedoch in diesem Fall nicht das Problem. Eine kleine Änderung, das Schlüsselwort "virtual" vor der zeige()-Funktion in der Basisklasse, löst das Problem. //Programm Nichtvir.cpp //Funktionen mit Zeigerzugriff #include <iostream.h> #include <conio.h> class Basis { public: virtual void zeige() { cout << "\nBasis"; } }; class Abgel1 : public Basis { public: virtual void zeige() { cout << "\nAbgeleitete Klasse 1"; } }; class Abgel2 : public Basis { public: virtual void zeige() { cout << "\nAbgeleitete Klasse 2"; } }; void main() { Abgel1 ab1; Abgel2 ab2; Basis* zeiger; zeiger = &ab1; zeiger->zeige(); zeiger = &ab2; D:\75807564.doc vom 13.10.99 4-112 BBS III Mainz zeiger->zeige(); OOP für IT-Berufe mit C++ getch(); } Die Ausgabe des Programms ist nun: Abgeleitete Klasse 1 Abgeleitete Klasse 2 Das Programm entscheidet nun erst zur Laufzeit, welche zeige()-Funktion aufgerufen wird, je nachdem, auf welches Objekt zeiger zeigt. Dies nennt man “späte Bindung” oder “dynamische Bindung”. 4.4.2 Virtuelle Basisklassen Eltern Kind1 Kind2 Enkel Dieses Beispiel ist im Sinne der Vererbung in der OOP zu verstehen. Es entstehen Probleme, wenn eine Funktion aus einem Enkel-Objekt Daten oder Funktionen aus der Eltern-Klasse benutzen möchte. //Programm basis1.cpp //Mehrdeutigkeit beim Zugriff auf Basisklassen #include <iostream.h> #include <conio.h> class Eltern { protected: int basisdaten; }; class Kind1 : public Eltern { }; class Kind2 : public Eltern { }; class Enkel : public Kind1, Kind2 { public: int holedaten() { return basisdaten; //Fehler: Mehrdeutigkeit } }; Es gibt einen Compiler-Fehler, wenn die holedaten()-Funktion in Enkel auf basisdaten in der Elternklasse zugreifen möchte. Da Kind1 und Kind2 von Eltern abgeleitet sind, erbt jedes eine Kopie der Daten und Funktionen des Elternobjekts, darunter auch basisdaten. Wenn das Enkel-Objekt nun auf basisdaten zugreift, weiß es nicht, auf welche Kopie es zugreifen soll. Durch das Schlüsselwort virtual vor Kind1 und Kind2 erreicht man, dass beide Klassen sich eine Kopie der Daten und Methoden von basisdaten teilen. //Programm basisvi.cpp //Virtuelle Basisklassen #include <iostream.h> D:\75807564.doc vom 13.10.99 4-113 BBS III Mainz #include <conio.h> OOP für IT-Berufe mit C++ class Eltern { protected: int basisdaten; }; class Kind1 : virtual public Eltern { }; class Kind2 : virtual public Eltern { }; class Enkel : public Kind1, Kind2 { public: int holedaten() { return basisdaten; //Funktioniert jetzt } }; 4.4.3 Friend-Funktionen Das Konzept der Kapselung und des Schutzes der Daten eines Objekts gegen unbefugten Zugriff von außen durch Schlüsselworte wie private oder protected kann sich manchmal nachteilig auswirken. Stellen Sie sich vor, Sie möchten eine Funktion schreiben, die auf Objekten zweier verschiedener Klassen funktioniert und als Argumente zwei Klassen hat, die in keinerlei Beziehung zueinander stehen. Wären sie von derselben Klasse abgeleitet, könnte man die Funktion in die Basisklasse schreiben. Ist dies aber nicht so, dienen FriendFunktionen als Brücke zwischen zwei Klassen. Mit solchen Funktionen kann man auf alle Datenelemente einer befreundeten Klasse zugreifen. //Porgramm friend.cpp //Friend-Funktionen #include <iostream.h> #include <conio.h> class beta; class alpha { private: int data; public: alpha() { data = 5; } friend int frifunc(alpha, beta); }; class beta { private: int data; public: beta() { data = 8; } friend int frifunc(alpha, beta); }; int frifunc(alpha a, beta b) { return( a.data + b.data ); } void main() { alpha aa; D:\75807564.doc vom 13.10.99 4-114 BBS III Mainz beta bb; cout << frifunc(aa,bb); getch(); } OOP für IT-Berufe mit C++ In diesem Beispiel gibt es zwei Klassen alpha und beta, deren Konstruktoren die jeweiligen Datenelemente mit 5 und 8 initialisieren. Damit die Funktion frifunc() nun auf die Datenelemente beider Klassen zugreifen kann, muß sie mit dem Schlüsselwort friend zu einer Friend-funktion gemacht werden. Eine solche Funkton kann an einer beliebigen Stelle in der Klasse definiert werden. Friend-Funktionen sollten sehr vorsichtig eingesetzt werden, da sie dem eigentlichen Konzept der OOP (nur Funktionen der eigenen Klasse können auf die jeweiligen Daten zugreifen) zuwiderlaufen und zu unübersichtlichen Programmen führen können. 4.4.4 This-Zeiger Alle Klassen verfügen über ein verborgenes Datenelement, den sogenannten this-Zeiger, der auf die Instanz der Klasse im Speicher verweist. Wie oben bereits beschrieben, erhält jede Instanz einer Klasse ihre eigene Kopie der Datenelement, aber alle Klassen-Instanzen teilen sich dieselben Funktionen. Da alle Methoden über einen verborgenen this-Parameter verfügen, weiß der Compiler, welche Instanz zu welchem Funktionsaufruf gehört. Der This-Zeiger spielt bei der Arbeit mit der VCL (Visual Component Library) des C++-Builders eine Rolle und arbeitet bei der “normalen” C++-Programmierung im Hintergrund. 4.4.5 Übungsaufgaben 1. (Rechteck.cpp) Schreiben Sie ein Programm rechteck.cpp, das eine Klasse Quadrat und eine davon abgeleitete Klasse Rechteck besitzt. Die Klasse Quadrat hat das Datenelement laenge und die Methoden Laenge, Breite und Flaeche, wobei Flaeche die Methoden Laenge und Breite aufruft. Die Klasse Rechteck hat neben dem Konstruktor nur noch eine Methode Breite. Die Seitenlaengen eines Quadrats bzw. eines Rechtecks können zu Programmbeginn festgelegt werden. Dann sollen die jeweiligen Flächen berechnet und zusammen mit den Seitenlängen ausgegeben werden. D:\75807564.doc vom 13.10.99 4-115 BBS III Mainz OOP für IT-Berufe mit C++ Lösungen zu den Übungsaufgaben: 6.2.9 // Programm Intclass.cpp #include <iostream.h> #include <conio.h> class Int { private: int i; public: Int() { i = 0; } Int(int j) { i = j; } void add(Int i1, Int i2) { i = i1.i + i2.i; } void display() { cout << i; } }; void main() { Int Int1(9); Int Int2(14); Int Int3; Int3.add(Int1,Int2); cout << "\nInt3 = "; Int3.display(); getch(); } // Programm Maut.cpp #include <iostream.h> #include <conio.h> const char ESC = 27; const double Kosten = 0.5; class Mautkasse { private: unsigned int anzfz; double geldmenge; public: Mautkasse() { anzfz = 0; geldmenge = 0.0; } void Bezfahrzeug() {anzfz++; geldmenge +=Kosten; } void Nbezfahrzeug() {anzfz++; } void Anzeige() { cout << "\nFahrzeuge = " << anzfz << ", Einnahmen = " << geldmenge << " Euro"; } }; void main() { Mautkasse Mkasse1; char ch; cout << "\nGeben Sie 0 ein für jedes nichtbezahlende Fahrzeug," "\n 1 ein für jedes bezahlende Fahrzeug," "\n ESC ein zum Beenden des Programms.\n" << flush; do { ch = getche(); if ( ch == '0' ) Mkasse1.Nbezfahrzeug(); if ( ch == '1' ) Mkasse1.Bezfahrzeug(); } D:\75807564.doc vom 13.10.99 4-116 BBS III Mainz while ( ch != ESC ); Mkasse1.Anzeige(); OOP für IT-Berufe mit C++ getch(); } 6.3.9 // Programm Verlag1.cpp #include <iostream.h> #include <conio.h> const int LEN = 80; class Publikation { private: char titel[LEN]; float preis; public: void holedaten() { cout << "\nBitte geben Sie den Titel cout << "\nBitte geben Sie den Preis } void zeigedaten() { cout << "\nDer Titel ist: "; cout << cout << "\nDer Preis ist: "; cout << } }; ein: "; cin >> titel; ein: "; cin >> preis; titel; preis; class Buch : private Publikation { private: int seitenzahl; public: void holedaten() { Publikation::holedaten(); cout << "\nBitte geben Sie die Anzahl der Buchseiten ein: "; cin >> seitenzahl; } void zeigedaten() { Publikation::zeigedaten(); cout << "\nDie Anzahl der Seiten beträgt: " << seitenzahl; } }; class CD : private Publikation { private: float spielzeit; public: void holedaten() { Publikation::holedaten(); cout << "\nBitte geben Sie die Spielzeit der CD ein: "; cin >> spielzeit; } void zeigedaten() { Publikation::zeigedaten(); cout << "\nDie Spielzeit der CD beträgt: " << spielzeit; } }; D:\75807564.doc vom 13.10.99 4-117 BBS III Mainz OOP für IT-Berufe mit C++ void main() { Buch buch1; CD cd1; buch1.holedaten(); cd1.holedaten(); buch1.zeigedaten(); cd1.zeigedaten(); getch(); } // Programm Verlag2.cpp #include <iostream.h> #include <conio.h> const int LEN = 80; const int Monate = 3; class Publikation { private: char titel[LEN]; float preis; public: void holedaten() { cout << "\nBitte geben Sie den Titel cout << "\nBitte geben Sie den Preis } void zeigedaten() { cout << "\nDer Titel ist: "; cout << cout << "\nDer Preis ist: "; cout << } }; ein: "; cin >> titel; ein: "; cin >> preis; titel; preis; class Verkauf { private: char vzahlen[Monate]; public: void holedaten() { int i; cout << "Bitte die Verkaufsergebnisse der letzten" << "3 Monate eingeben:\n"<<endl; for(i=0; i<Monate; i++) { cout << " Monat " << (i+1) <<": "; cin >> vzahlen[i]; } } void zeigedaten() { int i; for(i=0; i<Monate; i++) { cout << "\n Verkäufe für Monat " << i+1 << ": " << endl;; cout << vzahlen[i]; } D:\75807564.doc vom 13.10.99 4-118 BBS III Mainz OOP für IT-Berufe mit C++ } }; class Buch : private Publikation, private Verkauf { private: int seitenzahl; public: void holedaten() { Publikation::holedaten(); cout << "\nBitte geben Sie die Anzahl der Buchseiten ein: "; cin >> seitenzahl; Verkauf::holedaten(); } void zeigedaten() { Publikation::zeigedaten(); cout << "\nDie Anzahl der Seiten beträgt: " << seitenzahl; Verkauf::zeigedaten(); } }; class CD : private Publikation, private Verkauf { private: float spielzeit; public: void holedaten() { Publikation::holedaten(); cout << "\nBitte geben Sie die Spielzeit der CD ein: "; cin >> spielzeit; Verkauf::holedaten(); } void zeigedaten() { Publikation::zeigedaten(); cout << "\nDie Spielzeit der CD beträgt: " << spielzeit; Verkauf::zeigedaten(); } }; void main() { Buch buch1; CD cd1; buch1.holedaten(); cd1.holedaten(); buch1.zeigedaten(); cd1.zeigedaten(); getch(); } 6.4.5 // Programm Rechteck.cpp #include <iostream.h> #include <conio.h> class Quadrat { protected: double laenge; D:\75807564.doc vom 13.10.99 4-119 BBS III Mainz OOP für IT-Berufe mit C++ public: Quadrat(double len) { laenge = len; } double Laenge() { return laenge; } virtual double Breite() { return laenge; } double Flaeche() { return Laenge() * Breite(); } }; class Rechteck : public Quadrat { protected: double breite; public: Rechteck(double len, double wide) {laenge = len; breite= wide; } virtual double Breite() { return breite; } }; void main() { Quadrat quad(10); Rechteck recht(10, 12); cout << "Das Quadrat hat eine Seitenlaenge von " << quad.Laenge() << endl << " und eine Flaeche von " << quad.Flaeche() << endl; cout << "Das Rechteck hat eine Laenge von " << recht.Laenge() << endl << " und eine Breite von " << recht.Breite() << endl << " und eine Flaeche von " << recht.Flaeche() << endl; getch(); } D:\75807564.doc vom 13.10.99 4-120