Konstruktor (programowanie obiektowe)

specjalna metoda wołana podczas tworzenia instancji danej klasy w programowaniu obiektowym

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

edytuj

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 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:

  • w C++, Javie, C#, PHP 4 i innych – jest to metoda o nazwie zgodnej z nazwą klasy,
  • w Object Pascalu – metoda, której nazwę poprzedzono słowem kluczowym constructor,
  • w PHP 5 – metoda o nazwie __construct,
  • w Pythonie – metoda o nazwie __init__,
  • w języku Swift – metoda o nazwie init.

Rodzaje konstruktorów w języku C++

edytuj

W języku C++ wyróżnia się następujące szczególne rodzaje konstruktorów:

Konstruktor domyślny

edytuj

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;
 }

Zwykły konstruktor

edytuj

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;
}

Konstruktor kopiujący (C++)

edytuj

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. (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

edytuj

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->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 pole wsk będące wskaźnikiem na zmienną typu int. Każdy obiekt tej klasy ma posiadać własną zmienną typu int którą wskazuje wskaźnik wsk. W kodzie programu znajduje się deklaracja obiektu klasy MojaKlasa 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ówna 5. Następnie tworzony jest drugi obiekt klasy o nazwie kopiaObiektu 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 obiektu kopiaObiektu. Następnie wyświetlana jest ponownie wartość zmiennej wskazywanej przez wskaźnik obiektu obiektMojejKlasy (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 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.

Konstruktor konwertujący (C++)

edytuj

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.

Rodzaje konstruktorów w języku Object Pascal

edytuj

Konstruktor domyślny

edytuj

Jest 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

edytuj

Kolejność wywołań konstruktorów klasy bazowej, czy też obiektów składowych danej klasy, jest określona kolejnością:

  1. Konstruktory klas bazowych w kolejności w jakiej znajdują się w sekcji dziedziczenia w deklaracji klasy pochodnej.
  2. Konstruktory obiektów składowych klasy w kolejności, w jakiej obiekty te zostały zadeklarowane w ciele klasy.
  3. 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:
    • zadeklarować wszystkie konstruktory w sekcji prywatnej (konstruktor kopiujący może, ale nie musi spełniać tego warunku),
    • klasa nie może deklarować przyjaźni z klasą ani funkcją.
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ż

edytuj

Przypisy

edytuj
  1. Delphi - How to hide the inherited TObject constructor while the class has overloaded ones? [online], Stack Overflow [dostęp 2021-06-15].
  2. Andrew Koening, Barbara E. Moo: C++ Made Easier: The Rule of Three. Dr.Dobb's, 2001-06-01. [dostęp 2013-11-27]. (ang.).