Programowanie w języku C/C++

Konstruktory

Konstruktorem nazywamy metodę klasy, wywoływaną podczas tworzenia obiektu. Konstruktory wykorzystywane są głównie do inicjowania początkowych wartości zmiennych i tworzenia zmiennych dynamicznych. Nazwa konstruktora jest zawsze identyczna jak nazwa klasy. Konstruktor nie posiada typu (nie oznacza to, że jego typ to void).

Możliwe jest przeciążanie konstruktora. Dzięki temu możemy deklarować konstruktory potrzebne w różnych sytuacjach. Generalnie wyróżniamy trzy typy: bezparametryczny, kopiujący oraz wieloargumentowy.

Konstruktor bezparametryczny


Konstruktorem bezparametrycznym nazywamy konstruktor, który może zostać wywołany bez parametrów. Jest on domyślny dla klasy więc w razie jego braku jest definiowany automatycznie (są jednak pewne wyjątki od tej reguły).
class Punkt
{
  public:
    int X, Y;
    Punkt(int X=0, int Y=0) {this->X=X; this->Y=Y;}   //konstruktor bezparametryczny
};

Konstruktor ten inicjuje klasę początkowymi wartościami. Jego działanie objawia się w następujących sytuacjach:
int main() 
{
   Punkt p1, p2[10], *p3 = new Punkt(), *p4 = new Punkt[10];
}

Konstruktor bezparametryczny jest jedynym, którego możemy użyć do definiowania tablic obiektów danej klasy.

Konstruktor wieloargumentowy


Konstruktor wieloargumentowy to taki, który pobiera parametry podczas tworzenia klasy. O dziwo przykład takiego konstruktora znajduje się w poprzednim podpunkcie. Konstruktor klasy Punkt jest jednocześnie wieloargumentowy i bezparametryczny. To jak go będziemy nazywali zależny od sytuacji w której został użyty (czy bez parametrów, czy z parametrami).

Nie jest możliwe wywołanie konstruktora wieloargumentowego na rzecz utworzenia tablicy obiektów.

#include <cmath>
 
class Punkt
{
  public:
    int X, Y;
    Punkt(double, double);  //konstruktor wieloargumentowy
};
 
Punkt::Punkt(double fi, double r)
{
   X = r * std::cos(fi);
   Y = r * std::sin(fi);
}
 
int main()
{
   Punkt A(M_PI/4, 4.5); //kat 45 stopni i odległość 4.5
   return 0;
}


Konstruktor kopiujący


Jest to szczególny przypadek konstruktora wieloargumentowego. Konstruktor kopiujący jako argument otrzymuje tylko obiekt danej klasy. Jeśli nie stworzylibyśmy takiego konstruktora, to zostałby on utworzony automatycznie, a jego działanie polegałoby na kopiowaniu wartości pól klasy. Jego stworzenie jest niemal niezbędne, jeśli we wnętrzu klasy znajdują się składowe wskaźnikowe.

class Punkt
{
  public:
    int X, Y;
    Punkt() {X=0; Y=0;}  //jeżeli go nie zdefiniujemy to w tej sytuacji kompilator nie zrobi tego za nas i deklaracja Punkt X() będzie błędna
    Punkt(double fi, double r);
    Punkt(Punkt &Pkt) {X=Pkt.X; Y=Pkt.Y;}   //konstruktor kopiujący
};

Konstruktor kopiujący wywoływany jest w następujących sytuacjach:
Klasa A; //tutaj oczywiście działa konstruktor bezparametryczny
Klasa B(A);
Klasa C = A;


Lista inicjacyjna konstruktora


Zauważ, że na ogól, konstruktory wypełniaja pola jakimiś wartościami. Z tego powodu konstruktory wyposażono w mechanizm który to usprawnia. Zamiast wpisywać w ciele konstruktora nazwaPola=cośtam wystarczy zadeklarować to w liście inicjacyjnej. Dzięki temu mam "czysty" blok kodu:

class Test
{
private:
   int _pole1;
   double _pole2;
   char * _pole3;
public:
   Test(): _pole1(1), _pole2(_pole1+3), _pole3(NULL) {}
   Test(int _pole1, double B, char *C): _pole1(_pole1+3), _pole3(C) {
      _pole2 = 10 * _pole1;
   }
};


Na liście wiadomo co jest polem, a co argumentem więc nie działa przesłanianie zmiennych (_pole1(_pole1+3)).

Należy zapamiętać, że pola w liście inicjacyjnej są wypełniane wg. kolejności ich wystąpienia w klasie, a NIE kolejności w liście. Może to niekiedy powodować trudne do wykrycia błędy.

Konwersja konstruktorowa


Mówiąc z grubsza polega to na tym, że w pewnych przypadkach, gdy kompilator potrzebuje obiektu danej klasy, a otrzymuje jakiś inny typ oraz:
  - możliwa jest konwersja w tym miejscu,
  - klasa docelowa posiada konstruktor, którego jedynym argumentem jest (lub może być) aktualnie otrzymywany typ,
wtedy kompilator używa tego konstruktora do stworzenia tymczasowego obiektu, który może być z powodzeniem użyty. Prościej jest podać przykład:

#include <iostream>
 
using namespace std;
 
class Liczba
{
public:
   int Value;
 
   Liczba(int Value=0): Value(Value) { //domyślny konstruktor (przy okazji rzutuje z typu int)
      cout << "Liczba::Liczba(" << Value << ")" << endl;
   }
   Liczba(char * text) { //konstruktor rzutującu z typu char *
      cout << "Liczba::Liczba(\"" << text << "\")" << endl;
      Value = std::atoi(text);
   }
   ~Liczba() {
      cout << "Liczba::~Liczba(" << Value << ")" << endl;
   }
   Liczba & operator + (const Liczba & lp) {
      this->Value += lp.Value;
      return *this;
   }
};
 
std::ostream & operator << (std::ostream & out, Liczba & lp)
{
   out << lp.Value;
   return out;
}
 
int main()
{
   Liczba A = 7;
   Liczba B = A + 6 + "8";
   cout << B;
   getchar();
}


Do klasy Liczba możemy dodawać tylko inne klasy Liczba. W pewnym miejscu do obiektu A dodajemy 6 a potem "8". Kompilator jednak nie panikuje bo wie, że może dla przeciążonego operatora + dokonać konwersji oraz, że klasa Liczba posiada odpowiednie konstruktory.

Wynikiem działania programu jest (a raczej może być):

Liczba::Liczba(7)
Liczba::Liczba("8")
Liczba::Liczba(6)
Liczba::~Liczba(8)
Liczba::~Liczba(6)
21


Kluczowym elementem jest tutaj argument dla operatora + (const Liczba &). Bez const z oczywistych powodów ów mechanizm by nie zadziałał już na etapie kompilacji.

Co najśmieszniejsze jednak jest to, że owe konstruktory nie muszą w ogóle rzutować z danego typu do typu klasy (oczywiście powinny), mogą robić zupełnie coś innego, a kompilator tego oczywiście nie zweryfikuje, tylko na ślepo ich użyje. Prze to możemy się w bardzo ciekawy sposób wyprowadzić na manowce.

By uniknąć zamieszania z konwersją konstruktorową, możemy ją wyłączyć dla wskazanego konstruktora poprzedzając go słowem kluczowym explicit.

Konstruktor obiektów globalnych bądź statycznych


Za pomocą konstruktora takich obiektów można wywołać kod jeszcze przed wejściem programu do funkcji main (entry point). Na przykład:

#include <iostream>
 
class ExecTest
{
public:
   ExecTest() {
      std::printf("ExecTest();\n");  //celowo używam std::printf bo nie wiadomo, czy std::cout będzie już zainicjowane !!!
   }
} Obj;
 
int main()
{
   std::cout << "int main();" << std::endl;
   std::cin.get();
   return 0;
}


Wyświetli (przynajmniej powinno):

ExecTest();
int main();


Zobacz też: