• Nie Znaleziono Wyników

Przeciążanie operatora przypisania ( = )

W dokumencie Język C++ – podstawy programowania (Stron 175-183)

operator == ( wektor3d v , wektor3d w)

{ i f ( ( v . x == w . x ) && ( v . y == w . y ) && ( v . z == w . z ) ) return 1 ;

e l s e return 0 ; }

operator != ( wektor3d v , wektor3d w) { return ! ( v == w) ; }

Widzimy, że operator przeciążony może być zdefiniowany jako funkcja skła-dowa i jako funkcja globalna (lub zaprzyjaźniona). Wobec tego musimy od-powiedzieć na pytanie, jakie są kryteria wyboru implementacji przeciążania operatora. Prawdę mówiąc nie ma generalnej zasady, wszystko zależy od zastosowania przeciążonego operatora.

Jeżeli operator modyfikuje operandy to powinien być zdefinio-wany jako funkcja składowa klasy. Przykładem są tu takie opera-tory jak: =, +=, -=, *=, ++, itp. Jeżeli operator nie modyfikuje swoich operandów to należy go definiować jako funkcję globalną lub zaprzyjaźnioną. Przykładem są tu takie operatory jak: +, -,

==, &, itp.

7.7. Przeciążanie operatora przypisania ( = )

Operator przypisania jest szczególnym operatorem, ponieważ w przy-padku, gdy nie zostanie przeciążony, jest on definiowany przez kompilator.

Tak więc, operacja przypisani jednego obiektu drugiemu jest zawsze wyko-nalna. Ilustruje ten fakt kolejny program. W naszym przykładzie została zaimplementowana klasa data:

c l a s s data {

private: i n t d z i e n ; i n t m i e s i a c ; i n t rok ; public:

data (i n t = 1 , i n t = 1 , i n t = 2004 ) ; // k o n s t r u k t o r void pokaz (void) ; // f u n k c j a skladowa , p o k a z u j e d a t e } ;

Ta klasa nie zawiera funkcji operatorowej przypisania. W funkcji głównej main() mamy instrukcję:

d1 = d2 ;

166 7. Przeciążanie operatorów co oznacz, że odpowiednie pola obiektu d2 są przypisane polom obiektu d1.

Ten typ przypisania nosi nazwę przypisania danych składowych (member-wise assignment).

Listing 7.8. przypisanie bez przeciążania operatorów

1#include <i o s t r e a m . h>

Po uruchomieniu programu otrzymujemy komunikat:

p i e r w s z a data , d1 : 13/03/2003 druga data d2 : 15/05/2004

7.7. Przeciążanie operatora przypisania ( = ) 167

po p o d s t a w i e n i u d1 : 15/05/2005

Jeżeli mamy proste przypisanie, tak jak to pokazano powyżej, wygenerowane przez kompilator przeciążenie jest wystarczające, jednak w bardziej skom-plikowanych przypadkach (np. gdy chcemy mieć wielokrotne przypisanie typu: d1 = d2 = d3) musimy zaprojektować odpowiednią funkcję operato-rową. W pokazanym programie wykorzystano przeciążenie operatora przy-pisania wygenerowanego przez kompilator. Plik nagłówkowy <iomanip.h>

jest potrzebny, ponieważ w programie wykonywane są formatowane operacje wyjścia z tak zwanymi parametryzowanymi manipulatorami strumienia. Do ustawienia szerokości pola wydruku zastosowano manipulator strumienia setw(), manipulator setfill() określa znak wypełnienia pola. Jak mówiliśmy, operator przypisania ( = ) jest szczególnym operatorem, podobnie jak ope-rator pobrania adresu ( & ) i przecinkowy ( , ) mają predefiniowane zna-czenie w odniesieniu do obiektów klas. Oczywiście możemy, przestrzegając odpowiednich reguł przeciążyć operator przypisania. Deklaracja prostego operatora przypisania może mieć postać:

void operator = ( nazwa_klasy & )

Słowo kluczowe void wskazuje, że przypisanie nie zwróci żadnej wartości, napis operator = wskazuje, że przeciążamy operator przypisania, a nazwa klasy i znak & wewnątrz nawiasów okrągłych wskazują, że argumentem operatora jest referencja do klasy. Na przykład, aby zadeklarować operator przypisania dla naszej klasy data, możemy użyć deklaracji:

void operator = ( data &) ;

Implementacja funkcji operatorowej może mieć postać:

void Data : : operator= ( Data & d ) {

d z i e n = d . d z i e n ; m i e s i a c = d . m i e s i a c ; rok = d . rok ;

}

W definicji operatora zastosowano referencję. W tej definicji d jest zdefinio-wane jako referencja do klasy Data. W ciele funkcji operatorowej składowa dzien obiektu d jest przypisana składowej dzien aktualnego obiektu:

d z i e n = d . d z i e n ;

Ta sama operacja powtórzona jest dla składowych miesiac i rok. Przypisanie typu:

168 7. Przeciążanie operatorów

a .operator=(b ) ;

może być zastosowane do wywołania przeciążonego operatora przypisania i przypisania wartości składowych obiektu b do obiektu a. W tej sytuacji zapis a.operator=(b) może być zastąpiony wygodnym zapisem a = b;.

Listing 7.9. przypisanie; przeciążania operatorów

1#include <i o s t r e a m >

void operator= ( Data &) ;

11 } ;

7.7. Przeciążanie operatora przypisania ( = ) 169 Po uruchomieniu tego programu mamy wydruk:

data nr 1 : 31/12/04 data nr 2 : 01/01/05 P r z y p i s a n i e dat : data nr 1 : 01/01/05

Funkcja operatora przypisania zaprezentowana w tym przykładzie, aczkol-wiek poprawna, nie obsłuży poprawnie próby przypisania wielokrotnego po-staci:

a = b = c ;

Dzieje się tak, ponieważ powyższy zapis interpretowany jest jako:

a = ( b = c ) ;

Zgodnie z definicją, nasza funkcja operatorowa nie zwraca żadnej wartości, wobec tego po wykonaniu przypisania b = c, żadna wartość nie będzie zwró-cona i nic nie możemy przypisać do a. W celu wykonywania wielokrotnego przypisania, potrzebna jest funkcja operatorowa zwracająca referencję do klasy swojego typu. Zanim pokażemy implementację przeciążonego opera-tora przypisania wielokrotnego, przypomnimy znaczenie słowa kluczowego this. Funkcje składowe są związane z definicja klasy, a nie z deklaracjami obiektów tej klasy. Za każdym razem, gdy obiekt jest kreowany, odrębne obszary pamięci są przydzielane do przechowywania danych składowych. W naszym przykładzie kreowane są dwa obiekty klasy Data – d1 i d2. Organi-zacja przechowywanych w pamięci danych pokazana jest na rysunku 7.1.

Jak widać na rysunku, każdy zbiór danych ma inny początkowy adres w pamięci, który jest adresem pierwszej danej składowej obiektu. Tego typu kopiowanie danych składowych nie odnosi się do funkcji składowych. Istnieje tylko jeden egzemplarz kodu definicji danej funkcji składowej. Każdy obiekt używa tych samych funkcji. Ponieważ jedna funkcja składowa musi obsłu-żyć wiele obiektów, musi istnieć sposób identyfikacji danych poszczególnych obiektów. Rozwiązane jest to w ten sposób, że do funkcji przekazywany jest adres wskazujący gdzie w pamięci znajdują się dane składowe konkretnego obiektu. Taki adres jest przekazywany jest poprzez nazwę obiektu. Na przy-kład, jeżeli używamy naszej klasy Data i założymy, że a jest obiektem tej klasy, to instrukcja a.pokaz() przesyła adres obiektu a do funkcji składowej pokaz(). Możemy zapytać jak taki adres jest przesyłany i gdzie jest prze-chowywany. Adres jest przechowywany w specjalnej zmiennej wskaźnikowej o nazwie this, która jest automatycznie dostarczana jako ukryty parametr do każdej niestatycznej funkcji składowej, gdy funkcja jest wywoływana. W naszym przykładzie, gdzie klasa Data ma dwie funkcje składowe:

170 7. Przeciążanie operatorów

Rysunek 7.1. Przechowywanie dwóch obiektów typu Data w pamięci

Data (i n t =1 , i n t = 1 , i n t = 2 0 0 0 ) ; // k o n s t r u k t o r void pokaz ( ) ;

Lista parametrów przekazywanych ma równoważną postać:

Data ( Date ∗this,i n t =1 ,i n t = 1 ,i n t = 2 0 0 0 ) ; // k o n s t r u k t o r void pokaz ( Date ∗th i s ) ;

W ten sposób, każda funkcja składowa otrzymuje aktualnie dodatkowy ar-gument, który jest adresem struktury danych. Aby się o tym przekonać możemy używać tych wskaźników w sposób jawny. Zademonstrujemy uży-cie wskaźnika this w krótkim programie (oczywiśuży-cie w praktyce nie korzysta się z tej techniki).

Listing 7.10. jawne użycie wskaźnika this

#include <i o s t r e a m >

2#include <c o n i o >

using namespace s t d ;

4 c l a s s punkt { i n t x , y ;

6 public:

i n t ustaw_1 (int, i n t) ;

8 i n t ustaw_2 (int, i n t) ; } ;

10 i n t punkt : : ustaw_1 (i n t a , i n t b ) { this−>x = a ;

12 this−>y = b ; return x ;

7.7. Przeciążanie operatora przypisania ( = ) 171

Po uruchomieniu programu mamy następujący wydruk:

Ustaw_1 , x = 10 Ustaw_2 , x = 30

Jak pamiętamy funkcja operatorowa postaci:

void Data : : operator= ( Data & d ) { d z i e n = d . d z i e n ;

m i e s i a c = d . m i e s i a c ; rok = d . rok ;

}

nie umożliwia użycia wielokrotnego przypisania postaci a = b = c. Przy pomocy wskaźnika this zmienimy implementację operatorowej funkcji przy-pisania tak, aby możliwe było wielokrotne przypisanie. Zasadniczą sprawą jest zaprojektowanie funkcji operator= tak, aby mogła zwrócić wartość typu Data. Prototyp takiej funkcji może mieć postać:

Data operator= ( const Data & ) ;

Zastosowaliśmy specyfikator const do parametru funkcji, aby mieć pewność, że ten operand nie będzie zmieniony przez funkcję. Implementacja nowej funkcji operatorowej może mieć postać:

Data Data : : operator=(const Data &d ) { d z i e n = d . d z i e n ;

m i e s i a c = d . m i e s i a c ; rok = d . rok ;

return th i s; }

172 7. Przeciążanie operatorów Należy pamiętać, że funkcja operatorowa przypisania musi być funkcją skła-dową klasy, nie może być funkcja zaprzyjaźnioną. W przypadku przypisania takiego jak b = c , (równoważna forma b.operator=(c) ), wywołana funkcja zmienia dane składowe obiektu b na dane obiektu c i zwraca nową wartość obiektu b. Taka operacja umożliwia wielokrotne przypisanie typu a = b = c. Implementacja przeciążonego operatora przypisania i jego zastosowanie pokazano na kolejnym wydruku.

Listing 7.11. rozszerzona wersja operatora przypisania

#i n c l u d e <i o s t r e a m >

10 Data operator=(const Data &) ; } ;

26 Data Data : : operator=(const Data &d ) { d z i e n = d . d z i e n ;

W dokumencie Język C++ – podstawy programowania (Stron 175-183)