Програмен език C++
Част 39
.
Презареждане (предефиниране) на бинарните аритметични оператори
(Overloading Binary Arithmetic Operators)
.
(съдържание)
.
Презареждане на оператори (Operator Overloading). Досега, в лекция 25 се запознахме с т.н. презаредени функции, които имат едно и също име и работят с различни по тип аргументи, и които функции извършват операциите си по начин, който зависи от типа на аргументите им. Подобно на тях, презаредените оператори позволяват удобство при програмирането, като се използва един и същ оператор с различни по тип операнди, който оператор извършва различни действия, в зависимост от типа на тези операнди.
.
! Авторът се затруднява с превода на Operator Overloading. Изразът би могъл да се преведе като предефиниране на оператори, защото се променя значението на операторите (т.е. те отново се дефинират). Но досега сме използвали термина предефинирани типове (built-in types), и въпреки че не сме използвали предефинирани оператори, за т.н. built-in operators, то използването на предефиниране на оператори би могло да бъде много объркващо. Затова по аналогия с презаредените функции (Overloaded Functions) ще използваме термина презареждане на оператори.

В серия от лекции, следвайки прекрасното изложение на Лафоор в неговия курс [1], ще се запознаем с презареждането на операторите. Това е една много мощна концепция на програмния език C++, която прави удобно ползването на различни оператори за действия с дефинирани от програмиста обекти.

Първо ще започнем с презареждането на бинарните аритметични оператори +, -, *, и /. В следващите лекции ще разгледаме презареждането на другите бинарни оператори, като операторите за сравнение <, >, <=, >=, == и !=, както и тези за присвояване: +=, -=, *= и /=. След това ще разпространим концепцията върху унарните оператори ++, -- и - (знакът минус).

Превръщането на обекти (данни) от един тип в друг (data conversion) е пряко свързано с презареждането на оператори и както ще видим с негова помощ ще се научим да превръщаме дефинираните от нас обекти в предефинираните типове (като int, float и т.н.), както и превръщането на обекти от един дефиниран от нас клас в такива от друг клас.

Накрая в тази серия ще завършим с презареждането на оператора за присвояване = и средните скоби [] (subscript operator, които се използват за обръщане към даден елемент на масив). Ще бъдат допълнително демонстрирани техники, които правят презареждането на оператори по-ефективно и по-безопасно.

В серията ще бъдат използвани редица програми от превъзходния курс на Лафоор [1] (както винаги това е изрично споменато в текста), но също така ще бъдат разширени и допълнени с новите концепции програмите за работа с комплексни числа - вижте например лекция 10 и лекция 11.
.
1. Защо се презареждат оператори? Спомнете си за класа xString, който създадохме да поддържа стрингове (низове) - вижте т. 6 от лекция 19. В него два стринга се свързваха един с друг (concatenate) с помощта на функцията void append(xString xs), която добавяше стринга в обекта xs към стринга на обекта, който извиква функцията. Например,

 s1.append(s2);  // append s2 to s1

ще добави стринга на обекта s2 към този на s1.

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

  // you can append two strings
  s1 = s2 + s1;

от програмата cppstr.cpp от т. 5 на същата лекция 19, която програма илюстрира класа string от стандартната C++ библиотека (добавена с #include <string>).

Спомнете си и за класа complex_number от програмата complex2.cpp от лекция лекция 11, който моделира комплексните числа. В нея, за да съберете комплексното число c2 към c1 трябва да извикате функцията void add(c2) по следния начин:

 c1.add(c2);

Разбира се, след време програмистът (или особено някой негов колега) ще бъде затруднен да разбере (точно като в преждецитирания случай) кое число към кое се събира и кое остава постоянно и кое се променя. Отново записът не е елегантен и е далеч от разбиранията в математиката за означаване на събирането на числа, което един програмист на C++ би записал така:

 c1 += c2;

Спомнете си също, че класът airtime от програмата timeret.cpp от лекция 14 беше изменен, така че да може да събира две времена със следния ред

 t3 = t1.add(t2);      // add t1 and t2, result in t3

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

t3.add(t1, t2);       // add t1 and t2, result in t3

и въпреки това най-ясно и разбираемо и близко до стандартния запис в математиката е да предефинираме оператора +, така че да можем да пишем:

 t3 = t1 + t2;      // add t1 and t2, result in t3

2. Още за презареждането на оператори. Тук трябва да се спомене, че обектите не на всеки клас могат да се събират! Просто понякога няма логика да събираме дадени обекти, като например тези от класа employee от лекция 16. Какво би означавало следното твърдение?

 emp3 = emp1 + emp2;

Нищо! (Освен, да съберете джобните пари на двама служителя и да ги дадете на трети :-))

Също така, не може да си измисляте оператори, които ги няма в програмния език C++ и да пишете за тях презареждащи функции. Може да презареждате само предифинираните оператори (built-in operators)!. Например, не може да напишете презареждащ оператор (например с името operator **) за Вашата идея за вдигане на степен или някакъв друг оператор за друга цел, като например operator <-.

Освен това, не всички предефинирани оператори могат да се презареждат! (You Can’t Overload Everything!). Например, не може да презаредите точковия оператор (dot operator .), оператора за решение за обхвата (the scope resolution operator ::), условния оператор (the conditional operator ?:,), както и някои други, които още не сме изучили.

3. Функцията-оператор (The operatorX() Function). В случая на бинарен оператор X, който е презареден за класа SomеClass, синтаксисът за функцията operator X () е следният:

   SomeClass operator X (SomeClass arg_obj)
   {
      // needs function body
   }

Нека в класа airtime от лекция 12 да презаредим оператора +, така че да може да събираме два часа (обекта) със следния ред

   // you can append two strings
   at3 = at2 + at1;

Функцията за презареждане на оператора + в този случай е:

   airtime operator + (airtime right)
    {
      // make a temporary object
      airtime temp;
      // add data
      temp.hours = hours + right.hours;
      temp.minutes = minutes + right.minutes;

      // check for carry
      if (temp.minutes >= 60)
      {
        temp.hours++;
        temp.minutes -= 60;
      }

      return temp;          // return temporary object
    }

Както става ясно, същият синтаксис важи за всеки един бинарен оператор, например, ако е за изваждане, вместо operator + ще напишем operator -, а също ще изменим и тялото на функцията, така че да отразява изваждането на времената.

В горенаписаната функция operator + има няколко съществени момента:

(1) функцията е с име operator, последвано от операторът, който ще презареждаме (в случая +).

(2) функцията има само един аргумент и той е от типа на класа airtime. На негово място влиза десният операнд на операцията събиране, т.е. обектът t2, в случая на извикването

 t3 = t1 + t2;

(3) левият операнд се подразбира, че е обектът за който се извиква функцията: грубо казано, в горния израз

 t1 +

се подразбира като

 t1.operator+()

(4) функцията връща стойност от типа на класа, т.е. airtime. Върната стойност в случая на горното извикване се присвоява на t3.

Забележете, че винаги функцията operatorX() има брой аргументи (в случая 1) с единица по-малък от броя операнди (в случая два - двете събираеми)!

4. Програма за събиране на обекти от класа airtime. Следната програма addair.cpp от курса на Лафоор [1] демонстрира събирането на обекти от този клас:

// addair.cpp
// class models time data type
// overloads the + operator

#include <iostream.h>
#include <conio.h>                     // for getch()
#include <iomanip.h>                   // for setw() & fill()

class airtime
{
   private:
      int hours;               // 0 to 23
      int minutes;             // 0 to 59

   public:

      // output to screen
      void display() const
      {
         cout << hours << ':' << setw(2) << setfill('0') << minutes;
      }

      // input from user
      void get()
      {
         char dummy;
         cout << "\nEnter time (format 12:59): ";
         cin >> hours >> dummy >> minutes;
      }

      // overloaded + operator
      airtime operator + (const right) const
      {
         // make a temporary object
         airtime temp;
         // add data
         temp.hours = hours + right.hours;
         temp.minutes = minutes + right.minutes;

         // check for carry
         if (temp.minutes >= 60)
         {
            temp.hours++;
            temp.minutes -= 60;
         }

         return temp;          // return temporary object
      }
};  // end class airtime

void main()
{
   airtime at1, at2, at3;

   cout << "Enter first airtime: ";
   at1.get();

   cout << "Enter second airtime: ";
   at2.get();

   at3 = at1 + at2;         // overloaded + operator
                            //    adds at2 to at1
   cout << "sum = ";
   at3.display();           // display sum

   getch();
}   // main()

Обясненията за тялото на функцията operator + са излишни, тъй като действията в нея са същите като тези във функцията airtime add(airtime at2) от програмата timeret.cpp от лекция 14. Обърнете внимание, че аналогично на извикването на тази функция във вида

 // add t1 and t2, result in t3
 t3 = t1.add(t2);

можем да запишем

 // a strange use of operator overloading
 // to add t1 and t2 and get the result in t3
 t3 = t1.operator+(t2);

но това не се прави! А просто се пише твърдението със знака плюс.

5. Програма за събиране на обекти от класа xString. Следната програма strplus от курса на Лафоор [1] демонстрира събирането на обекти от този клас:

// strplus.cpp
// overloads + operator to concatenate xStrings

#include <iostream.h>
#include <string.h>                  // for strlen(), strcpy(), etc.
#include <conio.h>                   // for getch()

class xString
{
   private:
      enum { MAX = 80 };             // maximum length of xStrings
      char str[MAX];                 // ordinary C string

   public:
      xString()                      // no-arg constructor
      { strcpy(str, ""); }

      xString( char s[] )            // 1-arg constructor
      { strcpy(str, s); }

      void input()                   // get string from user
      { cin.get(str, MAX); }

      void display()                 // display string
      { cout << str; }
                                     // concatenate two strings

      xString operator+(xString right)
      {
         xString temp;               // temporary xString
         if (strlen(str) + strlen(right.str) < MAX - 1)
         {
            strcpy(temp.str, str);   // copy us to temp
            strcat(temp.str, right.str);  // concatenate argument
         }
         else
            cout << "\nError: xString too long" << endl;
         return temp;                // return temporary
      }
};

void main()
{
   xString s1("Greetings, ");
   xString s2, s3;

   cout << "Enter your name: ";
   s2.input();                   // get s2 from user

   s3 = s1 + s2 + ".";          // concatenate period to s2, s2 to s1
   s3.display();                // display s3

   getch();
}   // main()

И в тази програма, както в предишната, във функцията operator+ се създава локален обект temp (в случая той е от класа xString), който да приеме резултата (в случая - обединяването на стринговете). Функцията връща по стойност този обект.

6. Програма за работа с комплексни числа. Това е аналог на програмата complex3.cpp от лекция 14, в която четирите функции на класа complex_number, add, sub, mult и div са променени с презаредените оператори.

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

// complex4.cpp
// a class that models a complex number data type

#include <iostream.h>
#include <iomanip.h>

class complex_number
{
  private:
    float real;          // real part
    float imaginary;     // imaginery part
  public:
    void set()
    {
      char dummy;        // for comma

      cout << "Enter number (format Re, Im): ";
      cin >> real >> dummy >> imaginary;
    }

    void display()
    {
      cout << '('
           << showpoint
           << setprecision(6) << real       // real part
           << ','
           << setprecision(6) << imaginary  // imaginary part
           << ')';
    }

    // add to the complex number another one (compl_num)
    complex_number operator + (complex_number compl_num)
    {
      complex_number cn;
      cn.real = real + compl_num.real;
      cn.imaginary = imaginary + compl_num.imaginary;

      return cn;
    }

    // subtract from the complex number another one (compl_num)
    complex_number operator - (complex_number compl_num)
    {
      complex_number cn;
      cn.real = real - compl_num.real;
      cn.imaginary = imaginary - compl_num.imaginary;

      return cn;
    }

    // multiply two complex numbers and assign the result to third one
    complex_number operator * (complex_number compl_num)
    {
      complex_number cn;

      cn.real = real*compl_num.real - imaginary*compl_num.imaginary;
      cn.imaginary = real*compl_num.imaginary + compl_num.real*imaginary;

      return (cn);
    }

    // divide the complex number by another one (compl_num)
    complex_number operator / (complex_number compl_num)
    {
      complex_number cn;

      cn.real = real;
      cn.imaginary = imaginary;

      float sqr_mod = compl_num.module();
      if (sqr_mod == 0)
      {
        cout << "Devision by zero!" << endl;
        return cn;
      }

      sqr_mod *= sqr_mod;

      cn.real = (real*compl_num.real + compl_num.imaginary*imaginary)/sqr_mod;
      cn.imaginary = (compl_num.real*imaginary - real*compl_num.imaginary)/sqr_mod;

      return cn;
    }

    // calculate the module of the complex number
    float module()
    {
      float m = real*real + imaginary*imaginary;
      return sqrt(m);
    }

};  // class complex_number

void main()
{
  // create three complex_number variables
  complex_number c1, c2, c3;
  char choice;

  do
  {
    // enter c1
    cout << "For c1, ";
    c1.set();       // set c1

    // enter c1
    cout << "For c2, ";
    c2.set();       // set c2
    cout << endl;

    // perform addition and ...
    c3 = c1 + c2;
    // ... display the result
    c1.display();
    cout << " + ";
    c2.display();
    cout << " = ";
    c3.display();
    cout << endl << endl;

    // perform subtraction and ...
    c3 = c1 - c2;
    // ... display the result
    c1.display();
    cout << " - ";
    c2.display();
    cout << " = ";
    c3.display();
    cout << endl << endl;

    // perform multiplication and ...
    c3 = c1 * c2;
    // ... display the result
    c1.display();
    cout << " * ";
    c2.display();
    cout << " = ";
    c3.display();
    cout << endl << endl;

    // perform division and ...
    c3 = c1 / c2;
    // ... display the result
    c1.display();
    cout << " / ";
    c2.display();
    cout << " = ";
    c3.display();
    cout << endl << endl;

    cout << "Do another (y/n)? ";
    cin >> choice;
  } while(choice != 'n');

}  //end main

Ако сравните кода на двете програми complex3.cpp и complex4.cpp ще забележите пълна прилика, с изключение на това, че четирите функции в complex3.cpp за действията с комплексни числа са сменени с презаредени оператори и начинът на извикването на операторите в главната функция е:

  c3 = c1 + c2;

вместо

  c3 = c1.add(c2);

и

  c3 = c1 - c2;

вместо

  c3 = c1.sub(c2);

и

  c3 = c1 * c2;

вместо

  c3 = c1.mult(c2);

и

  c3 = c1 / c2;

вместо

  c3 = c1.div(c2);

Обърнете внимание, че функцията float module() не може да бъде сменена с презареден оператор, наречен модул, защото такъв няма в програмния език C++. Но в следващите глави ще се спрем на едно преобразуване на комплексно число в реално, като под това ще рабираме изчисляване на модула на комплексното число.

Изходът от изпълнението на програмата при действията с комплексните числа 2i и 5i е следният:

For c1, Enter number (format Re, Im): 0,2
For c2, Enter number (format Re, Im): 0,5

(0.00000,2.00000) + (0.00000,5.00000) = (0.00000,7.00000)

(0.00000,2.00000) - (0.00000,5.00000) = (0.00000,-3.00000)

(0.00000,2.00000) * (0.00000,5.00000) = (-10.0000,0.00000)

(0.00000,2.00000) / (0.00000,5.00000) = (0.400000,0.00000)

Do another (y/n)?

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

(съдържание)
.
Литература
.
[1] Robert Lafore; C++ Interactive Course. Waite Group Press, Macmillan Computer Publishing, 1996.
.
Автор: Проф. Процесор Аритметиков
.
[ това е материал от брой 44 от октомври 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