Beszéljünk a konstruktorokról, de nem csak
azokról... |
||
Ez a cikk
gyakorlott/haladó C++ programozóknak szól, ezt csak azért szeretném
leszögezni, mert nem szeretném azokat megijeszteni, akik most állnak neki a
C++ rejtelmeinek, vagy egyáltalán nincs semmilyen C++ gyakorlatuk, és ide
betekintve érthetetlen ábrákat találván megrémülnek, majd hátat fordítanak
ennek a csodálatos programozási nyelvnek, amit C++-nak hívnak. Nem árt nekik
sem elolvasni a leírást, legfeljebb más lelkiállapotban fognak visszatérni
tankönyvük mellé, miután meglátták, hogy mire is lesznek képesek, miután
megtanulták ezt a programozási nyelvet. Egy konstruktor-függvény, mint ahogy a neve
mutatja, felépít egy osztályt, vagyis inicializálhatja az új objektum
alapértékeit. A konstruktor-függvények automatikusan meghívásra kerülnek,
amikor példányosítunk egy osztályt (vagyis objektumot hozunk létre az osztály
alapján). Kezdetnek nézzük, hogy miként definiálhatunk egy konstruktort:
Vagy definiáljuk "in lieu", az osztályban, vagy külön függvényként
az osztály deklarációs részén kívül:
Egy
osztálynak lehet több konstruktora is, csak arra kell vigyázni, hogy a
paraméterezésük legyen különböző, vagyis nem lehet kétszer deklarálni
ugyanazt a paramétertípust elfogadó konstruktort. Elfogadott, de nem
támogatott módszer az is, hogy a konstruktor függvény (és minden más
tagfüggvény) deklarálásakor nem adjuk meg a paraméterek típusait, és
később, amikor definiáljuk a konstruktort és egyéb függvényeket, akkor
hozzárendeljük a változót is a paraméterhez. Például: class
MyClass { MyClass(int); ... }; MyClass::MyClass(int value) { ... } A konstruktor-függvények mindig automatikusan kerülnek
meghívásra, explicit módon csak egy derivált osztály konstruktora hívhatja
meg őket, például: class
Baze { Baze(int); }; class
Derived { Derived(int
a, int b):Baze(a) { } }; A C++ a következő esetekben mindig automatikusan
generál konstruktor függvényeket:
|
||
A
konstruktorok meghívási sorrendje |
||
Tegyük fel,
hogy egy osztály (T) :
Amikor egy T típusú objektumot hozunk létre, akkor a
konstruktorok meghívási sorrendje a következő lesz:
Az alaposztályok konstruktorait abban a sorrendben
hívjuk meg, amilyen sorrendbe szerepelnek az osztály deklarálásánál. Ez a
szabály vonatkozik az osztály típusú változók konstruktorainak meghívására
is. Abban az esetben, ha ezek a konstruktorok olyan
paramétereket várnak el, amelyeket a konstruktornak adtunk át, akkor a
következő módon kell eljárnunk: class M { ... public: M(); M(int
a); ... }; class B { ... public: B(int
m, int n); }; class C { ... }; class T:
public B, public C { M m1; M m2; ... public: T(int
b); ... } T::T(int b) : m1(b), B(b,b) { ... } vagyis explicit módon meg kell hívnunk az m1 "initializer"-ét,
hogy elfogadja a b paramétert (ezt a típusú műveletet hívják "initializer"-nek).
Ha példányosítunk egy T objektumot, akkor a konstruktorok sorrendje a
következő lesz:
|
||
Másoljunk
osztály objektumokat |
||
Osztály
típusú objektumokat kétféleképpen másolhatunk: értékadással, és
inicializással. Amikor az értékadást használunk, akkor a C++ az
egyenlőségjel (=) operátort használja, amikor meg inicializálást
használunk, akkor pedig a konstruktor függvényt. Amennyiben az osztály tervezője nem definiálja
explicit módon az egyenlőségjel operátort, a C++ által generált
értékadási operátor a következőképpen néz ki: T& T::operator = (const T&); Ha viszont T valamely alaposztályának az értékadás operátora nem
képes const paramétert fogadni, akkor egyszerűen csak: T& T::operator =
(T&); Ez az alapértelmezett operátor a jobb oldali
osztályobjektum tagváltozóit egyszerűen berakja a bal oldali osztály
megfelelő tagjaiba. Ez a legtöbb esetben működik is, viszont vannak
esetek, amikor nekünk kell megírni ezt az értékadási operátort. Ilyen eset
lehet például, amikor az osztály tartalmaz egy tömböt, melyet dinamikusan
hoztunk létre a new operátorral. Ha ebben az esetben is az alapértelmezett
értékadásra bíznánk a dolgokat, akkor a következő esettel fogunk
szembeállni: mind a két osztály ugyanarra a tömbre fog mutatni, hogy miért,
azt meglátjuk a példából: class
Stack { int
max; int
sp; float
*stk; public: stack(int
size) { stk = new
float[max = size]; sp = 0; } void
push(float v) { if(sp<max) { stk[sp++] = v; return 1; } else
return 0; } ... }; ... Stack s1(255),s2(255),s3(255); ... s2 = s1; s3 = s2; Ennek az értékadásnak a következő hatása van: s2.max = s1.max; s2.sp = s1.sp; s2.stk = s1.stk; s3.max = s2.max; s3.sp = s2.sp; s3.stk = s2.stk; Ha ezt kódot
végrehajtjuk az alapértelmezett C++ által generált értékadás operátorral,
akkor a következő problémákkal kerülünk szembe:
Viszont a második probléma súlyosabb mint ahogy azt az
első pillanatba gondolnánk. A következő kódrészlet igencsak nagy
zavart fog okozni a rendszerbe: s1.push(1.0); s2.push(2.0); s3.push(3.0); Mivel mindhárom stack objektum azt hiszi, hogy ő
az egyedüli, aki a stacket kezeli, a következő lesz a kód hatása: -
Az első sor berakja a
(közös) tömbbe az 1.0-át az s1.sp pozícióra, majd növeli az s1 stack pointerét -
A második sor az s2.sp –re
berakja a 2.0-át, de mivel s2.sp ugyanaz, mint az s1.sp volt mielőtt
beraktuk volna az első elemet az s1-be, hát egyszerűen felül fogja
írni a már ott lévő 1.0 –át. -
És a harmadik sornak is
ugyanaz lesz a hatása mint a másodiknak. Tehát elvárásainkkal ellentétbe
mindhárom objektum stack-je csak és kizárólag a hármat fogja tartalmazni. Ha mi magunk akarnánk megírni az értékadási
műveletet, akkor az a következőképpen nézne ki: stack& stack::operator=(stack& s) { int
i; if(this != &s) { sp = s.sp; delete
stk; stk = new
float[max = s.max]; for(i=0;i<sp;i++) stk[i] = s.stk[i]; return
*this; } } Az objektum másolás másik módszere az úgynevezett
másolókonstruktorokkal történik, a következő esetek egyikében:
Az első esetben a C++ egyszer létrehoz egy temporális complex
objektumot, majd ezt a copy konstruktor segítségével “rámásolja” a mi
objektumunkra. Ha explicit módon nem definiáltunk egy copy konstructort,
akkor a C++ automatikusan generál egyet a következő prototípus alapján: T::T(const T&) A második eset már érdekesebb. Amikor egy paramétert
érték szerint adunk át, akkor az objektum értékét úgymond rá kell másolni a
paraméter értékére (a paraméter egy temporális változó, amit a fordító hoz
létre). Miután az objektum rámásolódott a paraméterre, azután történik az
ugrás a függvény testére. Hasonlóképpen a harmadik esetben, egy temporális
változó lesz létrehozva, a visszatérési értékkel, és a copy konstruktort
használván megtörténik a rámásolás az objektumra a függvény visszatérése
után. Ha az osztály valamely alaposztálya, vagy tagváltozója
nem képes kezelni a const paramétereket, akkor a következő prototípusú
copy konstruktor keletkezik: T::T(T&) A Copy konstruktor paramétere referencia kell legyen,
és nem az érték, mert ha értékátadást hajtanánk végre, végtelen ciklusba
kergetnénk a C++ -t. A T::T(T) konstruktort nem szabad definiálni, mert a C++ nem
képes különbéget tenni a következő két prototípus közt: T::T(T&) és T::T(T) A C++ által automatikusan generált Copy konstruktor
hasonlóan a szintén automatikusan generált értékadás operátorhoz szintén
tagonkénti megfeleltetést használ, és az előbb említett problémák megint
felmerülhetnek. A legközelebbi viszontolvasásáig kívánok sikeres
programozást
|
||
Deák Ferenc |