------------------------------------------------------- ------------------------------------------------------- Index: 0.1 Vorbemerkungen. 0.2 Kleinere Neuheiten von C++. 1.1 Ein Rückblick: Konzepte modularer Programmierung 1.2 Die data members der class: Punkte in der Ebene. 1.3 Constructors und destructors. 1.4 Die member functions der class. 1.5 Friend functions einer class. 1.6 Überlagern von Operatoren. 1.7 Entdecken von Logikfehlern. 2.1 Die Datei BSP1C.H - Beispielprogramm : das class-Konzept in C++ ------------------------------------------------------- ------------------------------------------------------- 0.1 Vorbemerkungen. ------------------------------------------------------- ------------------------------------------------------- Dieses Skript ist ein weiterer Baustein in der Reihe von Skripten, die zur Unterstützung der Lehrveranstaltungsreihe DVG 1 bis 3 und EDV 1 und 2 im Studiengang Mathematik erarbeitet wurden. Es stützt sich insbesondere auf die Inhalte der Skripte Programmieren und Die Sprache C. Das Skript ist nicht zum Selbststudium, sondern nur als Begleitmaterial zur Lehrveranstaltung gedacht. Die Inhalte werden anhand von 6 Programmbeispielen erarbeitet. Die Quellen dieser 6 Programme stehen den Hörern zur Verfügung; z.Z. (SS 1997) sind sie auf dem Server des Labors für Numerische Mathematik im Bereich S: abgelegt. Wir empfehlen jedem Hörer, sich Kopien dieser Programme zu ziehen und die Programme auf dem häuslichen Rechner zu bearbeiten. Bearbeiten heißt hier nicht, die Programme zu übersetzen und laufen zu lassen. Zu einem gründlichen Studium dieser Beispiele gehört auch, die Quellen in einzelnen Punkten zu verändern, Fehler einzubauen und die Fehlermeldungen zu analysieren: insbesondere die Compiler von Borland liefern unserer Erfahrung nach gute Fehlermeldungen, deren Analyse das Verständnis der Konzepte von C++ fördert. Im übrigen bitten wir darum, genau auf den Titel dieses Skripts und der zugehörigen Lehrveranstaltung zu achten. Das Thema lautet „Einführung in Prinzipien der objektorientierten Programmierung" und soll die Absolventen unseres Studienganges in die Lage versetzen, in einer Arbeitsgruppe mitzuarbeiten, welche objektorientiert vorgeht. Der objektorientierte Entwurf selbst übersteigt den Zeitrahmen dieser Lehrveranstaltung. 0.2 Kleinere Neuheiten von C++. ------------------------------------------------------- ------------------------------------------------------- Da wir dringend empfehlen, die beigefügten Beispiele gründlich zu studieren, müssen wir vorab einige kleinere Neuheiten von C++ erläutern, die keine großen Konzepte einführen, aber deren Kenntnis für das Verständnis der Programme von Anfang an notwendig ist. Kommentare: In C++ besteht die Möglichkeit, am Ende einer Zeile einen Kommentar einzufügen: Cpunkt ( void ) // DEFAULT CONSTRUCTOR Der Kommentar beginnt mit dem Symbol // und geht bis zum Ende der Zeile. Die Kommentarklammern /* und */ sind natürlich weiterhin gültig. Ein- und Ausgabe: Für die Eingabe von der Tastatur und die Ausgabe auf dem Bildschirm gibt es eine neue Möglichkeit, die sich durch eine kurze Schreibweise auszeichnet. Beispiele: cout << "Komponente k[" << I << "] = " ; bewirkt, daß auf dem Bildschirm Komponente k[3] = erscheint (wir nehmen an, i enthalte den Wert 3). Die Größe cout steht für character-output-stream (was in C als stdout beschrieben wurde), << ist eine Art Pfeil, der auf cout hinzeigt. Man beachte, daß man mehrere auszugebende Größen unterschiedlicher Typen einfach mit << hintereinander hängen kann. Ähnlich ist die Eingabe. Um zwei Werte in die Variablen x und y einzulesen, kann man kurz folgende Anweisung benutzen: cin >> x >> y ; Die Größe cin steht für character-input-stream. Abschließend jedoch eine WARNUNG: auch wenn cout und cin dem stdout bzw. stdin von C vom Konzept her sehr ähnlich sehen, sollte man in einem Programm entweder nur die IO-streams von C++ (also cout und cin) oder nur die streams von ANSI C (also stdout, stdin mit printf bzw. scanf) benutzen, da manche Compiler diese vier streams strikt trennen und man auf dem Bildschirm die Ausgaben nicht in der Reihenfolge erhält, die man gerne hätte. Parameter-Beschreibung: C++ enthält eine neue Möglichkeit, die Parameter einer function zu beschreiben. Dazu sehen wir uns noch einmal ein einfaches Beispiel aus PASCAL an: procedure quadriere ( zahl : real ; var qu : real ) ; begin qu := zahl * zahl end ; { quadriere --------------------------------- } Aufruf: var a, a2 : real ; ........ quadriere ( a, a2 ) ; Die procedure quadriere enthält einen Vorgabe- und einen Rückgabeparameter. Beide formalen Parameter sind mit einem Namen (zahl bzw. qu) und ihrem Typ (real) vereinbart und werden auch mit diesen Namen im Prozedurrumpf angesprochen ( qu := zahl * zahl ). Der Unterschied zwischen Vorgabe- und Rückgabeparameter, also zwischen call by value und call by reference wird nur im Prozedurkopf daran sichtbar, daß im einen Fall das var fehlt, im anderen Fall vor der Namensliste steht. Beim Aufruf wird der Unterschied zwischen Vor- und Rückgabe nicht sichtbar, beide aktuellen Parameter (a und a2) werden nur mit ihrem Namen angeführt. Die technischen Details bleiben dem Compiler überlassen, der aufgrund der Angaben im Prozedurkopf dafür sorgt, daß von der Variablen a der Wert übergeben wird (call by value) und von der Variablen a2 die Adresse übergeben wird (call by reference; reference = Adresse = pointer). In C war dann alles ganz anders. Die entsprechende function lautet dort void quadriere ( float zahl , float * p_qu ) { * p_qu = zahl * zahl ; } /* quadriere ---------------------------*/ und der Aufruf lautet float a , a2 ; ...... quadriere ( a , & a2 ) ; Im function-Kopf wird explizit angegeben, ob ein Wert (zahl; call by value) oder eine Adresse (p_qu; call by reference) übergeben wird. Auch im function-Rumpf muß man beachten, daß p_qu nur die Adresse des aktuellen Rückgabeparameters ist und daß * p_qu für die Variable selbst steht. Diesen Unterschied muß man auch beim Aufruf beachten, wo man im 2. Parameter die Adresse übergeben muß ( & a2 ) . Kurz: in C muß man sich bei Parametern stets selbst darum kümmern, daß bei Vorgabe ein Wert und bei Rückgabe eine Adresse übergeben werden muß. „Stets" soll hier heißen: im function-Kopf, im function-Rumpf und beim Aufruf. In C++ gibt es nun eine neue Notation, in der dieses Beispiel wie folgt lautet: void quadriere ( float zahl , float & qu ) { qu = zahl * zahl ; } // quadriere --------------------------- Der entsprechende Aufruf lautet float a , a2 ; ...... quadriere ( a , a2 ) ; Es fällt auf, daß hier nur an einer einzigen Stelle gesagt wird, daß qu Ausgabeparameter ist, nämlich im function-Kopf. Das Symbol dafür ist & . Wir bemerken: * Das Symbol & ist uns bereits mit zwei Bedeutungen bekannt, nämlich als unärer Operator mit der Bedeutung „Adresse von" und als dualer Operator mit der Bedeutung „bitweises AND". * Hier wird diesem Symbol eine weitere Bedeutung gegeben, nämlich im Kontext mit formalen Parametern lautet sie: „Anweisung an den Compiler: verwende call by reference". * Im Kontext mit formalen Parametern hat das Symbol & in C++ also die gleiche Bedeutung wie das VAR in PASCAL: wir brauchen uns um die Details des Parametermechanismus keine Gedanken zu machen, dies erledigt der Compiler automatisch. ------------------------------------------------------- ------------------------------------------------------- 1.1 Ein Rückblick: Konzepte modularer Programmierung. ------------------------------------------------------- ------------------------------------------------------- Ein wesentlicher Aspekt der modularen Programmierung war die Sicherung von Daten vor Programmierfehlern durch Datenkapselung. Wollte man z.B. sicherstellen, daß eine Variable zaehler stets nur Werte zwischen 0 und 100 annimmt und kein noch so trickreicher Programmierer diese Bedingung absichtlich oder unabsichtlich verletzt, so konnte man die Variable im privaten Teil eines Paket kapseln: Datei zz.c : #include "zz.h" static int zaehler = 0 ; int increment ( void ) { if ( zaehler >= 100 ) return 1 ; /* return code 1 = FEHLER */ zaehler ++ ; return 0 ; /* return code 0 = ALLES O.K. */ } /* increment -------------------- */ int decrement ( void ) { if ( zaehler <= 0 ) RETURN 1 ; /* RETURN CODE 1 = FEHLER */ ZAEHLER -- ; RETURN 0 ; /* RETURN CODE 0 = ALLES O.K. */ } /* DECREMENT -------------------- */ .... WEITERE ROUTINEN DATEI ZZ.H : INT INCREMENT ( VOID ) ; INT DECREMENT ( VOID ) ; PROTOTYPEN WEITERER ROUTINEN Die Variable zaehler ist nun zwar global für alle Routinen in der Datei zz.c, sie ist jedoch durch das static lokal in diesem Teil des Pakets zz. Dem Anwender dieses Pakets (ein anderer Programmierer, der mit uns zusammenarbeitet) ist nur erlaubt, die Routinen increment und decrement zu benutzen, er hat auf die Variable zaehler keinen Zugriff. (HINWEIS: in einem sauberen Betriebssystem wie UNIX geben wir dem Anwender ein Leserecht nur auf die Quelldatei zz.h (den öffentlichen Teil des Pakets zz) sowie auf die (übersetzte, binäre) Objektdatei zz.o). Dieses Konzept hat leider einen großen Nachteil: auch wenn wir die Korrektheit aller Teile des Pakets bewiesen haben (was ein großer Vorteil ist), haben wir nur 1 Zähler. Wir wollen das Paket aber vielen Anwendungsprogrammierern zur Verfügung stellen. Der eine braucht vielleicht 5 Zähler, der andere 99. Wir wollen das Paket nicht ändern, und dennoch soll jeder Anwendungsprogrammierer frei entscheiden können, wie viele Variablen dieses besonders sicheren Zählertyps er braucht. Mit den Konzepten der modularen Programmierung ist dieses Problem nicht lösbar. 1.2 Die data members der class: Punkte in der Ebene. ------------------------------------------------------- ------------------------------------------------------- Das class-Konzept von C++ ist das geeignete Mittel, die Datenkapselung der modularen Programmierung, die ja auf dem Paket als Kapsel aufsetzt, auf die Variable als Kapsel zu übertragen. Als Kapsel wird dabei der schon aus PASCAL bekannte Begriff des RECORD, der in C mit dem Schlüsselwort struct realisiert ist, benutzt. Wir betrachten folgendes Beispiel: * Es soll ein neuer Datentyp für Punkte der Ebene definiert werden. * Aus Gründen, die wir hier nicht weiter erläutern wollen, sollen die Punkte in Polarkoordinaten (Abstand r, Winkel phi) gespeichert werden. * Das System (die class, welche wir bauen) soll die Programmentwicklung durch Fehlererkennung und -vermeidung so weit wie möglich unterstützten. Wir sehen hier schon einen möglichen gravierenden Fehler: die Mathematik verlangt, daß r stets >= 0 sei. Sollte eine Variable dieses Typs je einen negativen Wert für r enthalten, sind die Folgen im Programm unvorhersehbar. Wir vereinbaren den neuen Datentyp in C++ daher durch class Cpunkt { private: double r, phi ; .... Das Schlüsselwort class besagt, daß wir einen neuen Datentyp definieren wollen, der hier Cpunkt heißt (viele Entwickler beginnen die Namen neuer classes mit einem C). Jede Variable vom Typ Cpunkt ist ein struct (ein RECORD in PASCAL-Sprache) mit jeweils 2 Komponenten: Bild von zwei Objekten p1 und p2 Wir bleiben also bei unserem Modell, daß Variablen Kästchen seien. Hier sind die Variablen p1 und p2 daher Kästchen, die jeweils 2 kleinere Kästchen mit Namen r und phi enthalten. Die Komponenten r und phi sind jedoch als private deklariert: dies heißt, daß die Komponenten p1.r , p1.phi , p2.r und p2.phi zwar existieren, daß jedoch der Anwendungsprogrammierer nicht auf die Komponenten zugreifen kann. Dies sieht man sehr gut an den Fehlermeldungen des TURBO-C-compilers: p1 . r = 0.0 ; erzeugt: „p1 . r is not accessible ..." . p1 . x = 0.0 ; erzeugt: „p1 . x is not a member ..." . Ähnlich wie beim Entwurf des öffentlichen Teils eines Pakets müssen wir nun festlegen, welche Unterprogramme (functions) wir dem Anwender zur Verfügung stellen wollen, mit deren Hilfe er dann p1 und p2 bearbeiten kann. Diese functions, die wir (die Programmierer der class) dem Anwendungsprogrammierer zur Verfügung stellen wollen, müssen mit ihren Prototypen auch innerhalb der class angegeben werden. Da die Prototypen öffentlich sein sollen, folgen sie dem Schlüsselwort public: class Cpunkt { private: double r, phi ; public: ..... // Prototypen öffentlicher Routinen } ; 1.3 Constructors und destructors. ------------------------------------------------------- ------------------------------------------------------- Als erstes hinter dem Schlüsselwort public finden wir drei functions, die alle als Namen den Namen der class tragen: es sind sogenannte constructors. ..... public: // CONSTRUCTORS ohne und mit Parameter Cpunkt ( void ) // DEFAULT-CONSTRUCTOR { r = -1.0 ; /* unmöglicher Wert, um später ggfs. die fehlende Initialisierung zu erkennen */ } ; Cpunkt ( double r_anf , double phi_anf ) // INIT-CONSTRUCTOR { if ( r_anf < 0.0 ) CPUNKT_ERROR ( "Negativer Radius in Cpunkt :: Cpunkt (double,double)" ); ELSE { R = R_ANF ; PHI = PHI_ANF ; } } ; CPUNKT ( DOUBLE PHI_ANF ) // INIT-CONSTRUCTOR { R = 1.0 ; PHI = PHI_ANF ; } ; Constructors sind functions, die zu Beginn der „lifetime" einer Variablen aufgerufen und ausgeführt werden. Von PASCAL her sind wir gewohnt, daß eine Variable am Beginn ihrer Lebenszeit irgendwelche zufälligen Werte enthält, die wir in der LV „Datenschrott" nannten. Unter C++ wollen wir jederzeit volle Kontrolle über den Inhalt von Variablen haben, wir nutzen daher die Möglichkeiten, die uns mit den constructors gegeben sind. Die Lebenszeit einer Variablen beginnt mit ihrer Definition: Cpunkt a , b (3.0 , 1.2 ) , c ( -2.0 ) , d ( -4.2 , 0.0 ) ; Für jede der vier Variablen a, b, c und d wird der passende constructor aufgerufen, der nicht nur am Namen Cpunkt, sondern m.H. der Parameterliste erkannt wird: Für a wird der default-constructor Cpunkt ( void ) aufgerufen. Für b wird der init-constructor Cpunkt ( float, float ) aufgerufen. Für c wird der init-constructor Cpunkt ( float ) aufgerufen. Für d wird der init-constructor Cpunkt ( float, float ) aufgerufen. Für a, b und c ergeben sich folgende Variablen: Bild von drei Objekten a, b und c Bei b und c haben wir konkrete Anfangswerte vorgegeben, bei a die Komponente a.r auf den Wert -1.0 gesetzt: daran wollen wir später beim Programmlauf erkennen können, daß a noch keinen gültigen Wert enthält - jede Operation, die den Wert von a benutzen will, kann nur auf falscher Programmlogik beruhen, und diesen Fehler wollen wir erkennen können. Bei d liegt offensichtlich ein Programmierfehler vor, es wird versucht, der Variablen den Anfangswert d.r = -4.2 zuzuweisen. Dieser Fehler wird zur Laufzeit sofort erkannt, das Programm wird durch den Aufruf einer Fehlerroutine beendet. Hier (in diesem Beispiel) sieht man auch als Mensch den Fehler sofort. Bei Cpunkt d ( rr , winkel ) ; jedoch ist es eigentlich Aufgabe des Anwendungsprogrammierers, durch seine Programmlogik sicherzustellen, daß rr einen nicht-negativen Wert enthält. Enthält rr aber dennoch einen negativen Wert, wird dies durch den von uns geschriebenen constructor sofort entdeckt werden. Aus den „Aufrufen" der constructors ergibt sich auch, weshalb ihre Aufrufe nie einen Wert zurückliefern können: in Cpunkt a , b (3.0 , 1.2 ) , c ( -2.0 ) , d ( -4.2 , 0.0 ) ; ist von der Syntax her keine Gelegenheit vorgesehen, einen return-Wert zu verwerten. Ebenso wie constructors, die automatisch am Anfang der Lebenszeit einer Variablen aufgerufen werden, gibt es destructors, die am Ende der Lebenszeit automatisch aufgerufen werden. Das 1. Beispiel ist jedoch zu einfach, um sinnvolle destructors zeigen zu können. Der interessierte Leser möge jedoch die constructors um eine Meldung am Bildschirm ergänzen, einen destructor in die class einfügen und sich den Programmlauf am Bildschirm ansehen. ~Cpunkt ( void ) // der destructor { cout << "destructor aufgerufen\n" ; } 1.4 Die member functions der class. ------------------------------------------------------- ------------------------------------------------------- Die functions, die wir zur Verarbeitung von Variablen des Typs Cpunkt zur Verfügung stellen wollen, heißen member functions. Als Mitglieder der class Cpunkt haben sie Zugriff auf alle privaten Mitglieder der class, also Zugriff auf die Komponenten r und phi einer Variablen. Betrachten wir als erstes die member function read, mit der man einen neuen Wert für eine Variable einlesen kann. Der Prototyp innerhalb der class weist read als member function von Cpunkt aus: class Cpunkt { private: .... public: ..... void read ( void ) ; .... } ; Für den Aufruf einer member function ist eine besondere Notation vorgesehen: Cpunkt A ; ..... cout << "Bitte Koordinaten von A eingeben : " ; A . READ () ; Hier benutzen wir folgende Sprechweisen: * A . read () ; ist der Aufruf von read , * A ist das aktuelle Objekt (die aktuelle Variable) dieses Aufrufs. Der function-Rumpf ist außerhalb der class so definiert: void Cpunkt :: read ( void ) { do { cout << "r = " ; CIN >> r ; } while ( r < 0.0 ) ; COUT << "phi = " ; CIN >> phi ; } ; Der function-Kopf void read ( void ) ist ergänzt um die Angabe Cpunkt :: , zu welcher class dieses read gehört. Wenn man viele classes schreibt, kann man nicht für jede class neue Namen festlegen. Die Notation classname :: functionname erlaubt es, jede function eindeutig zu identifizieren. Wir sehen, daß innerhalb der function auf die data members r und phi zugegriffen wird, ohne zu sagen, zu welchem Objekt (zu welcher Variablen) diese Komponenten gehören. Dies ist auch nicht nötig. Innerhalb einer member function gilt: * r meint stets: Komponente r des aktuellen Objekts. * phi meint stets: Komponente phi des aktuellen Objekts. Innerhalb der class sind nach den constructors alle weiteren member functions aufgeführt. Dort sind erst einmal die member functions aufgeführt, die gleich innerhalb der class komplett mit Kopf und Rumpf beschrieben werden: double ko_r ( void ) { return r ; } ; Sinn: Lesender Zugriff auf die Komponente r einer Variablen. if ( a . ko_r () < 1.0 ) COUT << "Punkt a liegt innerhalb des Einheitskreises\n" ; double ko_phi ( void ) { return phi ; } ; Sinn: Lesender Zugriff auf die Komponente phi einer Variablen. if ( abs ( b . ko_phi () ) < PI_HALBE ) COUT << "Punkt b liegt rechts der y-Achse\n" ; (Man beachte, daß pi_halbe in der Datei BSP1C.H #defined ist). void set_r ( double r_neu ) { if ( r_neu >= 0.0 ) r = r_neu ; else Cpunkt_error ( "Negativer Wert in Cpunkt :: set_r" ) ; } ; Sinn: Schreibender Zugriff auf die Komponente r einer Variablen mit der Möglichkeit, den zuzuweisenden Wert vorher zu prüfen. c . set_r ( distance ) ; Der Anwendungsprogrammierer sollte durch die Programmlogik sicherstellen, daß die Variable distance einen nicht-negativen Wert enthält. Falls dies zur Laufzeit nicht der Fall ist (die Programmlogik also fehlerhaft ist), wird der Fehler durch die Routine set_r zum frühestmöglichen Zeitpunkt erkannt und das Programm mit einer Fehlermeldung abgebrochen. void set_phi ( double phi_neu ) { phi = phi_neu ; } ; Sinn: Schreibender Zugriff auf die Komponente phi einer Variablen. d . set_phi ( pi_halbe ) ; void spiegeln ( void ) { phi += 2.0 * pi_halbe ; } ; Sinn: Spiegelung eines Punktes am Ursprung. e . spiegeln () ; Bei den obigen 5 functions ko_r, ko_phi, set_r, set_phi und spiegeln fällt folgendes auf: * Es wird jeweils wenig getan, meist enthält der function-Rumpf nur eine einzige Anweisung. * Man könnte eigentlich anstelle des Aufrufs einer dieser functions die enthaltene Anweisung direkt hinschreiben. * Der Aufwand des function-Aufrufs mit Parameter-Übergabe, Aufruf und Rücksprung erscheint überzogen, die Effizienz scheint zu leiden. * Da r und phi jedoch private sind, scheint es keine andere Möglichkeit zu geben. Für diese 5 functions gilt jedoch auch folgendes: * Der function-Rumpf mit den Anweisungen ist direkt innerhalb der class geschrieben. * Diese Tatsache bedeutet: der function-Aufruf wird durch in-line- expansion realisiert. * in-line-expansion ist ein Begriff aus dem Compilerbau und besagt, daß jeder Aufruf der function durch die entsprechenden Anweisungen der function ersetzt wird. * Vorteil dieser Methode ist, daß der „Verwaltungsaufwand" eines Aufrufs mit Parameterübergabe, Aufruf und Rücksprung entfällt. * Nachteil der Methode ist, daß die Anweisungen der function vielfach im Programm eingesetzt werden. Bei 67 Aufrufen hat man 67 Kopien der An- weisungen der function. * in-line-expansion eignet sich für ganz kleine functions von 1 oder 2 Anweisungen, wo die Anweisungen in der Größenordnung des Verwaltungsaufwandes eines Aufrufs liegen. KURZ: Bei functions mit in-line-expansion haben wir den Vorteil, daß der Anwendungsprogrammierer die functions benutzen muß (die Datenkomponenten r und phi also weiterhin gekapselt sind), die übersetzten Anweisungen zur Laufzeit aber ebenso effizient wie ein direkter Zugriff sind. Weitere member functions sind void read ( void ) ; Sinn: Eingabe eines Variablenwerts von der Tastatur. f . read () ; void write ( void ) const ; Sinn: Ausgabe eines Variablenwerts auf dem Bildschirm. g . write () ; int quadrant ( void ) const ; Sinn: Berechnung des Quadranten, in dem ein Punkt liegt. cout << "Punkt h liegt im " << H . QUADRANT () << " Quadranten." ; Für die 3 member function read, write und quadrant gilt: * Innerhalb der class ist jeweils nur der Prototyp der function deklariert. * Die komplette function ist außerhalb der class in der Datei BSP1C.CPP definiert. * Die Aufrufe dieser functions werden (wie die meisten function-Aufrufe in C) mit out-of-line-expansion übersetzt. * Dies heißt, daß die Anweisungen der function nur einmal im Programm abgelegt werden und daß jeder Aufruf als Übergabe der Parameter und Sprung in das Unterprogramm übersetzt werden. (Das Unterprogramm enthält dann die Anweisungen für den Rücksprung). * Vorteil dieser Methode ist, daß die Anweisungen der function nur 1 Mal übersetzt werden. * Nachteil ist, daß bei jedem Aufruf nicht nur die Anweisungen ausgeführt werden müssen, sondern daß noch der Verwaltungsaufwand für den Aufruf hinzukommt. * out-of-line-expansion eignet sich also für größere functions. 1.5 Friend functions einer class. ------------------------------------------------------- ------------------------------------------------------- Nun gibt es jedoch Fälle, wo man als Programmierer einer class noch zusätzliche Routinen schreiben muß, die nicht member functions sind (wo man also nicht direkten Zugriff auf die Komponenten r und phi hat), wo man aber den Aufwand scheut, jeden Zugriff als function-call schreiben zu müssen. Ein Beispiel dafür ist die function double abstand ( Cpunkt, Cpunkt ) ; die den Abstand zweier Punkte in der Ebene nach dem Satz des Pythagoras berechnet. Wir wollen diese function so aufrufen: distance = abstand ( P2 , P3 ) ; Da beide Punkte P2 und P3 mathematisch „gleichberechtigt" sind, wollen wir keinen dadurch hervorheben, daß wir ihn zum aktuellen Objekt des Aufrufs machen (dann könnte abstand nämlich member function werden). Eigentlich müßte der Zugriff auf r und phi innerhalb der function also m.H. der member-functions ko_r und ko_phi erfolgen: double abstand ( Cpunkt p1, Cpunkt p2 ) { double x1, y1, x2, y2, dx, dy, dxq, dyq; ........ x1 = p1.ko_r() * cos ( p1.ko_phi() ) ; y1 = p1.ko_r() * sin ( p1.ko_phi() ) ; x2 = p2.ko_r() * cos ( p2.ko_phi() ) ; y2 = p2.ko_r() * sin ( p2.ko_phi() ) ; ........ Innerhalb der class erklären wir jedoch abstand zur friend function dieser class: friend double abstand ( Cpunkt, Cpunkt ) ; Damit lauten dann die gleichen Anweisungen wie oben double abstand ( Cpunkt p1, Cpunkt p2 ) { double x1, y1, x2, y2, dx, dy, dxq, dyq; ............. x1 = p1.r * cos ( p1.phi ) ; y1 = p1.r * sin ( p1.phi ) ; x2 = p2.r * cos ( p2.phi ) ; y2 = p2.r * sin ( p2.phi ) ; .......... Die vollständigen functions kann man sich in der Quelle ansehen, die am Ende dieses Skripts gezeigt wird. 1.6 Überlagern von Operatoren. ------------------------------------------------------- ------------------------------------------------------- In der Mathematik ist es üblich (und nötig), daß ein Rechenzeichen viele unterschiedliche Operationen bezeichnet. Aus dem Kontext heraus ergibt sich, was gemeint ist. So bedeutet der Operator + einmal die Addition von ganzen Zahlen, ein anderes Mal die Addition von Vektoren, ein weiteres Mal die Addition von Matrizen. Ähnliches kennen wir von PASCAL und C her. Das + in TURBO PASCAL hat drei Verwendungen: Addition von integern, von reals und die Konkatenation von strings. Der * in C findet Verwendung in den beiden Multiplikationen (int und float), als „Zeiger auf" in der Typdefinition float * a ; und als „Variable bei Adresse ..." (indirection-operator) im Ausdruck *a = 0.0 ; Wir wollen jetzt den Operator * auch für Operationen mit Variablen unseres neuen Typs Cpunkt nutzen, und zwar mit folgender Bedeutung: * für zwei Objekte Cpunkt p1 , p2; soll p1 * p2 bedeuten: Ergebnis ist wiederum ein Objekt vom Typ Cpunkt, dessen Radius r gleich p1.r * p2.r und dessen Winkel phi gleich p1.phi + p2.phi ist. * für float f; und Cpunkt p; soll f * p definiert werden und bedeuten: f*p ist ein Objekt vom Typ Cpunkt, dessen Radius r gleich f * p1.r und dessen Winkel phi gleich p1.phi ist. Als erstes müssen wir uns überlegen, wie ein Ausdruck p1 * p2 programmtechnisch realisiert werden soll. Das übliche Mittel, eine Menge von Anweisungen zu einer Einheit zusammenzufassen, ist die function; die beteiligten Variablen werden Parameter der function. Der Name der function ist in C++ vorgeschrieben. Er besteht aus dem Wort operator gefolgt von dem gewählten Operator-Symbol: operator* Es bleibt jetzt die Frage, was für eine Art function operator* ist: eine member function oder eine (gewöhnliche) nicht-member function. * Falls operator* nicht eine member function ist, gilt: p1 * p2 ; wird interpretiert als Aufruf operator* (p1,p2) ; Beide Operatoren sind Parameter von operator* . * Falls operator* eine member function ist, gilt: p1 * p2 ; wird interpretiert als Aufruf p1 . operator* (p2) ; p1 ist aktuelles Objekt des Aufrufs, p2 ist Parameter von operator* . Im Prinzip ist es uns (dem Programmierer, der die class entwirft) überlassen, die Art der function (member oder nicht member) festzulegen. Bei der member function haben jedoch die beteiligten Operanden nicht den gleich Status: der linke Operand wird als aktuelles Objekt des Aufrufs hervorgehoben, der rechte Operand wird „nur" Parameter. Da die von uns entworfene Verknüpfung kommutativ ist, beide Operanden also „gleichberechtigt" sind, haben wir uns in diesem Beispiel dafür entschieden, operator* als nicht-member function zu schreiben. Um auf die Komponenten r und phi einfach zugreifen zu können, müssen wir dann operator* als friend deklarieren: friend Cpunkt operator * ( Cpunkt, Cpunkt ) ; Auch bei der Verknüpfung eines float mit einem Cpunkt haben wir uns für das Operatorsymbol * und die Realisierung als nicht-member function entschieden: friend Cpunkt operator * ( double , Cpunkt ) ; Wir haben jetzt also 2 functions des gleichen Namens operator* . Dies macht aber nichts, denn: In C++ wird eine function durch die komplette Schnittstelle bestehend aus - Name der function, - Parameterliste (Anzahl und jeweilige Typen) und - Typ des return-Wertes beschrieben. Es handelt sich oben also um 2 functions, die ohne Probleme unterscheidbar sind. Die Details der beiden functions möge der Leser sich bitte in der Quelle ansehen. 1.7 Entdecken von Logikfehlern. ------------------------------------------------------- ------------------------------------------------------- Angesichts der Schnelligkeit heutiger Rechner liegt der Schwerpunkt der Programmierung nicht darauf, durch trickreiches Programmieren das Programm ein Quentchen schneller zu machen. Das Hauptziel ist es, fehlerfreie Programme zu entwickeln und die Fehler, die sich dennoch einschleichen, zur Laufzeit so früh wie möglich zu erkennen. Wir haben deshalb bei anfangs „leeren" Variablen durch den DEFAULT-constructor dafür gesorgt, daß die Komponente r nicht einfach „Datenschrott", sondern den Wert -1.0 enthält. Dadurch können wir bei jedem Gebrauch der Werte einer Variablen prüfen, ob diese Variable vom Programm bereits mit einem gültigen Wert belegt wurde. Beispiel: void Cpunkt_error ( char * t ) { cout << "FEHLER: " << T ; EXIT ( 99 ) ; } // CPUNKT_ERROR -------------------------------------- VOID CPUNKT :: WRITE ( VOID ) CONST { IF ( R < 0.0 ) CPUNKT_ERROR ( "Nicht initialisierte Variable in Cpunkt :: write" ); ELSE COUT << "(r=" << R << ";phi=" << PHI << ")" ; } ; INT CPUNKT :: QUADRANT ( VOID ) CONST { INT Q = FABS ( PHI ) / PI_HALBE ; IF ( R < 0.0 ) CPUNKT_ERROR ( "Nicht initialisierte Variable" " in Cpunkt :: quadrant" ) ; ELSE { IF ( PHI >= 0.0 ) return ( q % 4 + 1 ) ; else return ( 4 - ( q % 4 ) ) ; } } ; Zur Ausgabe der Fehlermeldung und zum Programmabbruch gibt es die function Cpunkt_error. In den functions, in denen Werte von Typ Cpunkt verarbeitet werden sollen, wird als erstes geprüft, ob gültige Werte vorhanden sind. Da wir sicher sind, daß unsere member functions fehlerfrei programmiert wurden, schließen wir aus einem negativen Wert von r darauf, daß dieser Wert -1.0 sein muß, daß also eine vom Benutzerprogramm nicht weiter initialisierte Variable vorliegt. Ansonsten achten wir bei allen functions, die den Wert einer Variablen ändern, darauf, daß der neue Wert für r die Bedingung r >= 0.0 erfüllt: void set_r ( double r_neu ) { if ( r_neu >= 0.0 ) r = r_neu ; else Cpunkt_error ( "Negativer Wert in Cpunkt :: set_r" ) ; } ; void Cpunkt :: read ( void ) { do { cout << "r = " ; CIN >> r ; } while ( r < 0.0 ) ; COUT << "phi = " ; CIN >> phi ; } ; 2.1 Die Datei BSP1C.H ------------------------------------------------------- ------------------------------------------------------- /* 1. Beispielprogramm : das class-Konzept in C++ ; Einführung des neuen Datentyps Cpunkt hier: oeffentlicher Teil (header-file) */ /* Die Datenobjekte des Typs Cpunkt sind Punkte in der Ebene, die wir in Polarkoordinaten darstellen wollen (es gibt auch andere Möglich- keiten, aber wegen des Problemhintergrundes (den wir hier nicht weiter erläutern) wollen wir das so !!!). Die Datenobjekte wollen wir wie folgt bearbeiten: - Variablen einlesen, einander zuweisen, ausgeben; - Variablen bei der Definition mit Werten vorbelegen: hier inter- essieren insbesondere Punkte auf dem Einheitskreis; - einer Variablen einen Literalwert zuweisen; - Konstanten definieren; - den Abstand eines Punktes vom Ursprung vervielfachen: + der Faktor f muß stets positiv ( > 0.0 ) sein ; + diese Operation soll geschrieben werden können als f * P; - zwei Punkte verknüpfen, indem man die Radien multipliziert und die Winkel addiert; Verknüpfungssymbol: * ; - Punkte am Ursprung spiegeln; - den Abstand zweier Punkte bestimmen ; - man kann auf die Koordinaten einer Variablen sowohl lesend als auch schreibend zugreifen. Darüber hinaus verlangen wir: - das Arbeiten mit diesen Datenobjekten soll vom System so weit wie möglich unterstützt werden, d.h. + es ist stets sichergestellt, daß r >= 0.0 gilt (weder dem dümm sten noch dem trickreichsten Programmierer soll es möglich sein, eine Variable mit negativem r zu produzieren); + der Zugriff auf nicht initialisierte Variablen wird vom System als Fehler erkannt: man ist aber nicht gezwungen, jede Variable sofort zu initialisieren. */ #include void Cpunkt_error ( char * ) ; static const float pi_halbe = 2.0 * atan ( 1.0 ) ; /* ********************* HIER GEHT ES LOS ************************* */ class Cpunkt { private: double r, phi ; public: // CONSTRUCTORS ohne und mit Parameter Cpunkt ( void ) // DEFAULT-CONSTRUCTOR { r = -1.0 ; /* unmöglicher Wert, um später ggfs. die fehlende Initialisierung zu erkennen */ } ; Cpunkt ( double r_anf , double phi_anf ) // INIT-CONSTRUCTOR { if ( r_anf < 0.0 ) Cpunkt_error ( "Negativer Radius in Cpunkt :: Cpunkt (double,double)" ); else { r = r_anf ; phi = phi_anf ; } } ; Cpunkt ( double phi_anf ) // INIT-CONSTRUCTOR { r = 1.0 ; phi = phi_anf ; } ; // weitere MEMBER-FUNCTIONS : // Beispiele fuer functions mit in-line-expansion : double ko_r ( void ) { return r ; } ; double ko_phi ( void ) { return phi ; } ; void set_r ( double r_neu ) { if ( r_neu >= 0.0 ) r = r_neu ; else Cpunkt_error ( "Negativer Wert in Cpunkt :: set_r" ) ; } ; void set_phi ( double phi_neu ) { phi = phi_neu ; } ; void spiegeln ( void ) { phi += 2.0 * pi_halbe ; } ; // Beispiele fuer functions mit out-of-line-expansion : void read ( void ) ; void write ( void ) const ; int quadrant ( void ) const ; // FRIEND-FUNCTIONS: // Beispiel fuer eine "normale" friend-functions: friend double abstand ( Cpunkt, Cpunkt ) ; // Bitte mit der nicht-friend-Loesung vergleichen // Beispiel fuer ueberladene Operatoren: friend Cpunkt operator * ( double , Cpunkt ) ; friend Cpunkt operator * ( Cpunkt, Cpunkt ) ; } ; /* class Cpunkt ---------------------------------- */ Die Datei BSP1C.CPP /* 1. Beispielprogramm : das class-Konzept in C++ ; Einführung des neuen Datentyps Cpunkt hier: privater Teil ( cpp-file ) */ #include #include #include #include "bsp1c.h" // HILFS-FUNCTIONS der class Cpunkt: void Cpunkt_error ( char * t ) { cout << "FEHLER: " << t ; exit ( 99 ) ; } // Cpunkt_error -------------------------------------- // MEMBER-FUNCTIONS der class Cpunkt : // da die functions hier definiert werden, werden sie alle mit // out-of-line expansion uebersetzt. void Cpunkt :: read ( void ) { do { cout << "r = " ; cin >> r ; } while ( r < 0.0 ) ; cout << "phi = " ; cin >> phi ; } ; void Cpunkt :: write ( void ) const { if ( r < 0.0 ) Cpunkt_error ("Nicht initialisierte Variable in Cpunkt :: write" ); else cout << "(r=" << r << ";phi=" << phi << ")" ; } ; int Cpunkt :: quadrant ( void ) const { int q = fabs ( phi ) / pi_halbe ; if ( r < 0.0 ) Cpunkt_error ( "Nicht initialisierte Variable " "in Cpunkt :: quadrant" ) ; else { if ( phi >= 0.0 ) return ( q % 4 + 1 ) ; else return ( 4 - ( q % 4 ) ) ; } } ; // FRIEND-FUNCTIONS der class Cpunkt : double abstand ( Cpunkt p1, Cpunkt p2 ) { double x1, y1, x2, y2, dx, dy, dxq, dyq; if ( p1.r < 0.0 ) Cpunkt_error ( "Nicht initialisierte Variable in " " Cpunkt :: abstand, 1. Param." ) ; if ( p2.r < 0.0 ) Cpunkt_error ( "Nicht initialisierte Variable in " " Cpunkt :: abstand, 2. Param." ) ; // Es folgt der verkappte else-Teil: x1 = p1.r * cos ( p1.phi ) ; y1 = p1.r * sin ( p1.phi ) ; x2 = p2.r * cos ( p2.phi ) ; y2 = p2.r * sin ( p2.phi ) ; dx = x1 - x2 ; dxq = dx * dx ; dy = y1 - y2 ; dyq = dy * dy ; return ( sqrt ( dxq + dyq ) ) ; } /* ZUM VERGLEICH: die NICHT-FRIEND-LOESUNG double abstand ( Cpunkt p1, Cpunkt p2 ) { double x1, y1, x2, y2, dx, dy, dxq, dyq; if ( p1 . ko_r () < 0.0 ) Cpunkt_error ( "Nicht initialisierte Variable in " " Cpunkt :: abstand, 1. Param." ) ; if ( p2 . ko_r () < 0.0 ) Cpunkt_error ( "Nicht initialisierte Variable in " " Cpunkt :: abstand, 2. Param." ) ; // Es folgt der verkappte else-Teil: x1 = p1.ko_r() * cos ( p1.ko_phi() ) ; y1 = p1.ko_r() * sin ( p1.ko_phi() ) ; x2 = p2.ko_r() * cos ( p2.ko_phi() ) ; y2 = p2.ko_r() * sin ( p2.ko_phi() ) ; dx = x1 - x2 ; dxq = dx * dx ; dy = y1 - y2 ; dyq = dy * dy ; return ( sqrt ( dxq + dyq ) ) ; } ------------------------------------------------------------- */ Cpunkt operator * ( double f , Cpunkt P ) /* berechnet f * P */ { Cpunkt res ; if ( f <= 0.0 ) Cpunkt_error ( "Faktor f <= 0.0 in operator * ( double f, Cpunkt P )" ) ; if ( P.r < 0.0 ) Cpunkt_error ( "Nicht initialisierte Variable P in " " operator * ( double f, Cpunkt P )" ) ; res.r = f * P.r; res.phi = P.phi ; return res ; } /* --------------- Cpunkt operator * ( double f , Cpunkt P ) */ Cpunkt operator * ( Cpunkt P1, Cpunkt P2 ) /* berechnet P1 * P2 */ { Cpunkt res ; if ( P1.r < 0.0 ) Cpunkt_error ( "Nicht initialisierte Variable P1 in Cpunkt operator*" " (Cpunkt P1, Cpunkt P2)" ) ; if ( P2.r < 0.0 ) Cpunkt_error ( "Nicht initialisierte Variable P2 in Cpunkt operator*" " (Cpunkt P1, Cpunkt P2)" ) ; res . r = P1.r * P2.r ; res . phi = P1.phi + P2.phi ; return res ; } /* ------------- Cpunkt operator * ( Cpunkt P1, Cpunkt P2 ) */ Die Datei BSP1MAIN.CPP /* 1. Beispielprogramm : das class-Konzept in C++ ; hier: DEMO-Hauptprogramm */ #include #include #include #include "bsp1c.h" static const double pi = 4.0 * atan ( 1.0 ) ; void main ( void ) { Cpunkt p1 , p2 ( pi / 2.0 ) , p4 , p5 , p6 ; const Cpunkt p3 ( 3.0 , pi/4.0 ) ; double ff, dist ; int quad ; #ifdef __TURBOC__ clrscr () ; #else setbuf ( stdout , NULL ) ; setbuf ( stdin , NULL ) ; #endif cout << "Testprogramm BSP1 beginnt\n" ; cout << "T E I L 1 : Konstruktor und Zuweisung erläutern\n" ; p1 = p2 ; cout << "p1 ist jetzt " ; p1 . write () ; cout << "\n\n" ; do { cout << "T E I L 2 : Quadranten bestimmen und spiegeln\n" ; cout << "Koordinaten eines Punktes bitte : (r=0.0 = ENDE)\n" ; p1 . read () ; quad = p1 . quadrant () ; cout << "Der Punkt " ; p1 . write () ; cout << " liegt im " << quad << ". Quadranten.\n" ; p1 . spiegeln () ; quad = p1 . quadrant () ; cout << "Der Punkt " ; p1 . write () ; cout << " liegt im " << quad << ". Quadranten.\n\n" ; } while ( p1 . ko_r () > 0.0 ) ; cout << "\n\n" ; do { cout << "T E I L 3 : Strecken und Verknüpfen\n" ; cout << "Koordinaten eines Punktes bitte : (r=0.0 = ENDE)\n" ; p1 . read () ; cout << "Streckungsfaktor (auch negativ probieren) : " ; cin >> ff ; p4 = ff * p1 ; p5 = p3 * p1 ; dist = abstand ( p3 , p1 ) ; cout << "p3 ist " ; p3 . write () ; cout << " \n" ; cout << "ff * p1 liefert " ; p4 . write () ; cout << "\n" ; cout << "p3 * p1 liefert " ; p5 . write () ; cout << "\n" ; cout << "Abstand (p1,p3) ist " << dist << "\n\n" ; } while ( p1 . ko_r () > 0.0 ) ; cout << "\nNormalteil des Testprogramm BSP1 endete\n\n" ; cout << "T E I L 4 : Initialisierung + Prüfung erläutern\n" ; p1 = Cpunkt ( 1.2 , 3.4 ) ; cout << "p1 ist " ; p1 . write () ; cout << " \n" ; cout << "p6 ist " ; p6 . write () ; cout << " \n" ; } /* main ------------------------------------------- */