Wykład 3
Przeładowanie operatorów
W pierwszej części wykładu powiedzieliśmy, że typy zdefiniowane przez użytkownika są normalnymi typami w znaczeniu jake nadajemy typom wbudowanym, czyli na przykład float czy int. W szczególności dotyczy to wykonywania operacji arytmetycznych. Naturalne są przecież dla nas zapisy:

int x=3, y=4, z;

z=x+y;
Listing 3.1
Chcąc dodać do siebie dwa obiekty typu int zapisujemy to w sposób szkolny, czyli używając symbolu "+" a mówiąc bardziej "naukowo" - operatora +. Na ćwiczeniach poprzedzających ten wykład pisali Państwo program operujacy na ułamkach. Tam, aby pomnożyć dwa ułamki, należało napisać:

ulamek u1(1,2), u2(3,4), u3;

u1.mnoz1(u2); //można tak
u3 = mnoz2(u1,u2);  // mozna tez tak

Listing 3.2
Niby jest to poprawne, ale dalekie od naturalnego zapisu
u3 = u1 + u2;
Czyżby więc typy definiowane były typami gorszymi? Nie, wcale nie. Otóż wtstarczy, że programista napisze swoją funkcję nazywaną (w tym przypadku) operatorem dodawania aby zapis taki jak powyżej był możliwy. Słowo funkcja zostało wyróżnione aby zaakcentować, że operator nie jest niczym więcej jak normalną funkcją; co najwyżej o troszkę dziwnej nazwie :). Dla liczb zespolonych przyjęła by ona postać

ulamek perator + (ulamek u1, ulamek u2)
{
  ulamek u;
  
  u.licznik = u1.licznik + u2.licznik;
  u.mianownik = u1.mianownik + u2.mianownik;
  
  return u;
}
Listing 3.3
Tak zdefiniowaną funkcję możemy wywołać na dwa sposoby.
  1. Zwyczajnie jak każdą funkcję
    
    ulamek u1(1,2), u2(3,4), u3;
    
    u3 = operator + (u1,u2);
    
    Listing 3.4
    ale właśnie tego chcieliśmy uniknąć.
  2. Mamy więc drugi sposób:
    
    ulamek u1(1,2), u2(3,4), u3;
    
    u3 = u1 + u2;
    
    Listing 3.5
    i to jest to o co nam chodziło.

Mówiąc ogólnie aby stworzyć operator działający na obiektach naszej klasy należy napisać funkcję postaci

typZwracany operator symbolOperatora (argumenty)
{
  // ciało funkcji
}
Listing 3.6
Oto przykład jak można było napisać klasę ulamek.

#include <stdio.h>

class ulamek
{
public:
  int licznik;
  int mianownik;
  
  ulamek();
  ulamek(int l, int m);
  void wypisz();
};

ulamek::ulamek()
{
  licznik=0;
  mianownik=0;
  printf("Konstruktor 1\n");
}

ulamek::ulamek(int l, int m)
{
  licznik=l;
  mianownik=m;
  printf("Konstruktor 2\n");
}

void ulamek::wypisz()
{
  printf("Licznik: %d, mianownik %d\n",licznik,mianownik);
}

ulamek operator * (ulamek u1, ulamek u2)
{
  ulamek u;
  
  u.licznik = u1.licznik * u2.licznik;
  u.mianownik = u1.mianownik *u2.mianownik;
  
  return u;
}

ulamek operator / (ulamek u1, ulamek u2)
{
  ulamek u;
  
  u.licznik = u1.licznik * u2.mianownik;
  u.mianownik = u1.mianownik *u2.licznik;
  
  return u;
}

ulamek operator + (ulamek u1, ulamek u2)
{
  ulamek u;
  
  u.licznik = u1.licznik * u2.mianownik + u1.mianownik * u2.licznik;
  u.mianownik = u1.mianownik * u2.mianownik;
  
  return u;
}

ulamek operator - (ulamek u1, ulamek u2)
{
  ulamek u;
  
  u.licznik = u1.licznik * u2.mianownik - u1.mianownik * u2.licznik;
  u.mianownik = u1.mianownik * u2.mianownik;
  
  return u;
}

int main()
{
  ulamek u1(1,2), u2(3,4), u3;
  
  u1.wypisz();
  u2.wypisz();
  u3.wypisz();
  
  u3=u1*u2;
  
  u1.wypisz();
  u2.wypisz();
  u3.wypisz();
  
  u3=u1/u2;
  
  u1.wypisz();
  u2.wypisz();
  u3.wypisz();
  
  u3=u1+u2;
  
  u1.wypisz();
  u2.wypisz();
  u3.wypisz();
  
  u3=u1-u2;
  
  u1.wypisz();
  u2.wypisz();
  u3.wypisz();
  
  return 0;
}
Listing 3.7
a tak przedstawia się efekt działania tegoż programu

Konstruktor 2
Konstruktor 2
Konstruktor 1
Licznik: 1, mianownik 2
Licznik: 3, mianownik 4
Licznik: 0, mianownik 0
Konstruktor 1
Licznik: 1, mianownik 2
Licznik: 3, mianownik 4
Licznik: 3, mianownik 8
Konstruktor 1
Licznik: 1, mianownik 2
Licznik: 3, mianownik 4
Licznik: 4, mianownik 6
Konstruktor 1
Licznik: 1, mianownik 2
Licznik: 3, mianownik 4
Licznik: 10, mianownik 8
Konstruktor 1
Licznik: 1, mianownik 2
Licznik: 3, mianownik 4
Licznik: -2, mianownik 8
Output 3.1 Efekt działania programu z listingu 3.7
Ważne aby co najmniej jeden argument był typu klasy, dla której operator ma działać. Jako symbolOperatora możemy wybrać dowolny z poniższych operatorów
+  -  *  /  %  ^  &  |  ~

!  =  <  >  +=  -=  *=  /=  %=

^=  &=  |=  <<  >>  <<=  >>=  <=  =>

== !=  &&  ||  ++  --  ,  ->*  ->

new  delete  ()  []
Nie mozemy natomiast użyć:
. .* :: ?:
Teraz kilka zasad ogólnych.
  1. Przeładowć można tylko operatory podane powyżej; nie można tworzyć swoich własnych.
  2. Przeładowany operator może wykonywać dowolną operację; nic nie stoi na przeszkodzie, aby operator + mnożył dwie liczby. Pytanie tylko po co> :))
  3. Nie ma ograniczeń związanych z wartością zwracaną przez operator (wyjątki: new, delete).
  4. Nie można zmienić priorytetu wykonywania operatorów.
  5. Nie można zmienić łączności operatorów - operatory pozostają prawo- lub lewo- stronnie łaczne tak jak zdefiniowano to w języku.
  6. Nie można zmienić ilości argumentów na jakich działa operator.
  7. Ten sam operator można przeładować wielokrotnie pod warunkiem, że wszystkie listy argumentów będą różniły się kolejnością lub typem argumentów.
  8. Operatory = & , new delete są generowane automatycznie jesli nie zdefiniujemy ich samodzielnie.

W pierwszym wykładzie powiedzieliśmy, że klasy to sposób na stworzenie własnego typu; pozwalają wiązać dane z działaniami i jeśli zajdzie taka potrzeba, niektóre z nich ukryć przed użytkownikiem. Skoro więc pewien operator ma działać na rzecz jakiejś klasy to czemu odpowiadająca jemu funkcja operatorowa nie miałaby być składową tejże klasy? Wówczas zarówno dane jak i funkcje przeznaczone do operowania nimi zostałyby zamknięte w jednej "paczce" i stanowiłyby jedną całość.

Jest to chyba naturalna potrzeba i na szczęście może zostać spełniona. Możemy więc funkcję operatorową zdefiniować jako funkcję golbalną albo jako funkcję składową klasy. Jeśli operator definiować będziemy jako funkcję składową klasy, to zawsze będzie miał on o jeden argument mniej niż wynikałoby to z jego definicji. Dostęp do brakującego argumentu uzyskamy na pomocą wskaźnika this - wszak funkcja składowa musi być wywołana na rzecz jakiegoś obiektu. Stąd wniosek, że operator nie może być statyczną funkcją składową klasy, gdyż ta jak wiadomo nie posiada wskaźnika this. Wracając do naszej klasy zespolona możemy więc napisać

#include <stdio.h>

class ulamek
{
public:
  int licznik;
  int mianownik;
  
  ulamek (int l=0, int m=0);
  ulamek operator * (ulamek u);
  ulamek operator / (ulamek u);
  ulamek operator + (ulamek u);
  ulamek operator - (ulamek u);
  void wypisz();
};

ulamek::ulamek(int l, int m)
{
  licznik=l;
  mianownik=m;
  printf("Konstruktor...\n");
}

void ulamek::wypisz()
{
  printf("Licznik: %d, mianownik %d\n",licznik,mianownik);
}

ulamek ulamek::operator * (ulamek u1)
{
  ulamek u;
  
  u.licznik = licznik * u1.licznik;
  u.mianownik = mianownik * u1.mianownik;
  
  return u;
}

ulamek ulamek::operator / (ulamek u1)
{
  ulamek u;
  
  u.licznik = licznik * u1.mianownik;
  u.mianownik = mianownik * u1.licznik;
  
  return u;
}

ulamek ulamek::operator + (ulamek u1)
{
  ulamek u;
  
  u.licznik = licznik * u1.mianownik + mianownik * u1.licznik;
  u.mianownik = mianownik * u1.mianownik;
  
  return u;
}

ulamek ulamek::operator - (ulamek u1)
{
  ulamek u;
  
  u.licznik = licznik * u1.mianownik - mianownik * u1.licznik;
  u.mianownik = mianownik * u1.mianownik;
  
  return u;
}
Listing 3.8
A teraz sprawdźmy jak to działa...

int main()
{
  ulamek u1(1,2), u2(3,4), u3;
  
  u1.wypisz();
  u2.wypisz();
  u3.wypisz();
  
  u3=u1*u2;
  
  u1.wypisz();
  u2.wypisz();
  u3.wypisz();
  
  u3=u1/u2;
  
  u1.wypisz();
  u2.wypisz();
  u3.wypisz();
  
  u3=u1+u2;
  
  u1.wypisz();
  u2.wypisz();
  u3.wypisz();
  
  u3=u1-u2;
  
  u1.wypisz();
  u2.wypisz();
  u3.wypisz();
  
  return 0;
}
Listing 3.9
Jako efekt działania programu na ekranie powinni Państwo zobaczyć (proszę to porównać z listingiem 3.7 i outputem 3.1.

Konstruktor...
Konstruktor...
Konstruktor...
Licznik: 1, mianownik 2
Licznik: 3, mianownik 4
Licznik: 0, mianownik 0
Konstruktor...
Licznik: 1, mianownik 2
Licznik: 3, mianownik 4
Licznik: 3, mianownik 8
Konstruktor...
Licznik: 1, mianownik 2
Licznik: 3, mianownik 4
Licznik: 4, mianownik 6
Konstruktor...
Licznik: 1, mianownik 2
Licznik: 3, mianownik 4
Licznik: 10, mianownik 8
Konstruktor...
Licznik: 1, mianownik 2
Licznik: 3, mianownik 4
Licznik: -2, mianownik 8
Output 3.2 Efekt działania programu z listingu 3.8 i 3.9
Przy okazji definicji funkcji operatorowej jako funkcji składowej klasy należy powiedzieć, że taka funkcja wymaga aby obiekt stojący po lewej stronie operatora był obiektem tej klasy; operator będący funkcją globalną tego ograniczenia nie posiada.


W większości przypadków możemy decydować czy operator będzie funkcja składową klasy, czy funkcją globalną, jednak operatory = [] () -> muszą być niestatycznymi funkcjami składowymi klasy. Są to dosyć szczególne operatory, dlatego poczynimy pewne uwagi:

  1. Z operatorem przypisania wiążą się te sam problemy co z konstruktorem kopiujacym. Nie będe się tutaj powtarzał - zamiast tego odsyłam do odpowiedniej cześci z wykładu 2.
  2. Operator przypisania możemy zdefiniować na dwa sposoby
    • Pierwszy:
      typZwracany operator = (klasa)
      
    • Drugi:
      klasa & operator = (klasa &)
      
    Oba są poprawne; drugi sposób jest o tyle wygodny, że umożliwia zapis
    klasa a, b, c, d;
    
    // coś się dziej :))
    
    a = b = c = d;  // to jest ta istotna linijka
    
  3. Jeśli chcemy aby operator [] mógł stać po obu stronach znaku przypisani, czyli aby można było napisać zarówno
    x = naszaKlasa[2];
    
    jak i
    naszKlasa[3] = y;
    
    a w szczególności
    naszaKlasa[4] = naszaKlasa[5];
    
    musimy zadeklarować, że funkcja zwraca referencję do tego, czemu ma być przypisana. Oto stosowny przykład
    
    #include <stdio.h>
    
    class naszaKlasa
    {
    public:
      static int ile;
      int numer;
      int i1;
      int i2;
      int i3;
      int i4;
      int i5;
        
      naszaKlasa (int a1=1, int a2=2, int a3=3, int a4=4, int a5=5);
      int & operator [] (int t);
      void wypisz();
    };
    
    naszaKlasa :: naszaKlasa (int a1, int a2, int a3, int a4, int a5)
    {
      i1=a1;
      i2=a2;
      i3=a3;
      i4=a4;
      i5=a5;
      ile++;
      numer=ile;
    }
    
    int & naszaKlasa :: operator [] (int t)
    {
      int * temp;
       
      switch (t)
      {
        case 1:
          temp = &i1;
          break;
        case 2:
          temp = &i2;
          break;
        case 3:
          temp = &i3;
          break;
        case 4:
          temp = &i4;
          break;
        case 5:
          temp = &i5;
          break;
      }
       
      return (*temp);
    }
    
    void naszaKlasa ::  wypisz()
    {
      printf ("z%d: i1=%d, i2=%d, i3=%d, i4=%d, i5=%d\n",numer,i1,i2,i3,i4,i5);
    }
    
    int naszaKlasa :: ile=0;
    
    int main()
    {
      naszaKlasa z1,z2(6,7,8,9,10);
      int temp;
      
      z1.wypisz();
      z2.wypisz();
      
      printf("Najpierw z lewej...\n");
      
      temp = z1[3];
      printf("Zarartosc pola a3, zmiennej z1: %d\n",temp);  
      
      // pamietajmy, ze powyzsze wyrazenie rownowazne jest
      // wyrazeniu
      
      temp = z1.operator[] (3);
      printf("Zarartosc pola a3, zmiennej z1: %d\n",temp);
      
      printf("Teraz z prawej...\n");
      
      z2.wypisz();
      z2[2]=2;
      z2.wypisz();
      
      printf("Na koniec z obu stron...\n");
      
      z1.wypisz();
      z2.wypisz();
      
      z1[4]=z2[4];
      
      z1.wypisz();
      z2.wypisz();
      
      return 0;
    }
    
    Listing 3.10
    i wynik jego działania
    
    z1: i1=1, i2=2, i3=3, i4=4, i5=5
    z2: i1=6, i2=7, i3=8, i4=9, i5=10
    Najpierw z lewej...
    Zarartosc pola a3, zmiennej z1: 3
    Zarartosc pola a3, zmiennej z1: 3
    Teraz z prawej...
    z2: i1=6, i2=7, i3=8, i4=9, i5=10
    z2: i1=6, i2=2, i3=8, i4=9, i5=10
    Na koniec z obu stron...
    z1: i1=1, i2=2, i3=3, i4=4, i5=5
    z2: i1=6, i2=2, i3=8, i4=9, i5=10
    z1: i1=1, i2=2, i3=3, i4=9, i5=5
    z2: i1=6, i2=2, i3=8, i4=9, i5=10
    
    Output 3.3 Efekt działania programu z listingu 3.10
  4. Operator () może przyjmować więcje niż dwa argumenty.
  5. Operator -> jest jednoargumentowy i działa na argumencie stojącym po jego lewej stronie. Argumentem tego operatora jest obiekt a nie jak w przypadku "normalnego" operatora -> wskaźnik do obiektu.
    
    #include <stdio.h>
    
    class naszaKlasa1
    {
    public:
      int i;
      
      naszaKlasa1(int a);
      void wypisz();
    };
    
    naszaKlasa1 :: naszaKlasa1(int a)
    {
      i=a;
    }
    
    void naszaKlasa1 :: wypisz()
    {
      printf("Funkcja wypisz z naszaKlasa1: i=%d\n",i);
    }
    
    // ==================================
    // Druga klasa
    // Tutaj bedziemy rejestrowac
    // uzycie operatora ->
    // ==================================
    
    class naszaKlasa2
    {
    public:
      naszaKlasa1 *wsk;
      int ile;
      
      naszaKlasa2(naszaKlasa1 * a = NULL);
      void wypisz();
      naszaKlasa1 * operator -> ();
    };
    
    naszaKlasa2 :: naszaKlasa2(naszaKlasa1 * a)
    {
      wsk = a;
      ile=0;
    }
    
    void naszaKlasa2 :: wypisz()
    {
      printf("Funkcja wypisz z naszaKlasa2: ile=%d\n",ile);
    }
    
    naszaKlasa1 * naszaKlasa2 :: operator -> ()
    {
      ile++;
      return wsk;
    }
    
    int main()
    {
      naszaKlasa1 x(5);
      naszaKlasa1 * wsk1;
      naszaKlasa2 wsk2;
      
      wsk1 = &x;
      wsk2 = &x;
      
      wsk1->wypisz();
      wsk2.wypisz();
      printf("Uzycie normalengo operatora ->: i=%d\n",wsk1->i);
      printf("Uzycie operatora -> z mojaKlasa2: i=%d\n",wsk2->i);
      wsk1->wypisz();
      wsk2.wypisz();
      (wsk1->i)++;
      (wsk2->i)+=7;
      printf("Uzycie normalengo operatora ->: i=%d\n",wsk1->i);
      printf("Uzycie operatora -> z mojaKlasa2: i=%d\n",wsk2->i);
      wsk1->wypisz();
      wsk2.wypisz();
      
      return 0;
    }
    
    Listing 3.11
    Wynik działania programu przedstawiono poniżej
    
    Funkcja wypisz z naszaKlasa1: i=5
    Funkcja wypisz z naszaKlasa2: ile=0
    Uzycie normalengo operatora ->: i=5
    Uzycie operatora -> z mojaKlasa2: i=5
    Funkcja wypisz z naszaKlasa1: i=5
    Funkcja wypisz z naszaKlasa2: ile=1
    Uzycie normalengo operatora ->: i=13
    Uzycie operatora -> z mojaKlasa2: i=13
    Funkcja wypisz z naszaKlasa1: i=13
    Funkcja wypisz z naszaKlasa2: ile=3
    
    
    Output 3.4 Efekt działania programu z listingu 3.11


W stosunku do dwóch spośród szerokiego grona operatorów określono ściśle zwracane przez nie rezultaty wykonania. Operatorami tymi są new i delete. Z operatorami tymi wiążą się następujące uwagi

  1. Operator new jest statyczną funkcją składową klasy.
  2. Typ zwracany przez new to void *.
  3. Pierwszy argument operatora new jestu typu size_t (argumentem tym nie musimy się martwić - jego przesłaniem zajmuje się kompilator określając wartość wynikającą z rozmiaru obiektu danej klasy).
  4. Operator delete jest statyczną funkcją składową klasy.
  5. Typ zwracany przez delete to void.
  6. Pierwszy argument operatora delete jestu typu void *.

#include <stdio.h>

class naszaKlasa
{
public:
  int i;
  
  naszaKlasa(int a=5);
  void wypisz();
  void * operator new(size_t s);
};

naszaKlasa :: naszaKlasa(int a)
{
  i=a;
}

void naszaKlasa :: wypisz()
{
  printf("i=%d\n",i);
}

void * naszaKlasa :: operator new(size_t s)
{
  printf("Nasz wlasny operator new rezerwuje wlasnie %d bajty na nowy obiekt.\n",s);
  return (new char[s]);
}

int main()
{
  naszaKlasa * wsk1,* wsk2;
  
  wsk1 = new naszaKlasa; // nasz wlasny operator new
  wsk2 = :: new naszaKlasa; // "tradycyjny" operator new
  
  wsk1 -> wypisz();
  wsk2 -> wypisz();
  
  wsk1 -> i=9;
  wsk2 -> i=7;
  
  wsk1 -> wypisz();
  wsk2 -> wypisz();
  
  return 0;
}
Listing 3.12
Wynik działania przedstawiono poniżej

Nasz wlasny operator new rezerwuje wlasnie 4 bajty na nowy obiekt.
i=5
i=5
i=9
i=7
Output 3.5 Efekt działania programu z listingu 3.12


Na zakończenie pozostało jeszcze wyjaśnić pewien problem z operatorem ++. Jak wiadomo, może on być stosowany w jednej z dwóch postaci

++ naszaKlasa;

naszaKlasa ++;
O ile pierwszą z nich implementuje się zwyczajnie
naszaKlasa :: naszaKlasa operator ++ ()
{
  // ciało funkcji
}
o tyle druga wymaga specjalnego zapisu poprawnego tylko dla tego operatora
naszaKlasa :: naszaKlasa operator ++ (int)
{
  // ciało funkcji
}
Wówczas wywołania:
++ naszaKlasa;
naszaKlasa ++;
równoważne są wywołaniom
naszaKlasa.operator ++ ();
naszaKlasa.operator ++ (0);
Zatem zapis
naszaKlasa ++;
utożsamiany jest z zapisem
naszaKlasa ++ 0;
który teoretycznie nie jest poprawny, gdyż operator ++ jest jednoargumentowy. Jednak w tej wyjątkowej sytuacji pozwala to odróżnić wersję preinkrementacyjną tego operatora od postinkrementacyjnej.