Rozdział 10.
Funkcje zaawansowane
W rozdziale 5., Funkcje , poznałeś podstawy pracy z funkcjami. Teraz, gdy wiesz także, jak
działają wskazniki i referencje, możesz zgłębić zagadnienia dotyczące funkcji.
Z tego rozdziału dowiesz się, w jaki sposób:
" przeciążać funkcje składowe,
" przeciążać operatory,
" pisać funkcje, mając na celu tworzenie klas z dynamicznie alokowanymi zmiennymi.
Przeciążone funkcje składowe
Z rozdziału 5. dowiedziałeś się jak implementować polimorfizm funkcji, czyli ich przeciążanie,
przez tworzenie dwóch lub więcej funkcji o tych samych nazwach, lecz innych parametrach.
Funkcje składowe klas mogą być przeciążane w dokładnie ten sam sposób.
Klasa Rectangle (prostokąt), zademonstrowana na listingu 10.1, posiada dwie funkcje
DrawShape() (rysuj kształt). Pierwsza z nich, nie posiadająca parametrów, rysuje prostokąt na
podstawie bieżących wartości składowych danego egzemplarza klasy. Druga funkcja otrzymuje
dwie wartości (szerokość i długość) i rysuje na ich podstawie prostokąt, ignorując bieżące wartości
zmiennych składowych.
Listing 10.1. Przeciążone funkcje składowe
0: //Listing 10.1 Przeciążanie funkcji składowych klasy
1:
2: #include
3:
4: // Deklaracja klasy Rectangle
5: class Rectangle
6: {
7: public:
8: // konstruktory
9: Rectangle(int width, int height);
10: ~Rectangle(){}
11:
12: // przeciążona funkcja składowa klasy
13: void DrawShape() const;
14: void DrawShape(int aWidth, int aHeight) const;
15:
16: private:
17: int itsWidth;
18: int itsHeight;
19: };
20:
21: // implementacja konstruktora
22: Rectangle::Rectangle(int width, int height)
23: {
24: itsWidth = width;
25: itsHeight = height;
26: }
27:
28:
29: // Przeciążona funkcja DrawShape - nie ma parametrów
30: // Rysuje kształt w oparciu o bieżące wartości zmiennych
składowych
31: void Rectangle::DrawShape() const
32: {
33: DrawShape( itsWidth, itsHeight);
34: }
35:
36:
37: // Przeciążona funkcja DrawShape - z dwoma parametrami
38: // Rysuje kształt w oparciu o podane wartości
39: void Rectangle::DrawShape(int width, int height) const
40: {
41: for (int i = 0; i 42: {
43: for (int j = 0; j< width; j++)
44: {
45: std::cout << "*";
46: }
47: std::cout << "\n";
48: }
49: }
50:
51: // Główna funkcja demonstrująca przeciążone funkcje
52: int main()
53: {
54: // inicjalizujemy prostokąt 30 na 5
55: Rectangle theRect(30,5);
56: std::cout << "DrawShape(): \n";
57: theRect.DrawShape();
58: std::cout << "\nDrawShape(40,2): \n";
59: theRect.DrawShape(40,2);
60: return 0;
61: }
Wynik
DrawShape():
******************************
******************************
******************************
******************************
******************************
DrawShape(40,2):
****************************************
****************************************
Analiza
Listing 10.1 prezentuje okrojoną wersję programu, zamieszczonego w podsumowaniu wiadomości
po rozdziale 7. Aby zaoszczędzić miejsce, z programu usunięto sprawdzanie niepoprawnych
wartości, a także niektóre z akcesorów. Główny program został sprowadzony do dużo prostszej
postaci, w której nie ma już menu.
Najważniejszy kod znajduje się w liniach 13. i 14., gdzie przeciążona została funkcja
DrawShape(). Implementacja tych przeciążonych funkcji składowych znajduje się w liniach od
29. do 49. Zwróć uwagę, że funkcja w wersji bez parametrów po prostu wywołuje funkcję z
parametrami, przekazując jej bieżące zmienne składowe. Postaraj się nigdy nie powtarzać tego
samego kodu w dwóch funkcjach, może to spowodować wiele problemów z zachowaniem ich w
zgodności w trakcie wprowadzaniu poprawek (może stać się to przyczyną błędów).
Główna funkcja tworzy w liniach od 51. do 61. obiekt prostokąta, po czym wywołuje funkcję
DrawShape(), najpierw bez parametrów, a potem z dwoma parametrami typu int.
Kompilator na podstawie ilości i typu podanych parametrów wybiera metodę. Można sobie
wyobrazić także trzecią przeciążoną funkcję o nazwie DrawShape(), która otrzymywałaby jeden
wymiar oraz wartość wyliczeniową, określającą, czy jest on wysokością czy szerokością (wybór
należałby do użytkownika).
Użycie wartości domyślnych
Podobnie, jak w przypadku funkcji składowych klasy, funkcje globalne również mogą mieć jedną
lub więcej wartości domyślnych. W przypadku deklaracji wartości domyślnych w funkcjach
składowych stosujemy takie same reguły, jak w funkcjach globalnych, co ilustruje listing 10.2.
Listing 10.2. Użycie wartości domyślnych
0: //Listing 10.2 Domyślne wartości w funkcjach składowych
1:
2: #include
3:
4: using namespace std;
5:
6: // Deklaracja klasy Rectangle
7: class Rectangle
8: {
9: public:
10: // konstruktory
11: Rectangle(int width, int height);
12: ~Rectangle(){}
13: void DrawShape(int aWidth, int aHeight,
14: bool UseCurrentVals = false) const;
15:
16: private:
17: int itsWidth;
18: int itsHeight;
19: };
20:
21: // implementacja konstruktora
22: Rectangle::Rectangle(int width, int height):
23: itsWidth(width), // inicjalizacje
24: itsHeight(height)
25: {} // puste ciało konstruktora
26:
27:
28: // dla trzeciego parametru jest używana domyślna wartość
29: void Rectangle::DrawShape(
30: int width,
31: int height,
32: bool UseCurrentValue
33: ) const
34: {
35: int printWidth;
36: int printHeight;
37:
38: if (UseCurrentValue == true)
39: {
40: printWidth = itsWidth; // używa bieżących wartości
klasy
41: printHeight = itsHeight;
42: }
43: else
44: {
45: printWidth = width; // używa wartości z
parametrów
46: printHeight = height;
47: }
48:
49:
50: for (int i = 0; i 51: {
52: for (int j = 0; j< printWidth; j++)
53: {
54: cout << "*";
55: }
56: cout << "\n";
57: }
58: }
59:
60: // Główna funkcja demonstrująca przeciążone funkcje
61: int main()
62: {
63: // inicjalizujemy prostokąt 30 na 5
64: Rectangle theRect(30,5);
65: cout << "DrawShape(0,0,true)...\n";
66: theRect.DrawShape(0,0,true);
67: cout <<"DrawShape(40,2)...\n";
68: theRect.DrawShape(40,2);
69: return 0;
70: }
Wynik
DrawShape(0,0,true)...
******************************
******************************
******************************
******************************
******************************
DrawShape(40,2)...
****************************************
****************************************
Analiza
Listing 10.2 zastępuje przeciążone funkcje DrawShape() pojedynczą funkcją z domyślnym
parametrem. Ta funkcja, zadeklarowana w linii 13., posiada trzy parametry. Dwa pierwsze,
aWidth (szerokość) i aHeight (wysokość) są typu int, zaś trzeci, UseCurrentVals (użyj
bieżących wartości), jest zmienną typu bool o domyślnej wartości false.
Implementacja tej nieco udziwnionej funkcji rozpoczyna się w linii 28. Sprawdzany jest w niej
trzeci parametr, UseCurrentValue. Jeśli ma on wartość true, wtedy do ustawienia lokalnych
zmiennych printWidth (wypisywana szerokość) i printHeight (wypisywana wysokość) są
używane zmienne składowe klasy, itsWidth oraz itsHeight.
Jeśli parametr UseCurrentValue ma wartość false, podaną przez użytkownika, lub ustawioną
domyślnie, wtedy zmiennym printWidth i printHeight są przypisywane wartości dwóch
pierwszych argumentów funkcji.
Zwróć uwagę, że gdy parametr UseCurrentValue ma wartość true, wartości dwóch pierwszych
parametrów są całkowicie ignorowane.
Wybór pomiędzy wartościami domyślnymi a
przeciążaniem funkcji
Listingi 10.1 i 10.2 dają ten sam wynik, lecz przeciążone funkcje z listingu 10.1 są łatwiejsze do
zrozumienia i wygodniejsze w użyciu. Poza tym, gdy jest potrzebna trzecia wersja na przykład,
gdy użytkownik chce dostarczyć szerokości albo wysokości osobno można łatwo stworzyć
kolejną przeciążoną funkcję. Z drugiej strony, w miarę dodawania kolejnych wersji, wartości
domyślne mogą szybko stać się zbyt skomplikowane.
W jaki sposób podjąć decyzję, czy użyć przeciążania funkcji, czy wartości domyślnych? Oto
ogólna reguła:
Przeciążania funkcji używaj, gdy:
" nie istnieje sensowna wartość domyślna,
" używasz różnych algorytmów,
" chcesz korzystać z różnych rodzajów parametrów funkcji.
Konstruktor domyślny
Jak mówiliśmy w rozdziale 6., Programowanie zorientowane obiektowo , jeśli nie zadeklarujesz
konstruktora klasy jawnie, zostanie dla niej stworzony konstruktor domyślny, który nie ma
żadnych parametrów i nic nie robi. Możesz jednak stworzyć własny konstruktor domyślny, który
także nie posiada parametrów, ale odpowiednio przygotowuje obiekt do działania.
Taki konstruktor także jest nazywany konstruktorem domyślnym , bo zgodnie z konwencją, jest
nim konstruktor nie posiadający parametrów. Może to budzić wątpliwości, ale zwykle jasno
wynika z kontekstu danego miejsca w programie.
Zwróć uwagę, że gdy stworzysz jakikolwiek konstruktor, kompilator nie dostarcza już
konstruktora domyślnego. Gdy potrzebujesz konstruktora nie posiadającego parametrów i
stworzysz jakikolwiek inny konstruktor, musisz stworzyć także konstruktor domyślny!
Przeciążanie konstruktorów
Przeznaczeniem konstruktora jest przygotowanie obiektu; na przykład, celem konstruktora
Rectangle jest stworzenie poprawnego obiektu prostokąta. Przed wykonaniem konstruktora nie
istnieje żaden prostokąt, a jedynie miejsce w pamięci. Gdy konstruktor kończy działanie, w
pamięci istnieje kompletny, gotowy do użycia obiekt prostokąta.
Konstruktory, tak jak wszystkie inne funkcje składowe, mogą być przeciążane. Możliwość
przeciążania ich jest bardzo przydatna.
Na przykład: możesz mieć obiekt prostokąta posiadający dwa konstruktory. Pierwszy z nich
otrzymuje szerokość oraz długość i tworzy prostokąt o podanych rozmiarach. Drugi nie ma
żadnych parametrów i tworzy prostokąt o rozmiarach domyślnych. Ten pomysł wykorzystano na
listingu 10.3.
Listing 10.3. Przeciążanie konstruktora
0: // Listing 10.3
1: // Przeciążanie konstruktorów
2:
3: #include
4: using namespace std;
5:
6: class Rectangle
7: {
8: public:
9: Rectangle();
10: Rectangle(int width, int length);
11: ~Rectangle() {}
12: int GetWidth() const { return itsWidth; }
13: int GetLength() const { return itsLength; }
14: private:
15: int itsWidth;
16: int itsLength;
17: };
18:
19: Rectangle::Rectangle()
20: {
21: itsWidth = 5;
22: itsLength = 10;
23: }
24:
25: Rectangle::Rectangle (int width, int length)
26: {
27: itsWidth = width;
28: itsLength = length;
29: }
30:
31: int main()
32: {
33: Rectangle Rect1;
34: cout << "Rect1 szerokosc: " << Rect1.GetWidth() << endl;
35: cout << "Rect1 dlugosc: " << Rect1.GetLength() << endl;
36:
37: int aWidth, aLength;
38: cout << "Podaj szerokosc: ";
39: cin >> aWidth;
40: cout << "\nPodaj dlugosc: ";
41: cin >> aLength;
42:
43: Rectangle Rect2(aWidth, aLength);
44: cout << "\nRect2 szerokosc: " << Rect2.GetWidth() << endl;
45: cout << "Rect2 dlugosc: " << Rect2.GetLength() << endl;
46: return 0;
47: }
Wynik
Rect1 szerokosc: 5
Rect1 dlugosc: 10
Podaj szerokosc: 20
Podaj dlugosc: 50
Rect2 szerokosc: 20
Rect2 dlugosc: 50
Analiza
Klasa Rectangle jest zadeklarowana w liniach od 6. do 17. Posiada dwa konstruktory:
domyślny konstruktor w linii 9. oraz drugi konstruktor w linii 10., przyjmujący dwie liczby
całkowite.
W linii 33. za pomocą domyślnego konstruktora tworzony jest prostokąt; jego rozmiary są
wypisywane w liniach 34. i 35. W liniach od 38. do 41. użytkownik jest proszony o podanie
szerokości i długości, po czym w linii 43. wywoływany jest konstruktor, który otrzymuje dwa
parametry. Na koniec, w liniach 44. i 45. wypisywane są rozmiary drugiego prostokąta.
Tak jak w przypadku innych funkcji przeciążonych, kompilator wybiera właściwy konstruktor na
podstawie typów i ilości parametrów.
Inicjalizowanie obiektów
Do tej pory ustawiałeś zmienne składowe wewnątrz ciała konstruktora. Konstruktory są jednak
wywoływane w dwóch fazach: inicjalizacji i ciała.
Większość zmiennych może być ustawiana w dowolnej z tych faz, podczas inicjalizacji lub w
wyniku przypisania w ciele konstruktora. Lepiej zrozumiałe, i często bardziej efektywne, jest
inicjalizowanie zmiennych składowych w fazie inicjalizacji konstruktora. Sposób inicjalizowania
zmiennych składowych przedstawia poniższy przykład:
CAT(): // nazwa konstruktora i parametry
itsAge(5), // lista inicjalizacyjna
itsWeight(8)
{ } // ciało konstruktora
Po nawiasie zamykającym listę parametrów wpisz dwukropek. Następnie wpisz nazwę zmiennej
składowej oraz parę nawiasów. Wewnątrz nawiasów wpisz wyrażenie, którego wartość ma
zainicjalizować zmienną składową. Jeśli chcesz zainicjalizować kilka zmiennych, każdą z
inicjalizacji oddziel przecinkiem. Listing 10.4 przedstawia definicję konstruktora z listingu 10.3, w
której zamiast przypisania w ciele konstruktora zastosowano inicjalizację zmiennych.
Listing 10.4. Fragment kodu, przedstawiający inicjalizację zmiennych składowych
0: //Listing 10.4 - Inicjalizacja zmiennych składowych
1: Rectangle::Rectangle():
2: itsWidth(5),
3: itsLength(10)
4: {
5: }
6:
7: Rectangle::Rectangle (int width, int length):
8: itsWidth(width),
9: itsLength(length)
10: {
11: }
Bez wyniku.
Niektóre zmienne muszą być inicjalizowane i nie można im niczego przypisywać; dotyczy to
referencji i stałych. Wewnątrz ciała konstruktora można zawrzeć także inne przypisania i
działania, jednak najlepiej maksymalnie wykorzystać fazę inicjalizacji.
Konstruktor kopiujący
Oprócz domyślnego konstruktora i destruktora, kompilator dostarcza także domyślnego
konstruktora kopiującego. Konstruktor kopiujący jest wywoływany za każdym razem, gdy
tworzona jest kopia obiektu.
Gdy przekazujesz obiekt przez wartość, czy to jako parametr funkcji czy też jako jej wartość
zwracaną , tworzona jest tymczasowa kopia tego obiektu. Jeśli obiekt jest obiektem
zdefiniowanym przez użytkownika, wywoływany jest konstruktor kopiujący danej klasy, taki
mogłeś zobaczyć w poprzednim rozdziale na listingu 9.6.
Wszystkie konstruktory kopiujące posiadają jeden parametr; jest nim referencja do obiektu tej
samej klasy. Dobrym pomysłem jest oznaczenie tej referencji jako const, gdyż wtedy konstruktor
nie ma możliwości modyfikacji otrzymanego obiektu. Na przykład:
CAT(const CAT & theCat);
W tym przypadku konstruktor CAT otrzymuje stałą referencję do istniejącego obiektu klasy CAT.
Celem konstruktora kopiującego jest utworzenie kopii obiektu theCat.
Domyślny konstruktor kopiujący po prostu kopiuje każdą zmienną składową z obiektu
otrzymanego jako parametr do odpowiedniej zmiennej składowej obiektu tymczasowego. Nazywa
się to kopiowaniem składowych (czyli kopiowaniem płytkim), i choć w przypadku większości
składowych nie jest potrzebne nic więcej, proces ten jednak nie sprawdza się w przypadku
zmiennych będących wskaznikami do obiektów na stercie.
W płytkiej kopii (czyli bezpośredniej kopii składowych) kopiowane są dokładne wartości
składowych jednego obiektu do składowych drugiego obiektu. Wskazniki zawarte w obu
obiektach wskazują wtedy na to samo miejsce w pamięci. W przypadku głębokiej kopii, wartości
zaalokowane na stercie są kopiowane do nowo alokowanej pamięci.
Gdyby klasa CAT zawierała zmienną składową itsAge, będącą wskaznikiem do zmiennej
całkowitej zaalokowanej na stercie, wtedy domyślny konstruktor kopiujący skopiowałby wartość
zmiennej itsAge otrzymanego obiektu do zmiennej itsAge nowego obiektu. Oba obiekty
wskazywałyby więc to samo miejsce w pamięci, co ilustruje rysunek 10.1.
Rys. 10.1. Użycie domyślnego konstruktora kopiującego
Gdy któryś z obiektów CAT znajdzie się poza zakresem, nastąpi katastrofa. Jak opisano w
rozdziale 8., Wskazniki , zadaniem destruktora jest uporządkowanie i zwolnienie pamięci po
obiekcie. Jeśli destruktor pierwotnego obiektu CAT zwolni tę pamięć, zaś wskaznik w nowym
obiekcie CAT nadal będzie na nią wskazywał, oznaczać to będzie pojawienie się błędnego
(zagubionego) wskaznika, a program znajdzie się w śmiertelnym niebezpieczeństwie. Ten problem
ilustruje rysunek 10.2.
Rys. 10.2. Powstawanie zagubionego wskaznika
Rozwiązaniem tego problemu jest stworzenie własnego konstruktora kopiującego, alokującego
wymaganą pamięć. Po zaalokowaniu pamięci, stare wartości mogą być skopiowane do nowej
pamięci. Pokazuje to listing 10.5.
Listing 10.5. Konstruktor kopii
0: // Listing 10.5
1: // Konstruktory kopii
2:
3: #include
4: using namespace std;
5:
6: class CAT
7: {
8: public:
9: CAT(); // domyślny konstruktor
10: CAT (const CAT &); // konstruktor kopiujący
11: ~CAT(); // destruktor
12: int GetAge() const { return *itsAge; }
13: int GetWeight() const { return *itsWeight; }
14: void SetAge(int age) { *itsAge = age; }
15:
16: private:
17: int *itsAge;
18: int *itsWeight;
19: };
20:
21: CAT::CAT()
22: {
23: itsAge = new int;
24: itsWeight = new int;
25: *itsAge = 5;
26: *itsWeight = 9;
27: }
28:
29: CAT::CAT(const CAT & rhs)
30: {
31: itsAge = new int;
32: itsWeight = new int;
33: *itsAge = rhs.GetAge(); // dostęp publiczny
34: *itsWeight = *(rhs.itsWeight); // dostęp prywatny
35: }
36:
37: CAT::~CAT()
38: {
39: delete itsAge;
40: itsAge = 0;
41: delete itsWeight;
42: itsWeight = 0;
43: }
44:
45: int main()
46: {
47: CAT mruczek;
48: cout << "Wiek Mruczka: " << mruczek.GetAge() << endl;
49: cout << "Ustawiam wiek Mruczka na 6 lat...\n";
50: mruczek.SetAge(6);
51: cout << "Tworze Filemona z Mruczka\n";
52: CAT filemon(mruczek);
53: cout << "Wiek Mruczka: " << mruczek.GetAge() << endl;
54: cout << "Wiek Filemona: " << filemon.GetAge() << endl;
55: cout << "Ustawiam wiek Mruczka na 7 lat...\n";
56: mruczek.SetAge(7);
57: cout << "Wiek Mruczka: " << mruczek.GetAge() << endl;
58: cout << "Wiek Filemona: " << filemon.GetAge() << endl;
59: return 0;
60: }
Wynik
Wiek Mruczka: 5
Ustawiam wiek Mruczka na 6 lat...
Tworze Filemona z Mruczka
Wiek Mruczka: 6
Wiek Filemona: 6
Ustawiam wiek Mruczka na 7 lat...
Wiek Mruczka: 7
Wiek Filemona: 6
Analiza
W liniach od 6. do 19. deklarowana jest klasa CAT. Zwróć uwagę, że w linii 9. został
zadeklarowany konstruktor domyślny, a w linii 10. został zadeklarowany konstruktor kopiujący.
W liniach 17. i 18. są deklarowane dwie zmienne składowe, będące wskaznikami do zmiennych
typu int. Zwykle klasa nie ma powodów do przechowywania danych składowych typu int w
postaci wskazników, ale w tym przypadku służy to do zilustrowania operowania zmiennymi
składowymi na stercie.
Domyślny konstruktor, zdefiniowany w liniach od 21. do 27. alokuje miejsce na stercie dla dwóch
zmiennych typu int, po czym przypisuje im wartości.
Definicja konstruktora kopiującego rozpoczyna się w linii 29. Zwróć uwagę, że jego parametrem
jest rhs. Jest to często stosowana nazwa dla parametru konstruktora kopiującego, stanowiąca skrót
od wyrażenia right-hand side (prawa strona). Gdy spojrzysz na przypisania w liniach 33. i 34.,
przekonasz się, że obiekt przekazywany jako parametr znajduje się po prawej stronie znaku
równości. Oto sposób, w jaki działa:
W liniach 31. i 32. alokowana jest pamięć na stercie. Następnie, w liniach 33. i 34., wartościom w
nowej pamięci są przypisywane wartości danych z istniejącego obiektu CAT.
Parametr rhs jest obiektem CAT przekazanym do konstruktora kopiującego jako stała referencja.
Jako obiekt klasy CAT, parametr rhs posiada wszystkie składowe tej klasy.
Każdy obiekt CAT może odwoływać się do wszystkich (także prywatnych) składowych innych
obiektów tej samej klasy; jednak do tradycji programistycznej należy korzystanie z akcesorów
wszędzie tam, gdzie jest to możliwe. Funkcja składowa rhs.GetAge() zwraca wartość
przechowywaną w pamięci wskazywanej przez zmienną składową itsAge obiektu rhs.
Rysunek 10.3 przedstawia, co dzieje się w programie. Wartości wskazywane przez zmienne
składowe istniejącego obiektu CAT są kopiowane do pamięci zaalokowanej dla nowego obiektu
CAT.
Rys. 10.3. Przykład kopiowania głębokiego
W linii 47. tworzony jest obiekt CAT o nazwie mruczek. Wypisywany jest jego wiek, po czym w
linii 50. wiek Mruczka jest ustawiany na 6 lat. W linii 52. tworzony jest nowy obiekt klasy CAT,
tym razem o nazwie filemon. Jest on tworzony za pomocą konstruktora kopiującego, któremu
przekazano obiekt mruczek. Gdyby mruczek został przekazany do funkcji przez wartość (nie
przez referencję), wtedy kompilator użyłby tego samego konstruktora kopii.
W liniach 53. i 54. jest wypisywany wiek Mruczka i Filemona. Oczywiście, wiek Filemona jest
taki sam, jak wiek Mruczka i wynosi 6 lat, a nie domyślne 5. W linii 56. wiek Mruczka jest
ustawiany na 7 lat, po czym wiek obu obiektów jest wypisywany ponownie. Tym razem Mruczek
ma 7 lat, ale Filemon wciąż ma 6, co dowodzi, że dane tych obiektów są przechowywane w
osobnych miejscach pamięci.
Gdy obiekt klasy CAT wychodzi z zakresu, automatycznie wywoływany jest jego destruktor.
Implementacja destruktora klasy CAT została przedstawiona w liniach od 37. do 43. Dla obu
wskazników, itsAge oraz itsWeight, wywoływany jest operator delete, zwalniający
zaalokowaną dla nich pamięć sterty. Oprócz tego, dla bezpieczeństwa, obu wskaznikom jest
przypisywana wartość NULL.
Przeciążanie operatorów
C++ posiada liczne typy wbudowane, takie jak int, float, char, itd. Każdy z nich posiada
własne wbudowane operatory, takie jak dodawanie (+) czy mnożenie (*). C++ umożliwia
stworzenie takich operatorów także dla klas definiowanych przez użytkownika.
Aby umożliwić pełne poznanie procesu przeciążania operatorów, na listingu 10.6 stworzono nową
klasę o nazwie Counter (licznik). Obiekt typu Counter będzie używany do (uwaga!) zliczania
pętli oraz innych zadań, w których wartość musi być inkrementowana, dekrementowana czy
śledzona w inny sposób.
Listing 10.6. Klasa Counter
0: // Listing 10.6
1: // Klasa Counter
2:
3: #include
4: using namespace std;
5:
6: class Counter
7: {
8: public:
9: Counter();
10: ~Counter(){}
11: int GetItsVal()const { return itsVal; }
12: void SetItsVal(int x) {itsVal = x; }
13:
14: private:
15: int itsVal;
16:
17: };
18:
19: Counter::Counter():
20: itsVal(0)
21: {}
22:
23: int main()
24: {
25: Counter i;
26: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
27: return 0;
28: }
Wynik
Wartoscia i jest 0
Analiza
W obecnej postaci klasa Counter jest raczej bezużyteczna. Sama klasa jest zdefiniowana w
liniach od 6. do 17. Jej jedyną zmienną składową jest wartość typu int. Domyślny konstruktor,
zadeklarowany w linii 9. i zaimplementowany w linii 19., inicjalizuje jedyną zmienną składową,
itsVal (jego wartość), wartością zero.
W odróżnieniu od wbudowanego typu int, obiekt klasy Counter nie może być inkrementowany,
dekrementowany, dodawany, przypisywany, nie można też nim operować w inny sposób. Sprawia
za to, że wypisywanie jego wartości staje się jeszcze bardziej skomplikowane!
Pisanie funkcji inkrementacji
Dzięki przeciążeniu operatorów możemy odzyskać większość działań, których klasa ta została
pozbawiona. Istnieją na przykład dwa sposoby uzupełnienia obiektu Counter o inkrementację.
Pierwszy z nich polega na napisaniu metody do inkrementacji, zobaczymy to na listingu 10.7.
Listing 10.7. Dodawanie operatora inkrementacji
0: // Listing 10.7
1: // Klasa Counter
2:
3: #include
4: using namespace std;
5:
6: class Counter
7: {
8: public:
9: Counter();
10: ~Counter(){}
11: int GetItsVal()const { return itsVal; }
12: void SetItsVal(int x) {itsVal = x; }
13: void Increment() { ++itsVal; }
14:
15: private:
16: int itsVal;
17:
18: };
19:
20: Counter::Counter():
21: itsVal(0)
22: {}
23:
24: int main()
25: {
26: Counter i;
27: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
28: i.Increment();
29: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
30: return 0;
31: }
Wynik
Wartoscia i jest 0
Wartoscia i jest 1
Analiza
Listing 10.7 zawiera nową funkcję Increment() (inkrementuj), zdefiniowaną w linii 13. Choć ta
funkcja działa poprawnie, jest jednak nieco kłopotliwa w użyciu. Program aż prosi się o
uzupełnienie go o operator ++, co oczywiście możemy zrobić.
Przeciążanie operatora przedrostkowego
Operatory przedrostkowe można przeciążyć, deklarując funkcję o postaci:
zwracanyTyp operator op()
gdzie op jest przeciążanym operatorem. Operator ++ można przeciążyć, pisząc:
void operator++ ()
Tę alternatywę demonstruje listing 10.8.
Listing 10.8. Przeciążanie operatora++
0: // Listing 10.8
1: // Klasa Counter
2: // przedrostkowy operator inkrementacji
3:
4: #include
5: using namespace std;
6:
7: class Counter
8: {
9: public:
10: Counter();
11: ~Counter(){}
12: int GetItsVal()const { return itsVal; }
13: void SetItsVal(int x) {itsVal = x; }
14: void Increment() { ++itsVal; }
15: void operator++ () { ++itsVal; }
16:
17: private:
18: int itsVal;
19:
20: };
21:
22: Counter::Counter():
23: itsVal(0)
24: {}
25:
26: int main()
27: {
28: Counter i;
29: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
30: i.Increment();
31: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
32: ++i;
33: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
34: return 0;
35: }
Wynik
Wartoscia i jest 0
Wartoscia i jest 1
Wartoscia i jest 2
Analiza
W linii 15. został przeciążony operator++, który jest używany w linii 32. Jego składnia jest już
zbliżona do składni typów wbudowanych, takich jak int. Teraz możesz wziąć pod uwagę
wykonywanie podstawowych zadań, dla których została stworzona klasa Counter (na przykład
wykrywanie sytuacji, w której licznik przekracza największą wartość).
Jednak w zapisie operatora inkrementacji tkwi poważny defekt. Jeśli umieścisz obiekt typu
Counter po prawej stronie przypisania, kompilator zgłosi błąd. Na przykład:
Counter a = ++i;
W tym przykładzie mieliśmy zamiar stworzyć nowy obiekt a należący do klasy Counter, a
następnie, po inkrementacji tej zmiennej, przypisać mu wartość i. To przypisanie obsłużyłby
wbudowany konstruktor kopiujący, ale wykorzystywany obecnie operator inkrementacji nie
zwraca obiektu typu Counter. Zamiast tego zwraca typ void. Nie można przypisywać obiektów
void obiektom Counter. (Z pustego i Salomon nie naleje!)
Zwracanie typów w przeciążonych funkcjach
operatorów
To, czego nam teraz potrzeba, to zwrócenie obiektu klasy Counter, który mógłby być przypisany
innemu obiektowi tej klasy. Który z obiektów powinien zostać zwrócony? Jednym z rozwiązań
jest stworzenie obiektu tymczasowego i zwrócenie go. Pokazuje to listing 10.9.
Listing 10.9. Zwracanie obiektu tymczasowego
0: // Listing 10.9
1: // operator++ zwraca tymczasowy obiekt
2:
3: #include
4:
5: using namespace std;
6:
7: class Counter
8: {
9: public:
10: Counter();
11: ~Counter(){}
12: int GetItsVal()const { return itsVal; }
13: void SetItsVal(int x) {itsVal = x; }
14: void Increment() { ++itsVal; }
15: Counter operator++ ();
16:
17: private:
18: int itsVal;
19:
20: };
21:
22: Counter::Counter():
23: itsVal(0)
24: {}
25:
26: Counter Counter::operator++()
27: {
28: ++itsVal;
29: Counter temp;
30: temp.SetItsVal(itsVal);
31: return temp;
32: }
33:
34: int main()
35: {
36: Counter i;
37: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
38: i.Increment();
39: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
40: ++i;
41: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
42: Counter a = ++i;
43: cout << "Wartoscia a jest: " << a.GetItsVal();
44: cout << " , a wartosc i to: " << i.GetItsVal() << endl;
45: return 0;
46: }
Wynik
Wartoscia i jest 0
Wartoscia i jest 1
Wartoscia i jest 2
Wartoscia a jest: 3 , a wartosc i to: 3
Analiza
W tej wersji operator++ został zadeklarowany w linii 15. jako zwracający obiekt typu Counter.
W linii 29. jest tworzona zmienna tymczasowa temp, której wartość jest ustawiana zgodnie z
wartością bieżącego obiektu. Ta tymczasowa wartość jest zwracana i natychmiast przypisywana
zmiennej a w linii 42.
Zwracanie obiektów tymczasowych bez nadawania
im nazw
Nie ma potrzeby nadawania nazwy obiektowi tymczasowemu tworzonemu w linii 29. Gdyby klasa
Counter miała konstruktor przyjmujący wartość, jako wartość zwrotną operatora inkrementacji
moglibyśmy po prostu zwrócić wynik tego konstruktora. Pokazuje to listing 10.10.
Listing 10.10. Zwracanie obiektu tymczasowego bez nadawania mu nazwy
0: // Listing 10.10
1: // operator++ zwraca tymczasowy obiekt bez nazwy
2:
3: #include
4:
5: using namespace std;
6:
7: class Counter
8: {
9: public:
10: Counter();
11: Counter(int val);
12: ~Counter(){}
13: int GetItsVal()const { return itsVal; }
14: void SetItsVal(int x) {itsVal = x; }
15: void Increment() { ++itsVal; }
16: Counter operator++ ();
17:
18: private:
19: int itsVal;
20:
21: };
22:
23: Counter::Counter():
24: itsVal(0)
25: {}
26:
27: Counter::Counter(int val):
28: itsVal(val)
29: {}
30:
31: Counter Counter::operator++()
32: {
33: ++itsVal;
34: return Counter (itsVal);
35: }
36:
37: int main()
38: {
39: Counter i;
40: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
41: i.Increment();
42: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
43: ++i;
44: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
45: Counter a = ++i;
46: cout << "Wartoscia a jest: " << a.GetItsVal();
47: cout << ", zas wartosc i to: " << i.GetItsVal() << endl;
48: return 0;
49: }
Wynik
Wartoscia i jest 0
Wartoscia i jest 1
Wartoscia i jest 2
Wartoscia a jest: 3, zas wartosc i to: 3
Analiza
W linii 11. został zadeklarowany nowy konstruktor, przyjmujący wartość typu int. Jego
implementacja znajduje się w liniach od 27. do 29.; inicjalizuje ona zmienną składową itsVal za
pomocą wartości otrzymanej jako argument konstruktora.
Implementacja operatora++ może zostać teraz uproszczona. W linii 33. wartość itsVal jest
inkrementowana. Następnie, w linii 34., tworzony jest tymczasowy obiekt klasy Counter, który
jest inicjalizowany wartością zmiennej itsVal, po czym zwracany jako rezultat operatora++.
To rozwiązanie jest bardziej eleganckie, ale powoduje, że musimy zadać następne pytanie:
dlaczego w ogólne musimy tworzyć obiekt tymczasowy? Pamiętajmy, że każdy obiekt
tymczasowy musi zostać najpierw skonstruowany, a pózniej zniszczony te operacje mogą być
potencjalnie dość kosztowne. Poza tym, obiekt już istnieje i posiada właściwą wartość, więc
dlaczego nie mielibyśmy zwrócić właśnie jego? Rozwiążemy ten problem, używając wskaznika
this.
Użycie wskaznika this
Wskaznik this jest przekazywany wszystkim funkcjom składowym, nawet przeciążonym
operatorom, takim jak operator++(). Wskaznik this wskazuje na i, więc gdy zostanie
wyłuskany, zwróci tylko obiekt i, który w swojej zmiennej itsVal zawiera już właściwą wartość.
Zwracanie wyłuskanego wskaznika this i zaniechanie tworzenia niepotrzebnego obiektu
tymczasowego przedstawia listing 10.11.
Listing 10.11. Zwracanie wskaznika this
0: // Listing 10.11
1: // Zwracanie wyłuskanego wskaznika this
2:
3: #include
4:
5: using namespace std;
6:
7: class Counter
8: {
9: public:
10: Counter();
11: ~Counter(){}
12: int GetItsVal()const { return itsVal; }
13: void SetItsVal(int x) {itsVal = x; }
14: void Increment() { ++itsVal; }
15: const Counter& operator++ ();
16:
17: private:
18: int itsVal;
19:
20: };
21:
22: Counter::Counter():
23: itsVal(0)
24: {};
25:
26: const Counter& Counter::operator++()
27: {
28: ++itsVal;
29: return *this;
30: }
31:
32: int main()
33: {
34: Counter i;
35: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
36: i.Increment();
37: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
38: ++i;
39: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
40: Counter a = ++i;
41: cout << "Wartoscia a jest: " << a.GetItsVal();
42: cout << ", zas wartosc i to: " << i.GetItsVal() << endl;
43: return 0;
44: }
Wynik
Wartoscia i jest 0
Wartoscia i jest 1
Wartoscia i jest 2
Wartoscia a jest: 3, zas wartosc i to: 3
Analiza
Implementacja operatora++, zawarta w liniach od 26. do 30., została zmieniona w taki sposób,
aby wyłuskiwała wskaznik this i zwracała bieżący obiekt. Dzięki temu zmiennej a może być
przypisany bieżący egzemplarz klasy Counter. Jak wspomnieliśmy wcześniej, gdyby obiekt klasy
Counter alokował pamięć, należałoby przysłonić domyślny konstruktor kopiujący. Jednak w tym
przypadku domyślny konstruktor kopiujący działa poprawnie.
Zwróć uwagę, że zwracaną wartością jest referencja do obiektu klasy Counter, dzięki czemu
unikamy tworzenia dodatkowego obiektu tymczasowego. Jest to zmienna const, ponieważ nie
powinna być modyfikowana przez funkcję wykorzystującą zwracany obiekt klasy Counter.
Dlaczego stała referencja?
Zwracany obiekt Counter musi być obiektem const. Gdyby nim nie był, można by wykonać na
zwracanym obiekcie operacje, które mogłyby zmienić jego dane składowe. Na przykład, gdyby
zwracana wartość nie była stała, mógłbyś napisać:
40: Counter a = ++++i;
Można to rozumieć jako wywołanie operatora inkrementacji (++) na wyniku wywołania operatora
inkrementacji, opcja ta powinno być zablokowane.
Spróbuj wykonać taki eksperyment: zarówno w deklaracji, jak i w implementacji (linie 15. i 26.)
zmień zwracaną wartość na wartość nie będącą const, po czym zmień linię 40. na pokazaną
powyżej (++++i). Umieść punkt przerwania w debuggerze na linii 40. i wejdz do funkcji.
Zobaczysz, że do operatora inkrementacji wejdziesz dwa razy. Inkrementacja zostanie
zastosowana do (teraz nie będącej stałą) wartości zwracanej.
Aby się przed tym zabezpieczyć, deklarujemy wartość zwracaną jako const. Gdy zmienisz linie
15. i 26. z powrotem na stałe, zaś linię 40. pozostawisz bez zmian (++++i), kompilator
zaprotestuje przeciwko wywołaniu operatora inkrementacji dla obiektu stałego.
Przeciążanie operatora przyrostkowego
Jak dotąd, udało się nam przeciążyć operator przedrostkowy. A co zrobić, gdy chcemy przeciążyć
operator przyrostkowy? Kompilator nie potrafi odróżnić przedrostka od przyrostka. Zgodnie z
konwencją, jako parametr deklaracji operatora dostarczana jest zmienna całkowita. Wartość
parametru jest ignorowana; sygnalizuje on tylko, że jest to operator przyrostkowy.
Różnica pomiędzy przedrostkiem a przyrostkiem
Zanim będziemy mogli napisać operator przyrostkowy, musimy zrozumieć, czym różni się on od
operatora przedrostkowego. Omawialiśmy to szczegółowo w rozdziale 4., Wyrażenia i
instrukcje . (Patrz listing 4.3.).
Przypomnijmy: przedrostek mówi: Inkrementuj, po czym pobierz , zaś przyrostek mówi:
Pobierz, a następnie inkrementuj .
Operator przedrostkowy może po prostu inkrementować wartość, a następnie zwrócić sam obiekt,
zaś operator przyrostkowy musi zwracać wartość istniejącą przed dokonaniem inkrementacji. W
tym celu musimy stworzyć obiekt tymczasowy, który będzie zawierał pierwotną wartość,
następnie inkrementować wartość pierwotnego obiektu, po czy, zwrócić obiekt tymczasowy.
Przyjrzyjmy się temu procesowi od początku. Wezmy następującą linię kodu:
a = x++;
Jeśli x miało wartość 5, wtedy po wykonaniu tej instrukcji a ma wartość 5, zaś x ma wartość 6.
Zwracamy wartość w x i przypisujemy ją zmiennej a, po czym inkrementujemy wartość x. Jeśli x
jest obiektem, jego przyrostkowy operator inkrementacji musi zachować pierwotną wartość (5) w
obiekcie tymczasowym, inkrementować wartość x do 6, po czym zwrócić obiekt tymczasowy w
celu przypisania oryginalnej wartości do zmiennej a.
Zwróć uwagę, że skoro zwracamy obiekt tymczasowy, musimy zwracać go poprzez wartość, a nie
poprzez referencję (w przeciwnym razie obiekt ten znajdzie się poza zakresem natychmiast po
wyjściu programu z funkcji).
Listing 10.12 przedstawia użycie operatora przedrostkowego i przyrostkowego.
Listing 10.12. Operator przedrostkowy i przyrostkowy
0: // Listing 10.12
1: // Operator przedrostkowy i przyrostkowy
2:
3: #include
4:
5: using namespace std;
6:
7: class Counter
8: {
9: public:
10: Counter();
11: ~Counter(){}
12: int GetItsVal()const { return itsVal; }
13: void SetItsVal(int x) {itsVal = x; }
14: const Counter& operator++ (); // przedrostkowy
15: const Counter operator++ (int); // przyrostkowy
16:
17: private:
18: int itsVal;
19: };
20:
21: Counter::Counter():
22: itsVal(0)
23: {}
24:
25: const Counter& Counter::operator++()
26: {
27: ++itsVal;
28: return *this;
29: }
30:
31: const Counter Counter::operator++(int theFlag)
32: {
33: Counter temp(*this);
34: ++itsVal;
35: return temp;
36: }
37:
38: int main()
39: {
40: Counter i;
41: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
42: i++;
43: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
44: ++i;
45: cout << "Wartoscia i jest " << i.GetItsVal() << endl;
46: Counter a = ++i;
47: cout << "Wartoscia a jest: " << a.GetItsVal();
48: cout << ", zas wartosc i to: " << i.GetItsVal() << endl;
49: a = i++;
50: cout << "Wartoscia a jest: " << a.GetItsVal();
51: cout << ", zas wartosc i to: " << i.GetItsVal() << endl;
52: return 0;
53: }
Wynik
Wartoscia i jest 0
Wartoscia i jest 1
Wartoscia i jest 2
Wartoscia a jest: 3, zas wartosc i to: 3
Wartoscia a jest: 3, zas wartosc i to: 4
Analiza
Operator przyrostkowy jest deklarowany w linii 15. i implementowany w liniach od 31. do 36.
Operator przedrostkowy jest deklarowany w linii 14.
Parametr przekazywany do operatora przyrostkowego w linii 32. (theFlag) sygnalizuje jedynie
kompilatorowi, że chodzi tu o operator przyrostkowy; wartość tego parametru nigdy nie jest
wykorzytywana.
Operator dodawania
Operator inkrementacji jest operatorem unarnym, tj. operatorem działającym na tylko jednym
obiekcie. Operator dodawania (+) jest operatorem binarnym, co oznacza, że do działania
potrzebuje dwóch obiektów. W jaki więc sposób można zaimplementować przeciążenie operatora
+ dla klasy Counter?
Naszym celem jest zadeklarowanie dwóch zmiennych typu Counter, a następnie dodanie ich, tak
jak w poniższym przykładzie:
Counter varOne, varTwo, varThree;
varThree = varOne + varTwo;
Także w tym przypadku mógłbyś zacząć od napisania funkcji Add() (dodaj), która jako argument
przyjmowałaby obiekt klasy Counter, dodawałaby wartości, po czym zwracałaby obiekt klasy
Counter jako wynik. Takie postępowanie ilustruje listing 10.13.
Listing 10.13. Funkcja Add()
0: // Listing 10.13
1: // Funkcja Add
2:
3: #include
4:
5: using namespace std;
6:
7: class Counter
8: {
9: public:
10: Counter();
11: Counter(int initialValue);
12: ~Counter(){}
13: int GetItsVal()const { return itsVal; }
14: void SetItsVal(int x) {itsVal = x; }
15: Counter Add(const Counter &);
16:
17: private:
18: int itsVal;
19:
20: };
21:
22: Counter::Counter(int initialValue):
23: itsVal(initialValue)
24: {}
25:
26: Counter::Counter():
27: itsVal(0)
28: {}
29:
30: Counter Counter::Add(const Counter & rhs)
31: {
32: return Counter(itsVal+ rhs.GetItsVal());
33: }
34:
35: int main()
36: {
37: Counter varOne(2), varTwo(4), varThree;
38: varThree = varOne.Add(varTwo);
39: cout << "varOne: " << varOne.GetItsVal()<< endl;
40: cout << "varTwo: " << varTwo.GetItsVal() << endl;
41: cout << "varThree: " << varThree.GetItsVal() << endl;
42:
43: return 0;
44: }
Wynik
varOne: 2
varTwo: 4
varThree: 6
Analiza
Funkcja Add() została zadeklarowana w linii 15. Otrzymuje ona stałą referencję do obiektu klasy
Counter, który zawiera wartość przeznaczoną do dodania do wartości w bieżącym obiekcie.
Zwraca obiekt klasy Counter, który jest przypisywany lewej stronie instrukcji przypisania w linii
38. Innymi słowy, varOne jest obiektem, varTwo jest parametrem funkcji Add(), zaś wynik tej
funkcji jest przypisywany do varThree.
Aby stworzyć varThree bez inicjalizowania wartości tego obiektu, potrzebny jest konstruktor
domyślny. Ten konstruktor inicjalizuje zmienną składową itsVal jako zero, co pokazują linie od
26. do 28. Ponieważ zmienne varOne i varTwo powinny być zainicjalizowane wartościami
różnymi od zera, został stworzony kolejny konstruktor, znajdujący się w liniach od 22. do 24.
Innym rozwiązaniem tego problemu jest zastosowanie wartości domyślnej 0 w konstruktorze
zadeklarowanym w linii 11.
Przeciążanie operatora dodawania
Funkcja Add() znajduje się w liniach od 30. do 33. listingu 10.13. Funkcja działa poprawnie, ale
jej użycie jest mało naturalne. Przeciążenie operatora + spowoduje, że użycie klasy Counter
będzie mogło przebiegać bardziej naturalnie. Pokazuje to listing 10.14.
Listing 10.14. operator+
0: // Listing 10.14
1: //Przeciążony operator dodawania (+)
2:
3: #include
4:
5: using namespace std;
6:
7: class Counter
8: {
9: public:
10: Counter();
11: Counter(int initialValue);
12: ~Counter(){}
13: int GetItsVal()const { return itsVal; }
14: void SetItsVal(int x) {itsVal = x; }
15: Counter operator+ (const Counter &);
16: private:
17: int itsVal;
18: };
19:
20: Counter::Counter(int initialValue):
21: itsVal(initialValue)
22: {}
23:
24: Counter::Counter():
25: itsVal(0)
26: {}
27:
28: Counter Counter::operator+ (const Counter & rhs)
29: {
30: return Counter(itsVal + rhs.GetItsVal());
31: }
32:
33: int main()
34: {
35: Counter varOne(2), varTwo(4), varThree;
36: varThree = varOne + varTwo;
37: cout << "varOne: " << varOne.GetItsVal()<< endl;
38: cout << "varTwo: " << varTwo.GetItsVal() << endl;
39: cout << "varThree: " << varThree.GetItsVal() << endl;
40:
41: return 0;
42: }
Wynik
varOne: 2
varTwo: 4
varThree: 6
Analiza
operator+ został zadeklarowany w linii 15., a jego implementacja znajduje się w liniach od 28.
do 31.
Porównaj go z deklaracją i definicją funkcji Add() w poprzednim listingu: są prawie identyczne.
Jednak ich składnia jest całkiem inna. Bardziej naturalny jest zapis:
varThree = varOne + varTwo;
niż zapis:
varThree = varOne.Add(varTwo);
Nie jest to duża zmiana, ale na tyle ważna, by program stał się łatwiejszy do odczytania i
zrozumienia.
Operator jest wykorzystywany w linii 36:
36: varThree = varOne + varTwo;
Kompilator tłumaczy ten zapis na:
varThree = varOne.operator+(varTwo);
Mógłbyś oczywiście napisać tę linię sam, a kompilator zaakceptowałby to bez zastrzeżeń.
Metoda operator+ jest wywoływana dla operandu po lewej stronie, a jako argument otrzymuje
operand po prawej stronie.
Zagadnienia związane z przeciążaniem operatorów
Przeciążane operatory mogą być funkcjami składowymi, takimi, jak opisane w tym rozdziale, lub
funkcjami globalnymi. Te ostatnie zostaną opisane w rozdziale 15., Specjalne klasy i funkcje ,
przy okazji omawiania funkcji zaprzyjaznionych.
Jedynymi operatorami, które muszą być funkcjami składowymi klasy, są operator: przypisania (=),
indeksu tablicy ([]), wywołania funkcji (()) oraz wskaznika (->).
Operator [] zostanie omówiony w rozdziale 13., przy okazji omawiania tablic. Przeciążanie
operatora -> zostanie omówione w rozdziale 15., przy okazji omawiania wskazników
inteligentnych.
Ograniczenia w przeciążaniu operatorów
Operatory dla typów wbudowanych (takich jak int) nie mogą być przeciążane. Nie można
zmieniać priorytetu operatorów ani ilości operandów operatora, tj. operator unarny nie może stać
się operatorem binarnym i na odwrót. Nie można także tworzyć nowych operatorów, dlatego
niemożliwe jest zadeklarowanie symbolu ** jako operatora podnoszenia do potęgi.
Niektóre operatory C++ są operatorami unarnymi i wymagają tylko jednego operandu (na
przykład myValue++). Inne operatory są binarne, czyli wymagają dwóch operandów (na przykład
a+b). W C++ istnieje tylko jeden operator trójargumentowy: operator ? (na przykład a > b ? x
: y).
Co przeciążać?
Przeciążanie operatorów jest jednym z najczęściej nadużywanych przez początkujących
programistów aspektów języka C++. Tworzenie nowych zastosowań dla niektórych z mniej
znanych operatorów jest bardzo kuszące, ale niezmiennie prowadzi do tworzenia nieczytelnego
kodu.
Oczywiście, doprowadzenie to tego, by operator + odejmował, a operator * dodawał może być
zabawne, ale żaden profesjonalny programista tego nie zrobi. Duże niebezpieczeństwo kryje się
także w machinalnym użyciu operatora + do łączenia ciągów liter czy operatora / do dzielenia
łańcuchów. Oczywiście, istnieją powody, dla których stosujemy te operatory, ale istnieje jeszcze
więcej powodów, by zachować przy tym dużą ostrożność. Pamiętaj że celem przeciążania
operatorów jest zwiększenie użyteczności i przejrzystości obiektów.
TAK NIE
Stosuj przeciążanie operatorów wtedy, gdy Nie twórz operatorów działających niezgodnie z
zwiększa to przejrzystość programu. przeznaczeniem.
Zwracaj z przeciążanego operatora obiekt jego
klasy.
Operator przypisania
Czwartą, ostatnią funkcją, która jest dostarczana przez kompilator (gdy nie stworzysz jej sam) jest
operator przypisania (operator=()). Ten operator jest wywoływany za każdym razem, gdy
przypisujesz coś do obiektu. Na przykład:
CAT catOne(5,7);
CAT catTwo(3,4);
// ... tutaj inny kod
catTwo = catOne;
W tym fragmencie tworzony jest obiekt catOne; jego zmienna składowa itsAge jest
inicjalizowana wartością 5, a zmienna składowa itsWeight wartością 7. W następnej linii
tworzony jest obiekt catTwo, którego zmienne składowe są inicjalizowane wartościami 3 i 4.
Po jakimś czasie obiektowi catTwo jest przypisywany obiekt catOne. Pojawiają się więc dwa
problemy: co się stanie, gdy zmienna składowa itsAge jest wskaznikiem i co się dzieje z
pierwotnymi wartościami w obiekcie catTwo?
Posługiwanie się zmiennymi składowymi, przechowującymi swoje wartości na stercie, zostało
omówione już wcześniej, podczas omawiania działania konstruktora kopiującego. Te same
zagadnienia odnoszą się także do przedstawionego tutaj przypadku, tak jak pokazano na rysunkach
10.1 i 10.2.
Programiści C++ dokonują rozróżnienia pomiędzy kopiowaniem płytkim, czyli kopiowaniem
składowych klasy, a kopiowaniem głębokim. Przy kopiowaniu płytkim kopiowane są jedynie
składowe, więc oba obiekty wskazują to samo miejsce na stercie. Przy kopiowaniu głębokim na
stercie alokowany jest nowy obszar pamięci. Ilustrował to rysunek 10.3.
Jednak w przypadku operatora przypisania pojawia się kolejny problem. Obiekt catTwo już
istnieje i posiada zaalokowaną pamięć. Jeśli nie chcemy doprowadzić do wycieku pamięci, pamięć
musi zostać zwolniona. Ale co zrobić, gdy przypiszemy obiekt catTwo samemu sobie?
catTwo = catTwo;
Nikt nie ma oczywiście zamiaru tego robić, ale może się to zdarzyć przypadkiem, gdy referencje i
wyłuskane wskazniki ukryją fakt, że przypisanie odnosi się do tego samego obiektu.
Jeśli nie rozwiążesz tego problemu, obiekt catTwo może usunąć swoją zaalokowaną pamięć, po
czym, gdy już będzie gotów do skopiowania pamięci z obiektu po prawej stronie operatora
przypisania, znajdzie się w ogromnym kłopocie: tej wartości już nie będzie!
Aby się przed tym zabezpieczyć, operator przypisania musi sprawdzać, czy operand po prawej
stronie operatora nie jest tym samym obiektem. W tym celu może sprawdzić wskaznik this.
Klasę z przeciążonym operatorem przypisania przedstawia listing 10.15.
Listing 10.15. Operator przypisania
0: // Listing 10.15
1: // Operator przypisania
2:
3: #include
4:
5: using namespace std;
6:
7: class CAT
8: {
9: public:
10: CAT(); // domyślny konstruktor
11: // konstruktor kopiujący oraz destruktor zostały usunięte!
12: int GetAge() const { return *itsAge; }
13: int GetWeight() const { return *itsWeight; }
14: void SetAge(int age) { *itsAge = age; }
15: CAT & operator=(const CAT &);
16:
17: private:
18: int *itsAge;
19: int *itsWeight;
20: };
21:
22: CAT::CAT()
23: {
24: itsAge = new int;
25: itsWeight = new int;
26: *itsAge = 5;
27: *itsWeight = 9;
28: }
29:
30:
31: CAT & CAT::operator=(const CAT & rhs)
32: {
33: if (this == &rhs)
34: return *this;
35: *itsAge = rhs.GetAge();
36: *itsWeight = rhs.GetWeight();
37: return *this;
38: }
39:
40:
41: int main()
42: {
43: CAT mruczek;
44: cout << "Wiek Mruczka: " << mruczek.GetAge() << endl;
45: cout << "Ustawiam wiek Mruczka na 6...\n";
46: mruczek.SetAge(6);
47: CAT filemon;
48: cout << "Wiek Filemona: " << filemon.GetAge() << endl;
49: cout << "Kopiuje Mruczka do Filemona...\n";
50: filemon = mruczek;
51: cout << "Wiek Filemona: " << filemon.GetAge() << endl;
52: return 0;
53: }
Wynik
Wiek Mruczka: 5
Ustawiam wiek Mruczka na 6...
Wiek Filemona: 5
Kopiuje Mruczka do Filemona...
Wiek Filemona: 6
Analiza
Listing 10.15 stanowi powrót do klasy CAT, z której (dla zaoszczędzenia miejsca) usunięto
konstruktor kopiujący oraz destruktor. Operator przypisania jest deklarowany w linii 15., zaś jego
definicja znajduje się w liniach od 31. do 38.
W linii 33. następuje sprawdzenie, czy bieżący obiekt (obiekt CAT, do którego następuje
przypisanie), jest tym samym obiektem, co przypisywany obiekt CAT. Odbywa się to poprzez
sprawdzenie, czy adres rhs jest taki sam, jak adres zawarty we wskazniku this.
Oczywiście, można przeciążyć także operator równości (==), dzięki czemu możesz sam określić
co oznacza równość twoich obiektów.
Obsługa konwersji typów danych
Co się stanie, gdy spróbujesz przypisać zmienną typu wbudowanego, takiego jak int czy
unsigned short, do obiektu klasy zdefiniowanej przez użytkownika? Na listingu 10.16
ponownie skorzystamy z klasy Counter, próbując przypisać obiektowi tej klasy zmienną typu
int.
OSTRZEŻENIE Listing 10.16 nie skompiluje się!
Listing 10.16. Próba przypisania obiektowi typu Counter zmiennej typu int
0: // Listing 10.16
1: // Ten kod nie skompiluje się!
2:
3: #include
4:
5: using namespace std;
6:
7: class Counter
8: {
9: public:
10: Counter();
11: ~Counter(){}
12: int GetItsVal()const { return itsVal; }
13: void SetItsVal(int x) {itsVal = x; }
14: private:
15: int itsVal;
16:
17: };
18:
19: Counter::Counter():
20: itsVal(0)
21: {}
22:
23: int main()
24: {
25: int theShort = 5;
26: Counter theCtr = theShort;
27: cout << "theCtr: " << theCtr.GetItsVal() << endl;
28: return 0;
29: }
Wynik
Błąd kompilacji! Nie można dokonać konwersji z typu int do
typu Counter.
Analiza
Klasa Counter zadeklarowana w liniach od 7. do 17. posiada jedynie konstruktor domyślny. Nie
deklaruje żadnej konkretnej metody zamiany zmiennych typu int w obiekty klasy Counter, więc
linia 26. powoduje błąd kompilacji. Kompilator nie wie, dopóki go o tym nie poinformujesz, że
mając zmienną typu int, powinien przypisać jej wartość do zmiennej składowej itsVal.
Listing 10.17 poprawia ten błąd, tworząc operator konwersji: konstruktor przyjmuje wartość typu
int i tworzy obiekt klasy Counter.
Listing 10.17. Konwersja typu int na typ Counter
0: // Listing 10.17
1: // Konstruktor jako operator konwersji
2:
3: #include
4:
5: using namespace std;
6:
7: class Counter
8: {
9: public:
10: Counter();
11: Counter(int val);
12: ~Counter(){}
13: int GetItsVal()const { return itsVal; }
14: void SetItsVal(int x) {itsVal = x; }
15: private:
16: int itsVal;
17:
18: };
19:
20: Counter::Counter():
21: itsVal(0)
22: {}
23:
24: Counter::Counter(int val):
25: itsVal(val)
26: {}
27:
28:
29: int main()
30: {
31: int theShort = 5;
32: Counter theCtr = theShort;
33: cout << "theCtr: " << theCtr.GetItsVal() << endl;
34: return 0;
35: }
Wynik
theCtr: 5
Analiza
Ważna zmiana pojawia się w linii 11., gdzie deklarowany jest przeciążony konstruktor,
przyjmujący wartość typu int, oraz w liniach od 24. do 26., gdzie konstruktor ten jest
implementowany. Efektem jego działania jest stworzenie z obiektu typu int obiektu typu
Counter.
Na tej podstawie kompilator może wywołać konstruktor, którego argumentem jest wartość int.
Krok 1: Tworzymy licznik o nazwie theCtr.
Odpowiada to zapisowi x = 5;, który tworzy zmienną całkowitą x i inicjalizuje ją wartością 5. W
naszym przypadku tworzymy obiekt klasy Counter o nazwie theCtr i inicjalizujemy go zmienną
całkowitą theShort.
Krok 2: Przypisujemy obiektowi theCtr wartość zmiennej theShort.
Zmienna theShort jest zmienną całkowitą, a nie zmienną typu Counter! Najpierw musimy
przekonwertować ją do typu Counter. Kompilator spróbuje dokonać dla nas pewnych konwersji
automatycznie, ale musimy go tego nauczyć. Osiągniemy to poprzez stworzenie dla klasy
Counter konstruktora, którego jedynym parametrem jest wartość całkowita:
class Counter
{
Counter(int val);
// ...
};
Konstruktor tworzy obiekty typu Counter na podstawie wartości typu int. Aby tego dokonać,
tworzy tymczasowy, pozbawiony nazwy obiekt klasy Counter. Dla zilustrowania tego przykładu
przypuśćmy, że ten tymczasowy obiekt typu Counter, tworzony ze zmiennej typu int, ma nazwę
wasShort.
Krok 3: Przypisujemy wasShort do theCtr, co odpowiada zapisowi:
*theCtr = wasShort;
W tym kroku wasShort (tymczasowy obiekt stworzony podczas działania konstruktora) jest
zastępowany zapisem znajdującym się po prawej stronie operatora przypisania. Teraz, gdy
kompilator potrafi stworzyć dla nas obiekt tymczasowy, może zainicjalizować nim zmienną
theCtr.
Aby zrozumieć ten proces, musisz uświadomić sobie, że wszystkie przeciążenia operatorów
działają w ten sam sposób deklarujesz przeciążony operator, używając słowa kluczowego
operator. W przypadku operatorów binarnych (takich jak = czy +), parametrem operatora staje
się zmienna położona po jego prawej stronie. Jest to zapewniane przez kompilator. Tak więc:
a = b;
staje się
a.operator=(b);
Co się jednak stanie, gdy spróbujesz odwrócić przypisanie:
0: Counter theCtr(5);
1: int theShort = theCtr;
2: cout << "theShort: " << theShort << endl;
Znów wystąpi błąd kompilacji. Choć kompilator wie już, w jaki sposób stworzyć obiekt typu
Counter z wartości typu int, nie ma pojęcia, jak odwrócić ten proces.
Operatory konwersji
Aby rozwiązać ten i podobne problemy, język C++ dostarcza operatorów konwersji, które mogą
być dodawane do tworzonych klas. Dzięki temu klasa może jawnie określić, w jaki sposób ma być
dokonywana konwersja do typów wbudowanych. Pokazuje to listing 10.18. Zwróć uwagę, że
operatory konwersji nie określają zwracanej wartości, mimo, iż w efekcie zwracają wartość
przekonwertowaną.
Listing 10.18. Konwersja z typu Counter na typ unsigned short()
0: // Listing 10.18 - Operatory konwersji
1:
2: #include
3:
4: class Counter
5: {
6: public:
7: Counter();
8: Counter(int val);
9: ~Counter(){}
10: int GetItsVal()const { return itsVal; }
11: void SetItsVal(int x) {itsVal = x; }
12: operator unsigned short();
13: private:
14: int itsVal;
15:
16: };
17:
18: Counter::Counter():
19: itsVal(0)
20: {}
21:
22: Counter::Counter(int val):
23: itsVal(val)
24: {}
25:
26: Counter::operator unsigned short ()
27: {
28: return ( int (itsVal) );
29: }
30:
31: int main()
32: {
33: Counter ctr(5);
34: int theShort = ctr;
35: std::cout << "theShort: " << theShort << std::endl;
36: return 0;
37: }
Wynik
theShort: 5
Analiza
W linii 12. deklarowany jest operator konwersji. Zwróć uwagę, że nie posiada on wartości
zwrotnej. Implementacja tej funkcji znajduje się w liniach od 26. do 29. Linia 28. zwraca wartość
itsVal, przekonwertowaną na wartość typu int.
Teraz kompilator wie już, w jaki sposób zamieniać wartości typu int w obiekty typu Counter i
odwrotnie. Dzięki temu można je sobie wzajemnie przypisywać.
Wyszukiwarka
Podobne podstrony:
Rozdział 10
03 Rozdzial 10 13
Zadania do rozdzialu 10
Rozdział2 (10)
rozdzial (10)
rozdzial (10)
10 Funkcje wykładnicze i logarytmiczne, zadania powtórzeniowe przed maturą
S Johansson, Origins of language (rozdział 10)
Dom Nocy 09 Przeznaczona rozdział 10 11 TŁUMACZENIE OFICJALNE
rozdzial (10)
więcej podobnych podstron