Konstruktor (programowanie obiektowe)
Konstruktor – specjalna metoda danej klasy, wywoływana podczas tworzenia jej instancji. Podstawowym zadaniem konstruktora jest zainicjowanie obiektu, a w niektórych językach programowania także utworzenie obiektu.
Zadania konstruktora
edytujWywoł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 niewymagane),
- wykonanie kodu wywołanego konstruktora.
Z wyjątkiem ostatniego punktu powyższe zadania są wykonywane wewnętrznie i są wbudowane w kompilator lub interpreter języka, a w niektórych językach stanowią kod klasy bazowej.
W językach programowania w różny sposób oznacza się konstruktor:
Rodzaje konstruktorów w języku C++
edytujW języku C++ wyróżnia się następujące szczególne rodzaje konstruktorów:
Konstruktor domyślny
edytujKonstruktor, 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;
}
Zwykły konstruktor
edytujKonstruktor, 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;
}
Konstruktor kopiujący (C++)
edytujKonstruktor, 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. (w języku 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.
Kopiowanie obiektu składnik po składniku
edytujW 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->wsk;
}
};
int main (){
MojaKlasa obiektMojejKlasy(5);
std::cout << *(obiektMojejKlasy.wsk) << std::endl; // wyświetli: 5
MojaKlasa kopiaObiektu(obiektMojejKlasy); // kopiowanie składnik po składniku, wskazanie też zostanie skopiowane
*(kopiaObiektu.wsk) = 3;
std::cout << *(obiektMojejKlasy.wsk) << std::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 to samo miejsce 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 klasieMojaKlasa
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.
Konstruktor konwertujący (C++)
edytujKonstruktor, 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.
Rodzaje konstruktorów w języku Object Pascal
edytujKonstruktor domyślny
edytujJest to konstruktor dziedziczony kolejno przez wszystkich przodków danej klasy, począwszy od przodka ultymatywnego (tj. pierwotnego, będącego przodkiem wszystkich klas). Ma on nazwę Create i nie pobiera żadnych argumentów (konstruktor bezargumentowy). Jeżeli w danej klasie zostanie zdefiniowany nowy konstruktor, to automatycznie zastępuje on konstruktor domyślny. Wtedy konstruktor domyślny nie jest dostępny do bezpośredniego wywołania, ale można go wywołać z wnętrza tego nowego konstruktora (inherited Create). Jeśli w klasie zostanie zdefiniowane co najmniej dwa konstruktory, to muszą one zostać oznaczone jako przeciążone (overload). Jeśli te nowe konstruktory pobierają argumenty, to wtedy konstruktor domyślny nadal jest dziedziczony. Może to być niepożądane w sytuacji, gdy klasa pobiera dane w konstruktorze, bez których jej działanie będzie nieprawidłowe (wstrzykiwanie zależności). W takim przypadku wywołanie konstruktora domyślnego miałoby nieprzewidywalne skutki dla działania klasy. Można wtedy oznaczyć konstruktor domyślny jako nie przewidziany do używania (deprecated) lub dodać klasę pośredniczącą w gałęzi dziedziczenia, posiadającą jeden konstruktor pobierający argumenty, co spowoduje niewidoczność konstruktora domyślnego[1].
Kolejność wywołań konstruktorów
edytujKolejność 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. Ze względu na brak dziedziczenia wielokrotnego oraz konieczność dziedziczenia od klasy bazowej (TObject) nie istnieje problem kolejności wywołań konstruktorów.
Właściwości
edytuj- W większości języków konstruktor nie może być wirtualny (w efekcie czego nie może być metodą czysto wirtualną), z wyjątkiem języka Object Pascal.
- Konstruktor nie może być statyczny (wyjątek: język C#). Oznacza to jedynie, że nie umieszcza się przed nim słowa kluczowego static, bo w praktyce konstruktor zachowuje się jak zwykła metoda statyczna, której wywołanie poprzedza zwykle operator new.
- 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, destruktora lub operatora przypisania, wymagane jest najczęściej istnienie wszystkich trzech[2].
- 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.
Zobacz też
edytujPrzypisy
edytuj- ↑ Delphi - How to hide the inherited TObject constructor while the class has overloaded ones? [online], Stack Overflow [dostęp 2021-06-15] .
- ↑ Andrew Koening, Barbara E. Moo: C++ Made Easier: The Rule of Three. Dr.Dobb's, 2001-06-01. [dostęp 2013-11-27]. (ang.).