ich war hier: ObjProg04Operator
 (image: http://wdb.fh-sm.de/uploads/QualipaktLehre/BMBF_Logo_klein.jpg)

Objektorientierte Programmierung - Kapitel 4 - Operator - Überladung


Inhalte von Dr. E. Nadobnyh

4.1. Grundlagen und Regeln


Operator-Überladung


Ein Operator ist überladen, wenn er je nach Typ der Operanden eine unterschiedliche Bedeutung hat.

Die meisten Operatoren für eingebauten Datentypen sind schon überladen.

Beispiel:

double a, b; short x, y;

a*b; x*y; //verschiedene Algorithmen


Operatorfunktion


In C++ gibt es die Möglichkeit , Operatoren für die Klassen zu definieren.

Eine Operatorfunktion ist eine Funktion mit einem besonderen Namen. Der Name der Operatorfunktion beginnt mit dem Schlüsselwort operator, dem das Operatorsymbol folgt. Ausdrücke mit Operatoren sind intuitiver und daher schneller zu erfassen als Ausdrücke mit Funktionsaufrufe.


Zwei Definitionsmöglichkeiten


Eine Operatorfunktion kann entweder als globale Funktion oder als Methode einer Klasse definiert werden.

1) Globale Operatorfunktion

class Bruch{ };
Bruch operator+ (Bruch, Bruch );

2) Operatormethode

bzw. Operatorfunktion als Methode

class Bruch
{ public:
Bruch operator+ (Bruch x);
};


4. Operator-Überladung


Aufruf der Operatorfunktion

Der Operator-Aufruf wird in einen Funktionsaufruf umgewandelt:

1) entweder in den Aufruf der globalen Operatorfunktion,
2) oder in den Aufruf der Operatormethode.

 (image: http://ife.erdaxo.de/uploads/ObjProg04Operator/oop15.gif)


Mehrdeutigkeit


Wenn Compiler mehr als eine Aufrufmöglichkeit findet, wird der Aufruf bemängelt.

⇒ Demo 1.


Globale Operatorfunktion


class Bruch;    //Vorwärtsdeklaration
Bruch operator+(Bruch a, Bruch  b);   //Deklaration
 
class Bruch
{    //Freundschaftserklärung
      friend Bruch operator+(Bruch a, Bruch  b);    
};
 
main()
{     Bruch a, b, c;  
       c = a + b;                     //Aufruf  
       c=operator+(a,b);       //identisch  
}


⇒ Demo 1.2


Operatormethode


class Bruch
{    public:    Bruch operator+(Bruch  b);
};
main()
{   Bruch a, b, c;
     c = a + b;                     //Aufruf
     c=a.operator+(b);       //identisch
}



Operanden


Der zweistellige Operator hat zwei Operanden. Der erste Operand ist ein Objekt, für das die Methode aufgerufen wird.

⇒ Demo 3.


Einsatzregeln


1) Es können bis auf wenige Ausnahmen alle Operatoren überladen werden.

2)Der Funktionsname besteht aus dem Schlüsselwort operator und dem Operatorzeichen.

3) Es können die übliche Operatoren überladen werden (z.B. *, +=, usw.).
Eine Definition von neuen Operatoren ist nicht möglich, z.B. neuen Operator ** zum Potenzieren. Andere Zeichen wie $ usw. sind nicht erlaubt.

⇒ Demo 4. C++ Operatoren

4) Wenigstens ein Argument der Operatorfunktion muss ein class –Objekt sein, oder die Operatorfunktion muss eine Methode sein. Die Bedeutung der Operatoren für Standard-Typen lässt sich nicht umdefinieren.

5) Die Anzahl der Operanden eines Operators (Stelligkeit) kann nicht geändert werden, d.h. ein unärer Operator bleibt unär.

6) Einige Operatoren (z.B.: +, -, * ) können getrennt unär und binär überladen werden.

⇒ Demo 5.

7) Der Operator-Aufruf wird in einen Funktionsaufruf umformuliert.

8) Die vorgegebenen Vorrangregeln können nicht verändert werden.

9) Die Reihenfolge der Auswertung (von links oder von rechts) bei der Operatoren mit gleicher Priorität bleibt erhalten.

10) Überladene Operatoren können keine Default-Parameter haben. (Eine Ausnahme ist der Funktionsoperator.)

⇒ Demo 6 + Demo 7.

Einzelne Operatoren sind nicht überladbar:

Operator Bedeutung
sizeof .
.*
::
?:
Größe von Objekten Zugriffsoperator (Strukturen, Unions, Klassen)
Zugriffsoperator für Elementzeiger
Bereichsoperator
Auswahloperator


Operatoren, die nur als Methoden überladen werden können

Operator Bedeutung
=
[ ]
( )
->
(Typ)
Zuweisung
Indizierung
Funktionsaufruf bzw. Funktionsoperator
Elementzugriff für Zeiger
Typumwandlungsoperator



4.2. Symmetrie der Operatoren


Symmetrische Operatoren


Einige binäre Operationen z.B. Addition sind kommutativ und ihre beide Operanden sind „gleichberechtigt“.

Beispiel:
Bruch a, b, c;
c=a+b;
c=b+a;


Gemischte Datentypen
Die Verwendung der Operanden vom Datentyp int soll auch möglich sein.


Beispiel:
Bruch a, b, c;
c=a+5;
c=5+a;


Symmetrie der globalen Operatorfunktionen


a) Ohne Konvertierungskonstruktor
Um gemischte Datentypen zu erlauben, kann man noch zwei globalen Operatorfunktionen entwerfen:

Bruch operator+(Bruch a, Bruch b);
Bruch operator+(Bruch a, long b);
Bruch operator+(long a, Bruch b);

b) Mit Konvertierungskonstruktor
Der Compiler konvertiert implizit den aktuellen
Parameter vom Typ long in Typ Bruch.

Bruch operator+(Bruch a, Bruch b);
Bruch (long x);

⇒ Demo 8.


Asymmetrie der Operatormethode


Die Symmetrie geht verloren, wenn die Operatorfunktion als Methode implementiert wird.

Beispiel: Addition einer rationalen und einer ganzen Zahl

Aufruf Operatormethode
c = a + 3;
c = a.operator+(3);
Bruch Bruch::operator+(int x);
c = 3 + b;
c = 3 .operator+(b);
unmöglich


Wenn die Symmetrie erhalten bleiben soll, dann ist den Operator als globale Funktion zu implementieren.


4.3. Einzelne Operatoren


Shift-Operatoren


In der iostream -Bibliothek wird der Links-Shift-Operator < als ein Ausgabeoperator und der Rechts-Shift-Operator >>
als ein Eingabeoperator interpretiert.

Um die iostream- Bibliothek für die Ein- und Ausgabe von Objekten des benutzerdefinierten Datentyps zu verwenden, sollen diese Operatoren überladen werden.

Globale Operatorfunktion oder Operatormethode


Aufruf Ersetzung durch Zusammenfassung
Bruch c;
cout<td>
cout.operator<<(c); Diese Methode ist nicht implementiert
operator<<(cout, c); Die globale Fkt. ist die einzige Möglichkeit



Rückgabe per Referenz für die Verkettung

#include
Bruch a;
cout<<“a= “<td>
ostream & operator<< (ostream & os, const Bruch & a)
{ ...
return os;
}


⇒ Demo 9


Implementierung globaler Operatorfunktion


Ohne der Freundschaftserklärung müssen die zusätzliche öffentliche Zugriffsmethoden vorhanden sein und in der globalen Funktion aufgerufen werden.

class Bruch
{  private:  long za, ne;     //za-Zaehler, ne-Nenner
    public:    long getZa(){return za;}
                   long getNe(){return ne;}
};
ostream & operator<<(ostream & out, const Bruch & b);
 
ostream & operator<<(ostream & out, const Bruch & b)
{  out<<b.getZa()<<"/"<<b.getNe();
    return out;
}



Globale Operatorfunktion als friend-Funktion


Damit eine globale Operatorfunktion direkt auf private Attribute zugreifen kann, wird sie als friend-Funktion deklariert.

class Bruch
{  private:    long za, ne; //za-Zaehler, ne-Nenner
    friend  
    ostream & operator<<(ostream & out, const Bruch & b);
};
 
ostream & operator<<(ostream & out, const Bruch & b)
{   out<<b.za<<"/"<<b.ne;
     return out;
}


⇒ Demo 10.


Index-Operator


Für eingebauten Datentypen dient der Index-Operator normalerweise dem Zugriff auf Feldelemente, z.B.:

short a, b[2];
b[1]=5;
a=b[1];

Im Ausdruck b[i] ist b der linke Operand und i der rechte Operand. Der rechte Operand heißt Index.

Für den nicht überladenen Index-Operator gelten folgende Einschränkungen:

  1. Der linke Operand muß ein Zeiger oder ein Feldname sein.
  1. Der rechte Operand muß ein ganzzahliger Ausdruck sein.
  1. Der Ausdruck b[i] impliziert eine versteckte Zeigerarithmetik: *(b + i)


Index-Operator für eine Klasse


Für eine Klasse muss ein Index-Operator als Methode überladen werden.

class  Array
      {       public:  float & operator[](int i);
      };


Für den überladenen Index-Operator gelten folgende Einschränkungen:

  1. Der linke Operand muß ein Objekt der Klasse sein.
  1. Der rechte Operand darf einen beliebigen Datentyp haben.
  1. Der Ergebnis-Typ ist nicht festgelegt.
  1. Das Ergebnis soll eine Referenz auf ein Objekt sein.

⇒ Demo 11.


Zuweisungsoperator


Ein existierender Objekt wird durch eine Kopie eines anderen ersetzt.

Für jede Klasse ist ein Standard-Zuweisungsoperator implizit vordefiniert. Die Standardzuweisung erfolgt elementweise: d.h. für jedes Attribut des Objektes wird der entsprechende Zuweisungsoperator aufgerufen.

Der Zuweisungsoperator für Grunddatentypen bewirkt eine bitweise Kopie.


Wenn eine Klasse dynamische Attribute enthält, muß für diese Klasse ein expliziter (statt impliziter) Zuweisungsoperator überladen werden. Ein Zuweisungsoperator kann nur als eine Operatormethode überladen werden:

class  Array
    {     public:  Array & operator=(const Array & neu);
    };


Es gibt zwei Formulierungen für den Aufruf:

       Array  a, b;
       a = b;                    //impliziter Aufruf
       a.operator=(b);    //expliziter Aufruf


Ein Zuweisungsoperator soll immer das Objekt, für das der Operator aufgerufen wurde, als Referenz zurückliefern. Dies ermöglicht verkettete Zuweisungen.


Array a, b, c; a = b = c;

Im Zuweisungsoperator sollte getestet werden, ob ein Objekt sich selbst zugewiesen wird. Dies ermöglicht so genannter „Zuweisung an sich selbst“.

Array a;    a = a;
 
      Array &  Array::operator=(const Array & neu)
      {     if(this== & neu) return *this;
              . . .
             return *this;
      }


Der Zuweisungsoperator ist im Prinzip eine Kombination aus Destruktor und Kopierkonstruktor.

typedef ... T;
   class Array
   {  private:    T*  z;    int n;
       public:
         Array &  operator=(const Array & neu)
         {  if(this== & neu) return *this;
             delete[] z;                      //alten Speicher freigeben
             n=neu.n;   z=new T[n];           //neuen reservieren
             for(int i=0; i<n; i++)  z[i]=neu.z[i];          //Kopie
             return *this;
         }
   };


⇒ Demo 12.

Auch der Funktionsaufruf ist ein Operator, der für eigene Klassen definiert werden kann.

Synonyme: Funktionsoperator, Klammeroperator, operator(), Funktionsaufrufoperator.

Im folgenden Prinzipbeispiel wird ein parameterlose unktionsoperator deklariert:

class XXX
{ …
public: void operator() ();
};

Ein Objekt mit der Funktionsoperator wird Funktionsobjekt der Funktor benannt. Die Funktionsobjekte werden in der C++-Standardbibliothek verwendet.


Der Funktionsoperator bringt überraschende Konsequenzen: ein Funktionsobjekt kann wie eine Funktion aufgerufen werden:

Es sieht so aus, als würde eine globale Funktion x1() aufgerufen. Tatsächlich wird der Funktionsoperator des Objektes x1 aufgerufen.

 (image: http://ife.erdaxo.de/uploads/ObjProg04Operator/oop16.gif)

Der explizite Aufruf des Funktionsoperators ist syntaktisch ebenfalls erlaubt:

 (image: http://ife.erdaxo.de/uploads/ObjProg04Operator/oop17.gif)

Die Funktionsobjekte können wie andere Methoden mehrfach überladen werden, z.B.:

class Polynom
{   float a, b, c;
   public:  
      Polynom(float aa, float bb, float cc){a=aa; b=bb; c=cc;}
      float operator( )(float x){ return a*x*x + b*x + c; }
      float operator( )(float a, float b, float c, float x)
      {    return a*x*x + b*x + c;    // Funktionswert
      }
};
int main()
{     Polynom po(5, 6, 7);
       cout<< po(3)<<&#8220;  &#8220;<<po(3, -2, -7, 5);     // 70  58
}



Ein wichtiger Vorteil ist, dass ein Funktionsobjekt statt Funktionszeiger als Parameter zu einer Funktion übergeben werden könnte. Das ist die objektorientierte Alternative.

Beispiel:

double Doppelt (Polynom p, float x)
      {    return  2*p(x);  
      }
      int main()
      {     Polynom po(5, 6, 7);
             cout<< Doppelt(po, 3)<<"  &#8220;
                    << Doppelt(Polynom(3, -2, -7), 5); // 140  116
 
      }


⇒ Demo 13, 14.


4.4. Iterator-Operatoren


Behälter und Iterator


Ein Behälter (container oder collection) ist ein Objekt, das zum Verwalten von anderen Objekten (Elementen) dient.

Jedes Element in einem Behälter besitzt eine bestimmte Position, in der es abgelegt ist. Man kann über die Position direkt auf das einzelne Element zugreifen. Die Darstellung der Position ist von der Implementierung des Behälters abhängig. Zeiger und Indexe sind bekannte Bespiele dafür.

Ein Iterator ist ein Objekt, das eine Position in einem Behälter repräsentiert (verweist, zeigt). Ein Iterator dient dazu, von den Implementierungsdetails des Behälters zu abstrahieren. Begriffsverwirrung: Mit dem Begriff „Iterator“ können auch eine Klasse und ein Konzept bezeichnet werden.

Prinzipbeispiel:

 (image: http://ife.erdaxo.de/uploads/ObjProg04Operator/oop18.gif)


Iterator-Operatoren


1. Operatoren zum Iterieren (Durchwandern). Die Position, welche durch einen Iterator repräsentiert wird, wird zur Position des nächsten (oder vorigen) Elementes im Behälter gewechselt. Beispielsweise wechselt ein Inkrement-Operator++ auf Position des nächsten Elementes.

++ -- + - += -=

2. Vergleichsoperatoren. Zwei Iteratoren können verglichen werden, z.B. der Gleich-Operator== liefert true, wenn zwei Iteratoren auf dasselbe Element verweisen.

== != > <= >=

3. Zugriffsoperatoren. Das Element, welches durch einen Iterator repräsentiert wird, wird gelesen oder geändert (überschrieben). Beispiel: ein Inhalts-Operator* des Iterators liefert die Referenz auf das repräsentierten Element.

* -> [ ]


Iterator als geschachtelte Klasse


Prinzipbeispiel

typedef … T; Typ des Elementes

 (image: http://ife.erdaxo.de/uploads/ObjProg04Operator/oop19.gif)


Anwendung von Iteratoren


1) Die Verbindung zwischen Behälter und Iterator wird mit den Methoden „start“ und „ende“ hergestellt.

Behaelter b;
Behaelter::iterator s= b.start();
Behaelter::iterator e= b.ende();

2) Ein Inkrement-Operator schiebt den Iterator auf die nächste Position: s++;

 (image: http://ife.erdaxo.de/uploads/ObjProg04Operator/oop20.gif)


Vorteile von Iteratoren


1) Der Benutzer muss nicht kennen, welche Datenstruktur der Behälter besitzt.
2) Wenn eine Iterator-Klasse als eine geschachtelte Klasse definiert ist, dann ist sie ein Bestandteil der Behälterdefinition. Dann muss ein Benutzer viele Besonderheitendes Iterators nicht kennen und kann den Iterator über üblichen Namen ansprechen.

Prinzipbeispiel:

main()                          
{    
Behaelter b;                  //Behaelter-Definition
Behaelter::iterator  i;   //Iterator-Definition

for (i=b.start( );  i!=b.ende( );  ++i)    
                  cout<<*i<<" ";
      }


⇒ Demo 13


Inhalts-Operator


Der Inhaltsoperator * (Dereferenzierungsoperator, Verweisoperator, Indirektionsoperator) liefert eine Referenz auf das Element, an dessen Position sich der Iterator befindet.

Prinzipbeispiel:

   typedef ... T;    //Typ des Elementes
     class Behaelter  {    
            ...
           public:
           class iterator {      
            T* c;  //Zeiger auf aktuelles Element
        public:
            T &  operator* (void) {return *c;}
      };



Die Verwendung des Inhaltsoperators *i bedeutet einen lesenden oder schreibenden Zugriff auf das Element, deren Position der Iterator i enthält.

Beispiel:

   typedef  &#8230;  T;    //Typ des Elementes
      main()
      {   Behaelter b;
           Behaelter::iterator  i= b.start( );   //Initialisierung
           T  tmp= *i;   //lesender Zugriff auf das Element
           *i = tmp;   //schreibender Zugriff auf das Element
      }


Anmerkung:
Der Operator * kann getrennt unär (Inhaltsoperator) und binär (Multiplikationsoperator) überladen werden.


Inkrement-Operator


Die Position, welche ein Iterator repräsentiert, wird zur Position des nächsten Elementes im Behälter gewechselt. Dabei ändert sich ein Iterator selbst.

Außerdem liefert dieser Operator einen Iterator als Ergebnis zurück. Präfix-Operator liefert den veränderten Iterator. Postfix-Operator liefert den ursprünglichen Iterator.

Beispiel für Präfix-Operator

class Behaelter
{    struct KN{ ...;  KN* next;};    
   public:
     class iterator
     {   KN* c;  //Zeiger auf aktuellen Knoten
        public:
          iterator & operator++() { c = c->next;   return *this;}
     };
};





CategoryObjektorientierteProgrammierung
Diese Seite wurde noch nicht kommentiert.
Valid XHTML :: Valid CSS: :: Powered by WikkaWiki