Една от причините да направим това презареждане е да работим с някакъв масив, 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
}
[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.
Но операторите за присвояване +=, *=, -=, /= и =, които променят обекта, за който се извикват, не трябва да се специфицират като постоянни! Унарните оператори -- и ++ (в своята префиксна или суфиксна форма) също променят обекта, за който се извикват, и не трябва да се специфицират като постоянни. Ако целта на индексния оператор, [], е промяна, а не само достъп до елементите на масива, то и той не трябва да е постоянен.
За да затвърдим казаното ето една програма от курса на Лафоор [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
ще получите следното:
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 ->.
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.