Wykład 4
Funkcje zaprzyjaźnione
i konstruktory
kopiujące
Funkcje zaprzyjaźnione
To takie funkcje, które, mimo, że nie są składnikami klasy, to
mają dostęp do jej składników czyli innych funkcji, zmiennych i
obiektów. Mają dostęp także do tych składników klasy, które są
hermetyzowane etykietą private. Pamietajmy, że jeśli nie ma innych
etykiet, to wszystkie składniki są private. Funkcja zaprzyjaźniona
jest wprowadzana instrukcją friend.
Sposób stosowania:
class figura{
int x,y;
…….
friend void goniec(figura&)
};
Sama funkcja goniec(figura&) jest zdefiniowana gdzieś w
programie w całkowicie innym miejscu nie powiązanym z
klasą pionek. W klasie figura {} chcemy z niej skorzystać
nawet, jeśli przynależy ona do innej klasy. Wtedy poprawnie
jest taką funkcję zaznaczyć etykietą public w jej klasie.
Cechy funkcji zaprzyjaźnionych:
*Funkcja może być zaprzyjaźniona z kilkoma klasami.
*Na argumentach jej wywołania może wykonywać operacje
zgodnie ze swoją definicją.
*Może być napisana w zupełnie innym języku niż C++ i
dlatego może nie być funkcją składową klasy.
*Ponieważ funkcja typu friend nie jest składnikiem klasy to nie ma
wskaźnika this, czyli musi się posłużyć operatorem wskaźnika, albo
przypisania aby wykonać działania (także te na składniku klasy, z
którą jest zaprzyjaźniona).
*Jest deklarowana w klasie ze słowem instrukcji friend i nie
podlega etykietom hermetyzcji (public, private, protected).
*Może być cała zdefiniowana w klasie i wtedy jest typu inline ale
nadal jest funkcją zaprzyjaźnioną.
*Nie musi być funkcją składową żadnej klasy ale może nią być.
*Klasa może się przyjaźnić z wieloma funkcjami, które są lub nie są
składnikami innych klas.
*Funkcje zaprzyjaźnione nie są przechodnie, to znaczy, że „przyjaciel
mego przyjaciela nie jest moim przyjacielem” czyli zaprzyjaźnienie
nie przenosi się od klasy do klasy.
*Zaprzyjaźnienie nie podlega mechanizmowi dziedziczenia.
*Z zasady umieszcza się funkcje zaprzyjaźnione na początku
wszystkich deklaracji w klasie.
Destruktor
Destruktorem klasy o nazwie X jest funkcja o nazwie ~X.
Jej deklaracja nie jest konieczna.
Za każdym razem,
gdy jest likwidowana klasa X automatycznie uruchamiany
jest destruktor. Destruktor nie likwiduje jednak obiektów
ani nie zwalnia fizycznie pamięci operacyjnej, która była
zajmowana przez składniki klasy X. Przygotowuje on
natomiast składniki klasy X do likwidacji prowadząc do
zablokowania ich czynności lub dostępu do nich.
Jeśli konstruktor lub obiekt klasy posługiwał się
operatorem new w celu dynamicznego przydzielenia
pamięci, to destruktor powinien zawierać operator
delete.
Jako funkcja destruktor nie może zwracać żadnych
wartości czyli jest typu void.
Musi być wywoływany bez argumentów, czyli nie może
być przeładowany. Czyli do każdego konstruktora
uruchomi się „jego” destruktor.
Jest uruchamiany automatycznie wtedy, kiedy obiekt
wychodzi ze swojego zakresu ważności, poza sytuacją,
kiedy obiekt jest typu static albo const. Taki obiekt jest
likwidowany destruktorem po zakończeniu programu.
Także wówczas, gdy obiekt typu static jest wywoływany
przez referencje lub wskaźnik wyjście obiektu z jego z
zakresu ważności nie uruchamia destruktora.
Konstruktor kopiujący
Przyjrzyjmy się wywołaniu konstruktora klasy o nazwie klasa:
klasa::klasa(klasa&)
Jego argumentem jest referencja do obiektu danej klasy. Taki
konstruktor nie konstruuje obiektu tylko tworzy kopię innego, który
już istnieje wśród obiektów klasy. Pozostałe argumenty konstruktora
są domniemane. Przykładami konstruktora kopiującego mogą być:
X::X(X&)
lub
X::X(X&, float=3.1415, int=0)
Taki konstruktor wprowadza obiekty identyczne z już
istniejącymi, czyli ich kopie.
Taki konstruktor może być wywołany przez program
niejawnie:
1.W sytuacji gdy do funkcji jest
przez wartość
przesyłany
obiekt klasy X. Wówczas tworzona jest kopia tego
obiektu.
2.W sytuacji kiedy funkcja zwraca przez wartość obiekt
klasy X. Wtedy także tworzona jest kopia obiektu.
To, że konstruktor kopiujący podaje obiekt kopiowany
przez referencję daje mu możliwość
zmiany zawartości
obiektu klasy!!
(patrz przesyłanie argumentu do funkcji
przez wartość)
Nie można pominąć referencji w konstruktorze
kopiującym, bo gdyby konstruktor X wywoływał obiekty
swojej klasy X przez wartość, czyli wytwarzałby swoją
kopię, to powstaje nie zamknięta pętla tworzenia kopii.
Konstruktor z przyczyn logiki języka otrzymuje więc
warunki do tego aby uszkodzić oryginał!!
Zabezpieczamy się przed taką sytuacją następująco:
X::X(const X&obiekt)
Teraz konstruktor X wie, że obiekt klasy X musi być
wywoływany jako stały. Konstruktor kopiujący jest
domyślnie typu const, czyli nie może zmienić sam siebie.
1.#include<iostream.h>
2.#include<string.h>
3.#include<conio.h>
4.class X
5.{public:char*p; X(char*);
6.};
7.class Y
8.{public:
9.char*p; Y(char*);
10.
Y(Y&);
// deklaracja konstruktora kopiajacego obiekty
klasy Y
11.};
12.void main()
13.{
14.X x("xxx"); X j=x; //powolanie do zycia obiektow
x,j klasy X
15.cout<<"\nx="<<x.p<<", j="<<j.p; // wydruk wskaznika czyli
adresu do obiektow x,j
16.strcpy(j.p,"111"); // skopiowanie pod wskaznik obiektu j lancucha
111
17.cout<<"\nx="<<x.p<<", j="<<j.p;
Przykład: konstruktor kopiujący będzie kopiował
wskaźnik do obiektu. (tzw. kopiowanie głębokie)
18.cprintf("\n\rx.p=%p, j.p=%p,x.p,j.p);
19.Y y("yyy"); Y d=y;
powołanie obiektów klasy Y
20.cout<<"\ny="<<y.p<<", d="<<d.p;
21.strcpy(y.p,"222");
22.cout<<"\ny="<<y.p<<", d="<<d.p;
23.cprintf("\n\ry.p=%p, d.p=%p,y.p,d.p);
24.getch();
25.}
26.X::X(char*s)
27.{p=new char[80]; if(p)strcpy(p,s);
28.}
29.Y::Y(char*s)
30.{p=new char[80]; if(p)strcpy(this->p,s);
31.}
32.Y::Y(Y&y)
33.{p=new char[80]; if(p)strcpy(p,y.p);
34.}
Omówienie przykładu:
Wiersz 5: etykieta public dla klasy X oraz deklaracje
zmiennej własnej p, która jest wskaźnikiem do zmiennej
znakowej oraz
konstruktor obiektów klasy X oczekującego na liście
parametrów formalnych wskaźnika do zmiennej typu
string lub charakter. Ciało tego konstruktora jest podane
w wierszu 26.
Wiersz 8: analogiczny jak wiersz 5 ale dla klasy Y
Wiersz 9: konstruktor kopiujący klasy Y. Będzie on
kopiował wskaźnik do zmiennej znakowej, którą
wskaże. Może to być zmienna z innej klasy. Na tym
polega kopiowanie głębokie. W klasie X funkcjonuje
konstruktor kopiujący domyślny tworzony podczas
kompilacji. Daje on kopiowanie płytkie, czyli dotyczące
tylko składników własnej klasy X.
Wiersz 13: tworzymy obiekt x oraz obiekt j klasy X. Do
obiektu x wpisywany jest element tablicy zarezerwowanej
dla niego przez konstruktor w wierszu 26. Obiekt j jest
inicjalizowany obiektem x. Kopiowanie x do j jest
realizowane przez konstruktor domyślny klasy X.
Przepisuje on wskaźnik do obiektu x do wskaźnika do
obiektu j. Dlatego wskaźnik p w obiekcie j będzie
wskazywał to samo miejsce co wskaźnik p w obiekcie x.
Dlatego wydruk w wierszu 14 powinien podać ten sam
wynik dla każdego z tych obiektów.
Zauważmy, że obiekt j nie ma zarezerwowanej swojej
przestrzeni na tablice znakową, korzysta natomiast ze
zmiennej wskaźnikowej własnej p z klasy X do
podłączenia się do tej samej tablicy co obiekt x. Dlatego
pojawia się szczególny zapis obiektów x oraz j połączony
ze zmienną własną wskaźnikową p.
Wiersz 15: do tablicy wskazywanej przez wskaźnik p
wpisujemy poprzez kopiowanie łańcucha wartość ’’111”.
Wiersz 16: wydruk wartości obiektu x oraz j
wskazywanych przez zmienną p
Wiersz 17: wydruk adresów wskazywanych przez p dla
obiektu x oraz j. Te adresy powinny być jednakowe.
Wiersze 18-22: powtórzenie takich samych działań ale dla
klasy Y. Wprowadzamy obiekty y oraz d, które grają takie
same role jak poprzednio x oraz j.
Wiersz 20: modyfikujemy łańcuch w obiekcie d.
Wiersz 21: drukujemy wartości obiektów y oraz d nie
spodziewając się ich identyczności jak poprzednio dla x
oraz j. Dlaczego? Dlatego, że konstruktor Y działa przez
referencję, a nie poprzez przypisanie jak konstruktor
kopiujący domyślny. Łańcuch d jest modyfikowany
tylko w miejscu d. Konstruktor Y zapewnia modyfikację
poprzez referencję.
Wiersz 22: wydruk adresów obiektów y oraz d. Powinny
być różne!!
Wiersz 25-27: ciało konstruktora obiektów klasy X.
Operatorem new jest dynamicznie przydzielona pamięć
dla tablicy 80cio znakowej. Kopiowanie łańcucha z listy
parametrów formalnych konstruktora do tablicy nastąpi
tylko wtedy, kiedy operator new tę pamięć przydzieli.
Rezultat na ekranie:
x=xxx, j=xxx
x=111, j=111
x.p=2707:0004, j.p=2707:0004
y=yyy, d=yyy
y=yyy, d=222
y.p=270D:0004, d.p=2713:0004