Twitter und Facebook-Anbindung
X
Tweet Follow @twitterapi
!!! Anbindung an twitter und facebook öffnen !!!

Wenn Ihnen mein Online-Buch gefällt,
dann bedanken Sie sich doch mit einer kleinen Spende...

23 Überladen von Operatoren

23 Überladen von Operatoren

Nachdem Sie jetzt wissen, was es mit der Vererbung zu tun hat, werde ich jetzt auf eine andere sehr nette Sache eingehen, welche den Umgang mit Objekten sehr vereinfachen kann, was aber zur Implementierungszeit etwas mehr Denkleistung fordert. Die Rede ist vom überladen von Operatoren. Was bedeutet dies überhaupt?

Nun, Sie wissen; was Operatoren sind und es ist auch einsehbar, dass man sie nur auf Primitivtypen (char, int, float) anwenden kann. Falsch, es geht auch mit Objekten, allerdings muss man dafür Methoden zur Verfügung stellen, welche dies ermöglichen.

Ich werde in diesem Kapitel Zeigen, wie man Objekte miteinander z.B. addieren, vergleichen, eine Ein - bzw. Ausgabe realisieren kann und was sonst noch so geht.

Zum Seitenanfang
Zum Inhaltsverzeichnis

23.5 Ein - und Ausgabe (<< und >>)

23.5 Ein - und Ausgabe (<< und >>)

Eine sehr nette Funktionalität, ist das überladen der Serialisierungsoperatoren "<<" und ">>". In Kombination mit "cin" und "cout", kann man auf diese Art und Weise, Werte für Objekte einlesen lassen (immer nur ein Wert einlesbar) oder für das Objekt eine Ausgabe realisieren (dabei spielt es keine Rolle, ob mit Ein- und Ausgabe von oder auf die Konsole oder von oder aus Dateien gemeint ist).

Das klingt ja alles ganz spannend, aber bevor ich das zeige, muss ich ein wenig ausholen, denn diese zwei Operatoren kann man nicht als Methoden der Klasse implementieren, sondern muss sie als globale Funktionen bereitstellen. Damit jene dann auf das Objekt zugreifen können, müsste man normalerweise entweder entsprechende Getter und Setter bauen oder alle benötigten Sachen "public" machen, was unter Umständen nicht gewollt sein könnte. Um dies zu vermeiden, kann man eine "Freundschaft" mit der Klasse eingehen. Das klingt zwar jetzt ein wenig komisch, ist aber tatsächlich möglich. Man realisiert dies, indem man die Funktionsprototypen der entsprechenden Funktionen, welche auf die privaten Elemente eines Objektes zugreifen möchten, in die Klassendefinition mit aufnehmen und der Klasse mitteilt, dass dies "Freunde" sind. Dies sieht für das Beispiel so aus.

 1
 2
 3
 4
 5
 6
 7
					
#include <iostream>

// ...

		// Serialisieren
		friend std::ostream& operator<< (std::ostream&, const CInteger&);
		friend std::istream& operator>> (std::istream&, CInteger&);
					

Natürlich sollte man direkt in der CPP-Datei der Klasse, diese globalen Funktionen packen. Andernfalls könnten andere diese Schnittstelle nutzen, um Inhalte des Objektes zu manipulieren und somit das gesamte Konzept der OOP aus hebeln. Also, wenn Sie Freunde definiert, dann implementieren Sie sie an einer Stelle, an der man sie nicht ersetzen kann (In der CPP-Datei der Klasse könnte man sie zwar auch manipulieren, wenn jemand anderes diese Datei besitzt, aber dann hat er eh vollen Zugriff). Schauen wir uns nun die entsprechende Implementierung dieser zwei Methoden an.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
					
// Ein - und Ausgabe //////////////////////////////////////////////////////////
std::ostream& operator<< (std::ostream& oStream, const CInteger& oInt) {
	oStream << "Wert=" << oInt.m_iValue << endl;
	return oStream;
}

std::istream& operator>> (std::istream& oStream, CInteger& oInt) {
	oStream >> oInt.m_iValue;
	return oStream;
} // operator<< ///////////////////////////////////////////////////////////////
  
// ...

// Ermöglicht wird jetzt z.B. so etwas
CInteger oObjekt(5);

std::cin >> oObjekt;
std::cout << oObjekt;

// oder auch
CInteger* pZeigerAufObjekt = new CInteger(5);

std::cin >>* pZeigerAufObjekt;
std::cout << *pZeigerAufObjekt;

delete pZeigerAufObjekt;
					

Wie Sie in Zeile 1 und 6 sehr schön sehen, besitzen diese Funktionen nicht mehr den Namespace der Klasse und gehören somit nicht offiziell zu ihr. Wie Sie aber in Zeile 2 und 7 sehen, ist es trotzdem möglich, auf das private Attribut "m_iValue" zuzugreifen.

Zu dem Rückgabe - und dem ersten Übergabeparameter ist zu sagen, dass es sich hier um Referenzen auf ein Streamobjekt handelt, welches die Mechanismen von "cin" und "cout" wieder spiegeln. Der zweite Parameter ist dann unser Objekt. Da ich in der zweiten Funktion nur eine Ausgabe mache, kann ich getrost eine konstante Referenz übergeben, was bei der ersten Funktion nicht möglich ist, da ich das Objekt ja manipuliere, also den eingelesenen Wert eintragen will.

Zum Seitenanfang
Zum Inhaltsverzeichnis

23.3 Berechnungen (+, -, ++, --)

23.3 Berechnungen (+, -, ++, --)

Da nun eine Grundlage geschaffen ist, komme ich jetzt zu den Berechnungsoperatoren. Jene sind zum Teil unär und zum Teil binär. Ich werde dann jeweils darauf hinweisen, um was es sich handelt.

Zuerst werde ich die Klassendefinition in der Header-Datei, um folgende Einträge, im "public" Bereich, erweitern.

 1
 2
 3
 4
 5
 6
 7
 8
 9
					
		// unär
		CInteger	operator+() const;	// Vorzeichen
		CInteger	operator-() const;	// Vorzeichen
		CInteger&	operator++(void);	// linksseitig
		CInteger	operator++(int);	// Rechtsseitig

		// binär
		CInteger	operator+(const CInteger&) const;
		CInteger	operator+(const int&) const;
					

Hier scheint es auf den ersten Blick Dopplungen zu geben, aber wie Sie evtl. schon an den Kommentaren erkennen können, haben diese vermeintlichen Dopplungen ihre Berechtigung (je nachdem, was man vor hat). Was dahinter steckt, werden Sie gleich sehen. Des weiteren gibt es jetzt Methoden, hinter welchen ein "const" steht und Sie fragen sich jetzt bestimmt, was es damit auf sich hat. Nun, das ist zum einen rein optional und soll zum anderen dem Benutzer dieser Methoden zeigen, das durch diese Methoden das eigentliche Objekt unberührt bleibt. Dies ist u.a. eine wichtige Eigenschaft von binären Operatoren. Kommen wir nun zur Implementierung.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
					
// Vorzeichen /////////////////////////////////////////////////////////////////
CInteger CInteger::operator+(void) const {
	return *this;
}

CInteger CInteger::operator-(void) const {
	return CInteger(-this->m_iValue);
} // operator- ////////////////////////////////////////////////////////////////

// ...

// Ermöglicht wird jetzt z.B. so etwas
CInteger oObjekt1(5);
CInteger oObjekt2(7);

oObjekt1			= +oObjekt2;
oObjekt1			= -oObjekt1;

// oder auch
CInteger* pZeigerAufObjekt1	= new CInteger(5);
CInteger* pZeigerAufObjekt2	= new CInteger(7);

*pZeigerAufObjekt1		= +(*pZeigerAufObjekt2);
*pZeigerAufObjekt2		= -(*pZeigerAufObjekt2);

delete pZeigerAufObjekt1;
delete pZeigerAufObjekt2;
					

Diese zwei Methoden erlauben es also, positive oder negative Vorzeichen zu benutzen. Wie bereits erwähnt, wird dabei aber nicht der eigene Wert manipuliert, sondern eine manipulierte Kopie zurückgegeben. Warum gibt man jetzt aber keine Referenz zurück wie vorhin? Überlegen wir uns folgende Situation. Sie wollen schreiben "oA = -oB". Jetzt wird zuerst die Operatormethode "-" des Objektes "oB" aufgerufen, welche ein neues Objekt (nennen wir es mal "oX") zurück gibt. Anschließend wird die Operatormethode "="; des Objektes "oA" aufgerufen und eine Kopie von "oX" übergeben. Jetzt braucht zwar die Operatormethode "=" eine Referenz, aber Vorsicht, was ist damit gemeint? Der Referenzierungsoperator hat etwas mit call-by-reference zu tun hat und wenn man so etwas baut, übergibt man die Variable normal und Dereferenziert sie nicht extra (in dem Fall müsste die Methode ja ein Pointer entgegen nehmen). Wenn Sie jetzt Kopfschmerzen haben, dann ist das normal, denn auch darüber muss man eine weile Nachdenken, bevor man es einsieht.

Die erste Methode ist reine Kür, da lediglich das Objekt selbst zurückgegeben wird. Aber Moment mal, jetzt wird das Objekt selbst zurückgegeben und vorhin war es eine Referenz? Das hat schon seine Richtigkeit, denn erinnern Sie sich. Vorhin wollte ich keine Kopie des Objektes erzeugen, sondern wirklich die Referenz haben. Jetzt will ich aber eine Kopie und wenn man im Debugg-Modus ist, wird man evtl. auch sehen, dass nach dem "return", der Kopierkonstruktor aufgerufen wird. Ich kann es nicht oft genug sagen, aber man muss sich immer vor Augen halten, dass eine Referenz und eine Adresse nicht immer das gleiche sind. Es kommt echt darauf an, in welchem Kontext man diesen Begriff verwendet. Jetzt könnten Sie sich aber fragen, wenn wir hier eh nichts gemacht wird, warum brauche ich so etwas überhaupt. Die Antwort ist, dass es später solche Ausdrücke wie "oA = +oB" geben könnte und dann ist dieser Operator notwendig.

Die zweite Methode ist dazu da, den negativen Wert zurückzugeben. Auch hier wird nicht das eigene Objekt manipuliert, sondern nur eine manipulierte Kopie zurückgegeben. Genau genommen erzeuge ich mir sogar eine Kopie und gebe sie zurück, welche dann wiederum kopiert wird, da mein in der Methode erzeugtes Objekt, nach dem Methodenaufruf, freigegeben wird.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
					
// Inkrementieren /////////////////////////////////////////////////////////////
CInteger& CInteger::operator++(void) {
	this->m_iValue++;
	return *this;
}

CInteger CInteger::operator++(int) {
	CInteger oResult(this->m_iValue);
	this->m_iValue++;
	return oResult;
} // operator++ ///////////////////////////////////////////////////////////////

// ...

// Ermöglicht wird jetzt z.B. so etwas
CInteger oObjekt1(5);
CInteger oObjekt2(7);

oObjekt1			= ++oObjekt2;
oObjekt1			= oObjekt2++;

// oder auch
CInteger* pZeigerAufObjekt1	= new CInteger(5);
CInteger* pZeigerAufObjekt2	= new CInteger(7);

*pZeigerAufObjekt1		= ++(*pZeigerAufObjekt2);
*pZeigerAufObjekt2		= (*pZeigerAufObjekt2)++;

delete pZeigerAufObjekt1;
delete pZeigerAufObjekt2;
					

Die erste Methode implementiert den linksseitigen "++" Operator. Hier wird erst der eigene Wert um eins erhöht und anschließend zurückgegeben. Da hier der eigene Wert des Objektes modifiziert wird, ist diese Methode nicht mit "const" deklariert und es wird wieder die eigene Referenz zurückgegeben (aus mittlerweile bekannten Gründen).

Die zweite Methode implementiert den rechtsseitigen "++" Operator. Hier wird erst der Wert zurück gegeben und anschließend der eigene Wert um eins erhöht. Um dies zu realisieren, benötige ich also eine Kopie des Originals, welches dann auch zurückgegeben wird und kann anschließend den Wert der Membervariable um 1 erhöhen. Da ich wie gesagt hier nur eine Kopie zurück gebe, ist der Rückgabeparameter auch anders, als beim linksseitigen Inkrementierungsoperator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
					
// Addition ///////////////////////////////////////////////////////////////////
CInteger CInteger::operator+(const CInteger& oIntRight) const {
	return CInteger(this->m_iValue + oIntRight.m_iValue);
}

CInteger CInteger::operator+(const int& iIntRight) const {
	return CInteger(this->m_iValue + iIntRight);
} // operator+ ////////////////////////////////////////////////////////////////

// ...

// Ermöglicht wird jetzt z.B. so etwas
CInteger oObjekt1(5);
CInteger oObjekt1(7);
CInteger oObjekt3(9);

oObjekt1			= oObjekt2 + oObjekt3;
oObjekt1			= oObjekt2 + 15;

// oder auch
CInteger* pZeigerAufObjekt1	= new CInteger(5);
CInteger* pZeigerAufObjekt2	= new CInteger(7);
CInteger* pZeigerAufObjekt3	= new CInteger(9);

*pZeigerAufObjekt1		= *pZeigerAufObjekt2 + *pZeigerAufObjekt3;
*pZeigerAufObjekt2		= *pZeigerAufObjekt2 + 15;

delete pZeigerAufObjekt1;
delete pZeigerAufObjekt2;
delete pZeigerAufObjekt3;
					

In der ersten Methode habe ich jetzt den eigentlichen "+" Operator implementiert, mit welchem ich jetzt zwei Objekte miteinander addieren kann. Die anderen Berechnungsmethoden (-, *, /, %, &, | usw.), sähen äquivalent aus. Berechne ich "oA + oB", wird der die Operatormethode "+" von "oA" aufgerufen und der Übergabeparameter "oIntRight" wäre dann "oB". Da hier das eigene Objekt nicht verändert wird, ist diese Methode als konstant definiert und muss logischerweise wieder ein neues Objekt zurück geben.

Die Zweite Methode ist fast so ähnlich, nur das ich hier zusätzlich erlaube, dass man mein Objekt auch mit einem Integer addieren kann. Entsprechend modifiziert, ist dies auch für andere Datentypen möglich.

Zum Seitenanfang
Zum Inhaltsverzeichnis

23.1 Allgemein zu Operatoren

23.1 Allgemein zu Operatoren

Bevor ich richtig anfange, sollte ich doch ein paar Worte im Vorfeld verlieren. Das Überladen von Operatoren macht nicht bei jeder Klasse Sinn. Im vorigen Kapitel ging es um diverse Fahrzeuge und die lassen sich bekanntlich schlecht miteinander verrechnen, aber hin und wieder gibt es Klassen, mit denen es wiederum sehr gut geht und dies sind meistens Klassen, welche irgendwelche Werte oder Zeichen(ketten) repräsentieren.

Bevor man anfängt, muss man einen geeigneten Kopierkonstruktor implementieren. Hat man eine flache Objektstruktur, welche nur Primitivtypen enthält, reicht der Standardkonstruktor. In dem Moment, wo man etwas dynamisch erzeugt (z.B. Arrays oder eingebettete Objekte) ist ein eigener Kopierkonstruktor vonnöten.

Sobald man dies getan hat, muss man sich überlegen, welche Operatoren man braucht und ob diese vonnöten und realisierbar sind. Überladen kann man z.B. Vergleiche (=, <, > usw.), Zuweisungen (=, (), [], usw.), Berechnungen (+, -, *, /, +=, ++ usw.), bis hin zu dem Punk, den Pfeil sowie "new" und "delete". Eine vollständige Liste, findet man in der Hilfe bzw. im MSDN. Dort sind auch die Prioritäten ersichtlich und ob es sich um s.g. unäre (einseitige) oder binäre (zweiseitige) Operatoren handelt.

Um all dies zu zeigen, habe ich mir ein kleines Beispiel ausgedacht. Es geht um eine Klasse, welche lediglich einen Integerwert verwaltet und den besonders kreativen Namen "CInteger" haben wird. Nun könnten Sie sich fragen, warum ich das ausgerechnet mit dem Integer mache, da dies scheinbar absolut unnötig ist. Da stimme ich Ihnen zu, aber man kann sehr schön zeigen, wie man das Überladen von Operatoren realisiert, ohne viel Quelltext zu schreiben. Im Internet findet man häufig Beispiele mit einer Klasse, welche einen Bruch oder eine komplexe Zahl darstellt. Allerdings sind die Berechnungen innerhalb der Klasse und das ganze Beiwerk, oftmals so umfangreich, dass der eigentliche Fokus verloren geht. Ich zeige Ihnen also, wie es prinzipiell funktioniert und Sie können sich das dann bei Gelegenheit adaptieren.

Ich weiße explizit darauf hin, dass ich für meine Klasse keinen Kopierkonstruktor benötige und somit auch keinen implementieren werde.

Zum Seitenanfang
Zum Inhaltsverzeichnis

23.6 Sonstige Operatoren (cast, (), [])

23.6 Sonstige Operatoren (cast, (), [])

Zum Schluss werde ich noch auf ein paar andere Operatoren eingehen, welche man überladen kann, wobei ich nicht auf alle eingehen werde, da ihre Verwendung zum einen mit größter Vorsicht zu genießen sind und zum anderen, sie in der Praxis sehr selten benötigt werden. Andere hingegen sind allerdings sehr nützlich, wie z.B. die Typumwandlungsoperatoren oder die Klammer bzw. Funktionsoperatoren. Dafür modifiziere ich ein letztes mal die Header-Datei wie folgt.

 1
 2
 3
 4
 5
 6
					
		// Typumwandlungen
		operator	float(void) const;

		// Funktionsoperatoren
		void		operator()(int, int) const;
		void		operator[](int) const;
					

Ich fangen zunächst mit der Implementierung für den Typumwandlungsoperator an.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
					
// Typumwandlung //////////////////////////////////////////////////////////////
CInteger::operator float(void) const {
	return (float)this->m_iValue;
} // operator float ///////////////////////////////////////////////////////////
  
// ...

// Ermöglicht wird jetzt z.B. so etwas
CInteger	oObjekt(5);
float		fZahl1			= (float)oObjekt

// oder auch
CInteger*	pZeigerAufObjekt	= new CInteger(5);
float		fZahl2			= (float)*pZeigerAufObjekt;

delete pZeigerAufObjekt;
					

Wie Sie am Beispiel sehr schön erkennen könn, ist es jetzt möglich, die Objekt durch eine Typumwandlung zu jagen. Für jede Umwandlung die gewünscht ist, muss man eine entsprechende Methode implementieren. Aber, warum braucht man dies? Nun, ich habe vorhin Operatoren für die Berechnung von Ausdrücken bereitgestellt, aber einen Fall habe ich dabei unter den Tisch Fallen lassen. Möchte an z.B. berechnen "oA = 5 + oB", dann wird dies der Compiler nicht zulassen. Ein möglicher Ausweg wäre jetzt zu schreiben "oA = 5 + (int)oB". Aber auch das könnte man anders handhaben (z.B. durch einen geeigneten expliziten Kopierkonstruktor, der aus der 5 ein Objekt zaubert oder man macht dies händisch mit "oA = CInteger(5) + oB").

Nun zu den Klammer - oder auch Funktionsoperatoren genannt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
					
// Funktionsoperatoren ////////////////////////////////////////////////////////
void CInteger::operator() (int iValue1, int iValue2) const {
	std::cout << "Es wurde " << iValue1 << " und " << iValue2 << " uebergeben\n";
}

void CInteger::operator[] (int iValue) const {
	std::cout << "Hole z.B. " << iValue << ". Element\n";
} // operator[] ///////////////////////////////////////////////////////////////

// ...

// Ermöglicht wird jetzt z.B. so etwas
CInteger oObjekt(5);

oObjekt(1, 2);
oObjekt[7];

// oder auch
CInteger* pZeigerAufObjekt = new CInteger(5);

(*pZeigerAufObjekt)(1, 2);
(*pZeigerAufObjekt)[7];

delete pZeigerAufObjekt;
					

Mit diesen Operatoren, kann man Objekte wie Funktionen verwenden. Dabei kann man selbst den Rückgabetyp beliebig festlegen und zumindest bei dem Operator mit den runden Klassern, kann man beliebig viele Parameter übergeben. Bei den eckigen Klammen jedoch immer nur ein Parameter. Im Falle meiner Klasse, machen diese Operatoren nicht wirklich viel Sinn, aber stellen Sie sich vor, Sie hätten eine Matrixklasse, die intern ein Array mit Vektoren besitzt. Mit dem "()" Operator könnte man jetzt z.B. den Wert aus dem zweidimensionalen Feld holen und mit dem "[]" Operator einen Vektor, der an übergebenen Index steht. Sicherlich kann man sich für diese Aufgaben auch Get-Methoden entwerfen, die sogar vom Aufbau her identisch wären, aber mit den zwei erwähnten Operatoren, lässt es sich schöner arbeiten. Trotzdem sollten Sie sich genau überlegen, ob Sie einen solchen Mechanismus implementieren wollen oder nicht, denn gerade für Anfänger, ist eine solche Syntax extrem verwirrend und hinzu kommt, dass die Klammeroperatoren, absolut keine Aussage mehr darüber treffen, was sie eigentlich tun.

Zu guter Letzt, möchte ich noch ein paar Worte über die Operatoren verlieren, welche ich in diesem Tutorial unterschlagen habe. Beispielsweise ist es möglich, den Pfeiloperator "->" zu überschreiben. Allerdings kann man sich bei falscher Implementation, so den Zugriff auf ein Objekt verbauen. Des weiteren ist es möglich, "new" und "delete" zu überladen. Letzteres ist aber auch mit aller höchster Vorsicht zu genießen, weil man hier effektiv Einfluss auf die Speicherverwaltung nimmt. Profis benutzen derartige Überladungen, um eine elegante Speicherprotokollierung zu erstellen. Falls Sie mehr dazu wissen wollen, empfehle ich entsprechende Artikel im Internet zu lesen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

23.2 Zuweisungen (=, +=)

23.2 Zuweisungen (=, +=)

Bevor ich anfange etwas zu berechnen, benötige ich einen Zuweisungsoperator. Im folgenden werden Sie sehen, wie meine Klasse definiert ist und wie man dann das "=" realisiert.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
					
// Klasse, welche einen Integer repräsentiert
class CInteger {
	private:
		int m_iValue;

	public:
		// Eigener Konstruktor
		CInteger(int);

		// (binäre) Zuweisung
		CInteger&	operator=(const CInteger&);
		CInteger&	operator=(const int&);
		CInteger&	operator+=(const CInteger&);
		CInteger&	operator+=(const int&);
};
					

Dies ist erst einmal die Grundlegende Klassenstruktur, welche in der Header-Datei steht und welche ich im Verlauf dieses Kapitels erweitern werde. Warum ich die einzelnen Methoden so definiert habe, wie ich sie definiert habe, werde ich dann bei der Implementierung jeder einzelnen, erläutern.

Auffällig ist hier eine neue Art der Schreibweise. Ich habe bei den Methoden-Prototypen nur noch den Übergabetyp angegeben und keinen Namen der Variable. Dies ist bei Prototypen immer möglich und ich habe dies lediglich gemacht, um Ihnen mal zu zeigen, dass es auch anders geht. Es wäre nicht falsch, wenn Sie Variablennamen mit hinschreibt. In der Regel lässt man sie aber nur Weg, wenn keine Missverständnisse auftreten können (ist nur für die Codevervollständigung notwendig). Hat man Beispielsweise eine Matrixklasse und übergibt im Konstruktor eine Zeilen - und Spaltenanzahl, wäre es fatal, den Variablennamen nicht mit anzugeben, da man sonst nicht wüsste, welcher der beiden was ist.

 1
 2
 3
 4
					
// Eigener Konstruktor ////////////////////////////////////////////////////////
CInteger::CInteger(int iValue)
	: m_iValue(iValue)
{} // CInteger ////////////////////////////////////////////////////////////////
					

Der Konstruktor ist nicht wirklich spannend und sollte soweit klar sein. Mit ihm realisiere ich die Initialisierung der Art:

 1
 2
 3
					
CInteger	oMeinObjekt(15);
// bzw.
CInteger*	pZeigerAufMeinObjekt = new CInteger(15);
					

Aber nun wird es spannend!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
					
// Einfache Zuweisungen ///////////////////////////////////////////////////////
CInteger& CInteger::operator=(const CInteger& oInt) {
	this->m_iValue = oInt.m_iValue;
	return *this;
}

CInteger& CInteger::operator=(const int& iInt) {
	this->m_iValue = iInt;
	return *this;
} // operator= ////////////////////////////////////////////////////////////////

// ...

// Ermöglicht wird jetzt z.B. so etwas
CInteger oObjekt1(5);
CInteger oObjekt2(7);

oObjekt1			= oObjekt2;
oObjekt2			= 14;

// oder auch
CInteger* pZeigerAufObjekt1	= new CInteger(5);
CInteger* pZeigerAufObjekt2	= new CInteger(7);

*pZeigerAufObjekt1		= *pZeigerAufObjekt2;
*pZeigerAufObjekt2		= 14;

// pZeigerAufObjekt1 = pZeigerAufObjekt2 wäre böse, da dort nicht der = Operator aufgerufen
// werden würde, sondern lediglich die Adressen kopiert und das erste Objekt somit in der Luft
// hängen würde.

delete pZeigerAufObjekt1;
delete pZeigerAufObjekt2;
					

Die erste Methode sorgt dafür, dass man einem Objekt der Instanz "CInteger", ein anderes Objekt des gleichen Typs, zuweisen kann. Wie Sie hier sehen, braucht man dann bei der Implementierung, natürluch einen Variablennamen, weil man sonst nicht auf den entsprechenden Wert zugreifen könnte.

Das Objekt was zugewiesen werden soll, steckt im Methodenparameter. Auffällig hierbei ist das "&". Dies bedeutet, dass ich kein neues Objekt benötige (im Hintergrund würde sonst der Kopierkonstruktor aufgerufen werden), sondern dass mir die Referenz auf das original Objekt genügt (Stichwort call-by-reference). Da ich jetzt das übergebene Objekt normalerweise verändern könnte, habe ich noch das "const" vorangestellt, damit ich dies zum einen nicht darf und zum anderen der Benutzer weiß, dass mit seinem Original nichts passiert.

Der Rückgabewert ist jetzt allerdings erläuterungsbedürftig. Sie sehen, dass ich wieder eine Referenz zurück gebe und wenn Sie sich die Zeile 3 und 8 anschauen, sehen Sie, dass ich die Referenz auf den dereferenzierten Zeiger auf sich selbst zurück gebe oder anders ausgedrückt, ich nehme das Objekt, worauf der eigene Zeiger zeigt (also sich selbst) und gebe das als Referenz zurück, was nicht zu verwechseln mit der Adresse des Objektes ist. Das ist eher wie call-by-reference, nur rückwärts. Nun, was bedeutet und warum mach ich das? Angenommen Sie schreiben "oA = oB". Dann wird die Operatormethode "=" des Objektes "oA" aufgerufen. Somit bezieht sich das "*this" auf "oA". Wenn Sie dann einfach nur ein Objekt zurückgeben würden, wird "oA" mit einer Kopie überschrieben. Dies scheint nicht weiter tragisch, aber schreiben wir "*pA = *pB", zeigt "pA" auf einmal auf etwas ganz anderes und das ursprüngliche Objekt wäre nicht mehr erreichbar und somit entstünde eine Speicherleiche. Um dies zu verhindern geben wir nicht eine Kopie, sondern das eigentliche Objekt, also die Referenz (und noch einmal nicht zu verwechseln mit der Adresse) zurück, damit ein Zeiger immer noch auf das Selbe zeigt und keine Speicherlecks entstehen. Ich weiß, das klingt jetzt erst einmal verwirrend, aber Sie müssen nur lange genug darüber nachdenken und dann werden Sie es einsehen. Dafür sollte aber der Rest der ersten Methode klar sein.

Die Zweite Methode ermöglicht es nun, einem Objekt ein Integer zuzuweisen. Auch hier wird wieder nur eine konstante Referenz übergeben und zurück kommt die eigene Referenz des Objektes. Über diesen Mechanismus kann man noch weitere "=" Operatoren überladen, damit dies auch für long, float usw. funktioniert.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
					
// Auf Objekt drauf addieren //////////////////////////////////////////////////
CInteger& CInteger::operator+=(const CInteger& oInt) {
	this->m_iValue += oInt.m_iValue;
	return *this;
}

CInteger& CInteger::operator+=(const int& iInt) {
	this->m_iValue += iInt;
	return *this;
} // operator+= ///////////////////////////////////////////////////////////////

// ...

// Ermöglicht wird jetzt z.B. so etwas
CInteger oObjekt1(5);
CInteger oObjekt2(7);

oObjekt1			+= oObjekt2;
oObjekt2			+= 14;

// oder auch
CInteger* pZeigerAufObjekt1	= new CInteger(5);
CInteger* pZeigerAufObjekt2	= new CInteger(7);

*pZeigerAufObjekt1		+= *pZeigerAufObjekt2;
*pZeigerAufObjekt2		+= 14;

delete pZeigerAufObjekt1;
delete pZeigerAufObjekt2;
					

Die erste Methode ermöglicht jetzt ein Objekt mit sich selbst und einem anderen zu Addieren. Da dies auch wieder eine unäre Operation ist, sehen die Übergabe - und Rückgabewerte, genauso aus, wie zuvor. Der einzige Unterschied ist lediglich, dass jetzt innerhalb der Methode, nicht mehr nur das "=", sondern das "+=" für die Werte angewendet wird.

Die zweite Methode ist wieder das Äquivalent mit einem Integer. Auch hier kann man weitere Methoden deklarieren, um die Zuweisung mit Floats usw. zu realisieren. Die Implementierung für "-=", "*=" und "/=" bzw. "%=", sehen dann entsprechend ähnlich aus.

Zum Seitenanfang
Zum Inhaltsverzeichnis

23.4 Vergleiche (==)

23.4 Vergleiche (==)

Natürlich ist es auch möglich, die Vergleichsoperatoren zu überladen. Wie das geht, werde ich jetzt am "==" Operator demonstrieren. Schauen Sie sich zuerst an, wie die Header-Datei erweitert werden mussn.

 1
 2
 3
					
		// Vergleich (Binär)
		bool		operator==(const CInteger&) const;
		bool		operator==(const int&) const;
					

Und jetzt der entsprechende Teil für die CPP-Datei

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
					
// Vergleichen ////////////////////////////////////////////////////////////////
bool CInteger::operator==(const CInteger& oInt) const {
	return this->m_iValue == oInt.m_iValue;
}

bool CInteger::operator==(const int& iInt) const {
	return this->m_iValue == iInt;
} // operator== ///////////////////////////////////////////////////////////////

// ...

// Ermöglicht wird jetzt z.B. so etwas
CInteger oObjekt1(5);
CInteger oObjekt2(7);

if ((oObjekt1 == oObjekt2) || (oObjekt1 == 15)) {
	// ...
} // end of if

// oder auch
CInteger* pZeigerAufObjekt1	= new CInteger(5);
CInteger* pZeigerAufObjekt2	= new CInteger(7);

if ((*pZeigerAufObjekt1 == *pZeigerAufObjekt2) || (*pZeigerAufObjekt1 == 15)) {
	// ...
} // end of if

delete pZeigerAufObjekt1;
delete pZeigerAufObjekt2;
					

Ich denke mal, das man jetzt nicht mehr so viel erklären muss, da mittlerweile nichts mehr spannendes neues passiert. Es wird wieder die Methode des links vom "==" stehenden Objekt aufgerufen und der Übergabeparameter ist wieder das rechts stehende Glied. Hier wird wieder unterschieden, ob reechter Hand ein Objekt der selben Klasse oder ein Integer steht. Die Implementierungen für andere Vergleichsoperatoren (<. <=, >, >= und !=) und oder anderen Typen, sieht logischerweise wieder ähnlich aus. Auch diese Methoden ändern das Objekt nicht und somit sind die Methoden wieder als konstant deklariert. "Logisch", sollte auch der Rückgabeparameter sein.

Zum Seitenanfang
Zum Inhaltsverzeichnis

© Copyright by Thomas Weiß, 2009 - 2012