Програмен език C++
Част 35
.
 Копиращите конструктори
(Copy Constructors)
.
(съдържание)
.
С копиращите конструктори също могат да бъдат създавани обекти, като при това съдържанието (променливите) на вече създаден обект се копират в новосъздадения. Ако не напишем копиращ конструктор, то компилаторът създава свой собстен копиращ конструктор по подразбиране (Default Copy Constructor).
.
1. Копиране на променливи (Copying Variables). При дефиницията на променливи много често се използва копирането на променливи, т.е. създаването на нова променлива и едновременно с това нейната инициализация със стойността на друга, както това е направено в следния примерен код:

int alpha = 19;     // create an int with value 19
int beta = alpha;   // create another int that's a copy of alpha; it has a value of 19, too!

Да не забравяме, че за компилатора променливите от предефинираните типове са обекти. Също така равенството не изразява присвояване (assignment operator), а означава инициализация на променлива (initializing a variable).

Еквивалентният запис на горните два реда е:

int alpha(19);
int beta(alpha);   // copy alpha to beta

Обърнете внимание, че този запис наподобява инициализацията в конструкторите и обикновенно се нарича функционално означаване (functional notation).

2. Копиране на обекти (Copying Objects). По същия начин могат да се копират и обекти. Ето един схематичен пример:

AClass obj1;
obj1.getdata();

AClass obj2 = obj1, obj3(obj1);

Първо се създава обект obj1 от класа AClass. След това той получава стойности на своите променливи с функцията getdata(). После се създават два други обекта - obj2 и obj3, който се инициализат със стойността на първия.

Последният ред от кода по-горе изисква класът AClass да има копиращ конструктор. Както споменахме, ако програмистът не създаде такъв конструктор, компилаторът създава такъв по подразбиране, който има задачата да копира всички променливи на единия обект в другия - това се нарича на английски memberwise copy. Много често точно това е необходимо на програмиста, затова той може и да не създаде свой копиращ конструктор.

Но понякога е необходимо да се извършат някакви действия при извикване на копиращия конструктор и тогава програмистът трябва да създаде свой. Например, спомнете си програмата static.cpp от лекция 28, която брои създадените обекти. Ето едно нейно подобрение, в което функцията init() е заменена от конструктор, а освен това третият обект се създава с копиращ конструктор. В тази програма се илюстрират два момента: (1) програмата е подобрена, чрез създаване на безаргументан конструктор, който брои и номерира джаджите, и (2) не е създаден копиращ конструктор, а в main() се копира един обект (widget w3(w1);) и това създава проблем с номерирането и броенето на обектите (джаджите в този случай). В следващата лекция ще срещнем подобрена версия на тази програма (със създаден от програмиста копиращ конструктор).
.
! Във версията по-долу на тази програма, програмистът не е създал собствен копиращ конструктор и това води до логическа грешка при преброяването на създадените обекти. Компилаторът създава свой собствен копиращ конструктор, който само копира всички променливи на единия обект в тези на другия, но не увеличава броя на създадените обекти.

// static1.cpp
// demonstrates static data and functions
// uses constructor to initialize one widget

#include <iostream.h>
#include <conio.h>
using namespace std;

////////////////////////////////////////////////////////////////
class widget
{
   private:
      int widget_number;          // a widget's serial number
      static int total_widgets;   // all widgets made so far
                                  // NOTE: declaration only
   public:
      widget()                    // constructor that initialize one widget
      {
        widget_number = 10000 + total_widgets++;
      }
 

      int get_number()            // get a widget's number
      {
         return widget_number;
      }

      static int get_total()      // get total widgets
      {
         return total_widgets;
      }
};  // class widget

int widget::total_widgets = 0;    // NOTE: definition

void main()
{
   cout << "Total widgets = " << widget::get_total() << endl;

   widget w1;    // create widgets
   cout << "Total widgets = " << widget::get_total() << endl;

   widget w2;    // create widgets
   cout << "Total widgets = " << widget::get_total() << endl;

   widget w3(w1);    // create widgets
   cout << "Total widgets = " << widget::get_total() << endl;

   cout << "w1 = " << w1.get_number() << endl;
   cout << "w2 = " << w2.get_number() << endl;
   cout << "w3 = " << w3.get_number() << endl;

  // added to stop the screen
  getch();

}

За съжаление, когато третият обект w3 е създаден с копиращ конструктор, програмата не работи правилно: тя наистина копира номера на първия обект в третия, но бърка в броенето на създадените джаджи (widgets). Ето защо е необходимо добавянето на копиращ конструктор, който е следният:

  widget(const widget & w)          // copy constructor
  {
    widget_number = w.widget_number;
    total_widgets++;
  }
.
! Примерът е схематичен, но е много показателен как програмистът може да забрави да добави свой собствен копиращ конструктор и програмата да не работи логически вярно, а както е известно логическите грешки са най-трудни за откриване!

3. Копиращ конструктор (Copy Constructor). Синтаксисът на копиращия конструктор е следният: той има един единствен аргумент и той е типа на същия клас и се предава по връзка (Reference Arguments). Обикновено се използват константни аргументи, целта за което ще бъде пояснена по-долу. Ето една схематична програма от курса на Лафоор [1], която демонстрира копиращия конструктор.
.
// copycon.cpp
// demonstrates copy constructors

#include <iostream.h>
#include <conio.h>                     // for getch()

class omega
{
   private:
      int intvar;

   public:
      omega(int v) : intvar(v)   // one-arg constructor
      {
        cout << "\nI am one-argument constructor";
      }

      omega(const omega & om)    // copy constructor
      {
         intvar = om.intvar;
         cout << "\nI am the copy constructor";
      }

      int retdata()
      { return intvar; }
};

void main()
{
   omega om1(27);                     // uses one-arg constructor
   omega om2 = om1;                   // uses copy constructor
   omega om3(om1);                    // uses copy constructor

   cout << endl << endl;

   cout << "\nom1 = " << om1.retdata() << endl;
   cout << "\nom2 = " << om2.retdata() << endl;
   cout << "\nom3 = " << om3.retdata() << endl;

   getch();
}   // main()

В тялото на двата конструктора е вкаран програмен ред, който да изписва кой конструктор се извиква: това е направено с цел да разбере читателят какво точно става при трите дефиниции на обектите. Както се вижда от горния копиращ конструктор в него има два нови момента - (1) аргументът е от същия клас и се предава по връзка, и (2) използвана е ключовата дума const. Затова ще разгледаме тези две особености.

4. Константни аргументи (const for Function Arguments). В лекция 29 беше разгледано предаването на аргументи по връзка (Reference Arguments) и там беше изяснено, че това се използва при големи по памет аргументи с цел спестяване на памет и постигане на бързина. Освен това, за разлика от предаването по стойност предаването по връзка прави възможно промяната на оригиналните стойности на аргументите, с които се извиква функцията. Но при копиращите конструктори обикновено не целим промяната на оригиналния обект, който се копира в другия, то ето защо използваме ключовата дума const.

При употреба на предаване по стойност (Passing by Value), ключовата дума const не е необходима, но в случая на копиращ конструктор е позволено само предаването по връзка (защо е така ще стане ясно от следващата лекция)

Ето една промяна на програмата richman2.cpp от лекция 32, в която е въведен копиращ конструктор. В копиращия конструктор, освен че се копират парите - променливата money - се запитва и за името на новия "притежател".

// richman2.cpp
// multiple constructors and a copy constructor
#include <iostream.h>
#include <conio.h>               // for getch()

class richman
{
    private:
      string name;
      int money;

    public:
      richman() : name("Jack"), money(0)
      {  }

      richman(int m) : name("Angel"), money(m)
      {  }

      richman(string n) : name(n), money(-1)
      {  }

      richman(string n, int m) : name(n), money(m)
      {  }

      richman(const richman & r) : name("Jack"), money(0)
      {
        money = r.money;
        cout << "Name? ";
        cin >> name;
      }

      void display()
      {
        cout << name << " has M$" << money << endl;
      }
};  // class richman

void main()
{
   richman plamen("Plamen", 10), sasha("Sasha", 1),
           angel(100), peter("Peter"), jack;

   plamen.display();
   sasha.display();
   angel.display();
   peter.display();
   jack.display();

   richman natasha(sasha);
   natasha.display();

   richman nick = jack;
   nick.display();

   getch();

}  // main()

Промяната в главната функция е създаването с копиращия конструктор на още два обекта natasha и nick.

Както споменахме, аргументите в копиращия конструктор могат да се предават само по връзка. Ако промените декларатора (declarator) на копиращия конструктор по следния начин:

  richman(richman r) : name("Jack"), money(0)

То компилаторът издава следното съобщение за грешка:

  The constructor 'richman::richman(richman)' is not allowed.

Ако изберете с мишката грешката и натиснете клавиша <F1> то помощната система на Борланд С++ Билдер 4.0 дава следния текст:

Constructors of the form

  X(X);  // Error

};

are not allowed.

This is the correct way to write a copy constructor:

class X {

  X(const X&); // Copy constructor
};

който означава на български, че при копиращите конструктори, аргументите от същия клас не могат да бъдат предавани по стойност и е коректно аргументите да бъдат предавани по връзка (Reference Arguments). Както споменахме по-горе, в следващата лекция ще разберем защо това е така, но тук ще изпреварим нещата (с цел да замислим читателя) и ще споменем, че ако се предаваше по стойност обектът, то щеше да се получи порочен кръг без край, който се състои от извикване на копиращия конструктор на класа!

В дефиницията на горния (а въобще и на всеки) копиращ конструктор може да бъде премахната ключовата дума const пред аргумента и да се компилира успешно новия код, но това не се прави!
.

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

Keywords: С++,  OOP programming , C++ , Classes , Inheritance , Reusability , Creating New Data Types , Polymorphism and Overloading
Ключови думи: клас , обект, обектно ориентирано програмиране , полиморфизъм switch if else ?
Copy Constructors
Copying Variables
Equivalent Syntax
Copying Objects
Passing by Value
A Simple Example of a Copy Constructor
Argument Must Be Passed by Reference
Argument Should Be const