Програмен език C++
Част 31

 Конструктори
(Constructors)
.
(съдържание)

В тази лекция ще бъдат разгледани конструкторите, които представляват специална функция на класа и които се използват при създаването на обектите на класа за инициализаране на някои променливи на класа. Също така, ще бъдат въведени деструкторите без да се разглеждат подробно. В тази и следващите лекции ще видим, че конструкторите могат да бъдат както с, така и без аргументи.

1. Инициализиране на обектите на класа. В лекция 16 беше разгледан класът Стек (The Stack Class), който имитира конструкцията стек и бе дадена една примерна програма от превъзходния курс на Лафоор [1]. Но в тази програма (написана за тогавашното ниво на знание на читателя) има функция, която стои изкуствено в светлината на C++ програмирането - това е функцията init(). Задачата на тази функция е да бъде иниализирана променливата на обекта top с -1. Проблемите са няколко (особено в голяма програма) и първият е, че програмистът може да забрави да извика тази функция в главната функция и по този начин да допусне логическа грешка, която се открива изключително трудно. Освен това, всеки един друг програмист ще трябва да вниква каква е тази функция и защо е написана и къде трябва да се извиква. Последното е особено важно и даже при тази малка програма трябва да се съобрази, че init() трябва да се извиква преди да е вкарана някаква стойност в стека с функцията void push(int var). Освен това, с тази функция имаме двустепенен процес - създаваме обект и после го инициализираме, което води до заблудата, че е необходимо да имаме два различни процеса. Това също е странно, защото нещата които се създават се създават с определени характеристики, а не първо се създават абстрактно и после да се доискусуряват. В т. 4 ще бъде разгледана една променена версия на гореспоменатата програма, но нека преди това се запознаем с конструкторите.

2. Конструктор (Constructor). Конструкторът е особена функция на класа, която има същото име като това на класа и която се изпълнява при създаване на обект от класа. Нека разгледаме една елементарна програма, която илюстрира някои от характеристиките на конструкторите и деструкторите и която е взета от курса на Лафоор [1].

! Първо, така написана, програмата трябва да се стартира извън интегрираната работна среда (IDE, Integrated Development Environment), понеже е необходимо да се види какво става след излизането от главната функция на програмата, main(). Това може да се осъществи при стартиране на Command Prompt от менюто Start/Programs/Accessories.

// condest.cpp
// demonstrates constructors and destructors
#include <iostream.h>

class Omega
{
  public:
    Omega()      // constructor
    {
       cout << "I am the constructor" << endl;
    }

    ~Omega()     // destructor
    {
      cout << "I am the destructor" << endl;
    }
};

void main()
{
  cout << "Starting main()" << endl;
  Omega om1, om2, om3;   // create three objects
  cout << "Ending main()" << endl;
}   // main()

В този странен клас, Omega, има само конструктор, Omega(), и деструктор, ~Omega(). Както споменахме първият има същото име като това на класа, докато деструкторът има име, започващо с тилда, ~, и последвано от името на класа. Това е така, за да може те да се разпознават от компилатора и програмистите. В тази програма, тези две функции нищо полезно не извършват, освен да ни "осветляват" кога се извикват. Както се вижда, в главната функция се създават три обекта на класа, изписва се Ending main() и програмата завършва. Ако се стартира програмата (от Command Prompt!) ще се получи следният изход на екрана:

D:\PROGRAMMING\C++\Cpp_Lafore\Programs\ch05>condest
Starting main()
I am the constructor
I am the constructor
I am the constructor
Ending main()
I am the destructor
I am the destructor
I am the destructor

D:\PROGRAMMING\C++\Cpp_Lafore\Programs\ch05>

От него ясно се вижда, че конструкторът е бил извикан три пъти, т.е. по един път за всеки от трите обекта при дефиницията им - Omega om1, om2, om3;. В главната функция се изпълнява cout << "Ending main()" << endl; и тогава програмата излиза, като при това се извикват деструкторите на трите обекта. Обърнете внимание, че в главната функция не се извикват конструкторите и деструкторите с явен код! Т.е. първото нещо, което трябва да се отбележи е, че конструкторът се извиква автоматично от програмата при създаване на обекти от класа, а деструкторът при разрушаване на обектите. Последното на практика означава, че деструкторът се извиква при излизане на програмата от обхвата на съответния обект. Написахме "излизане от обхвата", защото обектът може да е автоматична променлива и тогава нейният деструктор се извиква при нейното унищожаване, което става (вижте лекция 27) при излизането от процедурата или съставното твърдение и пр. или ако е глобална променлива, нейният деструктор се извиква при излизане от праграмата, както е в случая с трите обекта om1,om2 и om3.

Друга особеност е, че даже и да не напишете Ваши собствени конструктор или деструктор (explicit constructor / desructor), то конструктори и деструктори по подразбиране пак съществуват и извършват създаването и унищожаването на обектите.

И конструкторът и деструкторът не могат да връщат стойност (No Return Value)! Това е така, защото връщането на стойност от тях не е необходимо при програмирането в C++, а освен това какво би могъл да върне един деструктор!? Интересно е, че конструкторът не може да се извика с обект на класа, например om1.Omega(), докато деструкторът може - вижте примера в следващата точка.

3. Още нещо за конструкторите и деструкторите. Нека променим малко горната програма  като добавим една статична и една обикновена променливи на класа. Първата, om_count, ще брои колко обекта се създават, а втората, om_number, ще ги номерира с едно, две и т.н. Допълнително в главната функция сме извикали деструктора на втория обект - om2.~Omega(); - нещо, което е куриозно и е предназначено само за експеримент, който да покаже, че деструкторът не унищожава обекта, а просто извършва някакви действия при неговото унищожаване.

// condest1.cpp
// demonstrates constructors and destructors
#include <iostream.h>

class Omega
{
  public:
    Omega()      // constructor
    {  om_number = ++om_count;
       cout << "I am the constructor # " << om_number << endl;
    }

    ~Omega()     // destructor
    {
      cout << "I am the destructor # " << om_number << endl;
    }
  private:
    static int om_count;
    int om_number;
};

int Omega::om_count = 0;

void main()
{
  cout << "Starting main()" << endl;
  Omega om1, om2, om3;   // create three objects

  om2.~Omega();

  cout << "Ending main()" << endl;
}   // main()

Изходът на екрана от програмата е следният:

D:\PROGRAMMING\C++\Cpp_Lafore\Programs\ch05>condest1
Starting main()
I am the constructor # 1
I am the constructor # 2
I am the constructor # 3
I am the destructor # 2
Ending main()
I am the destructor # 3
I am the destructor # 2
I am the destructor # 1

D:\PROGRAMMING\C++\Cpp_Lafore\Programs\ch05>

От този изход се вижда, че конструкторите се извикват един след друг автоматично. След това програмата изпълнява странното твърдение om2.~Omega();, написано от нас, а после се отпечатва Ending main() на екрана. И накрая ясно се вижда, че обектите се унищожават обратно на реда, по който са създавани.

!Явното извикване на деструктора с om2.~Omega(); беше само експеримент, който няма смисъл в програмирането и даже е свързан с рискове във Ваша реална програма да загубите отделена памет или да объркате логиката на програмата! Ако се опитате да напишете извикване за конструктора (аналогично на това за деструктора), например, om2.Omega(); компилаторът ще ви отговори с 'Omega' is not a member of 'Omega'.

С тези две програми само показахме кога и как се извикват конструкторите и деструкторите, но не обяснихме тяхната роля. Тя ще стане ясно в следващите лекции, но тук ще кажем предварително, че обикновенно с написан от Вас конструктор Вие инициализирате определени променливи на класа, запазвате памет и/или извършвате други необходими действия с Вашите обекти. С деструкторите обикновено се освобождава заетата памет.

Нека първо разгледаме инициализацията на променливи в конструктора.
.
4. Конструктор, инициализиращ променливи на класа. В т.1 обсъдихме недостатъците на допълнителна инициализираща функция и ето един вариант за нейната замяна, чрез инициализация на една от променливите на обектите в конструктора: следва променената програма за поддържане на стек, която е една значително по-добра алтернатива на програмата от лекция 16. Функцията init() е премахната и е добавен конструкторът Stack(), който инициализира променливата top на обектите от класа Stack. Програмата stackcon.cpp е взета от курса на Лафоор [1].

// stackcon.cpp
// class models a stack; uses a constructor
#include <iostream.h>
#include <conio.h>                     // for getch()

class Stack                   // a stack holds up to 20 ints
{
   private:
      int st[20];             // integers are stored in array
      int top;                // index of last item pushed

   public:
      Stack() : top(-1)       // constructor
      {  }

      void push(int var)      // place an item on the stack
      {
         st[++top] = var;
      }

      int pop()               // remove an item from the stack
      {
         return st[top--];
      }
};

void main()
{
   Stack s1;                  // create a stack object
                              // (no need to initialize it)
   s1.push(11);               // push 3 items onto stack
   s1.push(12);
   s1.push(13);
   cout << s1.pop() << endl;  // pop 3 items and display them
   cout << s1.pop() << endl;
   cout << s1.pop() << endl;

   getch();
}   // main()

Забележете, че няма функция init() в класа Stack, както и няма извиквания от рода на "s1.init();" в главната функция. Както се и очаква, екранът след изпълнение на програмата е същият като този, който се получава с предишната версия. Тук трябва да обърнем внимание не само на това улеснение за програмиста, но и на самото инициализиране на променливата top. То би могло да се запише във вида

    Stack()            // constructor: not a practice!
    { top = -1; }

но е прието инициализацията да става с така наречения инициализиращ списък (Initialization List), който се изписва след името на конструктора и двете точки във вида - променлива, отваряща скоба, инициализираща стойност, затваряща скоба, запетая, следваща променлива и т.н.

    Stack() : top(-1)  // constructor OK!
    {  }

Разбира се, тук имаме само една променлива, но ако са две ще трябва да има запетая между тях - например "Stack() : top(-1), number(0)"!

И горният вариант (с присвояването в тялото на конструктора) работи, но се предпочита долният. Също така, ако променлива на класа е обект от друг клас, то не е възможно тя да се инициализира по друг начин, освен с инициализиращия списък.

Това, че тялото на функцията-конструктор остава празно не е проблем - просто трябва да се сложат двете големи скоби, иначе компилаторът ще се "оплаче". Но тялото на конструктора в повечето случаи не е празно, защото има и други задачи за вършене, както ще видим в следващите лекции. Освен това, например, масивите не могат да се инициализират по начин подобен на top(-1).

Деструктор на класа няма, защото никаква допълнителна памет не е отделена в конструктора и програмата автоматично изпълнява имплицитен деструктор, заложен от компилатора.

И последно ще споменем, че конструктор като горния, който не приема аргументи се нарича конструктор по подразбиране (Default Constructor). В следващите лекции ще видим, че конструкторът може да има аргументи, както и че може да има няколко конструктора (разбира се с едно и също име - това на класа), но с различен списък от параметри.

! Допълнение за инициализацията: Всяка една променлива, включително и предефинираните променливи (например целочислените) са също обекти за компилатора и могат да се инициализират при тяхната дефиниция. Разгледайте следния код, който присвоява 0 на k и 1 на m.

   int k(0), m(1);        // initialize k & m

   cout << "\nk = " << k; // display k
   cout << "\nm = " << m; // display m

.
(съдържание)
.
Литература
.
[1] Robert Lafore; C++ Interactive Course. Waite Group Press, Macmillan Computer Publishing, 1996.
.
Автор: Проф. Процесор Аритметиков
.
[ това е материал от брой 31 от юни 2009 г на списание "Коснос" www.kosnos.com ]

Keywords: С++,  OOP programming , C++ , Classes , Inheritance , Reusability , Creating New Data Types , Polymorphism and Overloading
Ключови думи: клас , обект, обектно ориентирано програмиране , полиморфизъм switch if else ?