Professional Documents
Culture Documents
PRZYKADOWY ROZDZIA
SPIS TRECI
KATALOG KSIEK
KATALOG ONLINE
ZAMW DRUKOWANY KATALOG
TWJ KOSZYK
DODAJ DO KOSZYKA
CENNIK I INFORMACJE
ZAMW INFORMACJE
O NOWOCIACH
ZAMW CENNIK
CZYTELNIA
FRAGMENTY KSIEK ONLINE
C++. Inynieria
programowania
Autor: Victor Shtern
Tumaczenie: Daniel Kaczmarek (rozdz. 1 6), Adam
Majczak (rozdz. 7 11), Rafa Szpoton (rozdz. 12 19)
ISBN: 83-7361-171-1
Tytu oryginau: Core C++: A Software Engineering Approach
Format: B5, stron: 1084
Wydawnictwo Helion
ul. Chopina 6
44-100 Gliwice
tel. (32)230-98-63
e-mail: helion@helion.pl
Spis treci
Wprowadzenie
15
21
23
53
95
133
187
Spis treci
235
313
315
381
Kohezja................................................................................................................. 385
Sprzganie ............................................................................................................ 386
Niejawne sprzenie ......................................................................................... 386
Jawne sprzenie.............................................................................................. 390
Jak zredukowa intensywno sprzgania? ......................................................... 395
Hermetyzacja danych.............................................................................................. 400
Ukrywanie danych .................................................................................................. 407
Wikszy przykad hermetyzacji danych...................................................................... 413
Wady hermetyzacji danych przy uyciu funkcji ........................................................... 422
Podsumowanie ...................................................................................................... 425
427
499
Spis treci
557
619
621
10
677
747
Spis treci
Widoczno klasy oraz podzia odpowiedzialnoci ..................................................... 796
Widoczno klas oraz relacje pomidzy klasami .................................................. 797
Przekazywanie odpowiedzialnoci do klas serwera............................................... 799
Stosowanie dziedziczenia .................................................................................. 801
Podsumowanie ...................................................................................................... 804
805
807
889
11
12
941
987
1035
Spis treci
Skadanie klas oraz dziedziczenie .................................................................... 1052
Funkcje wirtualne oraz klasy abstrakcyjne ......................................................... 1054
Szablony ........................................................................................................ 1055
Wyjtki........................................................................................................... 1056
Jzyk C++ a konkurencja ...................................................................................... 1058
Jzyk C++ a starsze jzyki programowania ........................................................ 1058
Jzyk C++ a Visual Basic................................................................................. 1058
Jzyk C++ a C ................................................................................................ 1059
Jzyk C++ a Java ............................................................................................ 1060
Podsumowanie .................................................................................................... 1062
Dodatki
Skorowidz
1063
1065
13
Konstruktory i destruktory
potencjalne problemy
Zagadnienia omwione w tym rozdziale:
n
Podsumowanie.
Funkcje operatorowe dokonujce przecienia operatorw przydaj nowego impulsu programowaniu obiektowemu. Okazao si, e oto zamiast koncentrowa si na czeniu w jedn,
logicznie powizan cao danych i operacji w oparciu o pewne wsplne koncepcje, zajmujemy si rozwaaniami w kategoriach estetycznych i zagadnieniami rwnego traktowania
wbudowanych, elementarnych typw danych oraz typw definiowanych przez programist
w programach pisanych w C++.
Ten rozdzia stanowi bezporedni kontynuacj poprzedniego. W rozdziale 10. Funkcje operatorowe jeszcze jeden dobry pomys omawiaem zagadnienia odnoszce si do projektowania klas typu numerycznego na przykadzie klas (liczba zespolona) oraz
(uamek zwyky). Obiekty stanowice zmienne (instancje) tych klas s penoprawnymi obiektami, rzec mona z prawdziwego zdarzenia. Stosuj si do nich wszystkie zagadnienia odnoszce si do klas i obiektw deklaracja klasy, kontrola dostpu do skadnikw klasy,
projektowanie metod, definiowanie obiektw i przesyanie komunikatw do obiektw.
Typy danych definiowanych przez programist (klasy) omawiane poprzednio s danymi numerycznymi. Nawet jeli maj wewntrzn struktur bardziej skomplikowan ni elementarne typy
liczb cakowitych czy liczb zmiennoprzecinkowych, obiekty takich klas mog by w kodzie
558
559
ilo pamici, mamy do czynienia w programie z dwoma niekorzystnymi, a skrajnymi zjawiskami marnowaniem pamici (gdy rzeczywista wielko tekstu jest mniejsza ni ilo
zarezerwowanej pamici) albo z przepenieniem pamici (gdy rzeczywista wielko tekstu
okae si wiksza ni ilo zarezerwowanej pamici). Te dwa niebezpieczestwa czyhaj stale
i w ktr z tych dwu puapek zawsze, prdzej czy pniej, wpadn ci projektanci klas, ktrzy rezerwuj t sam ilo pamici dla kadego takiego obiektu.
C++ rozwizuje ten problem poprzez przyporzdkowanie dla kadego obiektu tej samej iloci
pamici (na stercie albo na stosie) zgodnie ze specyfikacj klasy, a nastpnie ewentualne dodanie, w miar potrzeby, dodatkowej pamici na stercie2. Taka dodatkowa ilo pamici na
stercie zmienia si i moe by zupenie inna dla poszczeglnych obiektw. Nawet dla jednego,
konkretnego obiektu podczas jego ycia ta ilo dodatkowej pamici moe si zmienia. Na
przykad jeli do obiektu klasy
zawierajcego jaki tekst dodawany jest nastpny
acuch znakw (konkatenacja), taki obiekt moe powikszy swoj pami, by pomieci
nowy, duszy tekst.
Dynamiczne zarzdzanie pamici na stercie obejmuje zastosowanie konstruktorw i destruktorw. Ich nieumiejtne, niezrczne stosowanie moe negatywnie wpyn na efektywno
dziaania programu. Co gorsza, moe to spowodowa utrat danych przechowywanych
w pamici i utrat integralnoci programu, co jest zjawiskiem charakterystycznym dla C++,
nieznanym w innych jzykach programowania. Kady programista pracujcy w C++ powinien by wiadom tych zagroe. To z tego powodu zagadnienia te zostay wczone do tytuu
niniejszego rozdziau, mimo e ten rozdzia stanowi w istocie kontynuacj rozwaa o funkcjach operatorowych dokonujcych przecienia operatorw.
Dla uproszczenia dyskusji niezbdne podstawowe koncepcje zostan tu wprowadzone w oparciu o klas
(o staej wielkoci obiektw), znan z rozdziau 10. W tym rozdziale te
koncepcje zostan zastosowane w odniesieniu do klasy
z dynamicznym zarzdzaniem
pamici na stercie. W rezultacie twoja intuicja programistyczna wzbogaci si o wyczucie relacji
pomidzy instancjami obiektw po stronie kodu klienta. Jak si przekonasz, relacje pomidzy
obiektami oka si inne ni relacje pomidzy zmiennymi typw elementarnych, pomimo
wysikw, by takie zmienne i obiekty byy traktowane tak samo. Innymi sowy, powiniene
przygotowa si na due niespodzianki.
Lepiej nie pomija materiau zawartego w niniejszym rozdziale. Zagroenia zwizane z zastosowaniem i rol konstruktorw i destruktorw w C++ s to niebezpieczestwa zdecydowanie realne i naley wiedzie, jak si przed nimi broni (bronic przy okazji swojego szefa
i swoich uytkownikw).
560
Listing 11.1. Przykad przekazania obiektw jako parametrw funkcji poprzez warto
561
!"
#$ #% &
&
' &( )
# #
*
+ +
,
- &
&
&(.
# #
+ +
,
#
-
# #
+ +
,
/
&
+ +
,
1
2
3
, & 1&( &
""
+ +
++
,
""
1 ## $ # %
,
# %
1
$ # *% # * , 4 &
1
$ # * # * , ! ! !5
# # (0& ' &
6#
(
1
# * (( !0 (. 0&(
# * ,
# 7 # , &
2
3
73 2 37 73 ,
%8 !9:
# 2 !
+ 2+ !
+ #+
$
,
562
+ +
,
Dodano tu take konstruktor kopiujcy z wydrukiem komunikatu diagnostycznego. Ten komunikat diagnostyczny zostanie wyprowadzony na ekran zawsze, gdy obiekt klasy
bdzie inicjowany za pomoc kopiowania pl danych innego obiektu klasy
. Takie
kopiowanie obiektu na przykad nastpi wtedy, gdy parametry bd przekazywane poprzez
warto do funkcji
lub gdy bdzie nastpowa zwrot wartoci z funkcji (zwrot
obiektu poprzez warto).
""
- &
&
&(.
# # &(
+ +
,
+ +
,
563
Poddany tu przecieniu operator przypisania ( jest operatorem dwuargumentowym (binarnym). Skd to wiadomo? Po pierwsze, funkcja operatorowa ma jako jeden parametr obiekt
klasy
, a nie jest to funkcja zaprzyjaniona, lecz metoda. Skoro tak, jak kada metoda
z jednym parametrem obiektowym, dziaa na dwch argumentach. Jednym, niejawnym argumentem jest obiekt docelowy komunikatu (widziany przez metod jako wasny obiekt), drugim, jawnym argumentem jest obiekt parametr. Drugim wyjanieniem jest skadnia stosowania operatora przypisania (po stronie kodu klienta). Operator dwuargumentowy jest zawsze
wstawiany pomidzy pierwszy a drugi operand. Gdy dodaje si dwa operandy, kolejno
jest nastpujca: pierwszy operand, operator, drugi operand (np. ). Podobnie jest, gdy
dokonuje si przypisania. I tu kolejno jest nastpujca: pierwszy operand, operator, drugi
operand (np. ). Odnoszc to teraz do skadni wywoania funkcji, zauwaamy, e
obiekt jest docelowym obiektem komunikatu. W podanej funkcji operatorowej, dokonujcej
przecienia operatora przypisania, pola (licznik uamka) oraz (mianownik uamka)
nale do docelowego obiektu komunikatu, . Obiekt jest biecym argumentem danego
wywoania funkcji operatorowej. W podanej funkcji operatorowej, dokonujcej przecienia
operatora przypisania, pola oraz nale do biecego argumentu metody, .
Skoro tak, to operator ( ) przypisania odpowiada wywoaniu funkcji operatorowej o nastpujcej skadni:
.
Poniewa ta metoda ma typ wartoci zwracanej (nie zwraca adnej wartoci), taka
funkcja operatorowa nie umoliwia obsugi wyrae acuchowych po stronie kodu klienta
(np. ). Takie wyraenie jest przez kompilator interpretowane jako .
Oznacza to, e warto zwracana przez operator przypisania ( ), to znaczy poprzez
wywoanie funkcji operatorowej
, musiaaby zosta wykorzystana jako argument kolejnego wywoania tej samej funkcji operatorowej:
.
Aby takie wyraenie byo poprawne i zostao poprawnie zinterpretowane, operator przypisania powinien zwraca jako warto obiekt (tu klasy
). Skoro funkcja zostaa zaprojektowana tak, e jej typ wartoci zwracanej jest , wyraenie acuchowe po stronie
kodu klienta zostanie przez kompilator zasygnalizowane jako bd skadniowy. Przy pierwszym spojrzeniu na przecianie operatora przypisania to nie jest istotne. Wyraenia acuchowe bd stosowane pniej, w dalszej czci niniejszego rozdziau.
Wydruk wyjciowy programu z listingu 11.1 zosta pokazany na rysunku 11.1. Pierwsze trzy
wydrukowane komunikaty utworzony pochodz od konstruktorw w rezultacie utworzenia
i zainicjowania trzech obiektw klasy
w obrbie funkcji . Dwa komunikaty
o kopiowaniu, skopiowany, pochodz z wntrza funkcji przeciajcej operator dodawania
. Kolejny komunikat o utworzeniu obiektu,
!, wynika z wywoania konstruktora klasy
wewntrz ciaa funkcji
.
Wszystkie te wywoania konstruktorw maj miejsce na pocztku wykonania funkcji. Pniej nastpuje seria zdarze, gdy wykonanie kodu funkcji dochodzi do koca (tj. do nawiasu
klamrowego zamykajcego kod ciaa funkcji), a lokalne i tymczasowe obiekty s usuwane.
Pierwsze dwa komunikaty destruktora usunity) nastpuj wtedy, gdy dwie lokalne kopie
biecych argumentw (obiekty uamki 3/2 oraz 1/4) s usuwane i dla tych dwch obiektw
nastpuje wywoanie destruktorw. Ten obiekt, ktry zawiera sum dwch argumentw, nie
moe zosta usunity, zanim nie zostanie uyty w operacji (tzn. przez operator) przypisania.
Kolejny komunikat, przypisany, pochodzi z wywoania funkcji dokonujcej przecienia
operatora przypisania
. Wreszcie komunikat "
! pochodzi od destruktora
tego obiektu, ktry zosta utworzony w obrbie ciaa funkcji
. Ostatnie trzy komunikaty "
! pochodz od tych destruktorw, ktre s wywoywane, gdy wykonanie
564
Rysunek 11.1.
Wydruk wyjciowy
programu z listingu 11.1
Wymaganie zachowania spjnoci pomidzy rnymi czciami kodu w C++ jest postulatem rygorystycznym. W tym przypadku zmieniono interfejs (prototyp funkcji) i analogicznie
zmodyfikowano deklaracj teje funkcji w obrbie specyfikacji klasy. (Take tym razem nie
ma tu znaczenia, czy jest to metoda, czy te funkcja zaprzyjaniona kategorii ). W tym
przypadku niezapewnienie spjnoci rnych czci kodu programu nie jest miertelnym zagroeniem. W razie czego kompilator powinien nas ostrzec, e kod taki zawiera bdy skadniowe.
Wydruk wyjciowy, powstay w wyniku wykonania programu z listingu 11.1 po zmodyfikowaniu funkcji
tak jak poprzednio, pokazano na rysunku 11.2. Jak wida, cztery
wywoania funkcji przepady. Dwa obiekty-parametry nie s tworzone (brak komunikatu konstruktora) i odpowiednio dwa obiekty-parametry nie s usuwane (brak komunikatu destruktora).
Rysunek 11.2.
Wydruk wyjciowy programu
z listingu 11.2 przy przekazaniu
parametrw poprzez referencje
565
+ 2+ !
+ #+
$
,
W dalszej czci zastosowana zostanie podobna technika, by zademonstrowa rnic pomidzy zainicjowaniem a przypisaniem na przykadzie obiektw klasy
.
Rozrniaj inicjowanie obiektw od przypisywania wartoci obiektom. Przy inicjowaniu
obiektw wywoywany jest konstruktor, a zastosowanie operatora przypisania zostaje
pominite. Podczas operacji przypisania stosuje si operator przypisania, natomiast
wywoanie konstruktora zostaje pominite.
566
567
i zetknicia si z wyszym stopniem zoonoci. Z drugiej strony, kod klienta bdzie stawa
si lepszy dziki zastosowaniu zamiast starego stylu wywoa funkcji nowoczesnego
przeciania operatorw.
Klasa String
Omwi tu popularne zastosowanie funkcji przeciajcej operator w odniesieniu do klas
nienumerycznych zastosowanie operatora dodawania do konkatenacji acuchw znakw (doczenia jednego acucha znakw do koca drugiego acucha).
Rozwamy klas
zawierajc dwa pola danych wskanik do dynamicznie rozmieszczanej w pamici tablicy znakowej oraz liczb cakowit wskazujc maksymaln
liczb znakw, traktowanych jako wane dane, ktre mog zosta umieszczone w dynamicznie
przyporzdkowanej pamici na stercie. W istocie standardowa biblioteka C++ Standard
Library zawiera klas
(ktrej nazwa rozpoczyna si do maej litery, tj.
),
ktra to klasa jest przeznaczona do speniania wikszoci wymaga zwizanych z operowaniem acuchami tekstowymi. To wspaniaa i bardzo przydatna klasa. Ta firmowa klasa jest
znacznie bardziej rozbudowana ni klasa
, ktra bdzie tu omawiana. Nie mona jednak uy firmowej klasy
do naszych celw, poniewa jest zbyt skomplikowana i takie techniczne szczegy odwracayby nasz uwag od waciwej dyskusji o dynamicznym
zarzdzaniu pamici i jego konsekwencjach.
Po stronie kodu klienta obiekty naszej klasy
mog by tworzone na dwa sposoby
poprzez okrelenie maksymalnej liczby obsugiwanych znakw acucha tekstowego albo poprzez okrelenie bezporednio zawartoci acucha tekstowego obsugiwanego przez
dany obiekt. Okrelenie liczby znakw w acuchu wymaga podania jednego parametru liczby cakowitej. Okrelenie zawartoci acucha tekstowego take wymaga podania jednego
parametru tablicy znakowej. Typy tych parametrw s rne, dlatego parametry te powinny
zosta zastosowane w rnych konstruktorach. Poniewa kady spord tych konstruktorw
ma dokadnie jeden parametr typu innego ni typ wasnej klasy, ktry to parametr zostaje
poddany konwersji na warto obiektu danej klasy, obydwa te konstruktory s konstruktorami
konwersji.
Pierwszy z tych konstruktorw konwersji, ktrego parametrem jest liczba cakowita okrelajca ilo potrzebnej pamici, ktr naley obiektowi przydzieli na stercie, ma domyln
warto argumentu rwn zeru. Jeli obiekt klasy
zostanie utworzony z wykorzystaniem tej domylnej wartoci (tj. jeli nie wyspecyfikowano adnej wartoci argumentu), domylna wielko pamici przyporzdkowana danemu obiektowi w celu przechowywania
acucha tekstowego zostanie przyjta jako zerowa. W takim przypadku ten pierwszy konstruktor konwersji zostanie uyty w roli konstruktora domylnego (tzn. przy skadni deklaracji
).
Drugi spord tych konstruktorw konwersji, z tablic znakow (acuchem znakw) jako
parametrem, nie ma domylnej wartoci argumentu. Przyporzdkowanie mu jakiej wartoci
domylnej byoby do trudne, chyba eby zdecydowa si tu na pusty acuch znakw o zerowej dugoci: %%. Wtedy jednak kompilator mgby mie wtpliwoci z racji niejednoznacznoci przy wywoaniu konstruktora domylnego, tj. w razie deklaracji obiektu w postaci
568
+ # +
( CD
569
+ # +
(
CD
. !0 &'
1+A
1
!
+
" 1+? ( ! H!H * !0 +
+ # +
+I
+ &
&
!5!"
+I(
+
+ # +
$
,
Rysunek 11.4.
Schemat obsugi pamici
dla pierwszego konstruktora
konwersji z listingu 11.2
Na rysunku 11.4a pokazano pierwsz faz konstruowania obiektu, a na rysunku 11.4b odpowiednio drug faz. Prostokt przedstawia obiekt
klasy
zawierajcy dwa pola
danych wskanik
i liczb cakowit . Te pola danych w rzeczywistoci mog
3
570
Rysunek 11.5.
Schemat obsugi pamici
dla drugiego konstruktora
konwersji z listingu 11.2
S trzy metody postpowania z polem danych zawierajcym wielko obszaru pamici przyporzdkowanego na stercie. Pierwszy sposb polega na tym, e takie pole zawiera cakowit
wielko pamici przydzielon dla okrelonego pola danych (liczba znakw do przechowywania plus jeden). Drugi sposb polega na tym, e takie pole zawiera liczb rzeczywicie
uytecznych znakw po dodaniu do tej liczby jedynki. Takie dodanie jedynki nastpuje
wtedy, gdy dane s umieszczane w pamici przyporzdkowanej na stercie. My uyjemy tu
tej drugiej metody, cho zdecydowanie trudno byoby wyjani, dlaczego ten drugi sposb
miaby by lepszy od pierwszego. Autor nie zmieni jednak zdania, poniewa wtedy musiaby wyjani, dlaczego ten pierwszy sposb jest lepszy od tego drugiego.
571
Trzeci sposb polega na tym, by nie przechowywa dugoci acucha znakw jako zawartoci numerycznego pola danych ani nie przechowywa jej wcale, lecz za kadym razem oblicza t dugo w czasie wykonania programu poprzez wywoanie funkcji bibliotecznej
. To przykad wzgldnoci, a waciwie wspzalenoci czasu i przestrzeni. Ten trzeci
sposb jest lepszy, gdy dugo acucha znakw nie jest nam przydatna czsto, a z drugiej
strony odczuwamy niech do zajmowania pamici przez dodatkow liczb cakowit w kadym obiekcie obsugujcym acuchy znakw.
Skoro dynamiczny przydzia pamici na stercie nastpuje dla kadego obiektu w sposb indywidualny, wielu programistw moe mie odczucie, e taka dynamicznie przyporzdkowana pami powinna by rozpatrywana jako cz obiektu. Przy takim podejciu do problemu, obiekty klasy
mog by postrzegane od strony kodu klienta jako obiekty o zmiennej
dugoci, zalenej od przyporzdkowanej dla danego obiektu wielkoci pamici na stercie.
Taki punkt widzenia jest uprawniony, ale tego typu spojrzenie prowadzi do bardziej skomplikowanych i zaskakujcych rozwaa o dziaaniu konstruktorw i destruktorw, a i zakca
nieco koncepcj samej klasy.
Autor preferuje tu podejcie zaprezentowane na diagramach pokazanych na rysunkach 11.4
oraz 11.5. Odzwierciedla to zasad C++, e klasa jest planem-szablonem dla poszczeglnych
obiektw danej klasy. Taki plan-szablon jest tu taki sam dla wszystkich obiektw klasy
.
Zgodnie z tym planem, kady obiekt klasy
zawiera dwa pola danych, a wielko kadego obiektu klasy
jest taka sama. Gdy po stronie kodu klienta nastpuje wykonanie
nastpujcej instrukcji:
;
:$ 5 0
Dla obiektu
zostaje na stosie przydzielona pami przeznaczona na jego dwa pola danych.
Przydzia pamici na stercie nastpuje za porednictwem nalecych do tego obiektu klasy
metod, ktre zostaj wywoane i wykonuj si w odniesieniu do danego, konkretnego
obiektu. Rne obiekty klasy
mog mie przyporzdkowan rn wielko dynamicznej pamici na stercie. Mog (niezalenie) zwalnia t pami albo powiksza jej
objto, nie zmieniajc swojej tosamoci.
Takie podejcie w niczym si nie zmienia, jeli same obiekty, zamiast na stosie, lokalizowane
bd dynamicznie na stercie. Rozwamy nastpujcy przykad kodu klienta.
;
7
F !&
& ;
(
& &<&
# ;
+I6+ : 8 &
572
573
Autor jest daleki od sugerowania, jakoby programistw o takim sposobie mylenia naleao
zwalnia z pracy. Nie mog oni jednak uczestniczy w grupowych projektach, w ktrych
inne osoby musz prowadzi obsug techniczn ich kodw. W dzisiejszych czasach zupenie
nie ma si czym chwali, jeli programista pisze takie kody, ktrych zrozumienie wymaga
dodatkowego wysiku.
(
+I(
6+
Zauwa, e oburzenie autora skierowane jest przede wszystkim przeciw takim faktom, gdy
serwisant kodu musi woy dodatkowy wysiek w zrozumienie kodu. To, e podany kod
nie dokonuje uprzedniej oceny rozmiaru pamici dostpnej na stercie w obrbie danego obiektu
i z tego powodu moe doprowadzi do uszkodzenia danych w pamici jest oczywicie wane.
Ale to tylko drobiazg, ktry jednak powoduje wyczerpanie si naszej cierpliwoci. Mona
to skorygowa poprzez inny podzia odpowiedzialnoci pomidzy kodem klienta a obiektem
serwerem klasy
.
#
(
+I
+
&E )E '0
"
+I(
+ M
&
Na rysunku 11.6 pokazano wydruk wyjciowy programu z listingu 11.2. Ten wydruk demonstruje, e wywoanie funkcji ! chroni dynamiczn pami przed przepenieniem
(ang. overflow) poprzez obcicie danych z kodu klienta (ang. truncate).
Rysunek 11.6.
Wydruk wyjciowy
programu z listingu 11.2
574
Albo jeli wolimy notacj acuchow przy stosowaniu obiektw, moemy zrobi to samo,
uywajc tylko jednej instrukcji.
=:$$> # HNH &&))E & 0
Teraz, gdyby kod klienta podj prb zmodyfikowania zawartoci dynamicznie przyporzdkowanej pamici za porednictwem wskanika bdcego wartoci zwracan poprzez
metod , kompilator oznaczy tak prb jako bd skadniowy.
+I
+ +I
+ +I(
+
5 !5. &5
575
Po wykonaniu tego fragmentu kodu klienta zawarto reprezentowana przez obiekt powinna
pozosta bez zmian, natomiast zawarto reprezentowana przez obiekt powinna stanowi
poczenie dwch acuchw znakw: +
,
Jeli zaimplementujemy ten operator w formie metody operatorowej, to obiekt powinien
by docelowym obiektem komunikatu, natomiast obiekt powinien by biecym argumentem w danym wywoaniu takiej metody operatorowej. Rzeczywiste znaczenie ostatniego
wiersza w podanym fragmencie kodu jest nastpujce:
2# *
!&
2#
liczb znakw.
2. Przydzieli dynamicznie na stercie ilo pamici wystarczajc do przechowywania
na stercie pamici.
5. Kopiowanie tablicy znakowej z obiektu przekazanego jako argument do nowej,
576
Rysunek 11.7.
Schemat pamici
dla funkcji
operatorowej
konkatenacji
acuchw
znakowych
Moe wyda si nieco przesadne, by w celu wyjanienia kolejnych krokw tak prostego algorytmu zagbia si w tak drobne szczegy i wykrela odrbny rysunek dla kadego
malekiego kroku w zarzdzaniu pamici. Jeli tak to odczuwasz to bardzo dobrze. Ale
naleysz do szczliwej mniejszoci. Dla wikszoci ludzi operacje przy uyciu wskanikw s tajemnicze i sprzeczne z intuicyjnym wyczuciem.
577
Tylko dowiadczeni programici s tu w stanie zauway, e przestrze na stercie posiadana przez docelowy obiekt komunikatu nie zostaje poprawnie zwrcona do swobodnego
wykorzystania. Ten rysunek pokazuje to w sposb klarowny.
Autor uwaa, e rysowanie takich schematw to jedyny sposb na wyrobienie sobie intuicji
w odniesieniu do zarzdzania pamici i wychwytywania bdw. Lepiej spdzi kilka dodatkowych minut na rysowaniu i planowaniu, ni straci pniej godziny z debugerem i z innymi
skomplikowanymi narzdziami, poszukujc drogi w gszczu, bagnach i zarolach, pord
instrukcji, ktrych znaczenie i dziaanie nie jest dla nas do koca zrozumiae.
Takie rysunki pozostaj, oczywicie, tylko narzdziami. To my musimy tak uywa tych narzdzi, by upewni si, e dokadnie rozumiemy kad instrukcj.
Rysunek 11.8 jest podobny do rysunku 11.7. Pokazuje schematycznie, jak tablica znakowa
umieszczona na stercie, a wskazywana poprzez wskanik
docelowego obiektu komunikatu znika w wyniku zadziaania operatora
. Dopiero potem wskanik
zostaje przestawiony tak, by wskazywa now macierz znakow na stercie.
Po usuniciu tego wycieku pamici naleaoby si przyzna, e w tej dyskusji o funkcji dokonujcej przecienia operatora autor mwi sam prawd i tylko prawd, ale nie powiedzia
jeszcze caej prawdy. Powodem byo to, e najpierw naleao si upewni, e pokonalimy
ju mniejsze i atwiejsze przeciwnoci, zanim staniemy oko w oko z bardziej skomplikowanymi i bardziej niebezpiecznymi problemami. Autor chcia utrzyma uwag czytelnika w stanie
niepodzielnym.
Ta dyskusja ma za zadanie zaprezentowanie swoistych szablonw postpowania i niebezpieczestw, ktre powinnimy rozpoznawa, gdy piszemy wasne programy w C++. Istota
tego problemu to, rzec mona, ulubiony przeciwnik autora przekazywanie obiektw jako
parametrw poprzez warto.
578
Rysunek 11.8.
Schemat pamici
dla skorygowanej
funkcji operatorowej
konkatenacji
acuchw
znakowych wobec
klasy String
579
Gdy podczas przekazywania parametru poprzez warto wykonywana jest kopia biecego
argumentu-obiektu, nastpuje wywoanie dodanego przez kompilator konstruktora kopiujcego. Ten konstruktor kopiuje pola danych biecego argumentu do odpowiednich pl danych jego lokalnej kopii obiektu bdcego parametrem formalnym funkcji. Gdy kopiowany
jest wskanik
, wskanik w obiekcie stanowicym parametr formalny (lokalnej kopii) otrzymuje skopiowan zawarto wskanika
(adres) z biecego argumentu. Ten skopiowany wskanik wskazuje adres na stercie, gdzie przechowywana jest tablica znakowa reprezentowana przez obiekt bdcy biecym argumentem funkcji.
W efekcie wskaniki w obu tych obiektach biecym argumencie i jego lokalnej kopii
wskazuj t sam sekcj pamici na stercie, a kady z tych obiektw uwaa, e ma t
pami do wycznego uytku.
T sytuacj prbowano przedstawi schematycznie na rysunku 11.9. W istocie to, co dotd
zostao powiedziane, nie zmienia dziaania funkcji operatorowej dokonujcej przecienia
operatora (na razie). To dlatego wszystko to, co powiedziano do tej pory, byo sam prawd
i tylko prawd.
Rysunek 11.9, ktry mwi ca prawd, zawiera (dodatkowo obejmuje) lokalny obiekt ,
ktrego pola danych s inicjowane poprzez skopiowanie pl danych biecego argumentu
funkcji obiektu . Rysunek 11.9a pokazuje, e ten lokalny obiekt oraz biecy argument odwouj si do tej samej sekcji pamici na stercie. Rysunek 11.9b pokazuje, e
po przyporzdkowaniu i zainicjowaniu nowej pamici na stercie ta nowa pami zastpia star
w docelowym obiekcie komunikatu ( ), a lokalny obiekt oraz biecy argument nadal odwouj si do tej samej sekcji pamici na stercie.
Rysunek 11.9.
Schemat pamici
przy przekazaniu
przez warto obiektu
klasy String
580
Rysunek 11.9c przedstawia stan lokalnego obiektu i biecego argumentu po wywoaniu destruktora, ale zanim lokalny obiekt zostanie usunity. Rysunek pokazuje, e i lokalny
obiekt, i biecy argument straciy przyporzdkowan im pami na stercie (pami wskazywana przez wskanik
zostaa zwolniona). To dziaanie oczywicie nie ma wpywu na
stan obiektu docelowego , poniewa obiekt docelowy nie jest usuwany. Gdy zakoczy si
wykonanie funkcji przeciajcej operator, obiekt docelowy pozostanie w dokadnie tym
samym stanie, co podczas poprzedniej dyskusji, odzwierciedlonej schematycznie na rysunku 11.8. Taki kod klienta zwraca poprawne rezultaty.
;
+I + ;
+
6+
2#
+ # +
Tym niemniej pami zwolniona i zwrcona do systemu przez destruktor, gdy usuwany by
formalny parametr obiekt , nie naleaa do obiektu docelowego. Naleaa (i nadal powinna
nalee) do biecego argumentu funkcji, czyli do obiektu zdefiniowanego w przestrzeni
klienta. Po wywoaniu tej funkcji obiekt klienta, ktry by uyty jako biecy argument przekazany przez warto zosta pozbawiony przydzielonej mu uprzednio dynamicznie jego
pamici na stercie. Jeli kod klienta po wywoaniu tej funkcji sprbuje ponownie uy tego
obiektu spowoduje to wystpienie bdu.
;
+I + ;
+
6+
+ # +
+ # +
+ # +
+ # +
Nie wyglda szczeglnie elegancko ani mdrze powtrne kontrolowanie zawartoci obiektu ,
ktra bya przed chwil drukowana; sam obiekt cakiem niedawno by uywany jako rwarto
w wywoaniu funkcji operatorowej
. Nastpuje to tylko dlatego, e dokadnie
wiemy, e tu wanie wystpuje problem z tak implementacj. Jest jasne, e obiekt powinien
reprezentowa dokadnie t sam zawarto, ktr przed chwil reprezentowa, uczestniczc
w wyraeniu . Tak podpowiada intuicja programisty przyzwyczajonego do konwencjonalnego programowania. W wikszoci przypadkw w C++ ta intuicja znajdzie potwierdzenie, ale nie zawsze, i moliwie jak najszybciej powinnimy rozwin u siebie inn intuicj.
To wszystko zostao tu powiedziane, poniewa ten niewinnie wygldajcy kod klienta moe
581
doprowadzi do sytuacji, gdy tekst reprezentowany przez obiekt bdzie absolutnie przypadkowy, a wszelka prba uycia tego obiektu przy zaoeniu, e jego stan pozostaje niezmienny, jest zwyk lekkomylnoci.
No i jak ci si to podoba? Programowanie w C++ nie pozwala si nudzi. Niemniej jednak
programista piszcy w C++ musi rozumie, co dzieje si w sposb niejawny, pod stoem,
nawet w takim prostym programie, jak ten ostatni przykadowy fragment kodu.
To jeszcze nie koniec tej opowieci. Takie efekty wystpuj przy jeszcze jednym zamykajcym nawiasie klamrowym zamykajcym zakres widocznoci nazw. Zawsze zwracaj uwag
na nawiasy klamrowe ograniczajce zakresy widocznoci (dostpnoci). Powoduj one wykonanie znaczcej czci pracy. Gdy kod klienta dochodzi do nawiasu klamrowego zamykajcego jego przestrze widocznoci nazw i zamierza zakoczy swoje dziaanie, dla wszystkich lokalnych obiektw s wywoywane destruktory, wcznie z nieszczsnym obiektem ,
ktry by uywany przy wywoywaniu funkcji operatorowej i tu przed zakoczeniem tej
funkcji (powrotem z funkcji) zosta pozbawiony swojej dynamicznie przydzielonej pamici.
Destruktor prbuje zwolni pami wskazywan przez zawarty w tym obiekcie wskanik
,
jednake ta pami na stercie zostaa ju uprzednio uznana za niewan i zwrcona do systemu. Gdybymy projektowali jzyk programowania, moglibymy wprowadzi jakie no op
(NOP ang. no operation instrukcja nie powodujca adnego dziaania). Ale tu nam si
nie poszczcio. Nie z C++ te numery, Brunner. W C++ powtrne uycie operatora
w stosunku do tego samego wskanika jest zabronione. To jest bd.
Niestety, stwierdzenie to jest bd nie oznacza, e kompilator powstrzyma si od kompilacji i wydrukuje komunikat o bdzie skadniowym tak, bymy mogli to skorygowa. Projektant kompilatora nie ponosi odpowiedzialnoci za ledzenie wykonania kodu i informowanie nas, e popenilimy bd. Taki kod jest skadniowo poprawny. Nie oznacza to take,
e taki program skompiluje si, uruchomi, wykona i bdzie wykazywa w sposb powtarzalny nieprawidowe rezultaty. To znaczy tylko tyle, e rezultaty dziaania takiego kodu oka
si nieokrelone. W istocie rezultaty te s zalene od platformy uruchomieniowej, od tego,
w jaki sposb aplikacja okae si zalena od rodowiska operacyjnego. System moe si
zawiesi, program moe zadziaa w sposb nieprawidowy (znienacka), moe te przez pewien czas dziaa cakowicie poprawnie (do czasu).
Na listingu 11.3 pokazano kompletny program zawierajcy implementacj tej wadliwej
konstrukcji. Wyjciowy wydruk tego programu na monitorze autora przedstawiono na rysunku 11.10.
Listing 11.3. Przecienie operatora konkatenacji z obiektem parametrem przekazywanym
poprzez warto
;
7
0
!
!"
;
#$ &
&
&( )
;
7 &
&
&(
/;
5 ( 0
2#
;
&&
( !&
;
582
Rysunek 11.10.
Wydruk wyjciowy
programu
z listingu 11.3
+ # +
& ( CD
583
+ # +
& (
CD
2#
2#
+ # +
& (
CD
+ # +
+ # +
OOOO
$
,
To sympatycznie wygldajcy pomys, ale nie dziaa zgodnie z ich intencjami. Taki wskanik, ktry zosta ustawiony na zero, naley do obiektu, ktry to obiekt bdzie usuwany w cigu
najbliszych mikrosekund. Nadal istnieje drugi wskanik, ktry wskazuje ten sam adres pamici
i mgby zosta wyzerowany, ale nie jest dostpny dla takiego destruktora, wykonujcego
si przecie w odniesieniu do innego obiektu. Poza tym, nawet gdyby to zadziaao, stanowioby to tylko rodek zapobiegajcy wykonaniu bdnej instrukcji, nie powodujc przecie
przywrcenia pamici, ktra zostaa omykowo skasowana.
584
Na rysunku 11.11 pokazano wydruk wyjciowy programu z listingu 11.3 z funkcj operatora konkatenacji przy przekazaniu jej parametru poprzez referencj.
Rysunek 11.11.
Wydruk wyjciowy programu
z listingu 11.3 z funkcj
przeciajc operator
konkatenacji, w ktrej
parametr zosta przekazany
poprzez referencj
Naley zdecydowanie uruchomi ten program, poeksperymentowa z nim, by dokadnie zrozumie te zagadnienia, ktre mog sprawia problemy. Nie ulegaj pokusie przekazywania
obiektw jako parametrw poprzez warto, chyba e jest to absolutnie niezbdne.
To prawdziwa okropno, e dodanie bd usunicie jednego tylko znaku w kodzie rdowym (ampersanda $) moe zmieni zachowanie si programu w tak dramatycznym stopniu.
Zwr uwag, e obie wersje kodu s ze skadniowego punktu widzenia poprawne. Kompilator nie uprzedzi nas, e jest pewien problem, ktrym naleaoby si martwi.
Przekazywanie obiektu jako parametru poprzez warto przypomina kierowanie czogiem.
Zawsze dojedziemy tam, dokd chcemy, ale po drodze moemy cakiem niezamierzenie
dokona wielu zniszcze. Jak ju powiedziano wczeniej, opieraj si pokusie przekazywania obiektw poprzez warto, chyba e jest to absolutnie konieczne.
Nie przekazuj obiektw do funkcji poprzez warto. Jeli obiekty zawieraj wewntrz
wskaniki i dynamicznie zarzdzaj pamici na stercie, nie mona nawet myle
o przekazaniu takich obiektw do funkcji poprzez warto. Przekazuj obiekty do funkcji
poprzez referencje i nie zapominaj o uyciu modyfikatora
, jeli funkcja nie modyfikuje stanu obiektu-parametru i (lub) stanu docelowego obiektu komunikatu.
585
586
+ # +
& (
CD
+ # +
& (
CD
2#
2# ! 1
+ # +
& (
CD
+ # +
+ # +
& (
CD
1+?
+ 1&(
+ # +
& (
CD
+ # +
587
W prawdziwym, realnym yciu programisty nie wszystko jednak przebiega zgodnie z naszymi oczekiwaniami. Na listingu 11.4 pokazano kod klasy
(z przekazywaniem parametru do funkcji
poprzez referencj) i kod klienta zawierajcy podany powyej
fragment kodu. Fragment ten na listingu zmodyfikowano w taki sposb, e obiekt
zosta
utworzony w zagniedonym zakresie widocznoci nazw (ang. nested scope). Gdy ten zagniedony segment kodu bdzie si zblia do zakoczenia i obiekt
zostanie usunity,
moemy zweryfikowa stan obiektu i skontrolowa jego integralno. Na rysunku 11.13
pokazano rzeczywiste rezultaty wykonania programu z listingu 11.4.
Listing 11.4. Inicjowanie jednego obiektu za pomoc danych z innego obiektu
;
7
0
! &
!"
;
#$ &
&
&( )
;
7 &
&
&(
/;
0E
2#
;
- &&
( !&
1
7
)
! &(
7
&<&
!
,
;
"";
#
# =2%>
1
##?@AA 3
%
=$> # $ ,
54 ( 5)
F (
CD
;
"";
7
#
5)
&
()
# =2%> 5 0
1
##?@AA 3
% < 0 5
, &( ()
&
0
;
""/;
,
0
&<&6
;
""
2#
;
-
(& 1(
#
2
5&
5)E
7 # = 2 %>
(.. )E (
1 ##?@AA 3
% < 0 5
&( . 0)E
( . 0)E
F &&
# ,
&<& F &.E
7 ;
""
4
,
;
""1
=> ! . 0.
*% 5
=*%> # $ , &4
&
;
+G
+
;
+?
+
+ # +
& (
CD
+ # +
& ( CD
588
Rysunek 11.13.
Wyjciowy wydruk
programu z listingu 11.4
+ # +
& (
CD
+ # +
+ # +
& (
CD
1+?
+ ! B
+ # +
& (
CD
+ # +
+ # +
589
Jak nazywa si taki konstruktor z jednym parametrem tego samego typu, co wasna klasa
konstruktora? Jak zapewne pamitasz z rozdziau 9. Klasy w C++ jako jednostki modularyzacji, to jest konstruktor kopiujcy, poniewa jego dziaanie polega na skopiowaniu danych
z jednego obiektu do innego obiektu; ale klasa
nie zawiera konstruktora kopiujcego. Czy to oznacza, e prba wywoania takiego nieistniejcego konstruktora kopiujcego
spowoduje komunikat o bdzie skadniowym? Nie. Kompilator wygeneruje wywoanie domylnego konstruktora kopiujcego, ktry sam automatycznie doda do specyfikacji klasy.
Kompilator sam dodaje taki konstruktor i ten sam kompilator generuje jego wywoanie. Ten
konstruktor skopiuje pola swojego obiektu-argumentu do nowego obiektu, ktry wanie zosta
utworzony. Dla klasy
taki dodawany automatycznie przez kompilator konstruktor
kopiujcy wyglda nastpujco:
&
&
&(.
&
;
"";
;
-
# &( 5)E !&
&
#
, &( &<& !&
&
Na rysunku 11.14 pokazano, jak dziaa ten konstruktor. Gdy utworzony zostaje obiekt
klasy
, jego pole przybiera warto 9, a jego wskanik
jest ustawiany tak, by wskazywa ten sam obszar pamici na stercie, ktry wskazuje wskanik
nalecy do obiektu .
Rysunek 11.14.
Diagram pamici przy
inicjowaniu jednego
obiektu klasy String
za pomoc danych
zawartych w innym
obiekcie teje klasy
590
zostaje zmodyfikowany przez kod klienta, obiekt take zostaje zmodyfikowany. Czy jest
to wyranie widoczne na rysunku 11.13? Z punktu widzenia powszechnej intuicji programistycznej nie ma adnego racjonalnego powodu, by obiekt mia by zmodyfikowany w kodzie klienta tym niemniej tak si dzieje.
Zastanwmy si nad tym. To wydaje si dziwne tylko z punktu widzenia popularnej intuicji. Na kursach programowania dla pocztkujcych autor czsto spotyka suchaczy, ktrzy
maj kopoty z prostym kodem posugujcym si liczbami cakowitymi:
# %$
#
# :$ (& (
)E O
W tym przykadzie typowa intuicja programisty po prostu nas zawiedzie. Moe dlatego, e
to logika nowicjusza. Poczynilimy zaoenie, e dwie zmienne,
oraz , s takie same. I co?
Teraz lekkie zdziwienie, e stan obiektu zmienia si po zmianie stanu obiektu
? Teraz
obiekt zawiera 20. To jest logika, do ktrej musz przywykn wszyscy uytkownicy C++,
i nowicjusze, i eksperci. Co wicej, musz si z ni poczu komfortowo.
591
Oto dlaczego takie intuicyjne podejcie jest bardziej powszechne. Z takiego punktu widzenia,
gdy dwa obiekty maj t sam warto, maj dwa odrbne zbiory bitw, a zatem zmiana
wartoci jednego z tych obiektw nie moe mie wpywu na istniejcy ju zbir bitw odnoszcy si do drugiego z tych obiektw.
Inna, nieco mniej powszechna intuicja programistyczna, posuguje si semantyk referencji.
Wedug takiego wyobraenia, gdy obiektowi zostaje przypisana warto, otrzymuje on referencj (lub wskanik) do tej wartoci. Przyrwnanie dwch obiektw oznaczaoby zatem
ustawienie ich referencji (lub wskanikw) tak, by odwoyway si do tego samego adresu
w pamici. Jeli tablica znakowa wskazywana przez wskanik w jednym z tych obiektw
si zmieni, drugi z tych obiektw automatycznie dostrzega tak zmian, poniewa obydwa
wskaniki wskazuj t sam lokalizacj w pamici. W C++ taka semantyka referencyjna jest
stosowana dla wskanikw i referencji, przy przekazywaniu parametrw poprzez referencj
lub poprzez wskanik, dla tablic i wobec poczonych poprzez wskaniki struktur danych.
& 1( B
)E !0 E :$
# %$
-
#
# :$
Znowu zdziwienie? e semantyka referencji jest mniej powszechna? Jest ona stosowana
gwnie z powodu denia do wyszej efektywnoci (np. pozwala wyeliminowa kopiowanie obiektw przy przekazywaniu parametrw do funkcji). Czasem takie referencyjne dziaania przychodz same bez adnego zaproszenia, jak w tym przypadku, a my powinnimy by
przygotowani na ich rozpoznanie i na waciwe wobec nich postpowanie. Programista piszcy
w C++ powinien zawsze pamita o rnicy pomidzy semantyk wartoci (kopiowania)
a semantyk referencji (wskazania).
To jeszcze nie koniec kopotw z programem z listingu 11.4. Gdy wykonanie programu dochodzi do zamykajcego nawiasu klamrowego zagniedonego bloku instrukcji (zatem i zagniedonego zakresu widocznoci nazw), obiekt
powinien zosta usunity, poniewa jest
zdefiniowany tylko wewntrz tego zakresu (to ulubiony temat autora odnonie dyskusji i analiz
zachowania si kodw). Obiekt jest zdefiniowany w obrbie caego ciaa funkcji i powinien by dostpny dla dalszego uytkowania. Na listingu 11.4 autor podejmuje prb wydrukowania zawartoci obiektu pod koniec kodu funkcji . Zwr uwag, e ta instrukcja wyprowadzenia danych jest taka sama, jak poprzednia, a te dwie instrukcje rozdziela jedynie
nawias klamrowy zamykajcy zagniedony zakres widocznoci nazw. Powierzchowne wraenie jest takie, e nic przecie nie wydarzyo si pomidzy tymi dwiema instrukcjami w kodzie klienta, zatem te dwie instrukcje powinny da taki sam wydruk wyjciowy. Tak jednak
nie jest. Jeszcze raz tradycyjna intuicja programistyczna okazuje si niewystarczajca do zrozumienia programu napisanego w C++ i musimy rozwin nasz intuicj, by pozwolia nam
na czytanie i zrozumienie takich fragmentw kodu, jak ten.
Jak wida na rysunku 11.14, pierwsza instrukcja wyprowadza czytelny i zrozumiay wydruk.
Nie jest to dokadnie to, czego normalnie mona by si byo spodziewa, ale przynajmniej
jest. Druga instrukcja wyprowadza miecie. Co si wydarzyo pomidzy tymi dwiema instrukcjami? Gdy wykonanie programu doszo do nawiasu klamrowego zamykajcego zakres widocznoci zagniedonego bloku, wobec lokalnego obiektu
zdefiniowanego w tym
zagniedonym bloku zosta wywoany destruktor klasy
. Jak wida z listingu 11.4
i z rysunku 11.14, ten destruktor, posugujc si operatorem
, zwolni i zwrci do
systemu pami wskazywan na stercie przez wskanik
nalecy do likwidowanego
obiektu
. Ta sekcja dynamicznej pamici w istocie bya przyporzdkowana obiektowi ,
592
na stercie pamici.
Oto konstruktor kopiujcy zdefiniowany przez programist, stanowicy rozwizanie naszego problemu.
&
&
&(. 1
0
;
"";
;
-
# 5)E
&
<'5
# =2%> F. 0!( 0
1
## ?@AA 3
% < 0 5
&(
&
<'5
,
593
594
Rysunek 11.15.
Wydruk wyjciowy
programu z listingu 11.5
;
+G
+ +G (
+
;
+?
+ +? F '()E <+
+ # +
(
CD
+ # +
(
CD
2#
2#
+ # +
(
CD
+ # +
595
+ # +
+ # +
+ # +
+ # +
Kod z listingu 11.5 demonstruje, jak pierwszy konstruktor konwersji przekazuje do funkcji
(umie na stercie) pusty acuch znakw, drugi konstruktor konwersji przekazuje
do teje funkcji wasny argument, tablic znakow, natomiast konstruktor kopii przekazuje
do funkcji
tablic znakow wskazywan poprzez wskanik
jego wasnego
argumentu obiektu.
Gdy jeden obiekt inicjuje drugi obiekt, wywoywany jest konstruktor kopiujcy. To nieuniknione. Jest tylko kwestia, jaki konstruktor zostanie wywoany. Jeli klasa nie zawiera
wasnej, indywidualnej wersji konstruktora kopiujcego, kompilator wygeneruje wywoanie
automatycznie dodanego, domylnego konstruktora kopiujcego, ktry skopiuje pola danych
596
Gdy wywoywana jest ta funkcja i tworzona jest kopia jej rzeczywistego, biecego argumentu, nastpuje wywoanie konstruktora kopiujcego zdefiniowanego przez programist.
Ten konstruktor kopiujcy przydziela pami na stercie dla parametru formalnego tej funkcji obiektu klasy
. Gdy wykonanie tej funkcji dobiega koca i wywoany zostaje
destruktor wobec tego formalnego parametru, przyporzdkowana mu na stercie pami zostaje
zwolniona i zwrcona do systemu, natomiast nie nastpuje zwolnienie ani zwrot pamici na
stercie przydzielonej rzeczywistemu, biecemu argumentowi. Problem integralnoci programu
znika. Pozostaje natomiast problem efektywnoci wykonania takiego kodu. Gdy parametr jest
przekazywany poprzez warto, wywoanie funkcji operatorowej przy wykonaniu operatora
konkatenacji acuchw znakw obejmuje utworzenie obiektu, wywoanie konstruktora kopiujcego, przydzia pamici na stercie, kopiowanie acucha znakw z pamici jednego
obiektu do pamici drugiego obiektu, wywoanie destruktora i zwolnienie pamici na stercie.
Wywoanie poprzez referencj nie wymaga adnej czynnoci z tej listy. Semantyka referencyjna eliminuje obnienie wydajnoci poprzez wykluczenie niekoniecznego kopiowania.
Nie przekazuj obiektw do funkcji poprzez warto. Jeli obiekty zawieraj wewntrz
wskaniki i dynamicznie zarzdzaj pamici na stercie, nie przekazuj takich obiektw
poprzez warto. Jeli ju koniecznie musisz przekazywa takie obiekty poprzez warto,
zdefiniuj konstruktor kopiujcy, ktry eliminuje problem integralnoci aplikacji. Upewnij
si, e takie kopiowanie nie pogorszy efektywnoci dziaania programu.
597
+HJ+ ,
;
"";
7
#
5) ()
&
#
0E &(
&
+ @ " H+
+HJ+ ,
598
Rysunek 11.16.
Wydruk wyjciowy
programu z listingu 11.6
+ ;&" H+
+HJ+ ,
;
""/;
,
( 0
;
""
2#
;
-
(& 1(
#
2
5&
5)E
7 # = 2 %>
(. )E (
1 ##?@AA 3
% 0 5O
&( . 0)E
( . 0)E
F )E
# ,
F ! &E
! ;
""
##
;
-
'(
)E
##$ ,
$ ()
&
7 ;
""
4
,
;
""1
=> ! . 0.
*% 5
=*%> # $ , 5 &4 54 &'
;
Q
+ S +
599
+ S +
+
5 J+
$
,
Gdy w funkcji zostaje utworzona tablica skadajca si z obiektw, dla kadego z elementw tej tablicy nastpuje wywoanie domylnego konstruktora zawartego w specyfikacji
klasy
(to znaczy pierwszego konstruktora konwersji z domyln wartoci argumentu). Ten konstruktor umieszcza w pamici pusty acuch znakw o zerowej dugoci i wyprowadza na ekran komunikat Zapocztkowany. Gdy wywoywana jest funkcja operatorowa
, aby doczy na zasadzie przyrostkowej (ang. append) nazwy miast do zawartoci reprezentowanej przez poszczeglne obiekty, tablica znakowa (tj. acuch znakw) jest
przekazywana, jako parametr, do funkcji operatorowej realizujcej przecienie operatora. .
Przeciony operator oczekuje parametru typu
, zatem w tym momencie wywoany
zostaje drugi spord konstruktorw konwersji i ten wanie konstruktor wyprowadza na
ekran komunikat 1Utworzony dla kadego obiektu elementu tablicy.
Nastpnie wywoana zostaje funkcja
0
. Prosi ona uytkownika o wpisanie nazwy
miasta. Wczytuje i zapamituje dane od uytkownika i przekazuje wpisan nazw miasta jako
argument do konstruktora konwersji klasy
. Z tego powodu na ekranie ponownie widzimy komunikat -
! wyprowadzony przez ten konstruktor konwersji. Poniewa obiekt
jest jedynym obiektem klasy
w obrbie funkcji , gdy zostaje wywoana funkcja
0
, wywoanie konstruktora konwersji wewntrz kodu funkcji
0
nastpuje
w odniesieniu do obiektu jako wywoanie konstruktora dla tego obiektu. Konstruktor kopiujcy nie zostaje wywoany. Nawet jeli obiekty klasy
dynamicznie zarzdzaj pamici, integralno programu jest tu chroniona. Konstruktor kopiujcy nie ma tu nic do rzeczy
odnonie implementacji semantyki wartoci. Taki konstruktor konwersji ma za zadanie przydzieli obiektowi w funkcji jego wasn, odrbn pami na stercie. Tak jak
w dowcipie o mapie i krokodylu. Krokodyl chce to gra na pianinie. Krokodyl chce to
piewa. A mapa nie ma tu nic do rzeczy.
Bymy poczuli si bardziej komfortowo posugujc si obiektami, ktre dynamicznie zarzdzaj pamici, wprowadzimy w obrbie funkcji
0
niewielk modyfikacj
dodamy tylko jeden lokalny obiekt sucy nam do przechowywania danych wprowadzonych
przez uytkownika.
;
Q
Zmiana jest niewielka. Gdyby by zmienn typu wbudowanego, w ogle nie byoby o czym
mwi. W przypadku obiektw z dynamicznym zarzdzaniem pamici pojawia si tu zupenie inny problem. Gdy utworzony zostaje lokalny obiekt , wywoywany jest wobec niego
konstruktor konwersji. Gdy natomiast funkcja koczy swoje dziaanie, obiekt w kodzie funkcji zostaje zainicjowany przy uyciu konstruktora kopiujcego. Jeli nie zosta zaimplementowany konstruktor kopiujcy zdefiniowany indywidualnie przez programist, stosowany jest tu domylny konstruktor kopiujcy dodany automatycznie przez kompilator.
Ten konstruktor kopiuje pola danych obiektu na pola danych obiektu , ale nie dokonuje
600
Rysunek 11.17.
Wydruk wyjciowy
programu z listingu 11.6
ze zmodyfikowan wersj
funkcji enterData()
oraz z konstruktorem
kopiujcym
601
Po wprowadzeniu tej zmiany system na komputerze autora si zawiesi. Mona sobie oszczdzi ogldania jeszcze jednego okienka dialogowego z bezuytecznymi informacjami o przyczynach tego problemu. W kocu to tylko przykadowe wykonanie na konkretnym komputerze
w konkretnym systemie operacyjnym. Istotne jest to, e ten program jest nieprawidowy.
Nawet jeli kompilacja jego kodu przebiega poprawnie, jego zachowanie pozostaje nieprzewidywalne i taki program nie moe by uruchamiany. Skoro kompilator nie powiedzia nam,
e ten program jest bdny, nasza intuicja programisty powinna nam pomc w zrozumieniu
tego, co w niejawny sposb odbywa si w tle podczas wykonania tego programu.
602
603
Oznacza to, e przeciony operator przypisania, ktry jest nam potrzebny dla klasy
,
powinien mie nastpujcy interfejs:
1&( .F(.(
"
;
""
#
;
-
Funkcja dokonujca przecienia operatora przypisania powinna skopiowa z obiektu stanowicego jej biecy argument pola danych, poza wskanikiem, do obiektu docelowego komunikatu, a nastpnie przydzieli na stercie wystarczajc ilo miejsca w pamici, by skopiowa z pamici na stercie przyporzdkowanej obiektowi bdcemu argumentem funkcji dane
do pamici przydzielonej obiektowi docelowemu. Te dziaania przypominaj sposb postpowania konstruktora kopii:
1. Skopiuj dugo macierzy znakowej reprezentowanej przez obiekt parametr
do pola docelowego obiektu komunikatu.
2. Przyporzdkuj pami na stercie; ustaw wskanik
w docelowym obiekcie komunikatu
na stercie pamici.
Jeli trzeba przypisa jeden obiekt drugiemu obiektowi, a obiekty te dokonuj dynamicznego zarzdzania pamici na stercie, upewnij si, e klasa zawiera funkcj dokonujc przecienia operatora przypisania. Sam konstruktor kopiujcy w tej sytuacji
nie wystarczy.
604
To cakiem sympatycznie zaimplementowany operator przypisania, traktuje on jednak docelowy obiekt komunikatu dokadnie tak samo, jak czyni to konstruktor kopiujcy zupenie
tak, jakby obiekt wyszed wanie z fabryki i nie mia adnej historii. To cakiem uprawnione
podejcie w przypadku konstruktora kopiujcego, ale to nie jest typowa sytuacja, z ktr
miewa do czynienia operator przypisania. Obiekt docelowy zosta utworzony wczeniej.
Oznacza to, e wtedy, gdy ten obiekt zosta utworzony, wywoywany by wobec niego ktry
z konstruktorw i w trakcie wykonania kodu tego konstruktora wskanik
nalecy do
tego obiektu zosta ju raz ustawiony tak, by wskazywa pewien adres pamici na stercie.
Operator przypisania pomija i lekceway t przyporzdkowan na stercie pami. Operator
ustawia wskanik
tak, by wskazywa inn lokalizacj w pamici na stercie. Przez to pami
przyporzdkowana temu obiektowi poprzednio zostaje zgubiona (nie jest stosowana, a nie
zostaje zwrcona do systemu). Taki operator przypisania powoduje wyciek pamici. To drugi
rodzaj niebezpieczestwa czyhajcego w programach pisanych w C++ oprcz ryzyka zwalniania dwukrotnie tej samej pamici.
Jaki jest na to sposb? W przeciwiestwie do konstruktora kopiujcego, funkcja dokonujca przecienia operatora przypisania musi zwolni zasoby (tu pami), ktrych uywa
docelowy obiekt danej operacji przypisania, zanim rozpoczo si wykonanie biecej operacji. Ten brak mona stosunkowo atwo uzupeni. Trzeba tylko wiedzie, e naley to
zrobi. Oto ulepszona wersja tej samej funkcji dokonujcej przecienia operatora przypisania.
(
( ( 1&(
("
;
""
#
;
-
!E &
&
&(.
# &( ! &<&
# = 2 %> 5. 0E
1
## ?@AA 3
% < 0E
, &(
0
605
To cakowicie bezuyteczna instrukcja, ale jest to w peni legalna konstrukcja C++ dla
zmiennych elementarnych typw wbudowanych. Nie ma adnego powodu, by nie moga to
by rwnie konstrukcja legalne dla zmiennych typw zdefiniowanych przez programist.
W istocie jest cakowicie legalna i kompilator nie zasygnalizuje tej instrukcji jako bdu skadniowego. Tyle tylko, e pierwsza instrukcja z kodu naszej funkcji operatorowej
zwalnia pami na stercie przyporzdkowan obiektowi-argumentowi. Gdy dochodzi do wywoania funkcji bibliotecznej
!, funkcja ta bdzie w istocie kopiowaa znaki z nowej, przyporzdkowanej wanie na stercie pamici do tej samej pamici. Rezultat takiego
kopiowania zawartoci pomidzy wzajemnie nakadajcymi si obszarami pamici (ang.
overlaped memory areas) jest nieprzewidywalny. I oto mamy jeszcze jeden powd do blu
gowy. Jednak nawet gdyby taki rezultat by cakowicie przewidywalny, poprzednia zawarto pamici na stercie przyporzdkowanej naszemu obiektowi i tak przepada bez wieci.
Jakby si to nie wydawao dziwne, takie przypisanie obiektu samemu sobie nie jest a tak
rzadkoci. Takie operacje czsto wystpuj w algorytmach sortowania i w algorytmach manipulujcych wskanikami. Aby zapobiec prbie kopiowania zawartoci pamici do tego
samego obszaru pamici, funkcja operatorowa moe najpierw sprawdzi, czy referencja do
obiektu-argumentu wskazuje w pamici to samo miejsce (adres), gdzie zlokalizowany jest
docelowy obiekt komunikatu. Dobrym sposobem odwoania si do lokalizacji docelowego
obiektu komunikatu moe by posuenie si wskanikiem
.
& !
0
'! & + ! !+
;
""
#
;
-
1 - ##
()
+!5+ B
(
! &
&
&(.
# & ! &<&
# = 2 %> 5. 0E
1
## ?@AA 3
% < 0 5
&(
0
,
Taki test mona oczywicie przeprowadzi po stronie kodu klienta przed wywoaniem funkcji
operatorowej dokonujcej przecienia operatora przypisania, ale takie postpowanie spowodowaoby w rezultacie przenoszenie odpowiedzialnoci w gr, do kodu klienta, zamiast
w d, do kodu serwera.
Jeszcze jednym rozwizaniem moe tu by sprawdzenie, czy wskaniki
nalece odpowiednio do obiektu-argumentu i do docelowego obiektu komunikatu wskazuj ten sam obszar
pamici na stercie. Taki test w kodzie funkcji operatorowej powinien wyglda nastpujco:
!
O
1
##
Obydwa te rodki zapobiegawcze s rwnowane, ale z niewiadomych powodw ten pierwszy sposb stosowany jest czciej. Powodem moe by to, e wskanik
ma dla programistw pracujcych w C++ pewne dodatkowe walory estetyczne.
606
Nie jest do koca jasne, na ile istotna jest obsuga acuchowych operacji przypisania. W kocu zawsze moemy w kodzie klienta zastosowa sekwencj operacji przypisania z zastosowaniem operatora dwuargumentowego.
#
"
#
#
"
#
Tu jednak take caa istota zagadnienia sprowadza si do problemu jednakowego traktowania zmiennych elementarnych typw wbudowanych i zmiennych typw zdefiniowanych przez
programist. W przypadku zmiennych elementarnych typw wbudowanych stosowanie w kodach C++ wyrae acuchowych jest dopuszczalne. Skoro tak, powinno to by take dopuszczalne w kodach C++ wobec zmiennych typw definiowanych przez programist.
Operator przypisania jest prawostronnie czny, zatem znaczenie wyraenia acuchowego
jest nastpujce:
# #
#
#
Oznacza to, e funkcja operatorowa przeciajca operator przypisania musi zwrci warto,
ktra jest odpowiednia, by moga zosta uyta jako biecy argument w nastpnym wywoaniu tej samej funkcji operatorowej (innymi sowy, w kolejnym komunikacie). To z kolei
oznacza, e taka funkcja operatorowa powinna zwrci warto (obiekt) takiego typu, jak
klasa, do ktrej naley dana funkcja operatorowa.
;
;
""
#
;
- !&
1 - ##
7
+!5.+
! &
&
&(.
# &( ! &<&
# = 2 %> 0E
1
## ?@AA 3
% < 0 5
&(
0
7
,
Na listingu 11.7 pokazano zmodyfikowan wersj programu z listingu 11.6. Dodano funkcj dokonujc przecienia operatora przypisania. Ta metoda operatorowa posuguje si
wywoaniem prywatnej metody
(dos. przydziel pami, rozmie w pamici),
by zada miejsca w pamici na stercie i sprawdzi, czy ta prba przydziau pamici zakoczya si powodzeniem. Aby zmniejszy objto diagnostycznych wydrukw wyjciowych, z wntrza kodu konstruktora domylnego usunito instrukcj wyprowadzania komunikatu 2 3
# !. Zamiast tego dodano wydruk komunikatu 4! !, ktry bdzie
wyprowadzany na ekran zawsze, gdy nastpi wywoanie funkcji operatorowej przeciajcej operator przypisania. Usunito z kodu take wywoanie operatora konkatenacji w ptli
programowej po stronie kodu klienta, ktra adowaa zawarto do bazy danych, i zastpiono to wywoanie operatorem przypisania. Wydruk wyjciowy tego programu pokazano na
rysunku 11.18.
607
+ @ " H+
+HJ+ ,
;
"";
;
- &
&
&
# 5)
&
()
#
0E &(
&
+ ;&" H+
+HJ+ ,
;
""/;
,
0
;
""
2#
;
-
(& 1(
#
2
5&
5)E
7 # = 2 %> 5
(.( ) (
1 ##?@AA 3
% < 0 5
&( . 0)E
( . 0)E
F )E
# ,
F &.E
;
;
""
#
;
-
1 - ##
7
+!5+O
! &
&
&(.
# &( ! &<&
#
0E &(
&
+ R" H+
608
Rysunek 11.18.
Wydruk wyjciowy
programu z listingu 11.7
SNT # 8,
;
=8> 5F !&
' !
7=8> # +N
+ +U
+ +V+ +Q+ ,
1
(#$ (
SNT (22
=(> # =(> , "
=(>
#=(>
;
0(. F
5 &
&
&
#
Q
1 #$
SNT 22
1
=> ## !& ,
&( &
=>
##
1 ## SNT
+ S +
+ S +
+
5 J+ (
$
,
609
Jak wida, problem integralnoci programu znikn. Moemy postpowa z obiektami klasy
dokadnie w taki sam sposb, jak ze zmiennymi elementarnych, wbudowanych typw
numerycznych. Moemy utworzy takie obiekty bez ich zainicjowania, moemy je zainicjowa,
posuywszy si do tego celu macierz znakow, moemy take zainicjowa takie obiekty,
posugujc si innym, utworzonym wczeniej, obiektem tego samego typu
. Moemy
jeden obiekt klasy
przypisa operatorem drugiemu obiektowi klasy
tak,
jakby byy to liczby. Zwr uwag, e C++ nie pozwoli na to w odniesieniu do macierzy.
Macierze w C++ posuguj si semantyk referencji, a nie semantyk wartoci.
Moemy do klasy
doda tyle funkcji przeciajcych operatory arytmetyczne, ile
tylko si zmieci (dodawanie obiektw klasy
, odejmowanie, mnoenie itp.). Powinnimy
jednake pamita tu o losie programisty serwisanta kodw. Nie powinnimy doprowadza
do tego, by zadanie polegajce na zrozumieniu naszych kodw byo trudniejsze, ni jest to
rzeczywicie konieczne.
Wbudowana w C++ moliwo przeciania operatorw stanowi znaczcy wkad do estetyki
programowania komputerw.
Elastyczno C++ ma swoj cen. Jak zawsze co za co. Jeli chcemy zainicjowa jeden obiekt, posuywszy si w tym celu innym obiektem (przy definiowaniu obiektu, przy
przekazywaniu go jako parametru poprzez warto lub przy zwrocie obiektu poprzez warto
z funkcji), powinnimy zdecydowanie doda do klasy konstruktor kopii. Jeli chcemy dokona
przypisania jednego obiektu innemu obiektowi, powinnimy zdecydowanie doda do specyfikacji klasy funkcj operatorow dokonujc przecienia operatora przypisania.
Problemy z zachowaniem integralnoci programu, ktre mog wystpi z powodu stosowania nieklarownego dynamicznego zarzdzania pamici, s na tyle niebezpieczne, e wielu
programistw implementuje konstruktor kopiujcy i funkcj operatorow operatora przypisania dla kadej klasy, ktra dynamicznie zarzdza pamici. Programici robi to czsto nawet w takich klasach, ktre nie zarzdzaj pamici w sposb dynamiczny. W kocu napisanie
tych funkcji nie wymaga wiele wysiku zawsze lepiej to zrobi, choby tylko tak, na
wszelki wypadek.
Autor uwaa, e to jest problem z kategorii dmuchania na zimne. Zamiast dodawa do kodu
wiele kompletnie bezuytecznych funkcji, projektant powinien uwanie przeanalizowa rzeczywiste wymagania kodu klienta i zrozumie konsekwencje rnorodnych decyzji projektowych.
Z dostarczaniem klas z iloci metod przewyszajc t, ktrej klasa rzeczywicie potrzebuje, wi si liczne problemy. Jednym z nich jest rozdty ponad miar projekt. To nie jest
mao znaczca konsekwencja. Gdy serwisant kodw (lub programista piszcy kod klienta)
przeglda kody bezuytecznych funkcji, nie zwraca uwagi na inne istotne szczegy.
Inn kwesti jest tu efektywno dziaania kodu. Jak wida na rysunku 11.18, problem ten
moe sta si zupenie realny. Dla kadego przypisania wejciowego acucha znakw w obrbie ptli programowej potrzebne s dwa wywoania funkcji oraz wywoanie funkcji operatorowej przeciajcej operator przypisania:
1. Wywoanie konstruktora konwersji wobec argumentu funkcji operatorowej
.
2. Wywoanie samej funkcji operatorowej
.
610
funkcji operatorowej.
Pomimo tych wysikw nadal wystpuj znaczne rnice pomidzy traktowaniem obiektw
klas definiowanych przez programist a traktowaniem zmiennych typw wbudowanych.
Gdyby tablice
56 oraz 56 skaday si z elementw typu elementarnego, wewntrz naszej
ptli programowej mogaby si znajdowa tylko jedna, pojedyncza instrukcja. Przy takiej
konstrukcji klasy
wntrze tej ptli programowej reprezentuje co zupenie innego
a trzy wywoania funkcji.
1
(#$ (
SNT (22
=(>#=(> , "
=(>
#;
=(>
Zwr uwag, e kada z takich operacji jest do kosztowna (w sensie czasu wykonania).
Poza tym oprcz wywoa funkcji kada z tych operacji pociga za sob konieczno przydziau pamici na stercie, skopiowania parametru acucha znakw do pamici przydzielonej na stercie, a nastpnie, podczas wywoania destruktora, zwolnienia tej pamici
i jej zwrotu do systemu. Wykonanie tych wszystkich dziaa jest nieuniknione jeden raz
dla operatora przypisania, ktry posuguje si semantyk wartoci w celu utrzymywania pamici
na stercie odrbnie dla swoich dwu operandw. Jednak robi to jeszcze dwa razy? Raz wobec parametru funkcji operatorowej przeciajcej operator przypisania. Drugi raz wobec
wartoci zwracanej z tej funkcji. Wyglda na to, e to zbyt wiele. Aby wyczerpa ten kopotliwy temat do koca dodajmy, e obiekt tworzony przez konstruktor kopiujcy nie jest
uywany przez kod klienta (przypomn, e zwrot obiektu z funkcji zosta wprowadzony
tylko po to, by poprawnie obsugiwa acuchowe operacje przypisania). Zostaje cakowicie
pozostawiony wasnemu losowi, a nastpnie usunity po wywoaniu destruktora.
+R" H+
611
Rysunek 11.19.
Wydruk wyjciowy programu
z listingu 11.7 po dodaniu
drugiej funkcji przeciajcej
operator przypisania
+ R" H+
To samo powinnimy zrobi w pierwszej funkcji przeciajcej operator przypisania z parametrem typu
. Gdy z funkcji zwracane s referencje (wicej na ten temat napisano
w dyskusji o funkcjach w rozdziale 9.), powinnimy postpowa ostronie i zawsze si upewni, e referencja cigle wskazuje na wany (funkcjonujcy) obiekt, ktry pozostanie przy
yciu, gdy dana funkcja zakoczy swoje dziaanie. W tym przypadku nie ma takiego niebezpieczestwa. Referencja, ktra zostaje zwrcona z funkcji operatorowej, jest referencj
do obiektu stanowicego lewostronny operand operatora przypisania w przestrzeni klienta,
np.
56 w podanym przykadzie ptli programowej. Ten obiekt pozostaje przy yciu po
zakoczeniu dziaania funkcji operatorowej, poniewa zosta zdefiniowany w przestrzeni klienta
(zakresie widocznoci nazw klienta). Bd ostrony przy prbach zwrotu referencji do obiektw
zdefiniowanych w przestrzeni nazw serwera, ktre znikaj po powrocie z wywoania funkcji
serwera. Wiele kompilatorw wydrukuje jedynie komunikat ostrzegawczy (ang. warning) i taki
bd moe ci uj bezkarnie.
Wydruk wyjciowy programu z listingu 11.7, w wersji z zastosowaniem dwch funkcji
przeciajcych operator przypisania zwracajcych referencje do obiektu, pokazano na rysunku 11.20.
To wyglda tak, jakbymy zamczyli ju biedny operator przypisania na mier, ale to nie jest
jego dumny koniec. Niektrzy puryci upieraliby si, e to jeszcze nie wystarczy, poniewa
taka konstrukcja nie chroni autora kodu klienta przed koniecznoci wykonywania czynnoci,
ktre przecie nie s niezbdne, jak np. modyfikowanie zawartoci zwrconego obiektu
612
Rysunek 11.20.
Wydruk wyjciowy
programu z listingu 11.7
z zastosowaniem dwch
funkcji przeciajcych
operator przypisania,
zwracajcych referencje
do obiektu klasy String
Ten kod przypisuje jeden obiekt drugiemu obiektowi, zwraca referencj do obiektu docelowego
i natychmiast wysya komunikat nakazujcy zmodyfikowane tego obiektu. Przypisana warto nie zostaje nigdy uyta. Nie ma to szczeglnego sensu, zatem powinno zosta zasygnalizowane jako bd skadniowy. Jeli chcemy, by kompilator rzeczywicie wygenerowa komunikat o bdzie skadniowym, powinnimy t zwracan referencj zadeklarowa jako
referencj do staej (przy uyciu sowa
).
;
- ;
""
#
=> !
O
! 0 &
&
&(.
#
#
5 0 &
&
+ R" H+
+HJ+ )
7
,
Rozwaania praktyczne
jak chcielibymy to zaimplementowa?
Dynamiczne zarzdzanie pamici powinno by obsugiwane w oparciu o wiedz i zrozumienie. Odstpienie na krok od regu w jakimkolwiek kierunku powoduje ryzyko albo spadku
efektywnoci dziaania, albo utraty integralnoci programu.
Wielu programistw uwaa, e zawsze gdy projektujemy klas, ktra dynamicznie zarzdza pamici, musimy wyposay t klas w peny zestaw pomocniczych metod:
n
konstruktor domylny,
konstruktor kopiujcy,
destruktor.
613
Autor nie jest przekonany, czy naley w sposb automatyczny przestrzega takich zalece.
W zalenoci od wymaga kodu klienta moemy potrzebowa tylko czci spord tych
funkcji. Jeli wyposaymy klas w funkcje dokonujce przecienia operatorw z nieprawidowymi interfejsami, wyeliminujemy w ten sposb problem integralnoci programu, ale
jednoczenie pogorszymy efektywno dziaania kodu programu bez adnych racjonalnych
powodw. Autor jest natomiast pewien, e musimy rozumie zagadnienia omwione w niniejszym rozdziale. To zrozumienie pozwoli nam na dobr metod zgodnie z oczekiwanymi
zadaniami do wykonania (tj. zgodnie z wymaganiami kodu klienta) oraz na zaprojektowanie klasy, ktra jednoczenie jest i efektywna, i poprawna. Jeli automatycznie wyposaamy klas w ca t maszyneri, kod klienta wykonuje si poprawnie, ale tracimy ostro widzenia zagadnie i zapominamy o rnicy pomidzy inicjowaniem a przypisaniem. To jest
wrcz niebezpieczne.
Naley zawsze si upewni, e w odniesieniu do klas stosuje si waciwe narzdzia. Jeli
pojawi si problemy, naley przeanalizowa sytuacj, zastosowa komunikaty uatwiajce
ledzenie dziaania programu, narysowa schematyczne diagramy, ale nie przecia klas
komponentami, ktre nie s im niezbdne. Upewnij si, e dobrae odpowiednie narzdzia
do pracy wymagajcej wykonania. Nie kr wok bezproduktywnie z motkiem i gwodziami w rkach (konstruktory, funkcje operatorowe przypisania i inne narzdzia), szukajc oparcia
dopiero na przeciwlegej cianie. Pamitaj, e konstruktor kopiujcy i operator przypisania
su do rozwizywania rnych zada i nie mog by stosowane zamiennie. W przenoni
mona o nich powiedzie, e su jakby do zawieszania obrazkw na przeciwlegych sobie
cianach.
Czsto kod klienta nie potrzebuje moliwoci zainicjowania jednego obiektu przy uyciu
innego obiektu ani przypisania jednego obiektu drugiemu obiektowi. Zamy, e klasa, ktr
mamy zaimplementowa, reprezentuje okno. Dla uproszczenia rozpatrzmy tylko jedno pole
danych, reprezentujce tekst, ktry ma by wywietlany w tym oknie. Taka klasa, powiedzmy
7, jest podobna do klasy
. Zawiera tablic znakow umieszczan na stercie
w dynamicznie przydzielanej pamici, destruktor i funkcj dokonujc przecienia operatora konkatenacji, akceptujc jako argument tablic znakow, ktra ma zosta wywietlona w oknie i dodaje ten tekst do zawartoci okna.
W
7
&
! &
!"
W
# $
#
=$># $ ,
54
/W
,
0
0
2#
=>
! (&
#
2
7 # = 2 %>
(.. )E (
1 ##?@AA 3
%
' &5&'
# ,
&<&
7
, , &<&
)
614
,
Nie ma sensu inicjowanie jednego okna przy uyciu innego ani przypisywanie jednemu obiektowi typu okno innego obiektu tego typu.
W B W
D
6
W % % 2# +W Q V
6+ .
W : # % .
: # % ( !( .
: &
)E B
Nie. Drugi i trzeci wiersz w podanym fragmencie kodu zdecydowanie nie maj sensu. Znakomita wikszo ludzi nie napisaaby czego takiego. Co wicej, argument do funkcji
!zostaje przekazany poprzez warto. Wikszo ludzi (szczeglnie spord tych,
ktrzy czytaj t ksik) nie napisaaby tak. Skoro wikszo ludzi nie pisaaby programu
w taki sposb, czy oznacza to, e klasa 7 mogaby zosta zbudowana bez konstruktora
kopiujcego albo bez przecienia operatora przypisania? Jeli kto (kto najwyraniej nie
czyta tej ksiki) napisa kod podobny do podanego fragmentu, to byby w stanie spowodowa
jednoczenie problemy z zachowaniem i integralnoci, i efektywnoci dziaania programu.
Cho przecie ten kod jest formalnie legalny w C++.
Czy do naszej klasy 7 powinnimy doda obszerne komentarze? Szanowny Programisto,
piszcy kod klienta, nie inicjuj, prosz, obiektw klasy 7 przy uyciu innych obiektw
klasy 7. No i, bardzo, bardzo prosz, nie przekazuj obiektw klasy 7 poprzez
warto jako parametrw do funkcji ani nie zwracaj tych obiektw poprzez warto z funkcji.
Inaczej Twj program bdzie mia kopoty. Mio byoby zrobi co wicej w celu ochrony
kodu klienta.
Jednym ze sposobw jest dodanie do klasy konstruktora kopiujcego i funkcji operatorowej
przeciajcej operator przypisania. Jeli teraz programista tworzcy kod klienta napisze niewaciwy kod, przynajmniej nie bdzie on powodowa problemw z zachowaniem integralnoci programu.
Innym sposobem jest doprowadzenie do takiej sytuacji, by niepodany kod by niedopuszczalny ze skadniowego punktu widzenia. To bardzo ciekawa koncepcja.
615
Istota sprawy polega na tym, by zaprojektowa nasz klas w taki sposb, e prba niewaciwego uycia obiektw tej klasy w kodzie klienta bdzie wychwytywana przez kompilator jako bd skadniowy. To projektant klasy decyduje, jakie zastosowanie jest nieprawidowe. Potem ju nie potrzebujemy adnego komentarza w stylu Szanowny Programisto,
piszcy kod klienta.
Tyle tylko, e to nie jest takie atwe. Moemy usun konstruktor kopiujcy i funkcj przeciajc operator przypisania zdefiniowane przez programist, ale kompilator automatycznie doda do naszej klasy domylne wersje konstruktora kopiujcego i operatora przypisania.
A to s wanie te funkcje, metody dodawane automatycznie przez system, ktre powoduj
powstawanie problemw z zachowaniem integralnoci w przypadku klas z dynamicznym
zarzdzaniem pamici. Aby temu zapobiec, dodajmy do klasy konstruktor kopiujcy i funkcj dokonujc przecienia operatora przypisania zdefiniowane przez programist. Trzeba
to jednake zrobi w taki sposb, by kod klienta nie mg ich zastosowa i by kada prba
ich wywoania powodowaa wystpienie bdu skadniowego.
Czy ju wida, dokd zmierza i prowadzi nas autor? Autor namawia oto czytelnika do napisania funkcji, ktrej kod klienta nie moe wywoa. Jak mona napisa tak funkcj, by kod
klienta nie mg jej wywoywa? Jednym z moliwych rozwiza jest zadeklarowanie tej funkcji jako metody niepublicznej poprzez umieszczenie jej w sekcji prywatnej (lub chronionej).
Na listingu 11.8 przedstawiono takie wanie rozwizanie. Konstruktor kopiujcy i metoda
dokonujca przecienia operatora przypisania zostay zadeklarowane jako prywatne. Po takiej
deklaracji nie musz nawet zosta zaimplementowane. Jeli podany jest tylko prototyp funkcji,
a funkcja zostaje wywoana przez kod klienta, jest to bd konsolidacji (ang. linker error).
W tym przypadku konsolidator nie znajdzie kodu funkcji. Kompilator zaprotestuje, e trzy
ostatnie wiersze kodu w obrbie funkcji s bdne. Jeli deklaracje tej metody dokonujcej przecienia operatora i konstruktora kopiujcego poprzedzimy znakiem komentarza (w ten sposb wyczajc je z kodu), kompilator zaakceptuje ten sam kod klienta, aprobujc
w ten sposb formalnie i przygotowujc do wykonania ten naprawd nierozsdny kod.
Listing 11.8. Przykad prywatnych prototypw w celu wykluczenia nieprawidowego posugiwania si
obiektami po stronie klienta
W
7
! &
W
W-
&
&
&(.
W-
#
W -
!"
W
# $
#
=$># $ ,
54
/W
,
0
0
2#
=>
! (&
#
2
7 # = 2 %>
(.. )E (
1 ##?@AA 3
%
' &5&'
616
,
W % % 2# +W Q V
6J+ .
W : # % . B !5. &5
: # % ( !( . B !5. &5
: &
)E B !5. &5
$
,
To doskonaa metoda, by zapobiec niewaciwemu stosowaniu naszych klas przez programist piszcego kod klienta. Oczywicie jeli taki kod, ktry w obrbie funkcji na
listingu 11.8 zosta opatrzony delikatnym komentarzem nierozsdne, musi by poprawnie
obsugiwany (z jakichkolwiek powodw), a nie ma ogranicze ze strony efektywnoci dziaania, klasa musi zawiera konstruktor kopiujcy i funkcj przeciajc operator przypisania
lub kilka wersji operatorw przypisania, jeli moliwe jest stosowanie wielu typw wyrae
wystpujcych po prawej stronie operatora przypisania. Dopki rozwaany jest taki operator
konwersji (operatory konwersji), powinnimy doczy do klasy stosowne funkcje, skoro
obiekty danej klasy maj by inicjowane za pomoc prostych zmiennych zawierajcych dane,
a nie za pomoc obiektw tego samego typu. Innym uzasadnieniem, by doda do klasy operatory konwersji, jest ch uniknicia koniecznoci stosowania wielokrotnych funkcji dokonujcych przecienia operatorw. W ten sposb zmniejszamy ilo funkcji zawartych
w obrbie klasy, ale w zamian mamy dodatkowe wywoania konstruktora i operacje przydziau dynamicznej pamici.
Podsumowanie
W niniejszym rozdziale przygldalimy si ciemnym stronom potgi C++. Autor nie zamierza przestraszy czytelnika, lecz raczej uwiadomi mu powag sytuacji i skal odpowiedzialnoci programisty piszcego w C++, od ktrego zaley i efektywno dziaania programu, i zachowanie jego integralnoci.
Autor wytoczy kolejne wakie argumenty przeciwko przekazywaniu parametrw poprzez
warto i ma nadziej, e w naszych programach nie bdziemy skonni pj na aden kompromis. Przekazujmy parametry poprzez referencje i stosujmy modyfikator
do wskazania,
e okrelony parametr nie zostanie zmodyfikowany podczas wykonania danej funkcji.
Autor przytacza take argumenty przeciwko zwrotowi obiektw z funkcji poprzez warto.
Jeli musimy zwrci z funkcji obiekt, zwrmy referencje do tego obiektu, upewniwszy
si jednak, e jest to referencja do takiego obiektu, ktry nie zniknie natychmiast po powrocie
z danego wywoania funkcji.
617
Jeli jestemy zdecydowanie przekonani, e kod klienta nie powinien przekazywa obiektw naszej klasy poprzez warto, zadeklarujmy konstruktor kopiujcy jako prywatny poprzez umieszczenie jego prototypu w prywatnej sekcji specyfikacji klasy. W takiej sytuacji
nie ma potrzeby implementowania takiego konstruktora.
Jeli nasza klasa dynamicznie zarzdza pamici, upewnijmy si, e wyposaylimy t klas w destruktor, ktry zwalnia i zwraca do systemu pami na stercie.
Jeli obiekty naszej klasy maj by po stronie kodu klienta wykorzystywane do inicjowania
jednego obiektu przy uyciu innego obiektu tej samej klasy, dodajmy do specyfikacji klasy
konstruktor kopiujcy, ktrego implementacja stosuje semantyk wartoci wobec naszej
klasy i wyposaa kady obiekt w jego wasny, odrbny segment pamici na stercie. W kodzie przeciajcym operator przypisania upewnijmy si, e zapobieglimy wyciekom pamici
poprzez zwrot do systemu tego segmentu pamici na stercie, ktrym dysponowa docelowy
obiekt przed przypisaniem mu nowej wartoci skopiowanej ze rdowego obiektu-parametru.
Upewnijmy si, e ta pami nie zostaje zwrcona przed sprawdzeniem, czy nie mamy tu
do czynienia z samokopiowaniem (czyli przypisaniem obiektowi jego wasnej wartoci). Zdecydujmy, czy chcemy obsugiwa operacje acuchowe. Czsto nasi klienci nie bd mie
takich potrzeb.
Zastosowanie konstruktorw konwersji pozwala na znaczce rozlunienie rygorystycznych
regu kontroli zgodnoci typw w C++. Jako rzeczywisty, biecy argument moemy przekazywa dane innego typu ni typ wymagany przez dan klas, a mimo to kod bdzie
uznawany za formalnie poprawny. To wspaniae, ale stosujmy takie techniki ostronie. Dodatkowe wywoania konstruktorw konwersji s kosztowne (w sensie czasu wykonania), szczeglnie wtedy, gdy musimy stosowa semantyk wartoci.
Oczywicie jeszcze jedno. Upewnijmy si, e rozrniamy, w ktrym miejscu kod klienta
wywouje konstruktor kopiujcy, a w ktrym funkcj operatorow przeciajc operator przypisania. W obu przypadkach operacja po stronie klienta jest oznaczana tym samym
symbolem rwnoci, ale powoduje to wywoanie rnych funkcji po stronie kodu serwera.
Powinnimy wiedzie, ktra z nich jest stosowana w ktrym miejscu.
Autor radzi czsto wraca do materiau, ktry obejmuje niniejszy rozdzia. Rysujmy diagramy
wykorzystania pamici, eksperymentujmy z przykadowymi kodami. Pamitajmy zawsze, e
dynamiczne zarzdzanie pamici w C++ moe atwo przerodzi si w co w rodzaju sonia
w skadzie porcelany albo wycieczki czogiem po ssiedzkich ogrdkach. To le, e do tradycyjnych kategorii bdw programistycznych, bdw skadniowych i bdw semantycznych (w ruchu, ang. run-time errors) C++ dodaje jeszcze jedn kategori bdw. Program
moe by formalnie poprawny skadniowo i rwnoczenie poprawny semantycznie, a mimo
to nadal by nieprawidowy. aden inny jzyk programowania nie obcia programisty
tak ogromn odpowiedzialnoci. Miejmy zawsze pewno, e przyjmujemy t odpowiedzialno z nalenym respektem.
Powodzenia.