Wykład 4
Konstruktor – cechy
główne
Co robi konstruktor?
Wywołanie konstruktora powoduje wykonanie następujących zadań:
• obliczenie rozmiaru obiektu
• alokacja obiektu w pamięci
• wyczyszczenie (zerowanie) obszaru pamięci zarezerwowanej dla
obiektu (tylko w niektórych językach)
• wpisanie do obiektu informacji łączącej go z odpowiadającą mu
klasą (połączenie z metodami klasy)
• wykonanie kodu klasy bazowej (w niektórych językach nie
wymagane)
• wykonanie kodu wywołanego konstruktora
Z wyjątkiem ostatniego punktu powyższe zadania są wykonywane
wewnętrznie i są wszyte w kompilator lub interpreter języka, lub
w niektórych językach stanowią kod klasy bazowej.
W językach programowania w różny sposób oznacza się konstruktor:
• w C++, PHP4, Javie i in. - jest to metoda o nazwie zgodnej z
nazwą klasy
• w Pascalu - metoda której nazwę poprzedzono słowem kluczowym
constructor.
• w PHP 5 - metoda o nazwie __construct
Uwagi ogólne do konstruktorów:
1. Konstruktor
NIE MUSI
wystąpić w opisie klasy, czyli
obiekty nie muszą być jawnie wprowadzane
konstruktorem.
2. Nazwa konstruktora może być przeładowana
, czyli
stosowana wielokrotnie w opisie klasy z różnymi
listami argumentów. Wtedy kompilator odróżnia
konstruktory po listach argumentów, tak, jak w
przypadku
przeładowanych
nazw
funkcji.
Konstruktorów może więc być wiele.
3. Konstruktor
może
być
wywoływany
(a
nie
deklarowany!!) bez żadnych argumentów
. Jest to tak
zwany konstruktor domniemany. Czasem nazywamy
go domyślnym albo standardowym. Ze względu na
istotę przeładowania nazwy konstruktor domniemany
czyli bezargumentowy może wystąpić tylko raz. Jeśli
nie deklarujemy w klasie żadnego konstruktora, to
kompilator sam ustanawia właśnie konstruktor
domniemany do obsługi obiektów w programie. Każdy
konstruktor z argumentami, którym nadamy wartości
domyślne
czyli
niedefiniowalne
jest
także
konstruktorem domniemanym.
•
Konstruktor jest zwykle deklarowany jako
publiczny, bo przecież wprowadzane nim
obiekty mogą być używane przez klasy
zewnętrzne, a ponadto jest funkcją, która
MUSI być dostępna dla składników klasy.
Możemy jednak dla konstruktora
przewidzieć ochronę tak, jak dla klas za
pomocą etykiet private lub protected.
Wówczas jednak także konstruowane
obiekty będą dostępne tylko w obrębie
klasy z tym konstruktorem jako private
albo jako protected tylko w zakresie klas
dziedziczących.
Konstruktor może zamiast definiować
obiekty podawać kopie obiektów zawartych
w innej klasie. Wtedy jest to tak zwany
konstruktor kopiujący
.
Konstruktor może dokonywać konwersji
typu obiekty z jednego w drugi. Nazywamy
go wtedy
konstruktorem konwertującym
.
Konstruktor kopiujący
Przyjrzyjmy się wywołaniu konstruktora klasy o nazwie klasa:
klasa::klasa(klasa&)
Jego argumentem jest referencja do obiektu danej klasy. Czyli do
elementu, który w chwili uruchomienia tego konstruktora już
istnieje. 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)
Konstruktor kopiujący c.d.
• 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. Jest to tzw. kopiowanie płytkie.
2.W sytuacji kiedy funkcja zwraca przez wartość obiekt
klasy X. Wtedy także tworzona jest kopia obiektu. To
także jest kopiowanie płytkie.
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ść)
Konstruktor kopiujący c.d.
•
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.
Kopiowanie płytkie i głębokie
Wyróżniamy dwa typy kopiowania obiektów
zawierających pola będące wskaźnikami
• Kopiowanie płytkie
a. Kopiowanie wszystkich składowych (w tym
wskaźników)
b. Kopiowane są wskaźniki, a nie to, na co wskazują
• Kopiowanie głębokie
a. Alokacja nowej pamięci dla wskaźników
b. Kopiowanie zawartości wskazywanej przez
wskaźniki w nowe miejsce
c. Kopiowanie pozostałych pól, nie będących
wskaźnikami
Głębokie kopiowanie
Kiedy obiekt zawiera wskaźnik do dynamicznie zaalokowanego
obszaru, należy zdefiniować operator przypisania
wykonujący głębokie kopiowanie
•
W rozważanej klasie należy zdefiniować operator
przypisania:
AType& AType::operator=(const AType& otherObj)
•
Operator przypisania powinien uwzględnić przypadki
szczególne:
1.
Sprawdzić przypisanie obiektu do samego siebie, np. A=A:
2.
if (this == &otherObj) // if true, do nothing
3.
Skasować zawartośc obiektu docelowego
4.
delete this->...
5.
Zaalokować pamięć dla kopiowanych wartości
6.
Przepisać kopiowane wartości
7.
Zwrócic *this
Konstruktor kopiujący a
operator przypisania
Konstruktor kopiujący jest więc używany do
stworzenia nowego obiektu
• Wydaje się prostszy od operatora przypisania -
nie musi sprawdzać przypisania do samego
siebie i zwalniać poprzedniej zawartości
• Jest użyty do skopiowania parametru aktualnego
do parametru formalnego przy przekazywaniu
parametru przez wartość
• Przy tworzeniu nowego obiektu, można go
zainicjalizować istniejącym obiektem danego
typu. Wywołany jest wówczas konstruktor
kopiujący.
Konstruktor kopiujący a
operator przypisania c.d.
• int main() {
• list a;
• //...
• list b(a); //copy constructor is
called
• list c=a; //copy constructor is called
• };
1.#include<iostream>
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. ( czy to 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:
5.{public:char*p; X(char*);
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-28:
26.X::X(char*s)
27.{p=new char[80]; if(p)strcpy(p,s);
28.}
Wiersz 8-9: analogiczny jak wiersz 5 ale dla klasy Y
8.{public:
9.char*p; Y(char*);
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.
13.{
14.X x("xxx"); X j=x; //powolanie do zycia obiektow
x,j klasy X
Wiersz 13-14: 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.
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;
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;
Wiersz 15-17: do tablicy wskazywanej przez
wskaźnik p wpisujemy poprzez kopiowanie
łańcucha wartość ’’111” ale przedtem
sprawdzamy adresy obiektów.
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, czy nie?
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;
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 21: modyfikujemy łańcuch w obiekcie d.
Wiersz 22: 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ę.
Wydruk adresów obiektów y oraz d. Powinny być różne!!
26.X::X(char*s)
27.{p=new char[80]; if(p)strcpy(p,s);
28.}
Wiersz 26-28: 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.
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.}
Konstruktory klasy Y. Konstruktor kopiujący powiela
postać konstruktora poza wskazaniem, że dozwala
na kopiowanie obiektów klasy Y do wskaźnika p
spod adresy każdego obiekty klasy Y.
Rezultat na ekranie (przykładowy):
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
Jakie mamy więc metody
tworzenia obiektów?
Zmienne automatyczne
• Atype a; //konstruktor domyślny
Zmienne automatyczne z argumentami
• Atype a(3); //konstruktor z parametrem int
Przekazywanie parametrów funkcji przez wartość
• void f(Atype b) {...} …..
• Atype a; //konstruktor domyślny
• f(a); //konstruktor kopiujący
Przypisanie wartości zmiennym
• Atype a,b;…..
• a=b; //operator przypisania
Inicjalizacja nowych obiektów
• Atype b; //konstruktor domyslny
• Atype a=b; //konstruktor kopiujący (NIE operator
przypisania)
Zwracanie wartości z funkcji
• Atype f() {
• Atype a; //konstruktor domyślny
• return a; //konstruktor kopiujący
• }
Cechy (zalecane) poprawnie
napisanej klasy
Jawny konstruktor
• Gwarantuje, że każdy zadeklarowany egzemplarz obiektu zostanie
w kontrolowany sposób zainicjalizowany
Jeżeli obiekt zawiera wskaźniki do dynamicznie zaalokowanej
pamięci:
A. Jawny destruktor:
• Zapobiega wyciekom pamięci. Zwalnia zasoby podczas usuwania
obiektu.
B. Jawny operator przypisania
• Używany przy przypisywaniu nowej wartości do istniejącego
obiektu.
• Zapewnia, że obiekt jest istotnie kopią innego obiektu, a nie jego
aliasem (inną nazwą).
C. Jawny konstruktor kopiujący
• Używany podczas kopiowania obiektu przy przekazywaniu
parametrów, zwracaniu wartości i inicjalizacji. Zapewnia, że obiekt
jest istotnie kopią innego obiektu, a nie jego aliasem.