Програмен език C++
Част 44
.
Презареждане на оператора за присвояване
(Overloading the Assignment Operator, =)
.
(съдържание)
.
В предишната лекция се запознахме с превръщане на обекти от различни класове един в друг. В настоящата лекция ще разгледаме презареждане на оператора за присвояване. Първо ще се спрем на този по подразбиране, след това ще го презаредим в една от формите му, която не връща стойност, след това в друга, която връща стойност, и накрая ще изпреварим малко изложението и ще споменем една друга форма, която връща стойност по връзка и която е най-оптималната.
.
1. Оператор по подразбиране за присвояване между обекти от един клас. Преди да се впуснем да пишем наши оператори за присвояване между обектите, то нека разгледаме оператора по подразбиране. Съобразно дефиницията на C++ [2], ако потребителят не презареди оператора за присвояване за даден клас X, то компилаторът създава такъв по подразбиране (implicitly-declared copy-assignment operator), който в зависимост от някои параметри, които още не сме изучили, има вида

X & X::opertor=(const X & )

или

X & X::opertor=(X & )

Точно този оператор ни позволяваше да пишем оператори за присвояване между обектите на един клас, като например в следния отрязък от код на програма

  X obj1, obj2;  // create two objects of class X
  obj2.setobj()  // set up the second object
  obj1 = obj2;   // assign second to first object

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

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

void operator=(const X & right)   // overloaded assignment
{
  // copy data if appropriate
}

Обърнете внимание на ключовата дума void, която означава че при такъв презареден оператор не може да пишете двойни, тройни и т.н. присвоявания от вида

  X obj1, obj2, obj3;  // create three objects of class X
  obj1.setobj()        // set up the first object
  obj3 = obj2 = obj1;  // not allowed because of the void return type!

! За това че в общия случай, присвояването (различно от горния случай) връща стойност вижте т. 4 в лекция 40.

Функцията-оператор има един аргумент от типа на класа, който аргумент обикновено е постоянен. В тялото на функцията се пишат твърдения за съответните присвоявания на променливите на обектите, както и някои други инициализации, промени на статитични променливи на класа и т.н. Всичко това много прилича на копиращия конструктор (вижте лекция 35), но трябва да се отбележи, че е различно в това, че не се създава нов обект - т.е. копиращият конструктор се "включва в работа" при създаването на нов обект, а не при операция за присвояване. Ето един илюстративен пример:

  omega beta = alpha;   // construction and initialization
                        // uses copy constructor

  beta = alpha;         // assignment
                        // uses assignment operator

3. Пример за презареждане на оператора за присвояване. Ще разгледаме една примерна програма от курса на Лафоор [1], която демонстрира презареждане на оператора за присвояване като използва вече познат клас, omega, от програмата copycon2.cpp от т.1. на лекция 36 .

В програмата assign.cpp всеки обект си има име, което може да се копира, но има и уникален номер, който не може да се копира. (Ако си спомняте в предишната програма, copycon2.cpp, този номер се даваше при създаване на обекта, включително и с помощта на копиращия конструктор!) Тази ситуация донякъде съответства на името и ЕГН на хората в България - името може да се смени в съда, но ЕГН-то остава същото.

! Обърнете внимание, че обектът от дясната страна на оператора за присвояване "влиза" като аргумент на функцията-оператор, която се "извиква" за обекта в ляво от оператора, когото променя!

// assign.cpp
// demonstrates overloaded assignment operator
// using objects that number themselves

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

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

   public:
      // one-arg constructor
      omega(char str[]) : number(++total)  // number the object
      {
         strncpy(name, str, size);        // give it a name
         cout << "\n1-arg constructor has "
              << "created " << name << "-" << number;
      }

      // overloaded assignment operator
      void operator = (const omega & right)
      {
         cout << "\n" << right.name << "-" << right.number
              << " assigned to " << name << "-" << number;

         strncpy(name, right.name, size);  // copy the name

         cout << ", making " << name << "-" << number;
      }

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

      // get the total value
      int get_total()
      { return total; }
};

int omega::total = 0;        // (no objects when program starts)

void main()
   {
   omega om1("Harriet");     // uses one-arg constructor
   omega om2("Bernard");

   om2 = om1;                // assignment

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

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

   cout << "\ntotal = " << om1.get_total()  << endl;

   getch();
}   // main()

Както винаги, леко сме изменили програмата в главната функция, с цел по-голяма яснота за читателя - прибавено е показването на броя на всички обекти от класа omega, създадени по време на изпълнение на програмата.

В главната функция на програмата се създават два обекта с различни имена, след това първият обект се присвоява на втория и те се отпечатват на екрана, заедно с броя създадени обекти (който в случая е ясен, че е 2, но в следващата програма ще видим някои неприятни изненади с този брой). Ето изходът от програмата:

1-arg constructor has created Harriet-1
1-arg constructor has created Bernard-2
Harriet-1 assigned to Bernard-2, making Harriet-2
om1 = Harriet-1
om2 = Harriet-2
total = 2

Друг особен момент в програма е, че сме дефинирали една от пременливите на класа omega, номерът number, постоянен с помощта на  const int number;. В този случай задължително трябва да презаредим операторът за присвояване, защото при липса на презареден оператор, компилаторът издава съобщение на реда с присвояването

[C++ Error] : E2125 Compiler could not generate operator= for class 'omega'.

което означава, че компилаторът не може да генерира оператор за присвояване по подразбиране. Това е така, понеже ако генерира такъв оператор и го използваме, то той ще трябва да променя постоянната променлива на обекта в ляво от =, а това е забранено.

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

  number++;

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

[C++ Error] : E2024 Cannot modify a const object.

4. Оператор за присвояване, който връща стойност (Assignment Operator That Allows Chaining). При това презареждане с, например функция-оператор от вида (предполагаме, че ще променяме само променливата name в класа X)

X operator=(const X & right)     // overloaded assignment op
   {
   // copy data if appropriate
   return X(name);               // returns by value; uses 1-arg constructor
   }

можем да използваме оператора във "верига" от присвоявания, като например

  X obj1, obj2, obj3;  // create three objects of class X
  obj1.setobj()        // set up the first object
  obj3 = obj2 = obj1;  // this time allowed because of the X return type!

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

Ето една примерна програма от от курса на Лафоор [1], която презарежда оператора = за да работи "във верига" с обекти от класа omega.

// assign2.cpp
// overloaded assignment operator with self-numbering objects
// returns a value

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

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

   public:
      // one-arg constructor
      omega(char str[]) : number(++total)
      {
         strncpy(name, str, size);
         cout << "\n1-arg constructor has "
              << "created " << name << "-" << number;
      }

      // Copy constructor
      omega(const omega & right) : number(++total)
      {
         cout << "\n\nWith copy constructor " << right.name << "-" << right.number
              << " assigned to " << name << "-" << number;
         strncpy(name, right.name, size);
         cout << ", making " << name << "-" << number;
      }

      ~omega()
      {
         cout << "\nDestructor has "
              << "destroyed " << name << "-" << number;
      }

      // overloaded assignment operator
      omega operator = (const omega & right)
      {
         cout << "\n\n" << right.name << "-" << right.number
              << " assigned to " << name << "-" << number;
         strncpy(name, right.name, size);
         cout << ", making " << name << "-" << number;
         return omega(name);
      }

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

      // get the total value
      int get_total()
      { return total; }

};

int omega::total = 0;          // no objects when program starts

void main()
{
   omega om1("Harriet");       // uses one-arg constructor
   omega om2("Bernard");
   omega om3("Damien");

   om3 = om2 = om1;            // chained assignment

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

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

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

   cout << "\ntotal = " << om1.get_total()  << endl;

}   // main()

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

   om3 = om2 = om1;            // chained assignment

не се извиква този конструктор, а операторът за присвояване.

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

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

При изпълнение на програмата извън  на екрана се изписва

D:\PROGRAMMING\C++\Cpp_Lafore\Programs\ch06>assign2

1-arg constructor has created Harriet-1
1-arg constructor has created Bernard-2
1-arg constructor has created Damien-3

Harriet-1 assigned to Bernard-2, making Harriet-2
1-arg constructor has created Harriet-4

Harriet-4 assigned to Damien-3, making Harriet-3
1-arg constructor has created Harriet-5
Destructor has destroyed Harriet-5
Destructor has destroyed Harriet-4
om1 = Harriet-1
om2 = Harriet-2
om3 = Harriet-3
total = 5

Вижда се, че общо се създават пет обекта, понеже функцията-оператор връща обект по стойност. Това са двата безименни обекта Harriet-4 и Harriet-5, създадени с твърдението

  return omega(name);

във функцията-оператор omega operator = (const omega & right). (Те имат име name, в случая Harriet, но ги наричат "безименни", защото самите обекти като променливи нямат име!)

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

! За връщането по връзка и ограниченията, свързани с него, вижте лекция 30.

5. Връщане по връзка при функцията-оператор =. Както споменахме в началота на настоящата лекция, ако не създадем своя собствена функция-оператор = за даден клас X, то компилаторът създава такъв по подразбиране, който в зависимост от някои параметри, които още не сме изучили, има вида

X & X::opertor=(const X & )

или

X & X::opertor=(X & )

Обърнете внимание, че в този оператор по подразбиране връщането на стойност е по връзка! Тогава в гореразгледаната програма assign2.cpp няма да се създават два излишни обекта. Това лесно може да се види, ако презаредената функция-оператор = за класа omega се загради в коментари - стартиращи с /* и завършващи с */. В този случай, изходът на екрана ще е:

D:\PROGRAMMING\C++\Cpp_Lafore\Programs\ch06>assign2

1-arg constructor has created Harriet-1
1-arg constructor has created Bernard-2
1-arg constructor has created Damien-3
om1 = Harriet-1
om2 = Harriet-1
om3 = Harriet-1
total = 3

Destructor has destroyed Harriet-1
Destructor has destroyed Harriet-1
Destructor has destroyed Harriet-1

Как ние самите, обаче, да напишем собствена презаредена функция-оператор = , която да връща по връзка, след като временен обект не може да се връща по връзка ? (за това последното вижте т. 2 от  лекция 30) Отговорът е чрез използване на указател. Използва се указател към обекта, за който се извиква функция-оператор =. Този указател е с името this, който представлява указател към обекта, за който се извиква тази функция. Ето променената  функция-оператор = :

    // overloaded assignment operator
    // that return by reference
    omega & operator = (const omega& right)
    {
       cout << "\n\n" << right.name << "-" << right.number
            << " assigned to " << name << "-" << number;
       strncpy(name, right.name, size);
       cout << ", making " << name << "-" << number;
       return *this;
    }

За упражнение, нека читателят копира тази функция в програмата assign2.cpp, да я стартира и да види, че не се създават двата излишни обекта.

В следващата лекция ще се занимаем с оператора за индекс (Overloading the Subscript Operator, []) .
.

(съдържание)
.
Литература
.
[1] Robert Lafore; C++ Interactive Course. Waite Group Press, Macmillan Computer Publishing, 1996.

[2] Programming Languages C++ . ISO/IEC 14882:1998(E). (това е книга от 776 стр., която може да се намери по Интернет във вид на PDF файл!)
.
Автор: Проф. Процесор Аритметиков

.
[ това е материал от брой 46 от декември 2010 г на списание "Коснос" www.kosnos.com ]
.
Ключови думи: клас , обект, обектно ориентирано програмиране , полиморфизъм
Keywords: С++,  OOP programming , C++ , Classes , Inheritance , Reusability , Creating New Data Types
OPERATOR OVERLOADING
Overloading Binary Arithmetic Operators The operatorX() Function Arguments Return Value
Overloading Other Binary Operators Overloading Relational Operators Passing the Argument by const Reference Assignment Operators Avoiding Temporary Objects
Overloading Unary Operators Prefix Version of Operator ++ Postfix Version of Operator ++ The Unary Minus Operator
Conversion from Objects to Basic Types  Type Casting: Conversion for Basic Types Conversion Function Invoked Automatically Casting for Clarity A Static Constant The static_cast Approach
Conversions Between Classes The One-Argument Constructor
Overloading the Assignment Operator (=) Syntax of the Overloaded Assignment Operator An Assignment Operator That Allows Chaining
Overloading the [ ] Operator Access with access() Function Access with Overloaded [ ] Operator
Fine-Tuning Overloaded Operators Constant Arguments Constant Functions Constant Overloaded Operators
Returns from Assignment Operators The Amazing *this Object The += Operator Revisited The Increment Operator Revisited