Przeciążenie funkcji wewnętrznych i operatorów.
Przeciążenie funkcji wewnętrznych
Przeciążenia deklaruje się przez pisanie wielu funkcji o tej samej nazwie lecz o różnej liczbie argumentów. Przeciążać można również wewnętrzne funkcje klasy (metody). Napiszemy program, w którym klasa Prostokąt posiada dwie metody RysujKsztalt(). Pierwsza, nie pobiera argumentów, rysuje prostokąt w oparciu o aktualne wartości zapisane w zmiennych klasy. Druga pobiera dwie wartości, długość i szerokość i rysuje prostokąt o podanych wymiarach, pomijając wartości zapisane w klasie.
#include <iostream.h>
class Prostokat
{
public:
Prostokat(int szerokosc, int wysokosc); //konstruktor
~Prostokat(){}
void RysujKsztalt() const;
void RysujKsztalt( int aSzer, int aWys) const;
private:
int jegoSzer;
int jegoWys;
};
Prostokat::Prostokat(int szerokosc, int wysokosc) //definicja konstruktora
{
jegoSzer = szerokosc;
jegoWys = wysokosc;
}
void Prostokat::RysujKsztalt() const; //Przeciążona funkcja - nie pobiera argumentów. Rysuje prostokąt w oparciu o zmienne klasy
void Prostokat::RysujKsztalt(int Szer, int Wys) const //Przeciążona funkcja, pobiera dwa argumenty i w oparciu o nie rysuje prostokąt
{
for (int i=0; i<Wys; i++)
{
for (int j=0; j<Szer; j++) cout<<”*”;
cout<<”\n”;
}
}
int main() //program główny
{
Prostokat Prost(30,5);
cout”RysujKsztalt():\n”;
Prost.RysujKsztalt();
cout<<”RysujKsztalt(40,2):\n”;
Prost.RysujKsztalt(40,2);
return 0;
}
Efekt działania programu:
RysujKsztalt():
******************************
******************************
******************************
******************************
******************************
RysujKsztalt(40,2):
****************************************
****************************************
Zwróćcie uwagę na zawartość linii, w której deklarujemy dwie funkcje o tej samej nazwie. W tych liniach jest przeciążona funkcja RysujKsztalt(). Implementacja tych metod zawarta jest w dalszej części programu. Funkcja nie pobierająca argumentów wywołuje funkcję z argumentami przekazując do niej wartości przechowywane w zmiennych klasy. Zawsze starajcie się unikać powielania kodu w dwóch funkcjach. Powtarzanie takich samych instrukcji w dwóch (lub więcej) miejscach utrudnia ich modyfikacje i często prowadzi do rozsynchronizowania programu. Program główny tworzy obiekt klasy Prostokat i wywołuje metodę RysujKształt() najpierw bez, a potem z dwoma argumentami. Kompilator, na podstawie liczby argumentów, decyduje, którą funkcję ma wykonać. Można sobie wyobrazić trzecią wersję funkcji, pobierającą np. tylko jeden rozmiar i dodatkową zmienną mówiącą, czy podana wartość ma być traktowana jako długość czy jako szerokość.
Wykorzystanie wartości domyślnych
Podobnie jak w przypadku zwykłych funkcji, podobnie w przypadku metod klasy można określić domyślnie wartości argumentów, które mają być przyjmowane w momencie pominięcia któregoś z argumentów. Przepiszmy teraz nasz program ale używając definiowania wartości domyślnych dla wewnętrznych funkcji prostej klasy.
#include <iostream.h>
class Prostokat
{
public:
Prostokat(int szerokosc, int wysokosc); //konstruktor
~Prostokat(){}
void RysujKsztalt() const;
void RysujKsztalt( int aSzer, int aWys) const;
private:
int jegoSzer;
int jegoWys;
};
Prostokat::Prostokat(int szerokosc, int wysokosc) //definicja konstruktora
{
jegoSzer = szerokosc;
jegoWys = wysokosc;
}
void Prostokat::RysujKsztalt(int szerokosc, int wysokosc, bool UzyjWartAktualnych) const
{
int rysSzer;
int rysWys;
if (UzyjWartAktlanych == true)
{
rysSzer = jegoSzer;
rysWys = jegoWys;
}
else
{
rysSzer = szerokosc;
rysWys = wysokosc;
}
for (int i=0; i<Wys; i++)
{
for (int j=0; j<Szer; j++) cout<<”*”;
cout<<”\n”;
}
}
int main()
{
Prostokat Prost(30,5);
cout<<”RysujKsztalt(0,0,true)...\n”;
Prost.RysujKsztalt(0,0,true);
cout<<”RysujKsztalt(40,2)...\n”;
Prost.RysujKsztalt(40,2);
return 0;
}
Efekt działania programu jest taki sam jak poprzednio. W programie tym zastąpiliśmy przeciążoną funkcję RysujKsztalt() pojedynczą funkcją z określonymi, domyślnymi wartościami argumentów. Funkcja jest zadeklarowana w deklaracji klasy. Pobiera ona trzy parametry. Pierwsze dwa, wysokość i szerokość, są typu int. UzyjWartAktulanych jest zmienną logiczną (true albo false) o domyślnej wartości false. W definicji (implementacji) funkcji RysujKsztalt() badana jest wartość trzeciego argumentu - UzyjWartAktulanych. Jeśli jest on prawdziwy, to zmiennym lokalnym rysSzer i rysWys nadawane są wartości zmiennych wewnętrznych jegoWys i jegoSzer. Jesli zmienną UzyjWartAktualnych jest równa false (niezależnie od źródła wartości), to zmiennym rysSzer i rysWys nadawane są wartości dwóch pierwszych argumentów. Zauważcie, że jeśli zmienna UzyjWartAktualnych jest równą true, to wartości dwóch pierwszych argumentów są całkowicie ignorowane.
WARTOŚCI DOMYŚLNE CZY PRZECIĄŻENIE FUNKCJI.
Oba programy robią to samo. Jednak pierwszy program napisany jest z wykorzystaniem przeciążenia funkcji, jest to prostszy bardziej intuicyjny sposób. Dodatkowym argumentem przemawiającym za rozwiązaniem wykorzystującym przeciążenie funkcji jest łatwość rozbudowy. Jeśli chcielibyśmy dać trzeciej wariant funkcji, to nie ma problemu - wystarczy dopisać kolejną realizację. Drugi program nie posiada tej zalety, wartość domyślna stanie się bezużyteczna w przypadku nowych wariantów. Jak zatem zdecydować się czy wykorzystać przeciążenie funkcji czy wartości domyślne. Oto kilka głównych reguł:
nie ma żadnej sensownej wartości domyślnej,
wykorzystywane są różne algorytmy w zależności od liczby argumentów,
musimy obsługiwać wiele typów w liście argumentów.
KONSTRUKTOR DOMYŚLNY.
Jak już mówiliśmy jeśli nie zadeklaruje się jawnie konstruktora klasy to przy każdym tworzeniu obiektu klasy wywoływany jest konstruktor domyślny, nie pobierający argumentów i nie wykonujący żadnych operacji. Można stworzyć własny konstruktor domyślny, czyli ten nie pobierający żadnych argumentów i wykorzystać go do inicjowania obiektów. Konstruktor dostarczany przez kompilator nie pobierający argumentów jest domyślny. Pojawia się tutaj pewne zagmatwanie, ale zazwyczaj z kontekstu wynika, o który konstruktor chodzi. Należy pamiętać, że jeśli stworzymy jakikolwiek konstruktor, to kompilator nie stworzy już konstruktora domyślnego. Zatem jeśli chcemy mieć konstruktor nie pobierający argumentów, a stworzyliśmy już inne konstruktory, to musimy go napisać samodzielnie!
PRZECIĄŻENIE KONSTRUKTORÓW.
Konstruktor służy do inicjalizowania obiektu. Np. konstruktor klasy Prostokat tworzy w pamięci pojedynczy prostokąt. Przed wywołaniem konstruktora w pamięci istniał tylko zarezerwowany obszar. Po wykonaniu konstruktora obszar ten stał się gotowym do użycia obiektem. Konstruktory, podobnie jak inne metody klasy, mogą być przeciążane. Możliwość przeciążenia konstruktorów daje programiście wiele możliwości np. można stworzyć klasę Prostokat posiadającą dwa konstruktory: pierwszy, pobierający dwa argumenty określające wymiary, drugi, bez argumentów tworzy prostokąt o wymiarach domyślnych. Kompilator wybierze konstruktor na podstawie typu i liczby parametrów, tak jak w przypadku zwykłych funkcji. Jednak w przeciwieństwie do konstruktorów, destruktorów przeciążać nie można. Każdy destruktor, z definicji tworzony jest tak samo: nazwa klasy poprzedzona znakiem tyldy (~). Destruktor nie pobiera żadnych argumentów.
INICJALIZACJA OBIEKTÓW.
Dotychczas, wartości wewnętrznych zmiennych klasy były ustalane wewnątrz treści konstruktorów. Jednakże, każdy konstruktor powinien składać się z dwóch części: inicjalizującej i treści. Większość zmiennych można zainicjować w dowolnej z tych części, albo poprzez inicjalizację w części inicjalizującej, albo poprzez przypisanie wartości w treści konstruktora. Jednak bardziej elegancko i bardziej efektywnie jest wykorzystać do tego część inicjalizującą. Oto przykład prawidłowej inicjalizacji zmiennych wewnętrznych:
kot::kot(): //nazwa konstruktora i parametry
jegoWaga(5), //lista inicjalizacji
jegoWiek(8)
{} //treść konstruktora
Po nawiasie zamykającym listę argumentów konstruktora należy postawić dwukropek. Następnie należy wypisać nazwę zmiennej i w nawiasach podać wyrażenie, którego wartość ma być nadana tej zmiennej. Inicjalizację poszczególnych zmiennych wewnętrznych jest bardziej efektywna niż przypisanie im wartości. Żeby to wytłumaczyć musimy najpierw dokładnie poznać zasady działania konstruktora kopiującego.
KONSTRUKTOR KOPIUJĄCY.
Podobnie jak w przypadku zwykłego konstrukotra i destruktora kompilator dostarcza również kopiujący konstruktor domyślny. Konstruktor kopiujący jest wywołany w momencie tworzenia kopii obiektu danej klasy. Kiedy obiekt jest przekazywany przez wartość, jako argument lub ewentualnie wartość zwracana, to jest wykonywana chwilowa, robocza kopia tego obiektu. Jeśli jest to obiekt klasy zdefiniowanej przez użytkownika, to jest wywoływany konstruktor kopiujący tej klasy. Konstruktor kopiujący ma tylko jeden argument: adres do obiektu tej samej klasy. Dobrze jest deklarować ten parametr jako const, gdyż konstruktor kopiujący nie ma prawa zmieniać zawartości obiektu. Oto przykład konstruktora kopiującego:
Kot(const Kot&kotek);
Konstruktor kopiujący z klasy Kot pobiera stały adres do istniejącego obiektu klasy Kot. Zadaniem tego konstruktora jest wykonanie w pamięci kopii obiektu kotek. Domyślny (dostarczany przez kompilator) konstruktor kopiujący, wykonuje kopię każdej zmiennej wewnętrznej obiektu źródłowego i umieszcza w nowym obiekcie. Jest to bardzo „płytkie” kopiowanie i o ile w przypadku zwykłych zmiennych będzie działać prawidłowo, to w przypadku wskaźników całkowicie zawiedzie, gdyż skopiowane zostaną adresy we wskaźnikach. „Płytkie kopiowanie polega na skopiowaniu wartości zmiennych obiektu źródłowego do obiektu tworzonego. Jeśli w obiekcie występują wskaźniki, to w efekcie końcowym otrzymamy dwa obiekty, w których te wskaźniki wskazują na tę samą pamięć. Wskaźników nie należy kopiować bezpośrednio. Należy wykonać kopię wartości przechowywanych pod adresami przez nie wskazywanymi do nowego obszaru pamięci. Jeśli klasa Kot, będzie zawierać zmienną jegoWiek wskazującą na wartość typu int na stercie, to konstruktor kopiujący skopiuje wartość tej zmiennej (czyli adres w niej zawarty) do zmiennej jegoWiek obiektu tworzonego. Oba wskaźniki będą wskazywać na ten sam obszar pamięci, tak jak pokazano na pierwszym rysunku:
Takie rozwiązanie spowoduje katastrofę, gdy jeden z obiektów zostanie usunięty z pamięci. Zostanie wtedy wywołany destruktor klasy kot, który zwolni zarezerwowaną na stercie pamięć. Załóżmy, że z pamięci zostanie usunięty oryginalny obiekt kot. Destruktor zwolni zarezerwowaną pamięć, Jednak kopia nadal będzie wskazywać na ten obszar. Jeśli będziemy próbowali dostać się do tej pamięci to nasz program przestanie działać... jeśli będziemy mieli szczęście. Sytuację tę ilustruje kolejny rysunek:
Rozwiązaniem tego problemu jest napisanie własnego konstruktora kopiującego i rezerwacja pamięci we własnym zakresie. Jeśli pamięć zostanie zarezerwowana to wartości z oryginalnego obiektu (w szczególności te wskazywane przez wskaźniki) mogą zostać do niej skopiowane. Takie kopioanie nazywa się głębokim. Rozważmy przykład:
#include <iostream.h>
class Kot
{
public:
Kot(); //konstruktor domyślny
Kot(const Kot&) //konstruktor kopiujący
~Kot(); //destruktor
int PobierzWiek() const {return *jegoWiek;)
int PobierzWaga() const {raturn *jegoWaga;}
void UstawWiek (int wiek) {*jegoWiek = wiek;}
private:
int *jegoWiek;
int *jegoWaga;
};
Kot::Kot()
{
jegoWiek = new int;
jegoWaga = new int;
*jegoWiek = 5;
*jegoWaga = 9;
}
Kot::Kot(const Kot &rhs)
{
jegoWiek = new int;
jegoWaga = new int;
*jegoWiek = rhs.PobierzWiek();
*jegoWaga = rhs.PobierzWaga();
}
Kot::~Kot()
{
delete jegoWiek;
jegoWiek = 0;
delete jegoWaga;
jegoWaga = 0;
}
int main()
{
Kot klakier;
cout<<”Wiek Klakiera: ”<<klakier.PobierzWiek()<<endl;
cout<<”Niech Klakier ma 6 lat.\n”;
klakier.UstaWWiek(6);
cout<<”Tworzenie filemona na podstawie Klakiera.\n”;
Kot filemon(klakier);
cout<<”Wiek Klakiera: ”<<klakier.PobierzWiek()<<endl;
cout<<”Wiek Filemona: ”<<filemon.PobierzWiek()<<endl;
cout<<”Niech klakier ma 7 lat\n”;
klakier.UstawWiek(7);
cout<<”Wiek Klakiera: ”<<klakier.PobierzWiek()<<endl;
cout<<”Wiek Filemona: ”<<filemon.PobierzWiek()<<endl;
return 0;
}
Efekt działania programu:
Wiek Klakiera: 5
Niech Klakier ma 6 lat
Tworzenie Filemona na podstawie Klakiera.
Wiek Klakiera: 6
Wiek Filemona: 6
Niech Klakier ma 7 lat
Wiek Klakiera: 7
Wiek Filemona: 6
W deklaracji klasy Kot jest deklarowany konstruktor domyślny i kopiujący. Deklarowane są również dwie zmienne wewnętrzne, każda jako wskaźnik do wartości typu int. Normalnie przechowywanie w klasie zmiennych typu int pod wskaźnikami jest niestosowane. Tutaj wykorzystaliśmy to tylko do zademonstrowania zarządzania danymi na stercie. Pierwszy (domyślny) konstruktor, rezerwuje dla dwóch zmiennych typu int pamięć na stercie i przypisuje im odpowiednie wartości. Drugi konstruktor (kopiujący). Zwróćcie uwagę na parametr rhs. Jest to często spotykana nazwa argumentów konstruktora kopiującego (z ang. right-hand-side - znajdujący się po prawej stronie). Jeśli spojrzymy na przypisania w definicji tego konstruktora, to zobaczymy, że obiekt przekazywany jako argument znajduje się po prawej stronie operatora przypisania. Oto zasada działania konstruktora kopiującego:
W pierwszych liniach definicji rezerwowana jest pamięć na stercie. następnie przypisywane tam są wartości z istniejącego obiektu klasy Kot.
Argument rhs to obiekt klasy Kot przekazywany do konstruktora kopiującego jako stały (const) adres. Wykorzystujemy wewnętrzne funkcje rhs.PobierzWiek() i rhs.PobierzWage() do odczytania wartości zmiennych wewnętrznych jegoWiek i jegoWaga i przypisania ich do tworzonego obiektu.
Kiedy jest wywoływany konstruktor kopiujący, to istniejący obiekt klasy Kot jest przekazywany do niego jako argument. Do zmiennych tworzonego obiektu można się odwoływać bezpośrednio, ale do odczytania zmiennych obiektu rhs trzeba wykorzystywać funkcje dostępu (metody).
Poniższy rysunek ilustruje działanie konstruktora kopiującego z naszego przykładu. Wartości z istniejącego obiektu są kopiowane do pamięci zarezerwowanej dla nowego obiektu
W programie głównym tworzony jest obiekt o nazwie Klakier. Wypisywany jest jego wiek. Następnie wiek klakiera jest zamieniany na 6. W następnej linii tworzony jest nowy obiekt Filemon z wykorzystaniem konstruktora kopiującego i obiektu Klakier. Gdyby Klakier był przekazywany przez wartość do funkcji, to również zostałby wywołany konstruktor kopiujący. W następnych liniach wypisywany jest wiek obydwu kotów. Faktycznie Filemon ma tyle samo lat co Klakier (a nie domyślną wartość 5). W następnej linii Klakier się starzeje i ma już 7 lat. Ponownie wypisujemy wiek obydwu kotów. Klakier ma 7 lat, a Filemon nadal 6. Dowodzi to, że obiekty znajdują się w oddzielnych obszarach pamięci. Podczas usuwania obiektów Kot z pamięci automatycznie są wywoływane destruktory klasy. Ich implementacja znajduje się wcześniej w programie. Za pomocą delete kasowane są obydwa wskaźniki: jegoWiek i jegoWaga, zwalniana jest zajmowana przez nie pamięć. Dla bezpieczeństwa wskaźnikom jest nadawana wartość NULL (0).
PRZECIĄŻENIE OPERATORÓW.
C++ posiada wiele wbudowanych typów danych, takich jak int, float, char itp. Każdy z nich posiada pewną liczbę określonych operatorów takich jak dodawanie (+), mnożenie (*), itd. C++ pozwala na dodawanie tych operatorów do własnych klas. Zacznijmy od początku i stwórzmy nową klasę Licznik. Obiekt klasy Licznik będzie wykorzystywany w pętlach i innych aplikacjach, które będą wymagały kontrolowanego zwiększania lub zmniejszania wartości:
#include <iostream.h>
class Licznik
{
public:
Licznik();
~Licznik();
int PobierzWart() const {return jegoWart;}
void UstawWart(int x) {jegoWart = x;}
private:
int jegoWart;
};
Licznik:Licznik():jegoWart(0)
{};
int main()
{
Licznik i;
cout<<”Wartosc i wynosi: ”<<i.PobierzWart()<<endl;
}
Efekt działania:
Wartosc i wynosi: 0
Jak widać stworzyliśmy całkowicie bezużyteczną klasę. Jej jedyna zmienna wewnętrzna jest typu int. Domyślny konstruktor nadaje jej wartość 0. W przeciwieństwie do zwykłej (takiej „z krwi i kości”) zmiennej typu int, obiekt klasy Licznik nie może zostać ani zwiększony, ani zmniejszony. Skomplikowane jest również wypisywanie jego wartości.
FUNKCJA INKREMENTUJĄCA.
Przeciążenie operatora przywraca część funkcjonalności utraconej podczas definiowania własnej klasy takiej jak Licznik. Napiszemy program ilustrujący przeciążenie operatora inkrementacji:
#include <iostream.h>
class Licznik
{
public:
Licznik();
~Licznik() {}
int PobierzWartosc() const {return jegoWart;}
void Increment() {++jegoWartosc;}
const Licznik& operator++();
private:
int jegoWart;
};
Licznik::Licznik():jegoWiek(0)
{};
const Licznik& Licznik::operator++() //<-implementacja operatora ++
{
++jegoWart;
return *this
}
int main()
{
Licznik i;
cout<<”Wartosc i wynosi: ”<<i.PobierzWartosc()<<endl;
i.Increment();
cout<<”Wartosc i wynosi: ”<<i.PobierzWartosc()<<endl;
++i;
cout<<”Wartosc i wynosi: ”<<i.PobierzWartosc()<<endl;
Licznik a=++i //<- operacja przypisania
cout<<”Wartosc a: ”<<a.PobierzWartosc();
cout<<” oraz i: ”<<i.PobierzWartosc()<<endl;
return 0;
}
Efekt działania:
Wartość i wynosi: 0
Wartość i wynosi: 1
Wartość i wynosi: 2
Wartosc a: 3 oraz i: 3
Implementacja operatora ++ została napisana tak, że zwraca aktualny obiekt za pomocą pośredniego odwołania do this. Dzięki temu możemy wykonać operację przypisania. Jeśli obiekt klasy Licznik rezerwowałby pamięć, to trzeba by było napisać własny konstruktor kopiujący, uwzględniający to. Jednak w tym konkretnym przypadku domyślny konstruktor kopiujący jest całkowicie wystarczający. Zauważcie, że wartość zwracana jest adresem do obiektu klasy Licznik, przez co unikamy zbędnego tworzenia kopii obiektu. Jest ona zadeklarowana jako const ponieważ wartość nie powinna być zmieniona przez funkcje wykorzystujące ten obiekt.
PRZECIĄŻENIE OPERATORA PRZYROSTKOWEGO.
Co zrobić, jeśli chcemy przeciążyć operator przyrostowy? Pojawia się tutaj pewien problem. Jak rozróżnić oba operatory - przedrostkowy i przyrostkowy. Przyjęta została konwencja, że w przypadku deklaracji operatora przyrostkowego (postfix) podaje się argument całkowity. Wartość tego argumentu jest ignorowana, stanowi on jedynie sygnał dla kompilatora. np.
const Licznik& operator++(); //<-przedrostkowy
const Licznik& operator++(int); //<-przyrostkowy
O ile operator przedrostkowy może zwiększyć wartość i zwrócić obiekt przez wartość, to operator przyrostkowy musi zwrócić wartość przed inkrementacją. W tym celu trzeba stworzyć pomocniczy obiekt, przechowujący początkową wartość obiektu przed inkrementacją. Czyli jeśli x jest obiektem, to operator przyrostkowy musi przechowywać oryginalną wartość x w obiekcie pomocniczym, zwiększyć x i zwrócić obiekt pomocniczy. Zwrócić należy ten chwilowy obiekt. Pamiętać należy jednak, że zwracając obiekt pomocniczy nie można go zwrócić przez adres gdyż jest to obiekt lokalny. Koniecznie trzeba go zwrócić przez wartość.
OPERATOR +.
Operator inkrementacji jest operatorem umownym co oznacza, że operuje tylko na jednym obiekcie. Operator dodawania (+) jest operatorem binarnym, działającym na dwóch obiektach. Jak przeciążyć operator dodawania w znanej już klasie Licznik? Celem jest możliwość zadeklarowania dwóch obiektów klasy Licznik, a następnie dodania ich tak, jak w przykładzie:
Licznik lJeden, lDwa, lTrzy;
lTrzy = lJeden + lDwa;
Można by napisać funkcję Dodaj(), przekazać do niej obiekt Licznik, dodać i zwrócić obiekt klasy Licznik jako wynik np.
#include <iostream.h>
class Licznik
{
public:
Licznik();
Licznik(int wartPocz);
~Licznik() {};
int PobierzWart() const {return jegoWart;}
void UstawWart(int x) {jegoWart = x;}
Licznik Dodaj(const Licznik&);
private:
int jegoWart;
};
Licznik::Licznik(int wartPocz):jegoWart(wartPocz)
{}
Licznik::Licznik():jegoWart(0)
{}
Licznik Licznik::Dodaj(const Licznik& rhs)
{
return Licznik(jegoWart+rhs.PobierzWart());
}
int main()
{
Licznik lJeden(2), lDwa(4), lTrzy;
lTrzy = lJeden.Dodaj(lDwa);
cout<<”lJeden: ”<<lJeden.PobierzWart()<<endl;
cout<<”lDwa: ”<<lDwa.PobierzWart()<<endl;
cout<<”lTrzy: ”<<lTrzy.PobierzWart()<<endl;
return 0;
}
Efekt działania:
lJeden: 2
lDwa: 4
lTrzy: 6
W programie deklarujemy funkcję Dodaj(). Jako argument pobiera ona stały adres do obiektu klasy Licznik, który będzie dodany do aktualnego obiektu tak, jak to pokazano w drugiej linii programu głównego. Widzimy, że lJeden jest obiektem, lDwa jest argumentem funkcji Dodaj(), natomiast lTrzy jest obiektem do którego będzie przypisany wynik dodawania. Obiekt lTrzy, jest tworzony bez podanej wartości początkowej. Musi być wykorzystany konstruktor domyślny. Inicjuje on zmienną jegoWart wartością 0. Ponieważ obiekty lJeden i lDwa muszą być zainicjowane niezerową, podaną wartością, dlatego stworzyliśmy jeszcze jeden konstruktor, który jest zdefiniowany w programie.
PRZECIĄŻENIE OPERATORA +.
Zdefiniowaliśmy funkcję Dodaj() w poprzednim programie. Wprawdzie ona działa ale jej użycie jest raczej nienaturalne. Lepszym, bardziej intuicyjnym rozwiązaniem jest przeciążenie operatora dodawania (+) np.
#include <iostream.h>
class Licznik
{
public:
Licznik();
Licznik(int wartPocz);
~Licznik() {};
int PobierzWart() const {return jegoWart;}
void UstawWart(int x) {jegoWart = x;}
Licznik operator+(const licznik&);
private:
int jegoWart;
};
Licznik::Licznik():jegoWart(0)
{}
Licznik Licznik::operator+(const Licznik& rhs)
{
return Licznik(jegoWart + rhs.PobierzWart());
}
int main()
{
Licznik lJeden(2), lDwa(4), lTrzy;
lTrzy = lJeden + lDwa;
cout<<”lJeden: ”<<lJeden.PobierzWart()<<endl;
cout<<”lDwa: ”<<lDwa.PobierzWart()<<endl;
cout<<”lTrzy: ”<<lTrzy.PobierzWart()<<endl;
return 0;
}
Efekt działania jak poprzednio.
W deklaracji klasy deklarujemy operator+ i w dalszej części programu jest jego definicja. Jeśli porównamy ją z funkcją Dodaj() z wcześniejszego przykładu, zauważmy, że są one prawie identyczne. Jednak składnia ich wykorzystania jest skrajnie różna. Łatwiej jest napisać:
lTrzy = lJeden + lDwa;
niż:
lTrzy = lJeden.Dodaj(lDwa);
Niby niewielka zmiana a program wygląda dużo lepiej.
OGRANICZENIA PRZY PRZECIĄŻANIU OPERATORÓW.
Przede wszystkim nie jest możliwe przeciążenie operatorów wbudowanych typów C++. Nie można zmienić kolejności wykonywania działań i arności (liczby argumentów). Nie można tworzyć nowych operatorów. Próba stworzenia np. operatora** dla podnoszenia potęgi nie powiedzie się.
CO I KIEDY PRZECIĄŻAĆ.
Przeciążenie operatorów jest często nadużywane przez programistów (szczególnie początkujących). Próbują oni tworzyć nowe, ciekawe zastosowania dla prostych operatorów, lecz nieodzownie prowadzi to do zbędnej komplikacji programu i niepoprawności. Duże niebezpieczeństwo kryje się w zasadzie poprawnego wykorzystania przeciążenia np. operatora+ do łączenia znaków w łańcuchy lub operatora | do dzielenia łańcuchów. Można rozważyć takie zastosowania ale z rozwagą. Zawsze należy pamiętać, że przeciążenie operatorów ma zwiększyć przejrzystość kodu i łatwość korzystania z niego.
- 11 -
wykorzystanie domyślnego konstruktora kopiującego
5
sterta
jegoWiek
nowy kot
jegoWiek
oryginalny kot
powstanie błędnego wskaźnika
5
sterta
jegoWiek
nowy kot
jegoWiek
oryginalny kot
5
sterta
jegoWiek
nowy kot
jegoWiek
oryginalny kot
5
ilustracja głębokiego kopiowania