|
I-PROGR2 SS03 - PROGRAMMIEREN2 - Vorlesung mit Übung
|
Aus Programmieren 1 am Beispiel der Sprache C sind die Standardoperatoren bekannt, die auf Variablen der Standardtypen anwendbar waren, etwa:
c = a + b mit a,b,c als int
c += b mit a,b als float
c = a[i] mit a als Array vom Typ char
c != a mit a,c vom Typ double
usw.
In C++ kann der Anwender eigene, neue Typen einführen, insbesondere Klassen. Die Standardoperatoren sind für diese neuen Typen zunächst nicht definiert. es soll in dieser Vorlesung gezeigt werden, wie dies möglich ist.
Bevor hier gezeigt werden wird, wie man bekannte Operatorsymble für neue, eigene Typen im Kontext von Klassen verwenden kann, hier eine Zusammenfassung der Standarddatentypen und der Standardoperatoren unter C/C++.
Wertebereich |
|||
int |
Ganze Zahl |
4 |
|
float |
Gleitkomma mit einfacher Genauigkeit |
4 |
|
double |
Gleitkomma mit doppelter Genauigkeit |
8 |
|
char |
Zeichen |
1 |
|
unsigned char |
Zeichen |
1 |
[0,255] |
signed char |
Zeichen |
1 |
[-128,127] |
bool |
Boolscher Wert (true/false) |
1 |
|
enum |
Aufzählungen von Namen, die ganze Zahlen repräsentieren |
||
void |
leer/ nichts; für Funktionen ohne Rückgabewert |
||
unsigned int |
4 |
[0,4294967295] |
|
signed int |
Ganze Zahl |
4 |
[-2147483648,2147483647] |
short int |
2 |
||
signed short int |
2 |
[-32768, 32767] |
|
unsigned short int |
|
4 |
[0,65535] |
long int |
4 |
||
signed long int |
4 |
[-2147483648,2147483647] |
|
unsigned long int |
4 |
[0,4294967295] |
|
long double |
12 |
In der vorausgehenden Aufstellung werden zusätzlich Attribute zu den Basis-Datentypen verwendet:
Bedeutung |
|
short |
eventuell kleiner als 'normal' |
long |
eventuell grösser als 'normal' |
unsigned |
vorzeichenfrei |
signed |
vorzeichenbehaftet |
Welche Auslegungen die einzelnen Datentypen genau haben, das kann man einerseits der Headerdatei limits.h entnehmen, andererseits kann man das mit dem Operator sizeof() überprüfen. Dieser Operator wurde von der Sprache C übernommen; innerhalb von C++ hat er aber weiter keine grössere Bedeutung. Dennoch kann man ihn benutzen, wie das nachfolgende kleine beisoiel zeigt, um sich die aktuellen Datengrössen anzeigen zu lassen:
//************************* // // hallo2.cpp // // first: march-14,2002 // last: april-22,2003 // // Prints a message on standard out // Shows the size of some basic datatypes // // compile: g++ -o hallo2 hallo2.cpp // usage: hallo2 // //******************************* #include <iostream> using namespace std; int main(){ cout << "Hallo Welt!" << endl; // Using sizeof() to show the actual byte-size of some data types cout << "CHAR = " << sizeof(char) << endl; cout << "INT = " << sizeof(int) << endl; cout << "UNSIGNED INT = " << sizeof(unsigned int) << endl; cout << "SHORT INT = " << sizeof(short int) << endl; cout << "UNSIGNED SHORT INT = " << sizeof(unsigned short int) << endl; cout << "SIGNED SHORT INT = " << sizeof(signed short int) << endl; cout << "LONG INT = " << sizeof(long int) << endl; cout << "SIGNED LONG INT = " << sizeof(signed long int) << endl; cout << "FLOAT = " << sizeof(float) << endl; cout << "DOUBLE = " << sizeof(double) << endl; cout << "BOOL = " << sizeof(bool) << endl; cout << "LONG DOUBLE = " << sizeof(long double) << endl; }
gerd@goedel:~/WEB-MATERIAL/fh/I-PROGR2/I-PROGR2-EX/EX7> ./hallo2 Hallo Welt! CHAR = 1 INT = 4 UNSIGNED INT = 4 SHORT INT = 2 UNSIGNED SHORT INT = 2 SIGNED SHORT INT = 2 LONG INT = 4 SIGNED LONG INT = 4 FLOAT = 4 DOUBLE = 8 BOOL = 1 LONG DOUBLE = 12
Will man die vorhandenen Standardoperatoren für eigene Klassen in neuer Weise benutzen, dann muss man beachten, dass die Menge der möglichen Operatoren fest liegt; zusammen damit auch Priorität, Syntax und Auswertungsreihenfolge. Deswegen kann man mit den eigenen Definitionen die vorhandenen Definitionen nur erweitern. Ausserdem werden die Operationen, die für elementare Datentypen definiert sind, nicht automatisch auf abstrakte Datentypen übertragen.
Folgende Operatoren können nicht überladen werden:
{'.', '::', 'sizeof', '.*','?:'}
Sie sind schon für alle Objekte definiert ('.*' ist ein Zeiger auf die Komponente einer bestimmten Klasse).
Die Liste der Standard-Operatoren wird hier nochmals aufgelistet. Die Anordnung folgt der Prioritäts-Hierarchie der Operatoren; soweit möglich, wurden thematisch zusammengehörige Operatoren hervorgehoben.
BEREICHS OPERATOREN |
BEDEUTUNG |
(...) |
Klammerung |
:: |
Bereichszuordnung (z.B. Elementfunktion zur Klasse) |
ZUGRIFFS OPERATOREN |
BEDEUTUNG |
. |
Zugriff auf Komponente (z.B. a.v) |
-> |
Zugriff auf Komponente von Zeigern |
[...] |
Feldindizierung |
POSTFIX |
BEDEUTUNG |
++ |
Inkrement (a++) |
-- |
Dekrement (a--) |
Typumwandlung; IDs |
BEDEUTUNG |
type(...) |
Typumwandlung (int(a);) |
static_cast<typ>(...) |
Typumwandlung |
const_cast<typ>(...) |
Typumwandlung |
dynamic_cast<typ>(...) |
Typumwandlung |
reinterprete_<typ>(...) |
Typumwandlung |
typeid(...) |
Klassen-ID |
PRÄFIX |
BEDEUTUNG |
++ |
Inkrement (++a) |
-- |
Dekrement (--a) |
~ |
Bit-Komplement |
! |
Logische Negation |
+ |
Positives Vorzeichen |
- |
negatives Vorzeichen |
& |
Adresse von |
* |
Inhalt einer Adresse (Dereferenzierung) |
new |
Objekt anlegen |
delete |
Objekt freigeben |
new[] |
Feld von Objekten anlegen |
delete[] |
Feld von Objekten freigeben |
(typ)... |
Typumwandlung mit der cast-Notation |
sizeof(...) |
Speicherplatz |
KOMPONENTEN |
BEDEUTUNG |
.* |
Komponente über Komponentenzeiger |
->* |
Komponente von Zeiger über Komponentenzeiger |
ARITHMETISCHER |
BEDEUTUNG |
* |
Multiplikation |
/ |
Division |
% |
Modulo-Operator |
+ |
Addition |
- |
Subtraktion |
SHIFT |
BEDEUTUNG |
<< |
Links-Shift |
>> |
Rechts-Shift |
VERGLEICHSOPERATOREN |
BEDEUTUNG |
< |
kleiner |
<= |
kleiner-gleich |
> |
grösser |
>= |
grösser-gleich |
== |
gleich |
!= |
nicht gleich, ungleich |
BITOPERATOREN (infix) |
BEDEUTUNG |
& |
Bitweises UND |
^ |
Bitweises exklusives ODER (XOR) |
| |
Bitweises ODER |
LOGISCHE OPERATOREN (infix) |
BEDEUTUNG |
&& |
Logisches UND (AND) (Sucht von links nach rechts das erste false. Wird keines gefunden, dann ist das logische UND true) |
|| |
Logisches ODER (OR) (Sucht von links nach rechts das erste true. Wird keines gefunden, dann ist das logische ODER false) |
?: |
Bedingte Bewertung (a ? x : y) |
ZUWEISUNGEN (infix) |
BEDEUTUNG |
SCHREIBWEISE |
= |
Einfache Wertzuweisung |
(z.B. a=5;) |
*= |
Multiplikation |
"a op= b" entspricht "a = a op b" |
/= |
Division |
|
%= |
Modulo |
|
+= |
Addition |
|
-= |
Subtraktion |
|
<<= |
Shift links |
|
>>= |
Shift rechts |
|
&= |
Bitwises UND |
|
^= |
Bitwises exklusiv ODER |
|
|= |
Bitwises ODER |
Zu beachten ist, dass Wertzuweisungen Teil von grösseren Ausdrücken sein können!
AUSDRUCKSMERGE |
BEDEUTUNG |
, |
Kommaoperator; verbindet mehrere Anweisungen zu einer (speziell in For-Schleifen) |
Um nun zu zeigen, wie man die bekannten Operatoren im Kontext seiner eigenen neuen Klassen mit neuen Bedeutungen aufladen kann, betrachten wir ein einfaches Beispiel, das wir dem C++-Buch von [JOSUTTIS 2001:156ff] entnehmen (der Sourcekode der Beispiele ist weitgehend über Internet zugänglich).
Demo-Klasse Bruch nach Josuttis
Die Klasse Bruch hat die privaten Attribute zaehler und nenner vom Typ int. Dann verfügt die Klasse Bruch über drei öffentliche Konstruktoren, eine print()-Funktion sowie drei eigenen Operatoren, die von bekannten Operator-Symbolen Gebrauch machen.
Die zugehörige Headerdatei lautet wie folgt:
/* Die folgenden Code-Beispiele stammen aus dem Buch: * Objektorientiertes Programmieren in C++ * Ein Tutorial für Ein- und Umsteiger * von Nicolai Josuttis, Addison-Wesley München, 2001 * * (C) Copyright Nicolai Josuttis 2001. * Diese Software darf kopiert, verwendet, modifiziert und verteilt * werden, sofern diese Copyright-Angabe in allen Kopien vorhanden ist. * Diese Software wird "so wie sie ist" zur Verfügung gestellt. * Es gibt keine explizite oder implizite Garantie über ihren Nutzen. */ #ifndef BRUCH2_HPP #define BRUCH2_HPP namespace Bsp { class Bruch { private: int zaehler; int nenner; public: // Default-Konstruktor Bruch (); // Konstruktor aus int (Zähler) Bruch (int); // Konstruktor aus zwei ints (Zähler und Nenner) Bruch (int, int); // Ausgabe void print (); // neu: Multiplikation mit anderem Bruch Bruch operator * (Bruch); // neu: multiplikative Zuweisung Bruch operator *= (Bruch); // neu: Vergleich mit anderem Bruch bool operator < (Bruch); }; } #endif // BRUCH2_HPP
Die zugehörige Implementierung:
/* Die folgenden Code-Beispiele stammen aus dem Buch: * Objektorientiertes Programmieren in C++ * Ein Tutorial für Ein- und Umsteiger * von Nicolai Josuttis, Addison-Wesley München, 2001 * * (C) Copyright Nicolai Josuttis 2001. * Diese Software darf kopiert, verwendet, modifiziert und verteilt * werden, sofern diese Copyright-Angabe in allen Kopien vorhanden ist. * Diese Software wird "so wie sie ist" zur Verfügung gestellt. * Es gibt keine explizite oder implizite Garantie über ihren Nutzen. */ // Headerdatei mit der Klassen-Deklaration einbinden #include "bruch2.hpp" // Standard-Headerdateien einbinden #include <iostream> #include <cstdlib> // **** BEGINN Namespace Bsp ******************************** namespace Bsp { /* Default-Konstruktor */ Bruch::Bruch () : zaehler(0), nenner(1) // Bruch mit 0 initialisieren { // keine weiteren Anweisungen } /* Konstruktor aus ganzer Zahl */ Bruch::Bruch (int z) : zaehler(z), nenner(1) // Bruch mit z 1tel initialisieren { // keine weiteren Anweisungen } /* Konstruktor aus Zähler und Nenner */ Bruch::Bruch (int z, int n) { /* neu: Ein negatives Vorzeichen im Nenner kommt in den Zähler * Dies vermeidet u.a. eine Sonderbehandlung beim Operator < */ if (n < 0) { zaehler = -z; nenner = -n; } else { zaehler = z; nenner = n; } // 0 als Nenner ist allerdings nicht erlaubt if (n == 0) { // Programm mit Fehlermeldung beenden std::cerr << "Fehler: Nenner ist 0" << std::endl; std::exit(EXIT_FAILURE); } } /* print */ void Bruch::print () { std::cout << zaehler << '/' << nenner << std::endl; } /* neu: Operator * */ Bruch Bruch::operator * (Bruch b) { /* Zähler und Nenner einfach multiplizieren * - das Kürzen sparen wir uns */ return Bruch (zaehler * b.zaehler, nenner * b.nenner); } /* neu: Operator *= */ Bruch Bruch::operator *= (Bruch b) { // "x *= y" ==> "x = x * y" *this = *this * b; // Objekt, für das die Operation aufgerufen wurde (erster Operand), zurückliefern return *this; } /* neu: Operator < */ bool Bruch::operator< (Bruch b) { /* Ungleichung einfach mit Nennern ausmultiplizieren * - da die Nenner nicht negativ sein können, kann * der Vergleich dadurch nicht umgedreht werden */ return zaehler * b.nenner < b.zaehler * nenner; } } // **** ENDE Namespace Bsp ********************************
Beispiele für eine Anwendung der Operatoren zeigt die folgende usage-Datei:
//---------------------- // // use_bruch.cpp // // author: gerd d-h. // first: april-23, 2003 // // This usage-file uses demoprograms // from Josuttis (see there) // //-------------------------------------------- #include <iostream> #include "bruch2.hpp" using namespace std; int main() { char input; int z,n; Bsp::Bruch x,y; // Brueche x deklarieren Bsp::Bruch w(7,3); // Bruch w deklarieren cout << "Bruch x vor Kopie: " ;x.print(); x=w; cout << "Bruch w: " ;w.print(); cout << "Bruch x nach Kopie: " ;x.print(); input='y'; while (input == 'y' ){ cout << endl << "POSSIBLE ACTIONS:" << endl; cout << "ASSIGN y= x*w (a) ?" << endl; cout << "MULTIPLY x*w (m) ?" << endl; cout << "SQUARE w*w (s) ?" << endl; cout << "COMPARE x< w (c) ?" ; cin >> input; switch(input){ case 'a': {cout << "CASE a (STANDARD): " << endl; y = x * w; y.print(); cout << "CASE a (EXPLICIT): " << endl; y.operator= (x.operator*(w)); y.print(); break; } case 'm': {cout << "CASE m: " << endl; x = x * w; x.print(); break; } case 's': {cout << "CASE s: " << endl; w = w * w; w.print(); break; } case 'c': {cout << "CASE c: " << endl; if ( x < w ) { cout << "x kleiner w: ";x.print(); w.print(); } else { cout << "x nicht kleiner w: ";x.print(); w.print(); } break; } default: cout << "WRONG INPUT !!! " << endl; } cout << endl << "CONTINUE (y/n) ?" ; cin >> input; }//End of while input // solange x < 1000 while (x < Bsp::Bruch(1000)) { // x mit w multiplizieren x *= w; //Koennte man auch schreiben: x.operator= (w.operator* (w)) // und ausgeben x.print(); } }
Ein Aufruf des Programms 'btest2.cpp' erzeugt die folgende Ausgabe:
gerd@goedel:~/WEB-MATERIAL/fh/I-PROGR2/I-PROGR2-EX/EX7> ./bruch2 Bruch x vor Kopie: 0/1 Bruch w: 7/3 Bruch x nach Kopie: 7/3 POSSIBLE ACTIONS: ASSIGN y= x*w (a) ? MULTIPLY x*w (m) ? SQUARE w*w (s) ? COMPARE x< w (c) ?s CASE s: 49/9 CONTINUE (y/n) ?y POSSIBLE ACTIONS: ASSIGN y= x*w (a) ? MULTIPLY x*w (m) ? SQUARE w*w (s) ? COMPARE x< w (c) ?a CASE a (STANDARD): 343/27 CASE a (EXPLICIT): 343/27 CONTINUE (y/n) ?y POSSIBLE ACTIONS: ASSIGN y= x*w (a) ? MULTIPLY x*w (m) ? SQUARE w*w (s) ? COMPARE x< w (c) ?c CASE c: x kleiner w: 7/3 49/9 CONTINUE (y/n) ?y POSSIBLE ACTIONS: ASSIGN y= x*w (a) ? MULTIPLY x*w (m) ? SQUARE w*w (s) ? COMPARE x< w (c) ?m CASE m: 343/27 CONTINUE (y/n) ?y POSSIBLE ACTIONS: ASSIGN y= x*w (a) ? MULTIPLY x*w (m) ? SQUARE w*w (s) ? COMPARE x< w (c) ?c CASE c: x nicht kleiner w: 343/27 49/9 CONTINUE (y/n) ?n 16807/243 823543/2187 40353607/19683
Die Beispiele von JOSUTTIS zeigen sehr klar, wie man neue Klassenoperatoren definieren kann. Verwirrend ist am Anfang meistens, dass ein 2-stelliger Operator als 1-stellige Funktion definiert wird. Dies wird im nachfolgenden Schaubild nochmals verdeutlicht:
Definition von Klassen-Operatoren
Angenommen bei den Variablen x,y,w handelt es sich um Objekte mit der Klasse TYPE. Dann ist der Ausdruck x = y *w so zu lesen, dass dem TYPE-Objekt x mit dem Operator 'operator=' ein Wert zugewiessen ('assignment') werden soll; ausführlicher: x.operator=(...). Im vorliegenden Fall besteht der Wert nicht aus einem einzelnen Objekt --z.B. W--, sondern wiederum aus einer Operation, nämlich der Multilikation von Objekt y mit Objekt w, ausführlicher: y.operator*(w). Dies bedeutet, der Multiplikationsoperator vom Objekt x bekommt als Argument das Objekt w. Das Ergebnis dieser Multiplikation wird dann als Wert dem Zuweisungsoperator von x übergeben.
Versuchen Sie am Beispiel der Klasse Bruch selbst einige Operatoren neu zu definieren:
- operator+
- operator-
-operator/
Versuchen sie am Beispiel der Klasse MensaConnection eigene Operatoren zu definieren, und zwar:
- operator< := die Menge aller Klienten, die in der Mensa sind und eine Erwartungszeit < einem
bestimten Wert haben
- operator> := die Menge aller Klienten, die in der Mensa sind und eine Erwartungszeit > einem
bestimten Wert haben
- operator[] := die Menge aller Klienten, die in der Mensa sind und die eine bestimmten Erwartungszeit haben
Versuchen Sie die Klasse MensaConnection für folgende Aufgabenstellung zu erweitern:
- alle, die eintreten, müssen erst die Essensausgabe passieren, die mehrere Alternativen bietet
- alle, die die Essensausgabe passiert haben, können einen der verfügbaren Plätze einnehmen (dies ist eine
'freundliche' Mensa ohne Kassen)
- jeder, der die Mensa verlassen will, sollte sein Tablett bei der Geschirrabgabe abgeben
Anmk: Halten sie bei der Analyse die Schritte ein, die bislang festgelegt worden sind (Problembeschreibung, UML-Klassen,
UML-Interaktion, Struktogramm, C++-Klassen, C++Implementierung, C++-Use-Datei, Test).
Anmk: Versuchen Sie die Erweiterung so zu gestalten, dass die bisherigen Funktionen ihre Gültigkeit behalten. Falls dies
nicht geht, beschreiben Sie, was sind die Gründe dafür sind.