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:

class MyClass

{

...

public:

  MyClass(paraméterlista);

...

};

...

MyClass::MyClass(paraméterlista)

{

...

}

class MyClass

{

...

public:

  MyClass(paraméterlista)

  {

   ...

  }

...

};

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:

  1. Ha explicit módon nem definiáltunk konstruktor függvényt, akkor mindig egy paramétermentes konstruktor-függvény kerül generálásra.
  2. "Copy" konstruktorok (Másoló-konstruktorok) egy osztálynak akkor generálódnak, ha nem definiáltunk explicit módon egy másoló konstruktort, és mégis ilyen irányú műveletet hajtottunk végre. Ezekről többet lehet olvasni majd a cikk vége felé.

A konstruktorok meghívási sorrendje

Tegyük fel, hogy egy osztály (T) :

  1. tartalmaz osztály típusú tagokat (member), valamint
  2. egy vagy több osztályból deriváljuk

Amikor egy T típusú objektumot hozunk létre, akkor a konstruktorok meghívási sorrendje a következő lesz:

  1. az a alaposztályok konstruktorai
  2. az osztály típusú tagok konstruktorai
  3. az osztály konstruktora

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:

  1. Az explicit módon meghívott B konstruktor ( B(b,b) )hívódik meg, hogy inicializálja a T osztály B alegységét.
  2. A C osztály egy paramétermentes konstruktora fog meghívódni, és mivel ezt explicit módon nem definiáltuk hát a C++ generálni fog egyet. Ezzel inicializáltuk a T osztály C alegységét
  3. Az explicit módon meghívott M konstruktor, egy int paraméterrel fog meghívódni, hogy inicializálja az m1 tagot.
  4. Az m2 tagot fogja inicializálni az M osztály paramétermentes konstruktora.
  5. Végül a T osztály konstruktora fog végrehajtódni.

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:

  1. Az s2 és s3 tömbjei "elkallódott" tömbök lesznek. Többé nem leszünk képesek hozzájuk férni, mert elvesztettük a pointereiket
  2. s1, s2, s3 ugyanarra a tömbre mutatnak.

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:

  1. változó inicializálás a következő kódrészlettel: complex f = complex(1.0,1.0)
  2. paraméterátadáskor
  3. függvény visszatérési értékek inicializálásánál.

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