Програмен език C++
Част 45
.
Презареждане на оператора за индекс
(Overloading the Subscript Operator, [])
.
(съдържание)
.
В предишната лекция се запознахме с презареждане на оператора за присвояване. В настоящата лекция ще разгледаме как да презаредим оператора за индекс, който обикновено се използва за достъп до елементите на масивите.

Една от причините да направим това презареждане е да работим с някакъв масив, somearr, променлива в някакъв обект, someobj, по нормалния начин, т.е. да имаме достъп до елементите на масива с someobj[i], а не с някаква специална функция. Освен това, презареждайки оператора за индекс, с наш програмен код можем да осигурим определена защита, която се изразява в това, че ще откриваме кога имаме достъп до несъществуващ елемент на масива и ще избягваме този достъп.

Както в повечето случаи, ще илюстрираме новите концепции с примерни програми от курса на Лафоор [1], като първо ще започнем с тривиалния достъп със специална функция до елементите на масив, който е променлива в даден обект. Обектът е от клас, който имитира целочислен масив, и целта на този клас е да имаме защитен достъп до елементите на масива. За да имаме достъп и в двете посоки, т.е. за да може с една и съща функция да получаваме, и едновременно с това, да променяме стойността на даден елемент, трябва функцията да връща стойност по връзка.

! Припомнете си за връщането на стойност по връзка от функциите, което е разгледано в лекция 30.
.
1. Достъп със специална функция (Access with access() Function). За да разберем идеята за презареждане на индексния оператор, ето един пример от курса на Лафоор [1], който използва функцията access() за да има достъп до елементите на масива arr, който е променлива на класа safearay.

// arrover1.cpp
// creates safe array (index values are checked before access)
// uses access() function for put and get

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

class safearay
{
   private:
      enum { SIZE = 100 };             // array size
      int arr[SIZE];                   // ordinary array
   public:
      int & access(const int & n);     // function declaration
};

int & safearay::access(const int & n)  // access() function
{                                      // returns by reference
   if ( n < 0 || n >= SIZE )
      { cout << "\nIndex out of bounds"; exit(1); }
   return arr[n];                      // return the value by reference
}

void main()
{
   int j;
   int temp;
   safearay sa;                     // make a safe array
   const int LIMIT = 20;            // amount of data

   for (j = 0; j < LIMIT; j++)      // insert elements
      sa.access(j) = j*10;          // *left* side of equals

   for (j = 0; j < LIMIT; j++)      // display elements
   {
      temp = sa.access(j);          // *right* side of equals
      cout << "\nElement " << j << " is " << temp;
   }

//   temp = sa.access(SIZE); // will give a termination of the program

   getch();
}   // main()

Главният момент в програмата е, че функцията access() връща стойност по връзка (типът на връщане е int&), т.е. тя може да бъде използвана, не само в дясно от оператора за присвояване, но и в ляво от него, както сочат тези две присвоявания от програмата по-горе.

  temp = sa.access(j);        // *right* side of equals
  sa.access(j) = j*10;        // *left* side of equals

Друг важен момент е, че в тялото на функцията за достъп се прави проверка, дали индексът излиза извън обхвата на масива arr. При индекс извън този обхват, се извиква библиотечната функция exit(), която прекъсва изпълнението на програмата и връща управлението на операционната система. Редът, който е с коментар отпред, би дал въпросната грешка, която ще бъде открита от програмата и нейното изпълнение ще бъде прекъснато. За да проверите това, махнете двете наклонени черти (коментара), //, и изпълнете програмата в операционната система (извън интегрираната работна среда), защото при грешката се излиза от програмата преди да "стигнем" до функцията getch().

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

Разбира се, дефинирането на масива arr в класа safearay е доста тромаво - винаги се отделя памет за SIZE на брой цели числа от масива arr. Този проблем може да бъде преодолян чрез използването на указатели, които ще изучим по-късно в следващите лекции.

2. Достъп с презареден индексен оператор (Access with Overloaded [ ] Operator). Следва друг пример от курса на Лафоор [1], в който функцията за достъп от горната програма arrover1.cpp е заменена с функцията-оператор [].

// arrover2.cpp
// creates safe array (index values are checked before access)
// uses access() function for put and get

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

class safearay
{
   private:
      enum { SIZE = 100 };                    // array size
      int arr[SIZE];                          // ordinary array
   public:
      int & operator [] (const int &);      // function declaration
};

int & safearay::operator [] (const int & n)   // function declaration
{                                             // returns by reference
   if ( n < 0 || n >= SIZE )
      { cout << "\nIndex out of bounds"; exit(1); }
   return arr[n];                             // return the value by reference
}

void main()
{
   int j;
   int temp;
   safearay sa;                     // make a safe array
   const int LIMIT = 20;            // amount of data

   for (j = 0; j < LIMIT; j++)      // insert elements
      sa[j] = j*10;                 // *left* side of equals

   for (j = 0; j < LIMIT; j++)      // display elements
   {
      temp = sa[j];                 // *right* side of equals
      cout << "\nElement " << j << " is " << temp;
   }

   // temp = sa[100];

   getch();
}   // main()

Дефиницията на функцията-оператор [], която е извън класа, е подобна на дотук изучените, като изключим връщането по връзка, което е необходимо за използването на тази функция в двете страни на оператора за присвояване. На практика, тази функция сменя по-тромавите записи

  temp = sa.access(j);       // *right* side of equals

 и

 sa.access(j) = j*10;        // *left* side of equals

с по-разбираемите

 temp = sa[j];               // *right* side of equals

 и

 sa[j] = j*10;               // *left* side of equals

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

temp = sa.operator[](j);    // get element j of array sa

но на практика, компилаторът разпознава и по-краткия запис (първия от тези по-горе).

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

Добре е читателят да се замисли, че ако искаме само да четем стойности от даден масив, не е необходимо функцията-оператор да връща стойност по връзка. Разбира се, ако се връща по стойност обект от някакъв клас, специфициран от нас, то бихме имали някои проблеми, в зависимост от класа. Тези проблеми са свързани с това, че връщането по стойност от типа на даден клас създава нови обекти от класа и това създаване може да повлече неприятни странични ефекти - вижте дискусията по този проблем в края на т.4 от лекция 44.

3. Доискусуряване на презаредените функции-оператори (Fine-Tuning Overloaded Operators). Досега при писането на тези функции бяха изпуснати нарочно някои програмистки техники, за да не се разводнят обясненията по темата. Сега в тази част ще разгледаме главно писането на функции-оператори като постоянни функции и някои особености при връщането на резултата от функциите-оператори.

В написаните в предишните лекции функции-оператори, когато аргументът бе обект, той бе предаван по връзка за да се спести времето, необходимо за неговото създаване при предаването по стойност, както и да не настъпват нежелателни странични ефекти при изпълнението на копиращия конструктор на класа. Когато този обект не трябваше да се променя, той биваше определян като постоянен, както това е направено в програмата assign2.cpp от лекция 44:

  // 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);
  }

3.1. Постоянни функции (Constant Functions). Тези функции бяха разгледани в т.2. на лекция 37. Постоянната функция не позволява обектът, за който тя се извиква, да бъде променен, т.е. те не могат да променят променливите на обектите. Пример от тази лекция е програмата consta.cpp, в която функцията на класа airtime::display() е постоянна:

 // output to screen
 void airtime::display() const
 {
    cout << hours << ':'         // hours and colon
         << setfill('0')         // fill character is '0'
         << setw(2) << minutes;  // minutes is 2 chars wide
 }



! Малко повторение от лекция 37: Постоянният обект noon представлява 12 часа на обед и затова е уместно да бъде константа, т.е. час, който не се променя. Разбира се, с учебна цел, в главната функция е извикана функцията get() на класа airtime, с която се въвежда ново време. Тъй като обектът е дефиниран в главната програма с ключовата дума const, то компилаторът издава предупреждение:

[C++ Warning] Non-const function airtime::get() called for const object.

което се отнася за реда

  noon.get();                    // change noon (bad!)

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

При декларацията и дефиницията на постоянна функция е необходимо наличието на ключова дума const. Също така тази ключова дума не стои пред типа на връщане на функцията, а след името на функцията и преди точката и запетаята в декларацията. Ако забравите в дефиницията ключовата дума const, но в декларацията я има, компилаторът издава съобщение за грешка:

[C++ Error] 'airtime::display()' is not a member of 'airtime'.

Това е така, защото ключовата дума const променя името на функцията, добавяйки я към него. Разбира се, ако функцията е дифинирана вътре в спецификацията на класа, то тя се нуждае само от една ключова дума const.



3.2. Постоянни презаредени функции-оператори (Constant Overloaded Operators). Всеки оператор, който не променя обекта, за който се извиква, трябва да бъде постоянен. Това ще позволява на програмиста да дефинира постоянни обекти от този клас. Бинарните оператори +, *, < и == трябва да се специфицират като постоянни, защото те не променят обекта, за който се извикват, който при тях реално е обектът от лявата страна на оператора. Също така унарните оператори - и +, за които обектът, който ги извиква е отдясно на тях, но не се променя, трябва да бъдат специфицирани като постоянни.

Но операторите за присвояване +=, *=, -=, /= и =, които променят обекта, за който се извикват, не трябва да се специфицират като постоянни! Унарните оператори -- и ++ (в своята префиксна или суфиксна форма) също променят обекта, за който се извикват, и не трябва да се специфицират като постоянни. Ако целта на индексния оператор, [], е промяна, а не само достъп до елементите на масива, то и той не трябва да е постоянен.

За да затвърдим казаното ето една програма от курса на Лафоор [1], която е променената програма addair.cpp от лекция 39, така че да може да събираме постоянни обекти от класа airtime.

// addairco.cpp
// overloads the + operator for airtime class,
// using const function

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

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

   public:
      // no-arg constructor
      airtime() : hours(0), minutes(0)
      {  }

      // two-arg constructor
      airtime(int h, int m) : hours(h), minutes(m)
      {  }

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

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

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

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

         return temp;                     // return temporary object by value

      }
};  // end class airtime

void main()
{
   airtime at1, at2;
   const airtime noon(12, 0);             // constant object

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

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

   getch();
}   // main()

Обърнете внимание, че в главната функция noon е деклариран да бъде постоянен. Към него се прибавя обектът at1, и резултатът се присвоява на at2. Понеже операторът + е постоянна функция, то е съвсем в реда на нещата да я извикаме за постоянен обект. Подобно на по-горе, където обсъждахме предупрежденията на компилатора, то ако операторът + не бе постоянна функция (т.е. премахнато е последното const от нея), то и тук компилаторът щеше да издаде предупреждение, което е малко по-различно

[C++ Error] : E2093 'operator+' not implemented in type 'airtime' for arguments of the same type.

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

! Обърнете внимание, че независимо дали операторът + е постоянен или не, то е съвсем в реда на нещата да запишем

  at2 = at1 + noon;   // whether operator+() is const or not, this is perfectly all right

тъй като в този случай noon е аргумент на функцията-оператор, а не обектът, за който се извиква функцията! В този случай функцията-оператор + се извиква за непостоянния обект at1.

3.3. Оптимизиране на начина по който се връща стойността от операторите (Fine-Tuning  Return Values). В някои от случаите е невъзможно да бъде връщана стойност по връзка от функцията-оператор, което бихме искали да направим, за да се избегне създаването на временен обект в тялото на оператора. Пример за тази невъзможност е гореразгледаната функция, която презарежда оператора +  за обекти от класа airtime. В тази функция ние получаваме нова стойност (сумата), която не се пази нито в постоянния аргумент, нито в обекта (също постоянен), за който се извиква функцията и затова трябва да се създаде временен обект. А както знаем, временни (автоматични) променливи не могат да се връщат по връзка, а само по стойност!

Но има оператори, при които се променя обекта, за който се извиква функцията - такива са операторите за присвояване +=, *=, -=, /= и =. При тях е възможно да се връща стойност по връзка и така да се избягва създаването на временен обект в тялото на презаредената функция-оператор.

В т. 5 на лекция 44 показахме как би изглеждал операторът =, при който се връща стойност по връзка. Използва се указателят this към обекта, за който се извиква функцията. Ако имаме присвояването

 obj3 = obj2 += obj1;

то  функцията-оператор operator+=() се извиква за обекта obj2, който се променя на obj2 + obj1 и този резултат се присвоява на обекта obj3. Т.е. връщаната стойност е обекта, за който се извиква функцията и затова може да се използва указателят this.

Ето отново дефиницията на функцията-оператор operator=() за класа omega, която бе дадена в т. 5 на лекция 44 :

    // 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 от същата лекция ще забележите, че връщането по стойност е с твърдението

 return omega(name);

а това по връзка с

 return *this;

3.3. Чудният обект *this (The Amazing *this Object). Ако поискате помощна информация от Борланд С++-билдер за this ще получите следното:


this

Category
C++-Specific Keywords

Syntax

class X
{

 int a;
 public:

   X (int b) {this -> a = b;}
};

Description

In nonstatic member functions, the keyword this is a pointer to the object for which the function is called. All calls to nonstatic member functions pass this as a hidden argument.

this is a local variable available in the body of any nonstatic member function. Use it implicitly within the function for member references. It does not need to be declared and it is rarely referred to explicitly in a function definition.

For example, in the call x.func(y), where y is a member of X, the keyword this is set to &x and y is set to this->y, which is equivalent to x.y.

Static member functions do not have a this pointer because they are called with no particular object in mind. Thus, a static member function cannot access nonstatic members without explicitly specifying an object with . or ->.



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

 obj.func();

то *this вътре в тялото на func() ще означава това, което obj означава в главната функция на програмата.

Още не сме се разгледали указателите, но ако сте запознати с тях от другите програмни езици (например от C), то веднага ще се сетите, че звездата пред this означава деотнасяне (dereferencing operator), т.е. ако this е адресът на обекта, то *this е самият обект.

3.4. Отново за оператора += (The += Operator Revisited). В тази част ще разгледаме една примерна програма от курса на Лафоор [1], която е подобрената програма pleqair.cpp от т. 4 на лекция 40. В новата програма pleqret.cpp операторът += връща стойност по връзка.

// pleqret.cpp
// overloads the += operator, uses *this

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

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

   public:
      // no-arg constructor
      airtime() : hours(0), minutes(0)
      {  }

      // 2-arg constructor
      airtime(int h, int m) : hours(h), minutes(m)
      {  }

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

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

      // overloaded += operator
      airtime & operator += (const airtime & right)
      {
         // add argument to us
         hours += right.hours;
         minutes += right.minutes;

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

         return *this;     // return by reference
      }
};  // end class airtime

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

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

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

   at1 += at2;                    // overloaded += operator
                                  // adds at2 to at1
   cout << "\nat1 += at2 = ";
   at1.display();                // display result

   at3 = at1 += at2;             // do it again, use return value

   cout << "\nat1 (at3 = at1 += at2;) = ";
   at1.display();                // display result

   cout << "\nat3 (at3 = ";
   at3.display();                // display result

   getch();
}   // main()

Главната разлика между двете програми е във функцията-оператор operator+=. В новия оператор имаме връщане по връзка - първо типът на резултата от функцията е airtime &, и второ, във функцията имаме твърдението

  return *this;     // return by reference

! Този оператор не може да бъде направен постоянен, тъй като променя обекта, за който се извиква.

3.5. Отново за оператора за нарастване ++ (The Increment Operator Revisited). В тази част ще разгледаме друга примерна програма от курса на Лафоор [1], която е подобрената програма postfix.cpp от т. 2 на лекция 41. В новата програма pfixret.cpp презаредения оператор ++ връща стойност по връзка в префиксната си форма и стойност при постфиксната.

// pfixret.cpp
// overloads the ++ operator, prefix version uses *this

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

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

   public:
      // no-arg constructor
      airtime() : hours(0), minutes(0)
      {  }

      // 2-arg constructor
      airtime(int h, int m) : hours(h), minutes(m)
      {  }

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

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

      // overloaded prefix ++ operator
      airtime & operator++ ()
      {
         ++minutes;                   // increment this object

         if (minutes >= 60)
         {
            ++hours;
            minutes -= 60;
         }

         // return incremented value
         return *this;
      }

      // overloaded postfix ++ operator
      airtime operator++ (int)
      {
         airtime temp(hours, minutes); // save original value
         ++minutes;                    // increment this object

         if (minutes >= 60)
         {
            ++hours;
            minutes -= 60;
         }
         // return old original value
         return temp;
      }
};  // end class airtime

////////////////////////////////////////////////////////////////
void main()
{
   airtime at1, at2;                  // make two airtimes
   at1.get();                         // get value for one

   at2 = ++at1;                       // increment it (prefix) and assign

   cout << "\nat2=";
   at2.display();                     // display result

   at2 = at1++;                       // increment (postfix) and assign

   cout << "\nat1=";
   at1.display();                     // display incremented value

   cout << "\nat2=";
   at2.display();                     // display assigned value

   getch();
}   // main()

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

! И двете версии на двата оператора за нарастване не могат да бъде направени постоянни, тъй като променят обекта, за който се извикват.

3.6. Обобщение на презареждането на операторите (Summary of Fine-Tuning for Overloaded Operators). В следната таблица (взета от книгата на Лафоор [1]) са систематизирани оператори-функции, техните препоръчителни връщани стойност, тип на аргументите и тип на функциите. Таблицата е дадена на английски, тъй като много от термините в нея нямат общоприет превод в българския.

Table. Recommended functions, return values, and arguments for overloaded operators as member functions.


Type of Overloaded Operator  Return                Argument Passed by             Function

Arithmetic (+, -, *, /)      by value              const reference                const
Assignment (=, +=, -=)       by reference (*this)  const reference                non-const
Comparison (<, ==)           by value (boolean)    const reference                const
Unary prefix (++, --)        by reference (*this)  none                           non-const
Unary postfix (++, --)       by value              value (dummy int)              non-const
Unary (-, +)                 by value              none                           const
Subscript ([])               by reference          value (integer)                non-const

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