Definiując zmienne obiektowe klasy należy określić, jaki konstruktor ma być wywołany dla kreowanego obiektu. Robi się to zwykle podając w nawiasach argumenty dla konstruktora. Jeśli ma to być konstruktor domyślny, czyli bezargumentowy, to nawiasy czasem można umieścić, czasem trzeba umieścić, a czasem nie wolno umieszczać w definicji!
Rozpatrzmy na przykładzie sposoby tworzenia obiektów i kolejność wywoływania konstruktorów i destruktorów.
W poniższym programie definiujemy klasę
Klasa.
Ma ona konstruktor z jednym parametrem całkowitym (➋).
W takim razie żaden konstruktor bezparametrowy (domyślny) nie jest
generowany automatycznie. Zatem, jeśli chcemy, żeby taki konstruktor
istniał, to musimy sami go zdefiniować (➊).
1. #include <iostream> 2. using namespace std; 3. 4. class Klasa { 5. static char ID; 6. int a; 7. char id; 8. public: 9. Klasa() { ➊ 10. id = ID++; 11. a = 0; 12. cout << "Ctor() " << id << a << endl; 13. } 14. 15. Klasa(int aa) { ➋ 16. id = ID++; 17. a = aa; 18. cout << "Ctor(int) " << id << a << endl; 19. } 20. 21. ~Klasa() { ➌ 22. cout << "Dtor " << id << a << endl; 23. } 24. }; 25. char Klasa::ID = 'A'; ➍ 26. 27. Klasa k1; // <- A 28. //Klasa ka(); // NIE! 29. 30. int main() { 31. cout << "Wchodzimy do funkcji \'main\'" << endl; 32. 33. // Klasa kb = Klasa; // NIE! 34. { 35. Klasa k3 = Klasa(); // <- C 36. Klasa k4 = Klasa(4); // <- D 37. } 38. 39. Klasa* pk5 = new Klasa; // <- E 40. Klasa* pk6 = new Klasa(); // <- F 41. Klasa* pk7 = new Klasa(7); // <- G 42. 43. delete pk6; 44. delete pk7; 45. 46. cout << "Wychodzimy z funkcji \'main\'" << endl; 47. } 48. 49. Klasa k2(2); // <- B
Pola klasy są typu int (składowa a) i typu char (składowa id). Są to pola niestatyczne, a więc odpowiednie składowe będą istnieć w każdym obiekcie klasy. Prócz tego deklarujemy w klasie pole statyczne, też typu char o nazwie ID. Zgodnie z tym, co mówiliśmy na temat składowych statycznych klasy, deklaracja pola statycznego nie wystarczy: trzeba tę zmienną statyczną jeszcze zdefiniować poza klasą (czyli spowodować przydzielenie dla niej pamięci). Robimy to w linii ➍.
Oba konstruktory zawierają instrukcję ' id = ID++;', która w składowej id tworzonego obiektu zapamiętuje aktualną wartość zmiennej statycznej ID (istniejącej w jednym egzemplarzu), po czym zwiększa tę zmienną o jeden, zmieniając znak będący wartością tej zmiennej na następny według kolejności kodów ASCII (zmienna była zainicjowana kodem ASCII litery 'A' w linii ➍). W ten sposób każdy tworzony obiekt będzie miał unikalną składową znakową id identyfikującą ten obiekt. Oba konstruktory i destruktor (➌) drukują komunikat, pozwalający nam śledzić kolejność, w jakiej obiekty są tworzone i usuwane.
Zobaczmy najpierw, jak obiekty można tworzyć.
W linii A, za pomocą instrukcji ' Klasa k1;' tworzymy zmienną globalną k1. Zauważmy, że taka definicja nie różni się składniowo niczym od definicji zmiennej typu wbudowanego, na przykład ' int k;' — najpierw podajemy nazwę typu, w tym przypadku jest to Klasa, a następnie nazwę deklarowanej/definiowanej zmiennej. Przy tworzeniu obiektu użyty zostanie konstruktor domyślny (bezargumentowy), bo żaden argument nie został podany. Przy tej formie definiowania zmiennej obiektowej z wykorzystaniem konstruktora domyślnego nie wolno umieszczać nawiasów: wykomentowana następna linia (' Klasa ka();') byłaby nielegalna. Jest tak dlatego, że byłaby ona niejednoznaczna, nie dałoby się bowiem odróżnić tej definicji zmiennej od deklaracji funkcji bezparametrowej o nazwie ka i typie zwracanym Klasa (standard mówi, że jeśli coś może być zinterpretowane jako deklaracja, to jest deklaracją).
W ostatniej lini w podobny sposób tworzymy obiekt k2 za pomocą instrukcji ' Klasa k2(2);'. Teraz użyty ma być konstruktor jednoparametrowy, zatem argument dla tego konstruktora podajemy w nawiasie, tak jakby k2 było nazwą wywoływanej funkcji. Obecność nazwy typu po lewej stronie powoduje jednak, że niejednoznaczności nie ma — składnia tej instrukcji nie odpowiada żadnej możliwej formie wywołania funkcji. Zauważmy, że k2 nie jest tu wcale nazwą konstruktora, który przecież nazywa się Klasa, a jest nazwą tworzonego obiektu!
W linii C widzimy inną formę definicji obiektu: ' Klasa k3 = Klasa();'. Teraz to nie nazwa tworzonego obiektu, a nazwa typu (klasy) występuje tak, jakby była nazwą wywoływanej funkcji. Zauważmy, że k3 jest tu nazwą obiektu, a nie referencji czy wskaźnika do obiektu, jak w Javie (w Javie obiekty w ogóle nie mają nazw — nazwy mają tylko odnośniki do nich). Ponieważ w nawiasie nie podaliśmy żadnych argumentów, użyty będzie konstruktor domyślny. Przy tej formie tworzenia obiektów (poprzez nazwę klasy a nie obiektu) z wykorzystaniem konstruktora domyślnego trzeba użyć nawiasów — zatem wykomentowana linia ' Klasa kb = Klasa;' byłaby nielegalna. W linii D podobnie tworzymy obiekt k4, tym razem argument w nawiasie podajemy, więc wywołany zostanie konstruktor jednoparametrowy.
W liniach E, F i Gtworzymy trzy obiekty klasy Klasa, tym razem na stercie, za pomocą operatora new. Ponieważ operator ten zwraca adres utworzonego obiektu, więc wpisujemy go do zmiennej typu wskaźnikowego Klasa*. Nazwy pk5, pk6 i pk7 są więc nazwami wskaźników, a nie nazwami utworzonych obiektów; same obiekty nazw nie mają. Zauważmy, że teraz, tworząc obiekt z wykorzystaniem konstruktora domyślnego możemy (linia F), ale nie musimy (linia 39), użyć nawiasów.
To jeszcze nie wszystkie formy definiowania nowych obiektów! Jeśli k1 byłoby nazwą już istniejącego obiektu klasy Klasa, to możliwe byłyby też definicje
Klasa k8 = k1; Klasa k9(k1);Takie formy tworzenia obiektów poznamy przy okazji omawiania konstruktorów kopiujących .
Podsumowując, obiekty klas można definiować na następujące sposoby (zakładamy, że jest zdefiniowany publiczny konstruktor domyślny i konstruktor akceptujący jeden argument typu int; w ostatnich dwóch przypadkach musi istnieć konstruktor kopiujący):
Klasa a; Klasa a(5); Klasa a = Klasa(); Klasa a = Klasa(5); Klasa* pa = new Klasa; Klasa* pa = new Klasa(); Klasa* pa = new Klasa(5); Klasa b = a; Klasa b(a);
Zwróćmy teraz uwagę na kolejność tworzenia i usuwania obiektów. Pomocny będzie tu wydruk programu, w którym każde wywołanie konstruktora lub destruktora zostawia „ślad” (tradycyjnie skrót ctor oznacza konstruktor, a dtor — destruktor):
Ctor() A0 Ctor(int) B2 Wchodzimy do funkcji 'main' Ctor() C0 Ctor(int) D4 Dtor D4 Dtor C0 Ctor() E0 Ctor() F0 Ctor(int) G7 Dtor F0 Dtor G7 Wychodzimy z funkcji 'main' Dtor B2 Dtor A0Najpierw tworzone są obiekty globalne, w kolejności ich definicji, a zatem obiekt k1 o identyfikatorze A i obiekt k2 o identyfikatorze B i składowej a wynoszącej 2. Definicja tego drugiego obiektu występuje na samym końcu programu. Leży jednak w zasięgu globalnym (poza wszystkimi funkcjami i klasami), a zatem obiekt zostanie utworzony jeszcze przed wejściem do funkcji main. Na wydruku widzimy, że rzeczywiście konstruktory obiektów o identyfikatorach A i B wywołane zostały przed wejściem do main.
Następnie, już wewnątrz funkcji main, tworzone są zmienne k3 i k4 o identyfikatorach C i D. Zdefiniowane są one lokalnie wewnątrz bloku ograniczonego nawiasami klamrowymi. Zatem natychmiast po wyjściu sterowania z tego bloku są niszczone w kolejności odwrotnej do tej, w jakiej zostały utworzone w tym bloku: najpierw więc usuwany jest obiekt o identyfikatorze D, a potem obiekt o identyfikatorze C.
Trzy instrukcje następujące za blokiem powodują powstanie obiektów o identyfikatorach E, F i G. Dwa z nich natychmiast usuwamy „ręcznie” za pomocą delete. Następnie funkcja main kończy swoje działanie. Teraz dopiero usuwane są obiekty zdefiniowane w zasięgu globalnym B i A. Jak widzimy z wydruku, usuwane są w kolejności odwrotnej niż ta, w jakiej zostały utworzone.
Obiekt wskazywany przez wskaźnik pk5 (o identyfikatorze E) został utworzony na stercie za pomocą operatora new. A zatem usunąć go trzeba samemu; ponieważ tego nie zrobiliśmy, nie został w ogóle usunięty. Widzimy z wydruku, że destruktor dla obiektu o identyfikatorze E nie został wywołany.
Dla pewnych klas istnieje też możliwość zainicjowania obiektów za pomocą listy wartości poszczególnych składowych podanej w nawiasach klamrowych, podobnie jak to robiliśmy dla tablic . Z taką możliwością spotkaliśmy się już przy omawianiu C-struktur (patrz rozdział o strukturach ). Metoda ta, zastępująca wywołanie konstruktora, może być jednak stosowana nie tylko dla „czystych” C-struktur. Można jej użyć dla ogólniejszego typu klas, mianowicie dla klas będących agregatami danych. Są to klasy spełniające następujące warunki:
1. #include <iostream> 2. #include <string> 3. using namespace std; 4. 5. class A { 6. public: 7. int ia; 8. char ca; 9. void print() { 10. cout << "A: ia = " << ia << " ca = " << ca << endl; 11. } 12. }; 13. 14. struct B { 15. A obA; ➊ 16. double x; 17. void print() { 18. cout << "B: x = " << x << " "; 19. obA.print(); 20. } 21. }; 22. 23. int main() { 24. B b = { {4,'a'}, 7.5 }; ➋ 25. b.print(); 26. }
obie klasy, A i B, są takimi agregatami. W szczególności jest agregatem klasa B, choć zawiera pole obiektowe obA (➊). Pole to jest jednak typu A, a klasa A jest agregatem. W linii ➋ widzimy definicję obiektu klasy B i inicjowanie go wartościami podanymi na liście w nawiasach klamrowych. Na pierwszej pozycji tej listy, zgodnie z kolejnością pól w klasie B, występuje podlista wartości inicjujących obiekt-agregat klasy A będący składową tworzonego obiektu klasy B. Nawiasy wokół tej podlisty mogą być w pewnych sytuacjach opuszczone, ale lepiej je zawsze jawnie pisać. Wydruk z programu to
B: x = 7.5 A: ia = 4 ca = aW ten sposób można tworzyć obiekty-agregaty wyłącznie na stosie (czyli bez wywoływania operatora new).
Jak widzimy z tego przykładu, agregat może nie być czystą C-strukturą; w szczególności może zawierać metody (byle nie polimorficzne).
T.R. Werner, 21 lutego 2016; 20:17