Objekte in der Theorie
Die Klasse
Objekte
OOP-Begriffe
Definition von Memberfunktionen
Zugriffsrechte in Klassen
Zugriff auf Klassenmember
const-Memberfunktionen und mutable-Member
Aufruf von Funktionen aus Memberfunktionen
Kopieren von Objekten
Objekte als Parameter
Objekte als Rückgabewert
Entwicklung einer Klasse
Die objektorientierte Programmierung (OOP)
Beispiel und Übung
In dieser Lektion beginnen wir den Einstieg in die objektorientierte Programmierung (OOP). Die Grundidee der OOP ist die Zusammenfassung von Daten und die sie verarbeitenden Funktionen in einem so genannten Objekt.
Sehen wir uns zunächst einmal an, was ein Objekt eigentlich ist. Nun, ein Objekt ist eine Instanz einer bestimmten Klasse. Aber ich vermute einmal, dass Sie jetzt noch nicht viel schlauer sind als vorher. Denn was ist eine Instanz oder eine Klasse?
Fangen wir also mit der Beschreibung der Klasse an. Eine Klasse ist letztendlich nichts anderes als ein neuer Datentyp, der (vereinfacht ausdrückt) Daten und Funktionen vereint. Bisher konnten Daten und Funktionen unabhängig voneinander bestehen, d.h. eine Funktion war (bei sauberer Programmierung) nicht auf das Vorhandensein eines bestimmten Datums angewiesen und auch nicht umgekehrt. Eine Klasse bringt nun Daten und Funktionen zusammen. Sie besteht in der Regel aus Daten und Funktionen, die aber auf irgend eine Weise voneinander abhängig sind. Die Daten einer Klassen werden auch als deren Eigenschaften (Properties) bezeichnet und die Funktionen als Memberfunktionen. Diese Memberfunktionen einer Klasse verarbeiten nun aber nicht irgend welche Daten, sondern in der Regel nur die Eigenschaften ihrer Klasse. Alle Eigenschaften und Memberfunktionen einer Klasse werden auch als deren Member bezeichnet. Damit enthält eine Klasse also Member, die sich wiederum aus Eigenschaften und Memberfunktionen zusammensetzen. Später in dieser Lektion, wenn wir einmal eine richtige Klasse entworfen haben, werden wir uns die Begriffe nochmals anhand eines praktischen Beispiels ansehen.
Doch zunächst einmal ein (theoretisches) Beispiel für eine Klasse. Fast alle Betriebssysteme bieten heutzutage eine grafische Oberfläche (GUI = graphic user interface) und eine solche grafische Oberfläche enthält fast immer Fenster. Wenn wir nun versuchen wollen, ein solches allgemeines Fenster zu beschreiben, dann zählen wir dessen Eigenschaften auf. So hat ein Fenster zum Beispiel eine bestimmte Größe und Position. Aber diese Eigenschaften alleine machen ein Fenster noch nicht voll funktionsfähig. Wir benötigen letztendlich noch Funktionen, um die Eigenschaften des Fensters (Größe und Position) verändern zu können. Haben Sie die letzten Sätze aufmerksam gelesen? Dort tauchen wieder die Begriffe Eigenschaft und Funktion auf. Und sowohl die Eigenschaften (Größe und Position) wie auch die dazugehörige Funktionen (zum Verschieben und Vergrößern) bilden letztendlich eine Klasse Fenster. Wie bereits am Anfang erwähnt ist das Ziel der OOP, mehr oder weniger komplexe Gebilde als Ganzes abzubilden, d.h. die Eigenschaften und die darauf wirkenden Funktionen zusammenzufassen.
Kommen wir nun zum Begriff der Instanz. Eine Instanz ist prinzipiell nichts anderes, als die Realisierung einer Klasse. Dazu wird im Prinzip eine 'Variable' der entsprechenden Klasse definiert. Unter der OOP sprechen wir dann aber nicht von Variablen sondern von Objekten. Nehmen wir einmal an, Sie hätten eine Klasse Window definiert und folgende Anweisung geschrieben:
| Window myWindow; |
Dann ist myWindow eine Instanz der Klasse Window und man sagt auch: myWindow ist ein Objekt vom Typ. Sie können von einer Klasse beliebig viele Objekte definieren, die dann alle die gleichen Eigenschaften besitzen. Die Werte der Eigenschaften können aber unterschiedlich sein.
Aber sehen wir uns nun den prinzipiellen Aufbau einer Klasse näher an.
Genauso wie Variablen einen bestimmten Datentyp besitzen (bool, short, double usw.), besitzen auch Objekte einen bestimmten Datentyp. C++ stellt für Objekte die drei Datentypen struct, class und union zur Verfügung. Auf den Unterschied zwischen den Datentypen struct und class kommen wir nachher gleich zu sprechen. Der union ist dann eine eigene Lektion gewidmet.
Bilden wir nun Schritt für Schritt das erwähnte Fenster in einer grafischen Oberfläche als Klasse ab.
Zunächst benötigt jede Klasse einen 'Rahmen', in dem die Eigenschaften und Memberfunktionen zusammengefasst werden können. Dieser Rahmen besteht zunächst aus dem Schlüsselwort class (oder auch struct), gefolgt von einem Namen für die Klasse (im Beispiel unten Window). Anschließend folgt ein Block {...}, der mit einem Semikolon abgeschlossen werden muss.
|
class Window { ... // Hier folgen gleich die ... // Eigenschaften und Memberfunktionen }; |
|
|
Der Name für die Klasse muss immer eindeutig sein. Wenn Sie später mehrere Klassen definieren, so müssen diese unterschiedliche Namen besitzen. Auch dürfen Sie keine weiteren Variablen oder Funktionen mit dem gleichen Namen wie die Klasse definieren.
Fügen wir nun zur Klasse ihre Member (Eigenschaften und Memberfunktionen) innerhalb des Blocks {...} hinzu.
Fangen wir sinnvoller Weise mit den Eigenschaften an. Welchen Eigenschaften besitzt ein Fenster, d.h. was beschreibt ein solches GUI-Fenster? In der Praxis besitzt ein Fenster sicher ein Menge Eigenschaften. Wir werden uns aber auf vier Eigenschaften beschränken, damit es nicht zu unübersichtlich wird. Unser GUI-Fenster soll eine bestimmte Position und Ausdehnung besitzen.
Und damit könnte zum Beispiel im nächsten Schritt die Klasse für Fensterobjekte wie nachfolgend dargestellt aussehen. Ein Fenster besitzt hier die Eigenschaften, dass es eine bestimmte X- und Y-Position so wie ein definierte Breite und Höhe hat.
|
class Window { int xPos, yPos; // Position unsigned int width, height; // Grösse }; |
Für die Eigenschaft in einer Klasse können alle bisher bekannten Datentypen verwendet werden; so ist es zum Beispiel auch erlaubt, innerhalb einer Klasse als Eigenschaft eine weitere Klasse zu verwenden. Solche eingeschlossenen Klassen werden später noch gesondert behandelt, da sie bestimmte Anforderungen erfüllen müssen.
|
|
Aber diese Eigenschaften alleine machen das Fenster uninteressant. Es würde sich weder verschieben noch in der Größe verändern lassen. Um die Eigenschaften verändern zu können, werden in der OOP Memberfunktionen verwendet. Und auch diese Memberfunktionen werden, da sie vom Konzept her nur zur Manipulation der Eigenschaften angedacht sind, innerhalb der Klasse deklariert bzw. definiert.
Sehen wir uns die auch gleich wieder anhand der Klasse für Fensterobjekte an. Welche Memberfunktionen innerhalb der Klasse notwendig sind, ergibt sich hier fast von alleine aufgrund der Eigenschaften. Wir haben eine Fensterposition, also benötigen wir eine Memberfunktion zum Verschieben des Fensters, die wir im Beispiel mit Move(...) bezeichnen. Ferner besitzt das Fenster noch eine Größe, also benötigen wir zum Verändern der Fenstergröße eine weitere Memberfunktion, im Beispiel Size(...) genannt.
|
class Window { int xPos, yPos; // Position unsigned int width, height; // Grösse void Move(int x, int y); // Position verändern void Size(unsigned int w, unsigned int h); // Grösse verändern }; |
Die Datentypen der Parameter der Memberfunktionen ergeben sich hier automatisch aus den Datentypen der Eigenschaften, die nachher mit der Memberfunktion verändert werden sollen.
Die Gesamtheit aller Memberfunktionen einer Klasse wird auch als deren Schnittstelle (Interface) bezeichnet, da in einer 'sauberen' Implementierung einer Klasse nur über diese Memberfunktionen die Eigenschaften verändert werden sollten.
Beachten Sie bitte, dass in der obigen Klassendefinition die Memberfunktionen nur deklariert sind. Wie Memberfunktionen definiert werden, das sehen wir uns nachher an.
Folgt nun der nächste Schritt, die Definition von Objekten.
Ist der Aufbau eines Fensters vollständig beschrieben, kann an das eigentliche 'Erstellen' von Fenstern erfolgen. Bisher haben wir ja lediglich die Eigenschaften und die Schnittstelle eines Fensters festgelegt. Um ein Fenster anzulegen, wird ein Objekt der Fensterklasse Window definiert. Dies erfolgt analog zur bisherigen Definition einer Variablen, d.h. zuerst folgt der Datentyp des Objekts und danach der Name.
Der vollständige Datentyp eines Objekts besteht aus dem Schlüsselwort class bzw struct (je nach dem, welchen Typ die Klasse hat) und dem Klassennamen. C++ erlaubt es auch, bei Eindeutigkeit das Schlüsselwort class bzw. struct einfach wegzulassen, so wie im Beispiel bei der zweiten Objekt-Definition dargestellt. Diese zweite Form der Objekt-Definition ist auch die in der Praxis am gebräuchlichste.
|
// Klassendeklaration class Window { ... // Member-Definitionen }; // Objekte vom Typ class Window definieren class Window firstWin; Window secondWin; |
Beide Fensterobjekte enthalten zwar die gleichen Eigenschaften, diese können aber (und werden es in der Regel auch) unterschiedliche Werte besitzen. So kann das Fenster firstWin eine andere Größe besitzen als das Fenster secondWin.
Sie können aber nicht nur einfache Objekte definieren, sondern sogar auch Objektfelder. Die Definition eines Objektfeldes erfolgt analog zur Definition eines Feldes mit den Standard-Datentypen.
|
// Klassendeklaration class Window { ... // Member-Definitionen }; // Objektfeld definieren Window winArray[10]; |
Nach dem Sie nun wissen, wie eine Klasse prinzipiell aufgebaut ist, jetzt nochmals die wichtigsten OOP-Begriffe im Überblick:
| // Eine Klasse besteht aus Member class Window {
|
|||
| // Objekte sind Instanzen von Klassen Window myWindow; |
So weit, so gut. Sehen wir uns als nächstes an, wie die Memberfunktionen einer Klasse definiert werden.
Eine Memberfunktionen kann innerhalb der Klasse selbst definiert werden. Die Definition erfolgt dann prinzipiell gleich wie die Definition einer normalen Funktion, nur jetzt eben innerhalb der Klasse.
|
class Window { ... // Eigenschaften void Move(int x, int y) { ... // Fenster verschieben } void Size(unsigned int w, unsigned int h) { ... // Grösse verändern } }; |
|
|
Hier sind jetzt zwei Schritte notwendig:
|
class Window { ... // Deklaration der Memberfunktionen void Move(int x, int y); void Size(unsigned int w, unsigned int h); }; // Definition der Memberfunktion Move(...) void Window::Move(int x, int y) { ... // Fenster verschieben } // Definition der Memberfunktion Size(...) void Window::Size(unsigned int w, unsigned int h) { ... // Grösse verändern } |
Da bei der Definition der Memberfunktionen außerhalb der Klasse der Klassenname mit angegeben werden muss, können verschiedene Klassen durchaus Memberfunktionen mit gleichem Namen besitzen. Der Compiler kann die Definition der Memberfunktionen durch die Angabe des Klassennamens immer eindeutig zuordnen.
Sehen wir uns jetzt an, wie Klassen und deren Memberfunktionen in der Praxis definiert werden.
Wenn Sie eine Klasse in verschiedenen Modulen (Quellcode-Dateien) verwenden wollen, so müssen Sie die Klassendefinition in einer entsprechenden Header-Datei ablegen. Es hat sich in der Praxis als sinnvoll erwiesen, der Header-Datei den gleichen Namen wie der in ihr definierten Klasse zu geben. Innerhalb der Klassendefinition werden dann die Memberfunktionen nur deklariert.
|
// Datei: window.h // Klassendefinition class Window { int xPos, yPos; // Position unsigned int width, height; // Grösse void Move(int x, int y); // Verschieben void Size(unsigned int w, unsigned int h); // Grösse verändern }; |
Die Definitionen der Memberfunktionen erfolgen dann in einer Quellcode-Datei, die ebenfalls sinnvoller Weise den Namen der Klasse erhält, deren Memberfunktionen in ihr definiert werden; nur diesmal selbstverständlich mit der Extension .cpp anstelle von .h. In dieser Datei wird dann zuerst die Header-Datei mit der Klassendefinition eingebunden und danach folgen die entsprechenden Memberfunktionen-Definitionen.
|
// Datei: window.cpp // Einbinden der Header-Datei #include "window.h" // Definition der Memberfunktion Move(...) void Window::Move(int x, int y) { ... // Fenster verschieben } // Definition der Memberfunktion Size(...) void Window::Size(unsigned int w, unsigned int h) { ... // Grösse verändern } |
Diese Aufteilung in getrennte Header- und Quellcode-Dateien hat noch einen weiteren Vorteil: wollen Sie eine Klasse weitergeben, so reicht es aus, wenn Sie die Header-Datei und die übersetzte(!) Quellcode-Datei (also die entsprechende .obj Datei) weitergeben. Der Benutzer Ihrer Klasse muss bei der Verwendung der Klasse die Header-Datei einbinden und zu seinem Projekt noch die entsprechende .obj Datei dazu binden. Das heißt, Ihr Quellcode bleibt geschützt, da er den Quellcode der Memberfunktionen nicht erhält.
Nachdem die Klasse und deren Memberfunktionen definiert sind, könnten wir versuchen, auf die Elemente der Klasse zu zugreifen. Diese würde jedoch im Augenblick noch einen Fehler beim Übersetzen des Programms verursachen. Der Grund hierfür liegt darin, dass alle Klassen bestimmte Standard-Zugriffsrechte besitzen, die den Zugriff auf Ihre Member einschränken können. Das heißt, je nach Zugriffsrecht kann der Zugriff auf ein Member von außerhalb der Klasse verboten oder erlaubt sein. Der Sinn und Zweck dieser Zugriffsbeschränkung liegt darin, dass der Zugriff auf die Member nur noch kontrolliert erfolgen sollte. Stellen Sie sich dazu wieder einmal unsere bisherige Klasse Window vor. Diese Klasse enthält unter anderem die Breite und Höhe des Fensters. Ohne Zugriffskontrolle könnte der Anwender bis jetzt einfach hergehen und zum Beispiel die Fensterhöhe auf den (falschen) Wert -1 setzen, was dann wahrscheinlich ziemliche Probleme beim Darstellen des Fensters geben würde. Wird der Zugriff auf die Fenstergröße aber kontrolliert, so kann beim Setzen der Fenstergröße eine Plausibilitätsprüfung durchgeführt und falsche Werte abgewiesen werden.
Sehen wir uns an, welche Zugriffsrechte es gibt und wie sie vergeben werden.
Um den Zugriff auf Member von außerhalb der Klasse zu sperren, wird innerhalb der Klasse vor die zu sperrenden Member die Anweisung private: gestellt. Alle Member die nach dieser Anweisung folgen sind für den Zugriff zunächst einmal gesperrt. Diese Sperrung gilt aber nur für Zugriffe von außerhalb der Klasse. Die Memberfunktionen einer Klasse haben generell immer Zugriff auf alle anderen Member der eigenen Klasse.
|
class Window { private: int xPos, yPos; // Position unsigned int width, height; // Grösse void Move(int x, int y); // Verschieben void Size(unsigned int w, unsigned int h); // Grösse verändern }; |
|
|
Da aber eine Klasse mit nur gesperrten Member in der Regel nutzlos ist, muss diese Sperrung auch wieder aufgehoben werden können. Dies erfolgt durch die Anweisung public:. Auf alle Member die nach dieser Anweisung folgen, kann dann auch von außerhalb der Klasse heraus zugegriffen werden. Im nachfolgenden Beispiel sind damit die Eigenschaften der Klasse Window gegen den direkten Zugriff geschützt, während auf die Memberfunktionen zugegriffen werden kann.
|
class Window { private: int xPos, yPos; // Position unsigned int width, height; // Grösse public: void Move(int x, int y); // Verschieben void Size(unsigned int w, unsigned int h); // Grösse verändern }; |
Und somit haben wir unseren kontrollierten Zugriff auf die Fenstereigenschaften! Soll zum Beispiel die Fenstergröße verändert werden, muss dazu die Memberfunktion Size(...) aufgerufen werden, denn width und height sind ja gegen den direkten Zugriff geschützt. Und in der Memberfunktion Size(...) kann dann eine Überprüfung der übergebenen Parameter auf Plausibilität erfolgen.
Wie Sie aus den bisherigen Ausführungen entnehmen können, sind Zugriffsspezifizierer innerhalb einer Klasse so lange gültig, bis sie durch einen anderen 'überschrieben' werden. Die Anzahl und Reihenfolge der private: und public: Anweisungen innerhalb einer Klasse ist laut C++ Standard beliebig.
|
|
Je nach Klassentyp (struct, union und class) haben die Klassenmember voreingestellte Zugriffsrechte:
Somit ist das Verhalten der beiden unten angegebenen Klassen identisch. Der Unterschied liegt nur in der Reihenfolge der Member. Beim Klassentyp class müssen Sie die Memberfunktionen explizit für den Zugriff freigeben während Sie beim Klassentyp struct die Eigenschaften explizit schützen müssen. Und dieses voreingestellte Standard-Zugriffsrecht ist auch der einzige Unterschied zwischen Klassen vom Typ struct und Klassen vom Typ class!
|
class Window { // Member sind standardmäßig private int xPos, yPos; // Position unsigned int width, height; // Grösse public: void Move(int x, int y); // Verschieben void Size(unsigned int w, unsigned int h); // Grösse verändern }; struct Window { // Member sind standardmäßig public void Move(int x, int y); // Verschieben void Size(unsigned int w, unsigned int h); // Grösse verändern private: int xPos, yPos; // Position unsigned int width, height; // Grösse }; |
Um den Klassentyp union kümmern wir uns, wie bereits erwähnt, später noch.
War's bis jetzt mehr oder weniger leicht verständlich, so kommt nun der etwas kompliziertere Teil der Zugriffsrechte. Sehen Sie sich dazu zunächst einmal folgende Aussage an:
|
|
Sehen wir uns diesen Sachverhalt einmal an. Er spielt bei dem später beschriebenen Kopierkonstruktor eine entscheidende Rolle.
Das nachfolgende Beispiel zeigt die bekannte Klasse Window. Dieser Klasse wurde unter anderem die Memberfunktion CopyData(...) hinzugefügt, die die Eigenschaften eines als Parameter übergebenen Objekts in das aktuelle Objekt übernehmen soll. Hierzu erhält die Memberfunktion eine Referenz auf das zu kopierende Objekt, das natürlich ebenfalls der Klasse Window angehört. Obwohl nun die Eigenschaften des übergebenen Objekts gegen den direkten Zugriff geschützt sind (private), kann die Memberfunktion CopyData(...) auf die geschützten Daten des übergebenen Objekts zugreifen. Wie gesagt, dies funktioniert aber nur, wenn das übergebene Objekt der selben Klasse angehört wie die aufgerufene Memberfunktion.
|
class Window { // Geschützte Eigenschaften short width, height; string title; // Öffentliche Memberfunktionen public: void Size(short w, short h); void SetTitle(const char *const pT); void CopyData(const Window& source); // weitere Member der Klasse ... }; // Definition der Memberfunktion CopyData() void Window::CopyData(const Window& source) { width = source.width; height = source.height; title = source.title; } |
So, und das war's vorläufig auch schon zu den Zugriffsrechten. Später werden Sie noch ein weiteres Zugriffsrecht kennen lernen, das aber nur im Zusammenhang mit abgeleiteten Klasse eine Rolle spielt.
Innerhalb einer Memberfunktion kann auf alle Member (unabhängig davon, ob public oder private) der eigenen Klasse direkt zugegriffen werden. Hierbei spielt es keine Rolle, ob der Zugriff auf Eigenschaften erfolgt oder aber weitere Memberfunktionen der eigenen Klasse aufgerufen werden. Ausgangpunkt soll folgender Code sein:
|
// Klassendefinition class Window { private: int xPos, yPos; // Position unsigned int width, height; // Grösse public: void Move(int x, int y); // Verschieben void Size(unsigned int w, unsigned int h); // Grösse verändern }; // Definition der Memberfunktionen void Window::Move(int x, int y) { // Position setzen xPos = x; yPos = y; } void Window::Size(unsigned int w, unsigned int h) { // Grösse setzen width = w; height = h; } |
Mit obiger Klassendefinition könnte zum Beispiel eine weitere Memberfunktion WinPos(...) der Klasse zum Verändern der Position und Größe eines Fensters wie folgt aussehen:
|
void Window::WinPos(int x, int y, unsigned int w, unsigned int h) { Move(x, y); Size(w, h); } |
Die Wiederverwendung von bereits bestehendem Code, hier der Memberfunktionen Move(...) und Size(...), ist übrigens auch eines der erklärten Ziele der OOP. Auf neu-deutsch heißt dies auch Code-Reuse.
Sehen wir uns jetzt an, wie auf Member einer Klasse außerhalb von Memberfunktionen zugegriffen wird. Denken Sie aber immer daran, dass Sie außerhalb von Memberfunktionen nur Zugriff auf die public-Member einer Klasse haben!
Auf Member einer Klasse kann in der Regel nur über ein Objekt der Klasse zugegriffen werden. Denn letztendlich existieren die Member einer Klasse nur in Verbindung mit einem bestimmten Objekt. Der direkte Zugriff auf Member erfolgt durch die Angabe des entsprechenden Objekts, gefolgt vom Punktoperator '.' und dem Namen des jeweiligen Members.
|
// Klassendefinition class Window { private: int xPos, yPos; // Position unsigned int width, height; // Grösse public: void Move(int x, int y); // Verschieben void Size(unsigned int w, unsigned int h); // Grösse verändern }; // Objektdefinitionen Window myWin, yourWin; // Direkter Zugriff auf public-Member myWin.Move(10,200); yourWin.Size(640,480); |
Im Beispiel werden zwei Objekte myWin und yourWin definiert. Anschließend werden die Positions-Eigenschaften von myWin durch den Aufruf von Move(...) geändert. Diese Änderung der Positions-Eigenschaft von myWin hat aber keine Auswirkung auf die Positions-Eigenschaft von yourWin. Beide Objekte besitzen zwar die gleichen Eigenschaften, die aber völlig unabhängig voneinander sind. Im Anschluss daran wird die Größen-Eigenschaft von yourWin durch den Aufruf von Size(...) verändert.
|
myWin.xPos = 100; Da es in der OOP aber fast ein Vergehen ist, Eigenschaften direkt zu verändern, sollten Sie zur Veränderung von Eigenschaften auch immer entsprechende Memberfunktionen zur Verfügung stellen. Sie erhalten dadurch eine wesentlich bessere Kontrolle über den Inhalt der Eigenschaften. |
Der indirekten Zugriff, das heißt über einen Zeiger, auf ein Member erfolgt durch Angabe des entsprechenden Objektzeigers, folgt vom Zeigeroperator -> und anschließendem Namen des Members. Extrem wichtig dabei ist, dass dem Zeiger vor dem Zugriff auch eine gültige Adresse, zum Beispiel die eines bestehenden Objekts, zugewiesen wurde. Dieser Punkt mag auf den ersten Blick zwar trivial erscheinen, ist aber in der Praxis ein häufiger Fehlerfall.
|
// Klassendefinition class Window { private: int xPos, yPos; // Position unsigned int width, height; // Grösse public: void Move(int x, int y); // Verschieben void Size(unsigned int w, unsigned int h); // Grösse verändern }; // Objektdefinitionen Window myWin, yourWin; // Objektzeigerdefinition und Initialisierung Window *pWin = &myWin; // Indirekter Zugriff auf myWin-Member pWin->Move(10,200); // Zeiger umsetzen auf yourWin Objekt pWin = &yourWin // Indirekter Zugriff auf yourWin-Member pWin->Move(100,0); |
Einem einmal definierten Objektzeiger können im weiteren Verlaufe des Programms beliebig oft weitere Adressen zugewiesen werden. Es wird dann immer das Member des Objekts aufgerufen, dessen Adresse gerade im Objektzeiger abgelegt ist. So wird im Beispiel zunächst das Objekt myWin und dann das Objekt yourWin indirekt über den Objektzeiger pWin verschoben.
Im Zusammenhang mit dem Zugriff auf Eigenschaften müssen wir uns nochmals mit dem enum-Datentyp befassen. Werden innerhalb einer Klasse enum-Eigenschaften definiert, so gehören diese natürlich zur Klasse. Wollen Sie diese enum-Konstante auch außerhalb einer Memberfunktionen verwenden (z.B. als Parameter beim Aufruf einer Memberfunktion), so müssen Sie vor dem Namen der enum-Konstante noch den Klassennamen und den Zugriffsoperator :: angegeben.
|
|
Ebenfalls muss der enum-Datentyp als public deklariert sein, da Sie ansonsten keinen Zugriff darauf haben. Beachten Sie dabei aber bitte, dass nur der enum-Datentyp und nicht auch die mit ihr verknüpfte enum-Eigenschaft als public (im Beispiel die Eigenschaft winStyle) deklariert ist.
|
// Klassendefinition class Window { public: // enum-Datentyp definieren enum Style {FRAME, CLOSEBOX, SYSMENU}; private: // enum-Eigenschaft definieren enum Style winStyle; ... public: void DoAnything(Style s); }; // Objekt definieren Window myWin; ... // Aufruf einer Memberfunktion myWin.DoAnything(Window::FRAME); |
Sehen wir uns jetzt noch einen besonderen Typ von Memberfunktionen an. Vielfach werden Sie Deklarationen von Memberfunktionen in der folgenden Art finden:
| <Returntyp> MName ([Parameter]) const; |
Worauf es hierbei ankommt, ist das Schlüsselwort const am Ende der Deklaration. Memberfunktionen mit dieser Deklaration werden auch als const-Memberfunktionen bezeichnet. Solche const-Memberfunktionen können keine Eigenschaften eines Objektes verändern. Nachfolgend sehen Sie ein kleines Beispiel für die Anwendung einer const-Memberfunktion. Die Memberfunktion GetXPos() liefert lediglich die X-Position des Fensters zurück und verändert selbst keine Eigenschaft ihres Objekts.
|
// Klassendefinition class Window { int xPos, yPos; ... public: int GetXPos() const; // weitere Member der Klasse ... }; // Definition der Memberfunmtion GetXPos() int Window::GetXPos() const { return xPos; } ... int main() { Window myWin; ... int x = myWin.GetXPos(); ... } |
|
|
|
|
Im Zusammenhang mit const-Memberfunktionen muss noch das Schlüsselwort mutable erwähnt werden. Mit mutable definierte Eigenschaften können auch in const-Memberfunktionen verändert werden, d.h. mutable überschreibt sozusagen das const-Attribut der Memberfunktion für bestimmte Eigenschaften. Außerdem erlaubt mutable selbst dann noch die Veränderung einer Eigenschaft, wenn von der Klasse ein const-Objekt definiert wurde.
|
// Klassendefinition class Window { int xPos, yPos; mutable long any; public: void DoSomething() const; ... }; // Definition const-Memberfunktion void Window::DoSomething() const { xPos = 10; // nicht erlaubt wegen const any = 10L; // das geht wegen mutable } |
|
mutable
const int MAX=10; Auf die Speicherklasse static im Zusammenhang mit Klassen kommen wir später noch zu sprechen. |
Werden aus Memberfunktionen heraus 'normale' Funktionen aufrufen, so können dabei drei Fälle auftreten:
1.) Innerhalb der Klasse gibt es keine Memberfunktion mit der gleichen Signatur (Name und Parameter) wie die aufzurufende Funktion. Dann erfolgt der Aufruf der Funktion wie gewohnt, d.h. es reicht die alleinige Angabe des Funktionsnamens.
|
// Funktionsdeklaration bool CheckIt(); // Klassendefinition class Any { ... // Enthält keine Memberfunktion CheckIt() void DoAny() { ... bool result = CheckIt(); } }; |
2.) Innerhalb der Klasse gibt es eine Memberfunktion mit der gleichen Signatur wie die aufzurufende Funktion, jedoch liegt die aufzurufende Funktion in einem eigenen Namensraum (wie z.B. alle Funktionen aus der Standard-Bibliothek, die im Namensraum std liegen). Standardmäßig wird dann beim Aufruf immer die Memberfunktion der eigenen Klasse aufgerufen. Um die Funktion aus dem anderen Namensraum aufzurufen, stellen Sie vor dem Aufruf den Namen des Namensraums (z.B. std), gefolgt von zwei Doppelpunkten.
|
#include <cmath> // Bindet u.a. sin() ein // Klassendefinition class Any { // sin() Memberfunktion double sin(double x) { ... } void DoAny() { ... // Aufruf der eigenen sin() Memberfunktion double res1 = sin(1.2); // Aufruf der Funktion aus cmath double res2 = std::sin(0.5); } }; |
3.) Innerhalb der Klasse gibt es eine Memberfunktion mit der gleichen Signatur wie die aufzurufende Funktion, die jedoch in keinem eigenen Namensraum liegt. Auch hier wird standardmäßig wieder zuerst die Memberfunktion der eigenen Klasse aufgerufen. Um die "globale" Funktion aufzurufen, stellen Sie vor dem Funktionsnamen den globalen Zugriffsoperator :: (das sind zwei Doppelpunkte).
|
// Funktionsdeklaration bool CheckIt(); // Klassendefinition class Any { // CheckIt() Memberfunktion void CheckIt() { ... } void DoAny() { ... // Aufruf der CheckIt() Memberfunktion CheckIt(); // Aufruf der globalen Funktion bool res2 = ::CheckIt(); } }; |
Beachten Sie im obigen Beispiel, dass die "globale" Funktion einen anderen Returntyp besitzt als die Memberfunktion. Dies reicht für die Unterscheidung, welche Funktion aufgerufen wird, nicht aus, da nur die Signatur betrachtet wird.
|
|
Verlassen wird jetzt die Definition von Objekten und Memberfunktionen und sehen uns einmal an, wie Objekte kopiert werden. In der Praxis ist es oft notwendig, die Daten von einem Objekt in ein anderes zu übertragen, wobei beide Objekte der gleichen Klasse angehören. Im ersten Ansatz könnten Sie jetzt hergehen und eine Memberfunktion, z.B. CopyObject(...), schreiben, die Eigenschaft für Eigenschaft über Zuweisungen umkopiert.
Doch es geht auch wesentlich einfacher wenn beide Objekte der gleichen Klasse angehören: weisen Sie die Objekte einfach einander zu. Durch die Zuweisungen werden alle Eigenschaften kopiert, egal wie viele es sind. Ganz schön clever vom Compiler, oder?
|
// Klassendefinition class Window { ... }; // main() Funktion int main() { // Zwei Window Objekt definieren Window myWin, yourWin; ... // Eigenschaften von myWin nach yourWin übernehmen yourWin = myWin; ... } |
Nur eines müssen Sie dabei aber unbedingt beachten:
|
|
Werden Objekte an Funktionen oder Memberfunktionen übergeben, so sollte dies in der Regel über Referenzparameter erfolgen. Eine Übergabe per called-by-value sollte soweit wie möglich vermieden werden. Übergeben Sie ein Objekt per called-by-value, so erhält die Funktion, wie auch bei den einfachen Daten, nur eine Kopie des Objekts, d.h. alle Eigenschaften des zu übergebenden Objekts werden in ein temporäres Objekt umkopiert und dieses dann an die Funktion übergeben.. Dies kann je nach Objektgröße erheblich Zeit und Platz auf dem Stack benötigen. Bei einer Übergabe per Referenz entfällt dieser Kopiervorgang.
Innerhalb der aufgerufenen Funktion bzw. Memberfunktion haben Sie dann über den Parameternamen Zugriff auf alle public-Member des übergebenen Objekts. Ist die aufgerufene Funktion eine Memberfunktion die zur gleiche Klasse wie das übergebene Objekt gehört, so hat die Memberfunktion Zugriff auf alle Member, auch auf die private Member, des übergebenen Objekts (siehe nächsten Abschnitt).
|
class Window { ... }; // Funktionsdeklaration void DoAny (Window& obj); // Objekt definieren Window myWin; int main() { ... // Aufruf der Funktion DoAny(myWin); ... } // Funktionsdefinition void DoAny (Window& obj) { ... obj.Size (640,480); // Übergebenes obj.Move (0,0); // Fenster verändern } |
Den Nachteil den eine Übergabe eines Objekts als Referenzparameter mit sich bringt ist, dass die Funktion oder Memberfunktion das übergebene Objekt eventuell unbeabsichtigt verändern kann. In vielen Fällen kann es aber durchaus sinnvoll sein, dass eine Funktion bzw. Memberfunktion das übergebene Objekt nicht verändern können soll. In diesem Fall übergeben Sie das Objekt als konstante Referenz , so wie nachfolgend an der Funktion DoAny(...) dargestellt. Ein Versuch das Objekt dann innerhalb der Funktion/Memberfunktion zu verändern, führt zu einem Fehler beim Übersetzen des Programms. Dies gilt auch, wenn die Funktion/Memberfunktion eine Memberfunktion des übergebenen Objekts aufruft, die nicht als const-Memberfunktion definiert ist. Der Aufruf einer nicht-const-Memberfunktion würde ja ansonsten wieder eine Veränderung des übergebenen Objekts zulassen (siehe hier).
|
class Window { ... public: void NonConstMeth(); void ConstMeth(...) const; }; // Normale Funktion mit const-Referenzparameter void DoAny (const Window& obj) { obj.NonConstMeth(); // Aufruf nicht-constMemberfunktion -> FEHLER! obj.ConstMeth(); // Aufruf const Memberfunktion -> OK! } // Objekt definieren Window myWin; // main() Funktion int main() { ... // Aufruf der Funktion DoAny(myWin); ... } |
Normalerweise haben Funktionen/Memberfunktionen aus nur Zugriff auf die public-Member des übergebenen Objekts. Ein spezieller Fall ist jedoch der Aufruf einer Memberfunktion, die als Parameter ein Objekt der eigenen Klasse erhält. Da die aufgerufene Memberfunktion zur gleichen Klasse wie das übergebene Objekt gehört, kann innerhalb der Memberfunktion auch auf die private Eigenschaften des übergebenen Objekts zugegriffen werden. Nachfolgend sehen Sie hierzu ein Beispiel. Die Memberfunktion CopyObj(...) der Klasse Window erhält als Parameter ein Objekt der eigenen Klasse Window. Innerhalb von CopyObj(...) kann nun auch auf die ansonsten geschützten Eigenschaften (z.B. xPos und yPos) des übergebenen Objekts zugegriffen werden. Diese Sachverhalt spielt später nochmals eine wichtige Rolle.
|
class Window { int xPos, yPos; ... public: void CopyObj(const Window& obj); ... }; void Window::CopyObj(const Window& obj) { xPos = obj.xPos; yPos = obj.yPos; ... } |
Wie Sie bereits erfahren haben, lassen sich innerhalb von Funktionen und Memberfunktionen lokale Variablen definieren. Und selbstverständlich können Sie in Funktionen/Memberfunktionen auch lokale Objekte definieren. Etwas aufpassen müssen Sie nur, wenn Sie ein Objekt aus einer Funktion/Memberfunktion heraus zurückgeben. Sehen wir uns dazu einmal ein Beispiel an:
Die Funktion CreateWin() soll zur Erstellung eines Fenster dienen. Dazu wird innerhalb der Funktion zunächst ein Window-Objekt erzeugt und die Referenz darauf an den Aufrufer zurückgeliefert.
|
|
|
class Window { ... }; // FALSCH!!! // Funktion liefert Referenz zurück // was bis zum Programmabsturz führen kann Window& CreateWin() { Window myWin; ... // irgend etwas mit Fenster tun // und dann Referenz zurückliefern return myWin; } // main() Funktion int main() { ... // Fenster erstellen lassen Window newWin = CreateWin(); ... } |
Am Ende der Funktion wird nun das lokale Window-Objekt wieder gelöscht. Wenn Sie jetzt (wie im Beispiel) eine Referenz auf dieses lokale Window-Objekt zurückliefern, so würden Sie eine Referenz auf ein nicht mehr existierendes Objekt zurückgeben. Und das ist natürlich ein Fehlerfall! Je nach verwendetem Compiler erhalten Sie bei einer solchen Vorgehensweise entweder eine Warnung oder (was eigentlich richtiger ist) einen Fehler.
Doch wie geht's richtig?
Geben Sie anstelle einer Referenz einfach das Objekt selbst zurück. In diesem Fall führt der Compiler intern Folgendes durch: Da das erzeugte Window-Objekt nur bis zum Ende der Funktion existiert, aber trotzdem an den Aufrufer zurückgeliefert werden muss, wird am Ende der Funktion zunächst ein temporäres Window-Objekt erstellt. Dieses temporäre Window-Objekt wird dann mit dem lokale Window-Objekt initialisiert. Anschließend wird das lokale Window-Objekt gelöscht. Das zurückgelieferte temporäre Window-Objekt wird nach der Rückkehr aus der Funktion/Memberfunktion dem Ziel-Window-Objekt (im Beispiel ist dies newWin) zugewiesen. Und am Ende der Anweisung, die die Funktion aufgerufen hat, wird schließlich noch das temporäre Window-Objekt gelöscht. Sie sehen also, der Compiler hat hier (hinter ihrem Rücken) sehr viel zu tun.
|
class Window { ... }; // So geht's richtig!!! // Funktion liefert temporäres Window-Objekt zurück Window CreateWin() { Window myWin; ... // irgend etwas mit Fenster tun // und dann Referenz zurückliefern return myWin; } // main() Funktion int main() { ... // Fenster erstellen lassen Window newWin = CreateWin(); ... } |
|
|
Da die Entwicklung von Klassen die Grundlage der OOP ist, wollen wir uns jetzt einmal die Entwicklung einer komplette Klasse ansehen.
In diesem Beispiel werden wir eine Klasse zur Darstellung und Manipulation eines Rechtecks für eine fiktive Oberfläche entwickeln.
Wie Sie vielleicht noch wissen, sollten bei der Entwicklung einer Klasse zuerst deren Eigenschaften (Daten) definiert werden. Sind alle Eigenschaften bekannt, so ergibt sich daraus fast zwangsläufig die Schnittstelle (Memberfunktionen) der Klasse. Fangen wir also mit den Eigenschaften an, die die Klasse Rect zur Darstellung eines Rechtecks besitzt. Wir werden die Klasse, und damit ihre Eigenschaften, in einer eigenen Header-Datei rect.h definieren, so wie es in der Praxis üblich ist.
Unser darzustellendes Rechteck soll die die Eigenschaften Position, Größe und Farbe besitzen. Definieren wir also zunächst die Klasse und fügen ihr dann die Eigenschaften hinzu. Da wir vielleicht später noch weitere Klassen definieren, die ebenfalls eine Farbinformation enthalten, wird die Farbinformation in einer getrennten einfachen Klasse ohne eigene Memberfunktionen abgelegt. Diese Farb-Klasse enthält die jeweiligen Rot-, Grün- und Blauanteile der Farbe als unsigned char Werte. Für die Eigenschaften Position und Größe werden short-Datentypen verwendet.
|
Datei rect.h // Für verkürzte Schreibweise typedef unsigned char BYTE; // Struktur für Farbwerte struct Color { BYTE red; BYTE green; BYTE blue; }; // Klassendefinition class Rect { short xPos, yPos; // Position short width, height; // Grösse Color rectColor; // Farbe ... }; |
Sind die Eigenschaften definiert, kann es an die Definition der Schnittstelle gehen. Beachten Sie bitte, dass die Eigenschaften private sind und die nachfolgenden Memberfunktionen public, so wie es sich in der OOP gehört.
Zuerst benötigen wir eine Memberfunktion um Eigenschaften des Rechtecks mit Defaultwerten zu belegen. Nennen wir diese Memberfunktion Init(...). In unserem Fall erhält die Memberfunktion Init(...) als Parameter nur die Position und Größe des neuen Rechtecks. Die Farbe soll standardmäßig bei der Definition eines Rechteck-Objekts auf schwarz (RGB = 0,0,0) eingestellt werden.
|
|
Als nächstes deklarieren wir die restlichen Memberfunktionen, um die Eigenschaften des Rechtecks gezielt verändern zu können. Da unser Rechteck Eigenschaften für die Position, Größe und Farbe besitzt, definieren wir drei entsprechende Memberfunktionen Move(...), Resize(...) und SetColor(...). Zum Schluss benötigen wir noch eine Memberfunktion um das Rechteck letztendlich darzustellen. Die hierfür verwendete Memberfunktion erhält den Namen DrawIt().
|
Datei rect.h ... // Klassendefinition class Rect { // Die Eigenschaften der Klasse (private!) ... // Die Schnittstelle der Klasse (public!) public: void Init(short x, short y, short w, short h); void Move(short x, short y); void Resize(short w, short h); void SetColor(BYTE r, BYTE g, BYTE b); void DrawIt(); }; |
Nach der Definition der Klasse geht's ans Definieren der Memberfunktionen. Die Memberfunktionen werden in einer eigenen Datei rect.cpp definiert. Damit der Compiler beim Übersetzen dieser Datei die Klasse Rect auch kennt, müssen Sie am Anfang die vorhin erstellte Header-Datei rect.h einbinden.
Der Aufbau der Memberfunktionen dürfte aus ihrer Funktion hervorgehen. Lediglich die Memberfunktion DrawIt() wurde etwas vereinfacht, da wir hier keine Grafikprogrammierung betreiben wollen. Sie gibt nur die Eigenschaften des Rechtecks aus.
|
Datei rect.cpp // Definition der Memberfunktionen von Rect // Standard Headerdateien einbinden #include <iostream> using std::cout; using std::endl; // Klassendefinition einbinden #include "rect.h" // Definition der Init() Memberfunktion void Rect::Init(short x, short y, short w, short h) { xPos = x; yPos = y; width = w; height = h; rectColor.red = 0x00; rectColor.green = 0x00; rectColor.blue = 0x00; } // Definition der Move() Memberfunktion void Rect::Move(short x, short y) { xPos = x; yPos = y; } // Definition der Resize() Memberfunktion void Rect::Resize(short w, short h) { width = w; height = h; } // Definition der SetColor() Memberfunktion void Rect::SetColor(BYTE r, BYTE g, BYTE b) { rectColor.red = r; rectColor.green = g; rectColor.blue = b; } // Definition der DrawIt() Memberfunktion void Rect::DrawIt() { cout << "Position: " << xPos << ',' << yPos << endl; cout << "Grösse : " << width << ',' << height << endl; cout << std::showbase << std::hex; cout << "RGB-Wert: " << static_cast<int>(rectColor.red) << ',' << static_cast<int>(rectColor.green) << ',' << static_cast<int>(rectColor.blue) << endl; cout << std::dec; } |
Im Beispiel wurden eventuelle Plausibilitätsabfragen der Parameter weggelassen, damit das Beispiel noch einigermaßen überschaubar bleibt.
Anschließend ist die Datei rect.h noch so zu übersetzen, dass nur eine obj-Datei oder eine lib-Datei erzeugt wird. Sie können diese Datei nicht mit dem 'normalen' Compileraufruf übersetzen, da ansonsten der Linker versuchen würde, eine ablauffähige EXE-Datei zu erstellen. Und dies geht hier nicht, da die Datei wegen der fehlenden main() Funktion alleine nicht lauffähig ist. Wie Sie eine einzelne obj oder lib-Datei erzeugen, entnehmen Sie bitte der Beschreibung zu Ihrem Compiler.
Und damit sind die Arbeiten an unserer Klasse im Prinzip beendet. Selbstverständlich sollte die Klasse dann auch noch entsprechend dokumentiert werden, damit Sie später dann auch eingesetzt werden kann.
Nachfolgend sehen Sie eine kleine Anwendung, die den Einsatz der neuen Klasse Rect demonstriert. Um Objekte der neuen Klasse bilden zu können, müssen Sie zum einen im Programm die Header-Datei rect.h einbinden. Und zum anderen müssen Sie dem Linker noch mitteilen, dass er zu Ihrem Programm noch die im vorherigen Schritt erstellte obj- bzw. lib-Datei (mit dem Code der Rect-Memberfunktionen) dazu binden soll.
|
// Zuerst Dateien einbinden #include <iostream> #include "Rect.h" // Kompletten Namensraum std einblenden using namespace std; // Zwei Rechteck definieren Rect rect1, rect2; // main() Funktion int main() { // Beide Rechtecke initialisieren rect1.Init(10,10,640,480); rect2.Init(100,50,800,600); // Rechteckdaten ausgeben cout << "1. Rechteck:\n"; rect1.DrawIt(); cout << "2. Rechteck:\n"; rect2.DrawIt(); // 1. Rechteck verschieben rect1.Move(20,20); // 2. Rechteck vergrössern und Farbe abändern rect2.Resize(1024,786); rect2.SetColor(0xC0, 0xC0, 0xC0); // Rechteckdaten ausgeben cout << "1. Rechteck:\n"; rect1.DrawIt(); cout << "2. Rechteck:\n"; rect2.DrawIt(); } |
Damit wollen wir die Einführung der Klassen und Objekte beenden. Sie werden im weiteren Verlaufe des Kurses noch genügend Möglichkeiten haben, dies ausführlich zu üben.
Sehen wir uns nun noch kurz an, was eine objektorientierte Programmiersprache, wie z.B. C++, von einer prozeduralen Programmiersprache, wie z.B. C, unterscheidet.
Zum einen unterstützt OOP die Kapselung von Member (Encapsulation). Durch die Kapselung kann ein Klasse bestimmte Eigenschaften und Memberfunktionen für den direkten Zugriff sperren (private Member). Aber das sollten Sie in der Zwischenzeit ja schon wissen. Damit kann eine Klasse als eine Art Blackbox betrachtet werden, die eine genau definierte Schnittstelle (Interface, public Memberfunktionen) besitzt. Nur über diese Schnittstelle kann der Anwender dann mit der Klasse (eigentlich dem Objekt) agieren. Im vorherigen Beispiel waren z.B. die Memberfunktionen Size(...) und Move(...) zugänglich, während die Eigenschaften wie width oder yPos vor dem Anwender verborgen waren. Aufgrund diese Kapselung der Eigenschaften kann der Anwender nicht mehr direkt durch Manipulation z.B. der Eigenschaft width die Fensterbreite unzulässig verändern. Dies kann nur noch über die Memberfunktion Size(...) erfolgen, die dann natürlich auch entsprechende Plausibilitätsüberprüfungen durchführen sollte.
Als zweites Merkmal stellt die OOP die Vererbung (Inheritance) zur Verfügung. Durch Vererbung werden die Schnittstelle und die Eigenschaften einer Klasse (Basisklasse) an eine andere Klasse (abgeleitete Klasse) übertragen. Diese neue Klasse verfügt dann über alle Member der ursprünglichen Klasse so wie ihre eigenen Member. Um wieder zum vorherigen Beispiel zurück zu kommen, könnten Sie die Klasse Window als Basisklasse für eine neue Klasse Button verwenden, da ein Button (Schaltfläche) auch eine definierte Ausdehnung und Beschriftung besitzt. Zusätzlich würde die neue Klasse Button noch die Eigenschaft erhalten, dass sie z.B. einen bestimmten Zustand wie gedrückt oder ausgewählt annehmen kann.
Und als letzte Eigenschaft stellt OOP die Polymorphie (Polymorphism) zur Verfügung. Polymorphie kennzeichnet die Eigenschaft, dass Memberfunktionen in abgeleiteten Klassen zwar mit gleichem Namen aber unterschiedlichem Ablauf implementiert werden können. Um diesen etwas abstrakten Sachverhalt zu veranschaulichen, kehren wir zum Beispiel mit der neuen Klasse Button zurück. Button besitzt ja unter anderem die Member seiner Basisklasse Window. Sowohl Window wie auch Button benötigen eine Memberfunktion um sich darstellen zu können. Mithilfe der Polymorphie kann nun sowohl die Klasse Window wie auch die der Klasse Button jeweils eine Memberfunktion mit dem Namen Draw() hierfür verwenden. Wann welche Memberfunktion aufgerufen wird, hängt nur vom Objekttyp ab.
Mehr zu den einzelnen OOP Eigenschaften erfahren Sie aber im Verlaufe des Kurses noch.
So, nun kommt das Beispiel und dann sind Sie wieder dran.
|
|
|
|
1. Komplexe Zahl: Nach Addition 2. Zahl zur 1. Zahl Nach Subtraktion der 1. Zahl von der 2.
Zahl |
|
// Beispiel zu Klassen // Dateien einbinden #include <iostream> #include <iomanip> using std::cout; using std::endl; // Klassendefinition class Complex { // Geschuetzte Eigenschaften double real; // Real-Anteil double imag; // Imaginär-Anteil public: // Memberfunktionen void Init(double r, double i); void AddComplex(const Complex& op); void SubComplex(const Complex& op); void PrintComplex() const; }; // Definition der Memberfunktionen // Komplexe Zahl initialisieren void Complex::Init(double r, double i) { real = r; imag = i; } // 2 komplexe Zahlen addieren void Complex::AddComplex(const Complex& op) { real += op.real; imag += op.imag; } // 2 komplexe Zahlen subtrahieren void Complex::SubComplex(const Complex& op) { real -= op.real; imag -= op.imag; } // Komplexe Zahl ausgeben void Complex::PrintComplex() const { cout << "Realteil: " << real; cout << " / Imaginärteil: " << imag << endl; } // main() Funktion int main() { // 2 komplexe Zahlen (Objekte) definieren Complex Number1, Number2; // Objekte initialisieren Number1.Init(1.1, 2.2); Number2.Init(3.3, 4.4); // Objekte ausgeben cout << "1. Komplexe Zahl:\n"; Number1.PrintComplex(); cout << "2. Komplexe Zahl:\n"; Number2.PrintComplex(); // 2. Objekt zum 1. Objekt addieren Number1.AddComplex(Number2); // und Ergebnis ausgeben cout << "\nNach Addition 2. Zahl zur 1. Zahl\n"; cout << "Neue 1. Zahl:\n"; Number1.PrintComplex(); // 1. Objekt vom 2. Objekt subtrahieren Number2.SubComplex(Number1); // und Ergebnis ausgeben cout << "\nNach Subtraktion der 1. Zahl von der 2. Zahl\n"; cout << "Neue 2. Zahl:\n"; Number2.PrintComplex(); } |
Realisieren Sie eine Klasse zur Klassifizierung von Daten. Bei der Klassifizierung wird der Gesamtwertebereich der Daten in kleinere Wertebereiche (Klassen) unterteilt und die Häufigkeit des Auftretens von Daten innerhalb der Klasse gezählt.
| Beispiel: | Wertebreich: 0..99 Anzahl der Klassen: 20 |
| daraus folgt: | Wertebereich der 1. Klasse: 0..4 Wertebereich der 2. Klasse: 5..9 Wertebereich der 3. Klasse: 10..14 ... Wertebereich der 20. Klasse: 95..99 |
Die zu klassifizierenden Daten liegen als unsigned short Werte vor, welche über eine Memberfunktion an die Klasse zur Klassifizierung übergeben.
Des weiteren soll die Klasse eine Memberfunktion enthalten, die die Häufigkeit des Auftretens der Daten in den Klassen ausgibt.
Für eventuell notwendigen Initialisierung der Eigenschaften der Klasse, ist ebenfalls eine Memberfunktion zu verwenden.
Verwenden Sie keine Literale (magic numbers), sondern statt dessen entsprechende benannte Konstanten. Da bis jetzt noch keine Klassen-Konstanten definiert werden können, verwenden Sie globale benannte Konstanten. Legen Sie mithilfe dieser Konstanten die maximalen Anzahl der Klassen (Bereiche), die obere Grenze des Wertebereichs der Daten (Untergrenze soll 0 sein) und die Anzahl der Daten fest.
Erzeugen Sie in der main() Funktion entsprechend viele Zufallszahlen und geben Sie diese zur Kontrolle aus. Übergeben Sie dann die Zufallszahl an die Klasse zur Klassifizierung. Zum Schluss geben Sie noch die Häufigkeit der Daten in den Klassen aus.
Die nachfolgende Ausgabe geht von 10 Klassen, einem Wertebereich der Daten von 0...99 und 100 Daten aus.
|
41, 67, 34, 0, 69, 24, 78, 58, 62, 64, 5, 45, 81, 27, 61, 91, 95, 42, 27, 36, 91 , 95, 47, 26, 71, 38, 69, 12, 67, 99, 35, 94, 3, 11, 22, 33, 73, 64, 41, 11, 53, 3, 41, 29, 78, 16, 35, 90, 42, 88, 6, 40, 42, 64, 48, 46, 5, 90, 29, 70, 50, 6, 40, 66, 76, 31, 8, 44, 39, 26, 23, 37, 38, 18, 82, 29, 41, Verteilung: |
Da Klassen DIE zentrale Rolle spielen, hier gleich noch eine weitere Übung.
Es ist eine Klasse für die Implementierung eines Stack zu schreiben. Ein Stack ist ein Bereich, der zum temporären Sichern von Werte dient, d.h. auf diesem Stack können Werte abgelegt und dann später ausgelesen werden. Hierbei gilt, dass der zuletzt abgelegte Wert als erstes wieder ausgelesen wird. Im Prinzip können Sie sich ein Stack als eine Art Ablage vorstellen, auf dem Sie ein Blatt nach dem anderen ablegen. Wenn Sie die Ablage abarbeiten, holen Sie sich immer das zuoberst liegende Blatt wieder.
Der in dieser Übung zu realisierende Stack soll zur Ablage von short-Wert dienen. Damit die Übung zunächst einfach bleibt, sollen maximal 10 short-Werte auf dem Stack zwischengespeichert werden können. Verwenden Sie zur Definition der maximalen Anzahl von short-Werten wieder eine globale benannte Konstante und kein Literal!
Für die Ablage einer short-Zahl ist eine Memberfunktion Push(...) zu schreiben. Diese Memberfunktion liefert so lange true zurück, wie der ihr übergebene Wert abgelegt werden kann. Ist der Stack komplett belegt, so muss Push(...) false zurückliefern.
Zum Abholen der auf dem Stack abgelegten Werte soll eine Memberfunktion Pop(...) verwendet werden. Enthält der Stack noch Werte, liefert die Memberfunktion als Returnwert true zurück und über einen Parameter den abgeholten short-Wert. Ist der Stack leer, liefert die Memberfunktion false.
Für eine eventuelle Initialisierung des Stacks ist bei Bedarf eine gesonderte Memberfunktion zu erstellen.
Es ist ein globales Stackobjekt zu definieren. In der main() Funktion ist anschließend der Stack so lange mit 'Zufallszahlen' zu füllen, bis er voll ist. Die abgelegten Werte sind zur Kontrolle auszugeben.
Nach dem der Stack vollständig gefüllt ist, sind alle Werte wieder vom Stack abzuholen und erneut auszugeben.
|
Schiebe Werte auf Stack: |