Упражнение 6-5. Сравнете реализацията без член функция в Раздел 6.2. с тази реализация. Защо двете изглеждат различно? Еквивалентни ли са?
Член функции const
При опит за модифициране на обект const, който не е от тип клас, в една програма се отбелязва грешка по време на компилация. Например,
const char blank = ‘ ‘; blank = ‘�’; // error
Обектите класове, обаче, обикновено не се модифицират от прогрaмиста. По-скоро, се извиква публичния набор от член функции. За да бъде предизвикана константност на един класов обект, компилаторът трябва да разграничава безопасни и небезопасни член функции. Например,
const Screen blankScreen; // safe blankscreen.set( ‘*’ );
blankScreen.display();// unsafe Проектантът на класа отбелязва кои член функции са безопасни чрез думата const. Например,
class Screen { public:
char get() const { return *cursor; }
// ... };
Единствено член функциите, определени като const, могат да бъдат викани за класови обекти от тип const.
Ключовата дума const се поставя между списъка от аргументи и тялото на член функцията. Една член функция const, дефинирана вън от тялото на класа, трябва да добавя ключовата дума const както в декларацията, така и в дефиницията си. Например,
class Screen { public:
isEqual( char ch ) const;
// ... private:
char *cursor;
// ... };
Screen::isEqual( char ch ) const
{ return( ch == *cursor ); }
Не правилно да се декларират като const член функции, които изменят член данните. В следната oпростена дефиниция на Screen, например,
class Screen
{ public:
void ok(char ch) const { *cursor = ch; }
void error(char *pch) const { cursor = pch; }
// ... private:
char *cursor;
// ... };
ok() правилно е определена като const, понеже не променя стойността на cursor, а по-скоро променя стойността на адреса на обекта cursor. error(), обаче, изменя фактическата стойност на cursor и следователно не може да бъде дефинирана като член функция const. Като резултати от декларациите имаме следните съобщения за грешки:
error: assignment to member Screen::cursor of
const class Screen
Една член функция от тип const може да бъде презаредена с неконстантен представител, който дефинира същата сигнатура. Например,
class Screen { public:
char get( int x, int y ); char get( int x, int y ) const;
// ... };
В този случай константността на обекта от тип клас определя коя от функциите да бъде извикана:
const Screen cs; Screen s;
main() {
char ch = cs.get(0,0); // const member
ch = s.get(0,0); // nonconst member }
Конструкторите и деструкторите са изключения и не е необходимо да бъдат декларирани като const за да бъдат прилагани към константни обекти от тип клас. Изобщо, всеки клас, който се очаква да бъде многократно използван, трябва да декларира разрешените член функции за константните обекти от тип клас като const.
Упражнение 6-6. Определете онези член функции на Screen, които могат да бъдат дефинирани като const.
6.4. Неявен указател this
Може да се каже, че е налице определена липса на елегантност при настоящата реализация на член функциите на класа Screen. Обработките на един екран се грижат за извършване на редица действия: почистване, местене, писане, показване. Програмистът, обаче, е принуден да кодира всяко действие като отделен оператор:
Screen myScreen( 3, 3 ), bufScreen;
main() {
myScreen.clear();
myScreen.move(2,2);
myScreen.set(‘*’);
myScreen.display();
bufScreen.reSize(5,5);
bufScreen.display(); }
Необходимостта да се пишат нови оператори за всяко действие, приложено към Screen обектите предизвиква многословие. За предпочитaне е да се поддържа конкатенация на обръщения към даден обект. Например,
main()
{
myScreen.clear().move(2,2).set(‘*’).display();
bufScreen.reSize(5,5).display();
}
Този раздел илюстрира как може да бъде реализиран този синтаксис. Първо нека да разгледаме по-подробно член функцииите на класа сами по себе си?
Какво представлява указателя this
Всеки обект от тип клас поддържа собствено копие на член данните на класа. myScreen има свои width, heigth, cursor и screen; bufScreen има собствен отделен набор. Но както myScreen така и bufScreen, обаче, ще извикват едно и също копие на която и да е член функция. Съществува само по един представител на всяка член функция на класа. Поради това възникват два проблема:
1. Ако съществува само един представител на дадена член функции, той не може да бъде пазен вън от обекта клас. Тогава бързо ще се увеличи броя на копията на функциите, които всеки обект ще дефинира.
2. Ако съществува само един представител на дадена член функция, как определени член данни на някой обект от тип клас ще се свържат с член данните, обработвани в член функциите? Как, например, cursor, обработвана от move() ще се свърже с cursor, принадлежащ на myScreen или на bufScreen?
Отговор на това дава указателя this. Всяка член функция съдържа указател към своя тип клас, наречен this. В член функцията на Screen, например, указателят this е от тип Screen*; в член функцията IntList той е от тип IntList*.
Указателят this съдържа адреса на обекта от тип клас, за който член функцията е извикана. По този начин cursor, обработван чрез home() се свързва с cursor, принадлежащ на myScreen и bufScreen.
Един начин за разбиране на този указател e да се разгледа как един компилатор, например, AT&T езиковата система, го реализира. Тя го организира в следните две стъпки:
1. Транслира член функциите на класа. Всяка член функция на клас се транслира в уникално именувана нечлен функция с един допълнителен аргумент указателя this. Например,
home__Screen( Screen *this )
{ this->cursor = this->screen;
}
2. Транслира всяко извикване на член на клас. Например, myScreen.home() се превежда в home__Screen( &myScreen );
Програмистът може да използува указателя this и явно. Например, правилно е, въпреки че е безмислено, да се пише следното:
inline void Screen::home()
{ this->cursor = this->screen; }
Съществуват обстоятелства, обаче, при които на програмиста не се налага да използува указтеля this явно.
Използуване на указтеля this
Указателят this е ключът за реализиране на синтаксиса с конкатeнация за класа Screen. Член операторите за селекция (“.” и “- >”) са ляво асоциативни бинарни оператори. Редът за изпълнение е от ляво надясно. myScreen.clear() се извиква първо. За да бъде извикана след това move(), clear() трябва да върне обект от класа Screen. За правилното извикване на move(), clear() трябва да върне обект от тип myScreen. Всяка член функция трябва да бъде изменена така, че да връща обект от типа на класа, за който е била извикана. Достъпът до обекта от тип клас е през указателя this. Ето реализацията на clear():
Screen& Screen::clear( char bkground )
{ // reset the cursor and clear the screen
char *p = cursor = screen;
while ( *p ) *p++ = bkground;
return *this; // return invoking object }
Към дефинициите на move(), home(), функциите set() и четирите функции за движение на курсора трябва да бъде добавен оператора return *this и да бъде променен типа им за връщане от void на Screen&.
Нека да видим как могат да бъдат използувани тези функции - първо от самите член функции, и после от нечлен функции.
Screen& Screen::lineX( int row, int col, int len, char ch)
{ /* provide straight line in row beginning at col * of length len using character ch */
move( row, col );
for ( int i = 0; i < len; ++i )
set( ch ).forward();
return *this; }
Следнaтa нечлен функция предлага възможност за чертане на линия с някaква дължина надолу в определена колона:
Screen& lineY( Screen& s, int row, int col, int len, char ch )
{ // provide vertical line at col
s.move( x, y );
for ( int i = 0; i < len; ++i )
s.set(ch).down(); return s; }
Упражнение 6-7. Една член функция би отстранила лексикалната сложност на оператора за избор на член чрез механизма на указателя this. За да видите това по-ясно напишете отново lineY() като член функция на Screen.
Член функцията на Screen display() може да бъде реализирана по следния начин:
Screen& Screen::display()
{ char *p;
for ( int i = 0; i < heigth; ++i )
{ // for each row
cout << "n";
int offset = width * i; // row postion
for ( int j = 0; j < width; ++j )
{ // for each column, write element
p = screen + offset + j; cout.put( *p );
} }
return *this; }
Възможно е също така класовите обекти, адресирани чрез указателя this, да бъдат препокривани, което е показано посредством член функцията reSize(). reSize() генерира един нов обект от тип Screen. Следният оператор за присвояване заменят извикания обект от тип Screen с този нов обект. *this = *ps; където ps сочи към новият обект от тип Screen. Една реализация на reSize() има вида:
Screen& Screen::reSize( int h, int w, char bkground )
{ // reSize a screen to heigth h and width w
Screen *ps = new Screen( h, w, bkground );
char *pNew = ps->screen;
// Is this screen currently allocated?
// If so, copy old screen contents to new
if ( screen )
{ char *pOld = screen;
while ( *pOld && *pNew ) *pNew++ = *pOld++;
delete screen; }
*this = *ps; // replace Screen object
return *this; }
Управлението на свързан списък също често изисква наличието на достъп до указателя this. Например,
class DList { // doulby-linked list
public:
void append( DList* );
// ... private:
DList *prior, *next; };
void DList::append( DList *ptr )
{ ptr->next = next;
ptr->prior = this;
next->prior = ptr;
next = ptr; }
Ето една малка програма, която демонстрира някои от член функциите на Screen, дефинирани в този и предходните раздели.
#include "Screen.h" main() {
Screen x(3,3);
Screen y(3,3);
// if equal, return 1
cout << "isEqual( x, y ): (>1<) " << x.isEqual(y) << "n";
y.reSize( 6, 6 ); // double it
cout << "isEqual( x, y ): (>0<) " << x.isEqual(y) << "n";
lineY(y,1,1,6,’*’); // draw a line on the Y axix
lineY(y,1,6,6,’*’); // draw a line on the X axix
y.lineX(1,2,4,’*’).lineX(6,2,4,’*’).move(3,3);
// write to screen and display
y.set("hi").lineX(4,3,2,’^’).display();
// x and y equal in size, but not content
x.reSize( 6, 6 );
cout << "nnisEqual( x, y ): (>0<) " << x.Equal(y) << "n";
// now, both are equal
x.copy( y );
cout << "isEqual( x, y ): (>1<) " << x.isEqual(y) << "n";
return 0; }
Когато тази програма бъде компилирана и изпълнена се получава следния резултат:
isEqual( x, y ): (>1<) 1
isEqual( x, y ): (>0<) 0
****** #### #hi# #^^# #### ******
isEqual( x, y ): (>0<) 0
isEqual( x, y ): (>1<) 1
където стойностите, заградени със скоби са очакваните резултати, а стойностите вън от скобите са фактическите стойности, генерирани от обръщението към isEqual().