Програмен език C++
Част 36
.
Използване на копиращите конструктори
(Copy Constructors at Work)
.
(съдържание)
.
В тази лекция ще се спрем малко по-подробно на копиращия конструктор, който бе въведен в лекция 35. Ще разгледаме две ситуации - (1) програми, в които е необходимо да има собствено написан копиращ конструктор, и (2) ситуации, когато компилаторът извиква копиращ конструктор, без да се създават обекти с изричен код, написан явно от програмиста.
.
1. Интелигентен копиращ конструктор (An Intelligent Copy Constructor). В предишната лекция 35 в програмата static1.cpp показахме как липсата на собствено написан от програмиста копиращ конструктор ще дава логическа грешка, която се изразява в отчитане на грешен брой на създадените обекти. Там използвахме само един конструктор, а именно безаргументния конструктор, и това се оказа че не е елегантно решение, защото при копиране джаджите получават един и същ сериен номер. По-долу е дадена подобрена версия на тази програма:

// static2.cpp
// demonstrates static data and functions
// uses constructor to initialize one widget
// uses also a copy constructor

#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++;
      }

      widget(widget & wg)         // copy constructor
      {
        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();

}

Единствената промяна в старата версия е добавянето на копиращ конструктор в дефиницията на класа:

  widget(widget & wg)         // copy constructor
  {
    widget_number = 10000 + total_widgets++;
  }

Това води до правилния изход от програмата

Total widgets = 0
Total widgets = 1
Total widgets = 2
Total widgets = 3
w1 = 10000
w2 = 10001
w3 = 10002

Ето още една програма, подобрена версия на програмата от предишната лекция copycon.cpp, взета от превъзходния курс на Лафоор [1]. В нея се наименоват и номерират обектите, както и се отчита техният брой. Класът omega има две променливи, които характеризират обектите - име, name, и номер, number. Също така има една статична константа, която е необходима за определяне на размера на масива name, така както това бе обяснено в т. 4 на лекция 34. За да се отчита броят на създадени обекти е дефинирана статичната променлива total, която се инициализира с нула след кода на класа.

// copycon2.cpp
// demonstrates copy constructors,
// using objects that number themselves

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

class omega
{
   private:
      static const int size = 20;
      char name[size];
      static int total;
      int number;

   public:
      // one-arg constructor
      omega(char str[]) : number(++total)
      {
         strncpy(name, str, size);
         name[size - 1] = '\0';

         cout << "\nI am the 1-arg constructor. I have "
              << "created object " << name << "-" << number;
      }

      // copy constructor
      omega(const omega & om)
      {
         strncpy(name, om.name, size);
         name[size - 1] = '\0';

         number = ++total;
         cout << "\nI am the copy constructor. I have "
              << "created object " << name << "-" << number;
      }

      void display()
      { cout << "I am object " << name << "-" << number; }
};  // class omega

//   static int total;
int omega::total = 0;

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

    cout << endl;

    cout << "\nom1: ";
    om1.display();

    cout << "\nom2: ";
    om2.display();

    cout << "\nom3: ";
    om3.display();

   getch();
}   // main()

В двата конструктора се принтират съобщения на екрана за тях когато те се извикват. Първият едноаргументен конструктор инициализира обект с дадено име и повишава броят на обектите (брои ги с променливата total) като предава този брой на номера на обекта с оператора number(++total) в инициализиращия списък на конструктора.

В копиращия конструктор се копира името на обекта и отново се повишава броят на обектите (брои ги с променливата total) като предава този брой на номера на обекта с оператора number(++total), но не в инициализиращия списък на конструктора, а в тялото на конструктора. Компилаторът на Борланд не забранява копиращия конструктор да има инициализиращ списък, т.е. той да изглежда от вида

    omega(const omega & om) : number(++total)
    {
       strncpy(name, om.name, size);
       name[size - 1] = '\0';

       cout << "\nI am the copy constructor. I have "
            << "created object " << name << "-" << number;
    }

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

В главната функция се създава един обект, om1, от класа omega, а след това неговото съдържание се копира в два други обекта - om2 и om3.

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

I am the 1-arg constructor. I have created object wrench-1
I am the copy constructor. I have created object wrench-2
I am the copy constructor. I have created object wrench-3

om1: I am object wrench-1
om2: I am object wrench-2
om3: I am object wrench-3

! Нова функция и една защита: обърнете внимание на двата реда

   strncpy(name, om.name, size);
   name[size - 1] = '\0';

Тази функция е подобна на strcpy, но копира в случая size на брой символа от om.name в променливата name. В оригинала на програмата в  [1] вторият ред го няма, което води до грешка, ако името е по-голямо от size-1 символа. Това може да се поправи с втория ред, който слага като последен символ нулевия символ,  '\0'. Всъщност, по-правилно е да се запише, че се копират само size - 1 символа както е следният ред (защото така и така, последният символ в името, този с номер size - 1, го заменяме с нулевия символ):

   strncpy(name, om.name, size - 1);
   name[size - 1] = '\0';

2. Извикване на копиращ конструктори от функциите. При предаване на аргументите на функция по стойност се извиква копиращите им конструктори. Също така при връщане на стойност от дадена функция също се извиква копиращ конструктор. И двата случая най-лесно могат да бъдат разбрани като се илюстрират с примери.

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

// copycon3.cpp
// demonstrates copy constructors
// passes objects to functions by value

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

class omega
{
   private:
      static const int size = 20;  // array size
      char name[size];             // array for name string
      static int total;            // total number of omegas
      int number;

   public:
      // one-arg constructor
      omega(char str[]) : number(++total)
      {
        strncpy(name, str, size-1);
        name[size-1] = '\0';
        cout  << "\nI am the 1-arg constructor. I have "
              << "created object " << name << "-" << number;
      }

      // copy constructor
      omega(const omega & om)
      {
        strncpy(name, om.name, size-1);
        number = ++total;
        cout << "\nI am the copy constructor. I have "
             << "created object " << name << "-" << number;
      }
};  // class omega

int omega::total = 0;

void main()
{
   void func(omega);         // declaration

   omega om1("wrench");      // uses one-arg constructor
   omega om2("screw");

   func(om1);                // call the function
   func(om2);                // call it again

   getch();
}   // main()

void func(omega og)          // argument passed by value
{

}

В главната функция се създават два обекта, om1 и om2, които се предават на функцията func (тя се извиква два пъти). В резултат се получават два "клонинга" на създадените обекти.

Забележете, че функцията func нищо не върши и това кара компилаторът да "се оплаче".

Parameter 'og' is never used.

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

I am the 1-arg constructor. I have created object wrench-1
I am the 1-arg constructor. I have created object screw-2
I am the copy constructor. I have created object wrench-3
I am the copy constructor. I have created object screw-4

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

По същия начин, когато функцията връща по стойност също се извиква копиращия конструктор. Ето една кратка програма, която илюстрира този момент:

// copycon4.cpp
// demonstrates copy constructors
// function returns object by value

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

class omega
{
   private:
      static const int size = 20;  // array size
      char name[size];       // array for name string
      static int total;      // total number of omegas
      int number;

   public:
      // one-arg constructor
      omega(char str[]) : number(++total)
      {
        strncpy(name, str, size-1);
         cout << "\nI am the 1-arg constructor. I have "
              << "created object " << name << "-" << number;
      }

      // copy constructor
      omega(const omega& om)
      {
          strncpy(name, om.name, size-1);
          number = ++total;
          cout << "\nI am the copy constructor. I have "
               << "created object " << name << "-" << number;
      }
};

int omega::total = 0;

void main()
{
   omega retfunc();       // function declaration

   retfunc();             // call the function

   getch();
}   // main()

omega retfunc()           // return by value
{
    omega temp("bolt");   // uses one-arg constructor
    return temp;          // uses copy constructor
}

В главната функция се извиква функцията retfunc(), която създава в тялото си един обект temp от класа omega. След това с копиращ конструктор създава още един обект, който се връща - затова изходът от тази програма е следният:

I am the 1-arg constructor. I have created object bolt-1
I am the copy constructor. I have created object bolt-2

Ако в главната функция дефинираме един обект om от класа omega, се извиква и безаргументният конструктор. Създадените обекти са общо три. Това е илюстрирано с програмата по-долу:

// copycon4b.cpp
// demonstrates copy constructors
// function returns object by value

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

class omega
{
   private:
      static const int size = 20;  // array size
      char name[size];       // array for name string
      static int total;      // total number of omegas
      int number;

   public:
      // no-arg constructor
      *omega() : number(++total)
      {
          strncpy(name, "OMEGA", size-1);
          cout << "\nI am the 0-arg constructor. I have "
               << "created object " << name << "-" << number;
      }

      // one-arg constructor
      omega(char str[]) : number(++total)
      {
          strncpy(name, str, size);
          cout << "\nI am the 1-arg constructor. I have "
               << "created object " << name << "-" << number;
      }

      // copy constructor
      omega(const omega& om)
      {
          strncpy(name, om.name, size);
          number = ++total;
          cout << "\nI am the copy constructor. I have "
               << "created object " << name << "-" << number;
      }
};

int omega::total = 0;

void main()
{
   omega retfunc();         // function declaration

   omega om;
   om = retfunc();               // call the function

   getch();
}   // main()

omega retfunc()              // return by value
{
    omega temp("bolt");      // uses one-arg constructor
    return temp;             // uses copy constructor
}

Всъщност, промяната в тази програма са двата реда

   omega om;
   om = retfunc();               // call the function

и допълнително написания безаргументен конструктор, който е необходим за създаване на обекта om с твърдението omega om;:

   omega() : number(++total)
    {
       strncpy(name, "OMEGA", size);
       cout << "\nI am the 0-arg constructor. I have "
            << "created object " << name << "-" << number;
    }

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

omega & retfunc()              // return by reference IS IMPOSSIBLE!
{
    omega temp("Pandora");   // uses one-arg constructor
    return temp;             // uses copy constructor
}
.

(съдържание)
.
Литература
.
[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 ?