Konstruktor (programowanie obiektowe)
Z Wikipedii
Konstruktor w programowaniu obiektowym to specjalna metoda danej klasy, mająca za zadanie utworzyć obiekt tej klasy.
Spis treści |
[edytuj] Zadania konstruktora
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 odpowiadajacą 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++, Javie czy C# - jest to metoda o nazwie zgodnej z nazwą klasy
- w Pascalu - metoda której nazwę poprzedzono słowem kluczowym constructor.
[edytuj] Szczególne rodzaje konstruktorów
W języku C++ wyróżnia się następujące szczególne rodzaje konstruktorów:
[edytuj] Konstruktor domyślny
Konstruktor, który można wywołać bez podawania jakichkolwiek parametrów. Szczególnym przypadkiem konstruktora domyślnego jest konstruktor, w którym wartości wszystkich parametrów mają wartości domyślne, w efekcie czego (w C++) można go wywołać bez podawania ich, np.:
class MojaKlasa { public: MojaKlasa( int parametrDomyslny = 0 ) { // konstruktor domyślny this->dana = parametrDomyslny; } private: int dana; }; int main () { MojaKlasa obiektMojejKlasy; // użyty konstruktor domyślny return 0; }
[edytuj] Zwykły konstruktor
Konstruktor, który można wywołać, podając co najmniej jeden parametr. Jest to zwykły konstruktor stworzony przez twórcę klasy. Jego zadeklarowanie w C++ nie powoduje niejawnego generowania konstruktora domyślnego. Z reguły parametry takiego zwykłego konstruktora spełniają funkcję inicjalizatorów, które przypisują odpowiednie wartości wewnętrznym zmiennym tworzonego obiektu, np. (przykład w C++):
class Wektor { public: Wektor( double x , double y ) { this->x = x; this->y = y; } private: double x; double y; }; int main () { Wektor mojWektor( 3 , 2 ); return 0; }
[edytuj] Konstruktor kopiujący (C++)
Konstruktor, którego jedynym argumentem niedomyślnym jest referencja do obiektu swojej klasy. Jest on używany niejawnie wtedy, gdy działanie programu wymaga skopiowania obiektu (np.: przy przekazywaniu obiektu do funkcji przez wartość). Gdy konstruktor kopiujący nie został zdefiniowany, jest on generowany niejawnie (nawet gdy są zdefiniowane inne konstruktory) i domyślnie powoduje kopiowanie wszystkich składników po kolei, np. (przykład w C++):
class MojaKlasa { public: int dana; MojaKlasa( int parametrDomyslny ) { // inny konstruktor użytkownika this->dana = parametrDomyslny; } }; int main () { MojaKlasa obiektMojejKlasy( 5 ); MojaKlasa kopiaObiektu( obiektMojejKlasy ); // użyty zostanie wygenerowany niejawnie konstruktor kopiujący std::cout << kopiaObiektu.dana; // wyświetli "5" return 0; }
Zablokowanie tego konstruktora (np. przez umieszczenie go w sekcji prywatnej lub chronionej) oznacza brak zezwolenia na kopiowanie obiektu.
[edytuj] Kopiowanie obiektu składnik po składniku
W większości przypadków kopiowanie obiektu składnik po składniku jest tym, czego oczekuje użytkownik klasy i nie ma potrzeby definiowania własnej wersji konstruktora kopiującego. Jednak nie zawsze takie działanie jest pożądane (przykład w C++):
class MojaKlasa { public: int* wsk; // wskaźnik MojaKlasa( int parametrDomyslny ) { this->wsk = new int( parametrDomyslny ); } ~MojaKlasa() { // destruktor delete this->dana; } }; int main () { MojaKlasa obiektMojejKlasy( 5 ); std::cout << *( obiektMojejKlasy.wsk ) << endl; // wyświetli: 5 MojaKlasa kopiaObiektu( obiektMojejKlasy ); // kopiowanie składnik po składniku, wskazanie też zostanie skopiowane *( kopiaObiektu.wsk ) = 3 std::cout << *( obiektMojejKlasy.wsk ) << endl; // wyświetli: 3 return 0; }
- Klasa
MojaKlasa
zawiera polewsk
będące wskaźnikiem na zmienną typuint
. Każdy obiekt tej klasy ma posiadać własną zmienną typuint
którą wskazuje wskaźnikwsk
. W kodzie programu znajduje się deklaracja obiektu klasyMojaKlasa
gdzie następuje wywołanie zdefiniowanego konstruktora, a w nim rezerwacja pamięci na zmienną typu int i przypisanie jej adresu do wskaźnika. Następnie, w celu sprawdzenia wartości, wyświetlana jest wartość zmiennej wskazywanej przez wskaźnik z utworzonego obiektu, wartość jest równa5
. Następnie tworzony jest drugi obiekt klasy o nazwiekopiaObiektu
za pomocą niejawnie wygenerowanego konstruktora kopiującego, konstruktor ten kopiuje wszystkie pola klasy składnik po składniku. W kolejnym kroku przypisywana jest wartość3
zmiennej wskazywanej przez wskaźnik obiektukopiaObiektu
. Następnie wyświetlana jest ponownie wartość zmiennej wskazywanej przez wskaźnik obiektuobiektMojejKlasy
(gdzie wcześniej była wartość5
). Okazuje się, że teraz znajduje się tam wartość3
, oba obiekty wskazują na tę samą zmienną, a w założeniach każdy obiekt miał mieć własną zmienną. Problem wynikł z tego, że nie zdefiniowano konstruktora kopiującego. Kopiowanie składnik po składniku w tym przypadku okazało się rozwiązaniem niezgodnym ze wcześniejszymi założeniami, ponieważ została skopiowana wartość wskaźnika, a nie została utworzona nowa zmienna, do której wskazanie powinno być umieszczone we wskaźniku. W efekcie otrzymaliśmy 2 obiekty wskazujące na ten sam obiekt w pamięci i modyfikacja w jednym z nich miała swój efekt w drugim. Jeszcze większy problem pojawi się w trakcie zakończania programu. Niszczenie pierwszego z dwóch obiektów przebiegnie prawidłowo, destruktor drugiego zwalnianego obiektu będzie wykonywał operację zwolnienia już zwolnionej pamięci, co spowoduje błąd. Aby uzyskać działanie klasy zgodnie z założeniami, należy zaimplementować w klasie MojaKlasa własną wersję konstruktora kopiującego:
// ... MojaKlasa( const MojaKlasa& obiektWzorcowy ) { this->wsk = new int( *( obiektWzorcowy.wsk ) ); // oddzielna rezerwacja pamięci } // ...
- Teraz przy każdym kopiowaniu obiektu nastąpi oddzielne zarezerwowanie pamięci na zmienną typu int i przypisanie jej wartości z obiektu wzorcowego. Gdy zmieniana jest jej wartość w jednym z obiektów, nie nastąpi zmiana w drugim, ponieważ teraz są to dwa różne obszary w pamięci. Podczas niszczenia obiektów każdy z destruktorów zwolni zarezerwowany osobny obszar pamięci i poprzedni błąd z dwukrotnym zwalnianiem pamięci nie wystąpi.
[edytuj] Konstruktor konwertujący (C++)
Konstruktor, którego jedynym argumentem niedomyślnym jest obiekt dowolnej klasy lub typ wbudowany. Powoduje niejawną konwersję z typu argumentu na typ klasy własnej konstruktora. Na przykład (przykład w C++):
class MojaKlasa public: MojaKlasa( int parametr ) { // konstruktor konwertujący z typu int na typ MojaKlasa // ciało konstruktora } }; void funkcja( MojaKlasa obiekt ) { /* ciało funkcji */ } int main () { int zmienna = 5; funkcja( zmienna ); // wywołanie konstruktora konwertującego z int na MojaKlasa return 0; }
Obiekt konwertowanej klasy musi być przekazywany do funkcji przez wartość. Przekazywanie przez referencję spowoduje błąd kompilacji z powodu niezgodności typów. Nie zaleca się stosowania niejawnie takich konwersji. Zmniejszają czytelność kodu oraz mogą spowolnić program (obiekt do funkcji jest przekazywany przez wartość, co wymusza kopiowanie również dla wywołań bez konwersji).
Pozostałe konstruktory są wywoływane zawsze jawnie.
[edytuj] Kolejność wywołań konstruktorów
Kolejność wywołań konstruktorów klasy bazowej, czy też obiektów składowych danej klasy, jest określona kolejnością:
- Konstruktory klas bazowych w kolejności w jakiej znajdują się w sekcji dziedziczenia w deklaracji klasy pochodnej.
- Konstruktory obiektów składowych klasy w kolejności, w jakiej obiekty te zostały zadeklarowane w ciele klasy.
- Konstruktor klasy.
W Object Pascalu konstruktor może być dziedziczony i wirtualny, i ze względu na brak dziedziczenia wielokrotnego oraz konieczność dziedziczenia od klasy bazowej (TObject) nie istnieje problem kolejności wywołań konstruktorów.
[edytuj] Właściwości i ciekawostki
- W większości języków konstruktor nie może być wirtualny (w efekcie czego nie może być metodą czysto wirtualną).
- Konstruktor nie może być statyczny.
- W klasie, gdzie zadeklarowany jest konstruktor kopiujący, powinien być zadeklarowany dowolny inny konstruktor (domyślny lub inny), ponieważ nie byłoby możliwe stworzenie obiektu danej klasy. Aby stworzyć obiekt korzystając z konstruktora kopiującego, należałoby posiadać inny egzemplarz obiektu danej klasy, który nie może być utworzony, ponieważ jego stworzenie również wymagałoby egzemplarza danej klasy itd.
- W klasie, gdzie wymagane jest istnienie: konstruktora kopiującego lub destruktora lub operatora przypisania, wymagane jest najczęściej istnienie wszystkich trzech.
- Parametr konstruktora kopiującego nie może być przekazywany przez wartość, ponieważ powodowałoby to nieskończone wywołanie konstruktorów kopiujących. Dla potrzeb wywołania konstruktora należałoby wykonać kopię obiektu. Aby wykonać kopię obiektu należy wywołać jego konstruktor kopiujący, któremu również należy przekazać obiekt przez wartość, a więc wykonać jego kopię, itd. Błąd ten nie przejdzie procesu kompilacji, kompilator rozpoznaje taki przypadek i generuje sygnał błędu. Nie jest możliwe wygenerowanie nieskończonej pętli wywołań, ponieważ ciąg takich wywołań miałby teoretycznie nieskończoną długość i spowodowałby zablokowanie kompilatora.
- Aby uniemożliwić stworzenie obiektu danej klasy należy:
- Działanie takie stosuje się, gdy na przykład klasa ma służyć jako zbiór metod i pól statycznych i nie jest potrzebny jakikolwiek egzemplarz obiektu danej klasy (również jako klasy bazowej).
- W Object Pascalu jest stosowane inne podejście do konstruktora i żadne z powyższych problemów nie występują. Konstruktor jest metodą i tak jak każda inna metoda klasy podlega dziedziczeniu, a także może być wirtualny. Konstruktor różni się od innych metod tylko dodawaniem przez kompilator kodu tworzącego obiekt. Podejście to sprawia, że konstruktor tworzony w klasie abstrakcyjnej zazwyczaj nie wymaga pokrycia w klasach konkretnych.