Глава 6: Класът в С++
Съдържание на шеста глава :

6.1. Дефиниция на клас
6.2. Класови обекти
6.3. Член функции на клас
6.4. Неявен указател this
6.5. Приятели на клас
6.6. Статични членове на класа
6.7.Указател към член на клас
6.8. Обхват на клас
6.9. Обединение: Клас, който пести памет
6.10. Разредно поле: член, който пести памет
6.11. Аргумент клас и многоточие



--------------------------------------------------------------------------------



Класовият механизъм в С++ позволява на потребителите да дефинират техни собствени типове данни. Тези типове могат да обогатят функционално вече съществува и типове - такива като класа IntArray, дефиниран в глава 1. Класовете могат също така да бъдат използувани за дефиниране на напълно нови типове, такива като класа на комплексните числа Complex или класа BitVector. Класовете типично се използуват за дефиниране на абстракции, които не се вписват естествено в предварително дефинираните или произлезли типове данни; например, класа computer Task, класа за терминален дисплей Screen, класа Employee или ZooAnimal. Възможните класови типове са неограничен брой. В следващите четири глави ние ще реализираме известен брой класове. Всеки С++ клас има четири атрибута, свързани с него:

1. Съвкупност от членове данни (data members), определящи представянето класа. В един клас може да има нула или повече членове данни от всеки тип.

2. Съвкупност от член функции, т.е. наборът от операции, които могат да бъдат прилагани към обектите на класа. За всеки клас могат да се дефинират нула или повече член функции. Те се наричат интерфейс на класа.

3. Нива на програмен достъп. Членовете на класа могат да бъдат определени като private, protected или public. Тези нива управляват достъпа до членовете от вътрешността на програмата. Обикновено, представянето на класа е private докато операциите, което могат да бъдат прилагани върху представянето са public. Типът на public/private спецификации се свързва с термина информационно скриване. За вътрешно представяне от тип private се казва, че е капсулирано.

4. Име на класа (tag name), което служи за типов спецификатор на клас, дефиниран от потребителите. Име на клас може да се появява в програмата навсякъде, където може да се появи предварително дефиниран типов спецификатор. Например, ако е дадено името на класа Screen, потребителят може да напише: Screen myScreen; Screen *tmpScreen = &myScreen; Screen& copy( const Screen[] );

Всеки клас с представяне private и набор от операции public се нарича абстрактен тип данни. В тази глава и в глава 6 ще разгледаме проектирането, реализацията и използуването на абстрактните типове данни в С++.



6.1. Дефиниция на клас

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

class Screen { /* ... */ }

class Screen { /* ... */ }

myScreen, yourScreen;

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

Член данни

Декларацията на член данните на класа е същата, както и декларацията на променливи с едно изключение - не са позволени явни инициализатори. Например, класът Screen може да дефинира представянето си така:

class Screen

{ short height; // number of Screen rows

short width; // number of Screen columns

char *cursor; // current Screen position

char *screen; // screen array (height*width) };

Както и при декларацията на променливи не е необходимо два члена short или два члена char* да се декларират отделно. Следните дефиниции са еквивалентни:

class Screen {

/* * height and width refer to row and column * cursor points to current Screen position * screen addresses array height*width */

short height, width;

char *cursor, *screen; };

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

class StackScreen

{ int toptack; void (*handler)(); // handles exceptions Screen stack[ STACK_SIZE ];

}

Един класов обект може да бъде деклариран като член данни само ако неговата класова дефиниция вече е била открита. В случаите, когато класовата дефиниция не е била открита може да бъде приложена предварителна декларация на клас. Предварителната декларация разрешава указатели и псевдоними на обекти от даден клас да бъдат декларирани като член данни. Указателите и псевдонимите са разрешени понеже и двата са с фиксиран размер независимо от типа, който те адресират. Например, ето дефиницията на StackScreen с използуване на предварителна декларация:

class Screen; // forward declaration class StackScreen

{ // pointer to STACK_SIZE Screen objects

int topStack;

Screen *stack;

void (*handler)(); };

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

class LinkScreen {

Screen window;

LinkScreen *next;

LinkList *prev; }



Член функции

Потребителите на класа Screen трябва да изпълняват широк кръг от операции върху обектите от класа Screen. Ще бъде необходим един набор от операции за движение на курсора. Също трябва да бъде приложена способността да се проверяват и установяват точки върху екрана. Потребителят трябва да бъде в състояние да копира един обект от тип Screen в друг и да открива по време на изпълнение фактическата размерност на екрана. Този набор от операции за обработване на обект от тип Screen е деклариран в тялото на класа. За тези операции се говори като за член функции на класа.

Член функциите на класа се декларират в тялото на класа. Всяка декларация се състои от прототип на функция. Например,

class Screen

{ public

void home();

void move( int, int );

char get();

char get( int, int ); void chackRange( int, int );

// ... };

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

class Screen

{ public:

void home() { cursor = screen; }

char get() { return *cursor; }

// ... }

home() позиционира курсора в горния ляв ъгъл на екрана. get() връща стойността на текущата позиция на курсора. Понеже те са дефинирани вътре в тялото на класа автоматично се обработват като функции inline.

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

#include "Screen.h"

#include <stream.h>

#include <stdlib.h>

void Screen::checkRange( int row, int col )

{ // validate coordinates

if ( row < 1 || row > height || col < 1 || col > width )

{ cerr << "Screen coordinates ( " << row << ", " << col

<< " ) out of bounds.\n"; exit( -1 );

} }

Член функция, която е декларирана вън от тялото на класа трябва явно да декларира, че ще бъде от тип inline. Например, следният фрагмент дефинира move() като inline член функция на Screen:

inline void

Screen::move( int r, int c ) { // move cursor to absolute position

checkRange( r, c ); // valid address?

int row = ( r-1 )*width; // row location

cursor = screen + row + c - 1;

}

Член функциите могат да бъдат разграничени от обичайните функции по следните атрибути: - член функциите имат пълни права за достъп както до публичните, така и до личните членове на класа докато, докато обичайните функции имат достъп само до публичните членове на класа. Разбира се, член функциите на един клас нямат права за достъп до членовете на други класове. - Член функциите са дефинирани в обхвата на класа си; обичайните функции са дефинирани с файлов обхват. (Раздел 6.8 разглежда подробно класовия обхват).

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

inline char

Screen::get( int r, int c )

{ move( r, c ); // position cursor

return get(); // other

Screen get()

}

Скриване на информация

Често се случва така, че вътрешното представяне на един класов тип се изменя след началната му употреба. Например, представете си, че e проведено едно изследване сред потребителите на нашия клас Screen и се е установило, че всички класови обекти Screen се дефинирани с размерност 24 х 80. В този случай следното представяне на класа Screen ще бъде не така гъвкаво, но по- ефективно:

const Height = 24;

const Width = 80;

class Screen

{ char screen[ Height ][ Width ]; short cursor[2];

};

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

- Всяка програма, която е имала директен достъп до член данните на старото представяне на Screen вече не работи. Необходимо е да се открият и напищат отново всички онези части от текста, използуващи това представяне, преди програмите да могат да бъдат използувани отново.

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

Скриването на информацията е един формален механизъм за ограничаване на потребителския достъп до вътрешното представяне на класовия тип. То се определя чрез етикираните чрез public, private и protection раздели в тялото на класа. Членовете, декларирани в частта public стават публични членове; тези, декларирани в private и protected стават лични или защитени.

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

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

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

Следните дефиниции на Screen определят публичните и личните му части:

class Screen

{ public:

void home() { move( 0, 0 ); }

char get() { return *cursor; }

char get( int, int );

inline void move( int, int ); // ...

private:

short heigth, width;

char *cursor, *screen; };

Съществува споразумение, според което публичните членове се записват първи. Личните членове се задават в края на тялото на класа.

Един клас може да съдържа много много секции, означени като публични, защитени или лични. Статусът на всеки раздел остава в сила докато не бъде срещнат друг етикет на раздел или затваряща скоба на тялото на класа. Ако не е зададен никакъв етикет се приема по подразбиране, че секцията, започваща незабавно след лявата отваряща скоба е лична.



6.2. Класови обекти

Предходната дефиниция на Screen не предизвикваше отделянето на каквото и да е количество памет. С дефиницията на всеки класов обект се отделя памет за класа. Дефиницията: Screen myScreen; например, отделя памет, достатъчна да побере четерите член данни на класа Screen.

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

// bufScreen.height = myScreen.height

// bufScreen.width = myScreen.width

// bufScreen.cursor = myScreen.cursor

// bufScreen.screen = myScreen.screen

Screen bufScreen = nyScreen

По подразбиране класовете обекти се изпращат по стойност като аргументи на функции и типове за връщане на функции. Указател към обект клас може да бъде инициализиран или да му бъде присвоявана стойност чрез оператара new или чрез адресния оператор (“&”). Например,

Screen *ptr = new Screen;

myScreen = *ptr;

ptr = &bufScreen;

Вън от обхвата на класа на операторите за избор на член е необходимо да имат достъп или до член данните или до член функциите на класа. Селекторът на обект клас (“.”) се използува с обект клас или негов псевдоним; селекторът на указател към обект клас (“->”) се използува с указател към обект клас. Например,

#include "Screen.h"

isEqual( Screen& s1, Screen *s2 )

{ // return 0 if not equal, 1 if equal

if ( s1.getHeigth() != s2->getHeigth()

|| s1.getWidth() != s2->getWidth() )

return 0; // not equal

for ( int i = 0; i < s1.getHeigth(); ++i)

for ( int j = 0; j < s2->getWidth(); ++j)

if (s1.get( i, j ) != s2->get( i, j )) return 0; // not equal // still hehre? then screens are equal

return 1; }

isEqual() е една нечлен функция, която сравнява два Screen обекта за еквивалентност. isEqual() няма права на достъп до личните член данни на Screen; тя трябва да разчита на публичните член функции на класа Screen.

getHeigth() и getWidth(), наречени функции за достъп, предлагат достъп само за четене до личните член данни на класа. Тяхната реализация следва. За да бъдат по-ефективни те са дефинирани като inline:

class Screen { public: int getHeigth() { return heigth; }

int getWidth() { return width; } // ...

private: short heigth, width; // ... };

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

#include "String.h"

#include <string.h>

void Screen::copy( Screen& s )

{ // copy one Screen object with another delete screen;

// free up existing storage

heigth = s.heigth;

width = s.width;

screen = cursor = new char[heigth*width + 1];

strcpy( screen, s.screen ); }

Упражнение 6-1. copy() дава такъв размер на обекта начначение Screen, какъвто е размера на обекта Screen, който се копира. Реализрайте copy() така, че да позволява размерите на обектите източник и назначение Screen да бъдат различни.

Упражнение 6-2. Относно символите, съдържащи се в screen е било направено едно интересно, а вероятно и малко опасно предположение. Това предположение позволява членовете на screen да бъдат копирани като се използува библиотечната функция strcpy(). Какво е това предположение? Реализирайте copy() отново така, че да не зависи от това предположение.




6.3. Член функции на клас

Член функциите предлагат набор от операции, които потребителят може да изпълнява над типа клас. Този набор се нарича публичен интерфейс на класа. Дали класът е сполучлив или не зависи от сложността и ефективността на набора му от член функции. Този раздел разглежда член функциите, необходими на класа Screen. Той е разделен на следните четири подсекции: управляваши функции, функции за реализация, помощни функции и функции за достъп. Тези категории член функции не са част от езика С++. По-скоро те изразяват един метод за мислене относно типовете член функции, от които най-общо се нуждае един клас.

Управляващи функции

Един набор от специализирани член функции, управлявашите

функции, управляват обектите на класа, обработвайки инициализацията, присвояването, управлението на паметта и преобразуването на типовете. Управляващите функции обикновено се извикват неявно от компилатора.

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

Screen::Screen( int high, int wid, char bkground )

{ // Screen initializer function: constructor

int sz = high * wid;

heigth = high;

width = wid;

cursor = screen = new char[ sz + 1 ];

char *ptr = screen;

char *endptr = screen + sz;

while ( ptr != endptr ) *ptr++ = bckground;

*ptr = ‘/0’; // end of screen marked by null

}

Декларацията на конструктора в тялото на Screen предлага подразбиращи се стойности за аргументите high, wid и bkground.

class Screen

{ public: Screen ( int = 8, int = 40; char=’#’ ); // ... };

Всеки деклариран обект от тип Screen автоматично се инициализира чрез конструктора на Screen. Например,

Screen s1; // Screen(8,40,’#’)

Screen *ps = new Screen( 20 ); // Scrren(20,40,’#’)

main()

{ Screen s(24,80,’*’); // Screen(24,80,’*’) // ... }

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

Функции за реализация

Второто множество от член функции, наречени функции за реализация, предлагат възможности, отнасящи се до абстракцията клас. Например, от Screen се очаква да поддържа такива движения като home() и move(). Допълнителни изисквания към движението на курсора са forward(), back(), up() и down(). Член функците forward() и back() местят курсора по един символ. При достигане на края или началото на екрана курсорът се движи кръгово.

inline void Screen::forward()

{ // advance cursor one screen element

++cursor;

// check for bottom of screen; wrapround

// bottom of screen is null element

if ( *cursor == ‘\0’ ) home();

}

inline void Screen::back()

{ // move cursor backward one screen element

// check for top of screen; wrapround

if ( cursor == screen ) bottom();

else—cursor; }

bottom() е от функциите за реализация и позиционира курсора в последната колона на екрана.

inline void Screen::bottom()

{

int sz = width*height - 1;

cursor = screen + sz; }

up() и down() местят курсоро нагоре и надолу по един ред от екрана. При достигане на най-горния и най-долния ред на екрана курсорът не се движи кръгово, а предизвиква издаване на звуков сигнал, оставайки там където се намира.

const char BELL = ‘\007’;

void inline Screen::up()

{ // move cursor up one row of screen

// do not wraparound; rather, ring bell

if ( row() == 1 ) // at top?

cout.put( BELL );

else cursor -= width; }

void inline Screen::down()

{ if ( row() = heigth ) // at bottom ?

cout.put( BELL ); else cursor += width; }

Упражнение 6-3. Към движенията на курсора могат да бъдат добавени движение напред и назад с по една дума, където думата е ограничена с интервали. Реализирайте wordForward().

Упражнение 6-4. Друга интересна възможност е курсорът да може да бъде позициониран при появата на даден низ. Например,

myScreen.find( "this” ); Реализирайте find( char* ).

Помощни функции

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

row() връща реда, на който курсорът е позициониран:

inline Screen::row() { // return current row

int pos = cursor - screen + 1;

return (pos+width-1)/width; }

col() връща колоната, в която се намира курсорът:

inline Screen::col()

{ // return current column

int pos = cursor - screen + 1;

return ((pos+width-1) % width)+1;

}

remainingSpase() връща броя интервали, оставащи на екрана без да се брои текущата позиция:

inline Screen::remainingSpase()

{ // current position is no longer remaining

int sz = width*heigth;

return( screen + sz - cursor - 1 );

};

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

void inline Screen::stats()

{

cout << "row: " << row() << "\t";

cout << "col: " << col() << "\t";

cout << "rm: " << remainingSpace() << "\n";

}

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

#include "Screen.C"

#include <stream.h>

main()

{ // exercise cursor movements

Screen x(3,3); int sz = x.getHeigth() * x.getWidth();

cout << "Screen Object ( " << x.getHeight() << ", " << x.Width() << " ) ( size: " << sz << " )\n\n";

x.home();

for ( int i = 0; i <= sz; ++i )

{ // "<= " in order to wrapround

x.stats();

x.forward();

}

return 0;

}

Когато тази програма бъде компилирана и изпълнена се получават следните резултати:

Screen Object ( 3, 3 ) ( size: 9 )

row: 1 col: 1 rm: 8

row: 1 col: 2 rm: 7

row: 1 col: 3 rm: 6

row: 2 col: 1 rm: 5

row: 2 col: 2 rm: 4

row: 2 col: 3 rm: 3

row: 3 col: 1 rm: 2

row: 3 col: 2 rm: 1

row: 3 col: 3 rm: 0

Функции за достъп

Скриването на информацията капсулира вътрешното представяне на обектите класове, и чрез това предпазва потребителя от внасяне на промени в това представяне. Също така важно е да бъде защитено вътрешното състояние на обекта клас от случайни програмни модификации. Едно специфично малко множество от функции предлага всичко, необходимо за достъп до обект за запис. Ако се получи грешка, областта за търсене се ограничава до това множество от функции, което значително улеснява решаването на проблемите, свързани поддържането и корекциите на програмите. Член функциите, осигуряващи достъпа на потребителя до данните на обектите се наричат функции за достъп. До сега разглеждахме само функции за достъп за четене. Ето две функции set(), които позволяват на потребителя да пише на екрана:

void Screen::set( char *s )

{ // write string begining at screen element

int space = remainingSpace();

int len = strlen( s );

if ( space < len )

{

cerr << "Screen: warning: truncaton: " << "space: " << space

<< "string length: " << len << "\n"; len = space; }

for ( int i = 0; i < len; ++i )

*cursor++ = *s++; }

void Screen::set( char ch )

{ if ( ch == ‘\0’ )

cerr << "Screen: warning: " << "null character (ignored).\n";

else *cursor = ch; }

При реализацията на нашия клас Screen сме възприели eдно опростяващо допускане, че той не съдържа нулеви символи. Това е причината, поради която set() не разрешава нулевия символ да бъде изписан на екрана.

Функциите за достъп могат също така да обогатят класа чрез множество от предикатни операции. Класът Screen, например, може да бъде обогатен чрез набор от функции isEqual().

isEqual(char ch) връща истина ако ch е еднакъв със символа, намиращ се на текущата позиция на курсора. Ето реализацията й:

class Screen

{ public: isEqual( char ch )

{ return (ch == *cursor ); } // ... }

isEqual(char* s) връща истина ако масивът от символи, започващ от текущата позиция на курсора е еднакъв с s.

#include <string.h>

Screen::isEqual( char *s )

{ // yes? return 1; otherwise, 0

int len = strlen( s );

if ( remainingSpace() < len )

return 0;

char *p = cursor;

while ( len-- > 0 )

if ( *p++ != *s++ ) return 0;

return 1; }

isEqual(Screen&) връша истина ако два обекта Screen са еднакви; т.е. височината, ширината и съдържанието на двата екрана трябва да бъдат еднакви.

Screen::isEqual( Screen& s )

{ // first, are they phisically unequal?

if ( wigth != s.width || heigth != s.heigth )

return 0; // do both share the same screen?

char *p = screen;

char *q = s.screen;

if p == q return 1; // be careful not to walk off the Screens

while ( *p && *p++ == *q++ );

if ( *p ) // loop broke on not equal

return 0;

return 1; }

Упражнение 6-5. Сравнете реализацията без член функция в Раздел 6.2. с тази реализация. Защо двете изглеждат различно? Еквивалентни ли са?

Член функции const

При опит за модифициране на обект const, който не е от тип клас, в една програма се отбелязва грешка по време на компилация. Например,

const char blank = ‘ ‘; blank = ‘\0’; // 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 << "\n\nisEqual( 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().



6.5. Приятели на клас

В някои случаи правилата за достъп до скриваната информация са доста ограничаващи. Механизмът за приятелство дава права за достъп до членове на класа на нечленове. Преди да разгледаме правилата за деклариране на приятели, нека да разгледаме един пример, в който е необходим приятел. Операторите за вход и изход от iostream (“>>”,”<<”) могат да бъдат презареждани за да обработват типа клас. След като операторите веднъж са били дефинирани за типа клас, обектите на класа могат да бъдат извeждани по същия начин, както и вградените типове. Например,

Screen myScreen;

cout << myScreen;

cout << "myScreen: " << myScreen << "\n";

Както операторът за вход, така и операторът за изход изискват обект от iostream като техен ляв операнд и връщат обекта от iostream, с който са оперирали. Това позволява успешно завършилите оператори за вход и изход да бъдат конкатенирани. Например,

((( (cout << "myScreen: ") << myScreen ) << "\n")

Всеки подизраз, заграден в скоби, връща обекта cout от iostream, който става ляв операнд на следващия израз.

cout << myScreen

трябва да бъде реализиран като ostream

&operator<<( ostream&, Screen& );

Тази реализация, обаче, предотвратява дефинирането на тези оператор функции като член функции на Screen.

Ето декларацията на оператора за изход като член функция на Screen:

class Screen

{ public: ostream &operator<<( ostream& );

// ... };

Левият операнд на всяка член функция е обект или указател към обект от класа й. Поради това представителят на член функцията на оператора за изход декларира само един ostream аргумент. Обръщението към този представител има следния вид:

myScreen << cout;

Би било объркващо както за програмиста, така и за читателя на програмата да добави този представител. Ако бъде добавен представител, който не е член функция, обаче, операторът за изход няма да има права за достъп до непубличните членове на класа Screen - много от които се предвижда да бъдат показани. Ето къде е необходим механизма на приятелството.

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

class Screen {

friend iostream&

operator>>( iostream&, Screen& );

friend iostream&

operator<<( iostream&, Screen& );

public:

// ... rest of the Screen class };

Как може за бъде написан оператора за изход на Screen? Необходими са три от член данните на Screen - heigth, width и адреса на фактическият масив от символи screen. За oпростяване курсорът се връща в позиция home когато се чете обектът Screen. Форматът на изхода за обект Screen има вида:

<heigth,width> linear Screen Dump

Операторът за изход може да бъде реализиран по следния начин:

ostream& operator<<( ostream& os, Screen& s )

{ os << "\n<" << s.heigth << "," << s.width << ">";

char *p = s.screen;

while ( *p ) os.put( *p++ );

return os; }

Ето пример за използуване на оператора за изход. main() се реализира така:

#include <stream.h>

#include "Screen.h"

main() {

Screen x(4,4,’%’);

cout << x;

return 0; }

Когато компилираме и изпълним тази програма получаваме:

<4,4>%%%%%%%%%%%%%%%%

Операторът за вход ще чете като входни данни резултата от изпълнението на оператора за изход на Screen. Нека запазим изходния резултат във файл с име output. Реализацията на операторът за вход е представена по нататък; проверката на формата на входния текст е пропусната за да се спести място.

istream& operator>>( istream& is, Screen& s )

{ // read Screen object output by operator <<

int wid, hi;

char ch; // format verification not shown

// <hi,wid>screenDump

is >> ch; // ‘<’

is >> hi; // get heigth

is >> ch; // ‘,’

is >> wid; // get width

is >> ch; // // ‘>’

delete s.screen;

int sz = hi * wid; s.height = hi; s.width = wid;

s.cursor = s.screen = new char[ sz + 1 ];

char *endptr = s.screen + sz;

char *ptr = s.screen;

while ( ptr != endptr ) is.get( *ptr++ );

*ptr = ‘\0’;

return is; }

Следната малка програма дава пример за използване както на оператора за вход, така и на оператора за изход за класа Screen.

#include <stream.h>

#include "Screen.h"

main() {

Screen x(5,5,’?’);

cout << "Initial Screen: \t" << x;

cin >> x;

cout << "\nInput Screen: \t" << x;

return 0; }

Входните данни са като изхода на предходната програма. Когато тази програма бъде компилирана и изпълнена се получава следния резултат:

Initial Screen: <5,5>??????????????????????????

Input Screen: <4,4>%%%%%%%%%%%%%%%%

Упражнение 6-8. Операторът за вход трябва да проверява правилността на форматираните входни данни. Изменете го така, че това да бъде изпълнено.

Упражнение 6-9. Реализирането на оператора за изход така, че да запазва позицията на курсора не е трудно. За това могат да послужат член функциите col() и row(). Напишете отново оператора за изход, така че да запазва текущата позиция на курсора.

Упражнение 6-10. Напишете отново оператора за вход, така че да може да обработва формата на описания в предходното упражнение оператор за изход.

Един клас трябва да определи всеки представител на дадена презаредима функция, която желае да бъде приятел на класа. Например,

iostream& storeOn( iostream&, Screen& );

BitMap& storeOn( BitMap&, Screen& );

class Screen {

friend iostream& storeOn( iostream&, Screen& );

friend BitMap& storeOn( BitMap&, Screen& );

public: // ... rest of the Screen class };

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

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

// forward declarations

class Screen;

class Window;

Screen& isEqual( Screen&, Window& );

class Screen {

friend Screen& isEqual( Screen&, Window& );

// ... rest of Screen goes here };

class Window {

friend Screen& isEqual( Screen&, Window& );

// ... rest of Window goes here };

Във втория случай функцията логически принадлежи като член на единия клас. Освен това, обаче, й е необходим достъп до втория клас. Следователно, тя може да бъде направена приятел на втория клас:

class Window;

class Screen

{ public:

Screen& copy( Window& );

// ... remaining Screen members };

class Window {

friend Screen& Screen::copy( Window& )

// ... remaining Window members };

Screen& Screen::copy( Window& ) { /* ... */ }

Цял един клас може да бъде деклариран като приятелски на някой клас. Например,

class Window;

class Screen {

friend class Window;

public: // ... rest of the Screen class };

Сега непубличните членове на класа Screen са достъпни за всички член функции на Window.



6.6. Статични членове на класа

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

Всеки статичен член данни работи като глобална променлива за своя клас. Статичните член данни, за разлика от глобалните променливи имат две допълнителни характеристики:

1. Може да бъде предизвикано скриване на информация. Статичните член данни може да бъдат дефиниран като непублични; глобалните променливи не могат.

2. Статичните членове не влизат в глобалното “пространство на имената” на програмата и поради това се отстранява възможността за случаен конфликт между имената.

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

Един член данни може да бъде направен статичен като към декларацията му бъде добавена ключовата дума static. Статичните член данни се подчиняват на правилата за достъп public/ private/ protеcted.

Например, в дефинираният по-нататък клас CoOp costPerShare е декларирана като личен статичен член от тип double.

class CoOp

{

friend compareCost( CoOp&, CoOp* );

public: CoOp( int, char* );

inline double monthlyMaint();

void raiseCost( double incr )

{ cosrPerShare += incr ); }

double getCost() { return costPerShare; }

private:

static double costPerShare;

int shares;

char *owner;

};

С решението да бъде направена costPerShare static се целят две неща: да се запазва стойността й и да се ограничат възможностите за грешки. Всеки CoOp обект трябва да има достъп до costPerShare. Въпреки, че нейната текуща стойност е една и съща за всички обекти, тя се променя във времето. Следователно тя не може да бъде дефинирана като константа. Но понеже не ефикасно всеки обект от този клас да обработва копие на тази стойност ние я декларираме като static. След като costPerShare е static, с еднократното й изменение ние сме уверени, че всеки обект от класа има достъп до една и съща стойност. Ако всеки обект на класа имаше собствено копие, то /копието/ трябваше да бъде изменено, което би довело до неефективност и грешки. Всеки статичен член данни се инициализира вън от дефиницията на класа по същия начин, както и променливите нечленове. Единствената разлика е, че трябва да бъде използуван синтаксиса на оператора за обхват на класа. Например, ето как бихме могли да инициализираме costPerScope:

#include "CoOp.h"

double CoOp::costPerShare = 23.99;

Както променливите нечленове, така и статичните член данни могат да бъдат инициализирани в програмата еднократно. Това означава, че инициализирането на статичните член данни трябва да бъде поставяно във файл заедно с дефинициите на член функциите, които не са inline, а не в заглавния файл на класа. Нивото на достъп до един статичен член данни се отнася само до достъпа за четене и запис на този член, а не до инициализацията му. Ето защо costPerShare може да бъде инициализиран само във файлов обхват.

Достъпът до един статичен член на клас е същия, както и достъпът да статичен член. Например,

inline double CoOp::monthlyMaint()

{ return( costPerShare * shares ); }

// Pointer and Reference arguments in order to

// illustrate object and pointer access

compareCost( CoOp& unit1, CoOp* unit2 )

{ double maint1, maint2;

maint1 = unit1.costPerShare * unit1.shares;

maint2 = unit2->costPerShare * unit2->shares; // ... }

Както unit1.costPerShare, така и unit2->costPerShare се отнасят за статичния член CoOP::costPerShare.

Понеже съществува само едно копие на статичните член данни на класа, те са достъпни и директно. Това изглежда така:

if ( CoOp::costPerShare < 100.00 )

Операторът за обхват на класа (“CoOp::”) трябва да бъде използуван понеже статичните член данни имат обхвата на класа, а не глобален обхват за програмата.

Следващата дефиниция на приятелската функция compareCost е еквивалентна на представената току що:

compareCost( CoOp& until1, CoOp *unit2 );

{ double maint1, maint2;

maint1 = CoOp::costPerShare * unit1.shares;

maint2 = CoOp::costPerShare * unit2.shares;

// ... }

Двете член функции за достъп raiseCost() и getCost() работят само със статичния член на класа costPerShare. Проблемът тук е, че всяка от тях трябва да бъде викана за определен обект на класа. Въпросът е за кой обект? Обект тук е необходим само за да бъде спазен синтаксиса за обръщение, понеже съществува само един представител на costPerShare, разделен между всички обекти на класа. Това може да доведе до неясноти в кода на програмата.

Поради това член функции, които работят само със статични членове на класа могат също да бъдат декларирани като статични. Това може да бъде направено така:

class CoOp

{ friend compareCost( CoOp&, CoOp* );

public:

CoOp( int, char* );

inline double monthlyMaint();

static void raiseCost( double incr );

static double getCost() {return costPerShare;}

private: static double costPerShare;

int shares;

char *owner; };

void CoOp::raiseCost( double incr )

{ costPerShare += incr; }

Една статична член функция не съдържа указателя this; следователно произволно явно или неявно използуване на this ще предизвика грешка по време на компилация. Опитът за достъп до един нестатичен член на клас представлява неявно обръщение към указателя this. Например, monthlyMaint() не може да бъде декларирана като статична член функция, защото й е необходим достъп до член данните shares. Дефиницията на една статична член функция е същата както тази на една нестатична член функция.

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

compareCost( CoOp& unit1, CoOp * unit2 )

{ // equivalent calls of static member

getCost() double maint1, maint2;

if ( CoOp::getCost() == 0 ) return 0;

maint1 = unit1.getCost() * unit1.shares;

maint2 = unit2->getCost() * unit2->shares;

// ... }

Един статичен член на клас е достъпен и може да бъде викан директно дори ако няма декларирани никакви обекти на класа.

Уппражнение 6-11. Даден е класа Y, съдъжащ два статични члена данни и две статични член функции:

class X { public:

X( int i ) { val = i };

getVal() { return val; }

private: int val; };

class Y { public:

Y( int i );

static getXval();

static getCallsXval();

private:

static X Xval;

static callsXval; };

Инициализирайте Xval с 20 и callsXval с 0.

Упражнение 6-12. Реализирайте двете статични член фуннкции за достъп. CallsXval просто брои колко пъти е извикана getXval().



6.7.Указател към член на клас

Указателите, и особено указателите към функции, предлагат един полезен начин за обобщаване на програмите. Потребителите на класа Screen, например, желаят да напишат функция “повторител”, която да изпълнява някаква дефинирана от тях операция n пъти. Една възможна реализация има вида:

Screen &repeat( char op, int times )

{

switch( op )

{

case DOWN: // ... invoke

Screen::down() break;

case UP: // ... invoke

Sscreen::up() break;

// ...

} }

Въпреки, че тази функция работи, тя има няколко основни недостатъка. Единият проблем се състои в това, че тя много явно се осланя на член функциите на Screen. Винаги когато бъде добавяна или отстранявана някаква член функция, repeat() трябва да бъде изменяна. Вторият проблем е свързан с размера й. Поради наличието на проверка за всяка член функция пълният списък в repeat() е огромен и изглежда много сложен. Една алтернативна и много по-обща реализация заменя op с аргумент от тип указател към член функция на Screen. repeat() не е необходимо повече да определя предвижданите оператори. Целият оператор switch може да бъде отстранен. Дефинирането и използуването на указатели към членове на класове е тема на следващия подраздел.

Типът член на клас

На указател към функция не може да бъде присвоен адрес на член функция, дори когато типовете за връщане и сигнатурите на двете функции напълно си съответствуват. Например, нека pfi е дефиниран като указател към функция, която не получава аргументи и има тип за връщане int:

int (*pfi)();

Screen дефинира две функции за достъп, getHeigth() и getWidth(), които също не получават аргументи и имат типове за връщане int:

inline Screen::getHeigth() { return heigth; }

inline Screen::getWidth() { return width; }

С цел илюстрация са дефинирани също две нечлен функции HeigthIs() и WidthIs():

HeigthIs() { return HEIGTH; }

WidthIs() { return WIDTH; }

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

pfi = HeigthIs; pfi = WidthIs;

Присвояването на една от член функциите на класа Screen getHeigth() или getWidth(), обаче, се явява нарушаване на типовете и ще предизвика грешка по влеме на компилация:

// illegal assignment:

type violation pfi = Screen::getHeigth;

Защо се получава нарушаване на типовете?

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

1. Типът на данните и броя на формалните аргументи; т.е. сигнатурата.

2. Типът на данните за връщане.

3. Типът на класа, чийто член е функцията.

Декларацията на указател към член функция изисква един разширен синтаксис, при който се взема предвид и типа на класа. Същото се отнася и за поддържане на указатели към член данни на клас. Да разгледаме типа на члена на класа Screen heigth. Пълният тип на Screen::heigth e “член short на класа Screen”.

Следователно, пълният тип на указател към Screen::heigth е “указател към члена short на класа Screen”. Указателят this е дефиниран така:

short Screen::

Една дефиниция на указател към член на класа Screen от тип short изглежда така:

short Screen::*ps_Screen;

ps_Screen може да бъде инициализиран с адреса на heigth така:

short Screen::*ps_Screen = &Screen::heigth;

По подобен начин на него може да бъде присвоен адреса на width:

ps_Screen = &Screen::width;

ps_Screen може да бъде инициализиран както с width така и с heigth тъй като и двете са член данни на класа Screen от типа short. При опит да се вземе адреса на непубличен член на класа някъде в програмата без съответните права на достъп до класа се генерира грешка по време на компилация.

Упражнение 6-13. Какъв е типа на членовете screen и cursor на класа Screen?

Упражнение 6-14. Дефинирайте, инициализирайте и дайте стойност на един указател към членовете screen и cursor на класа Screen.

Указател към член функция се дефинира като се определи нейния тип за връщане, сигнатурата и класа. Например, указател към членовете getHeigth() и getWidth() се дефинира така:

int (Screen::*)()

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

// all pointers to class member may be assigned 0

int

(Screen::*pmf1)() = 0;

int (Screen::*pmf2)() = Screen::getHeigth;

pmf1 = pmf2;

pmf2 = Screen::getWidth;

Използуването на typedef може да направи синтаксиса на дефинирането на указатели към функции по-лесен за четене. Например, следният typedef дефинира Action като алтернативно име на тип за Screen& (Screen::*)() т.е. за указател към член функция на Screen, която не получава аргументи и връща псевдоним на обект от класа Screen.

typedef Screen& (Screen::*Action)();

Action deFault = Screen:home;

Action next = Screen::forward;

Упражнение 6-15. Дефинирайтe typedef за всички различни типове членове на Screen.

Указатели към членове могат да бъдат декларирани като аргументи на функции; може да бъде зададен и инициализатор на аргумент по подразбиране. Например,

action( Screen&, Screen& (Screen::*)() );

action() е декларирана да получава два аргумента:

1. Псевдоним на обект от класа Screen.

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

action() може да бъде извикана по един от следните три начина:

Screen myScreen;

typedef Screen& (Screen::*Action)();

Action deFault = Screen::home;

extern Screen& action( Screen&, Action = Screen::display );

ff();

{ action( Screen&, Action = Screen::display );

action( myScreen, deFault );

action( myScreen, Screen::bottom );

}

Упражнение 6-16. Ууказатели към членове също могат да бъдат декларирани като член данни на клас. Изменете дефиницията на класа Screen така, че да съдържа указател към член функция на Screen от типа на home() и bottom().

Упражнение 6-17. Изменете съществуващия конструктор на Screen (или задайте нов конструктор) така, че да получава указтел към член аргумент на Screen от типа, описан в предното упражнение. Задайте аргумент по подразбиране. Нека някоя функция за достъп да позволява на потребителя да задава стойност на този член.

Използуване на указател към член на клас

Указателите към членове на даден клас трябва винаги да бъдат достъпни през един специфичен обект на класа. Това може да бъде направено като се използуват двата указателя към операторите за избор на член (“.*” за обекти класове и псевдоними и “->” за указатели към обекти класове). Например, указатели към член фуннкции могат да бъдат извикани така:

int (Screen::*pmfi() () = Screen::getHeight;

Screen& (Screen::*pmfS) (Screen&) = Screen::copy;

Screen myScreen, *BufScreen;

// direct invocation of member function

if (myScreen.getHigth() == BufScreen->getHigth())

BufScreen->copy( myScreen );

//equivalent invocation through pointers to members

if ((myScreen.*pmfi)() == (BufScreen->*pmfi)())

(BufScreen->*pmfS) (myScreen);

Обръщенията (myScreen.*pmfi)() (BufScreen->*pmfi)() изискват скоби понеже приоритетът на оператора за обръщение (“()”) е по-висок от приоритета на указателя към оператора за избор на член. Без скобите myScreen.*pmfi() ще бъде интерпретирано като myScreen.*(pmfi()) т.е. като извикване на функцията pmfi() и свързване на връщаната стойност с указателя към оператора за избор на член на обект (“.*”).

По подобен начин са достъпни и указателите към член данните:

typedef short Screen::*ps_Screen;

Screen myScreen, *tmpScreen = new Screen(10,10);

ff() { ps_Screen pH = &Screen::heigth;

ps_Screen pW = &Screen::width;

tmpScreen->*pH = myScreen.*pH;

tmpScreen->*pW = myScreen.*pW;

}

Понеже heigth и wigth са лични членове на класа Screen инициализирането на pH и pW в ff() е възможно само ако ff() е декларирана като приятел за класа. Иначе всеки опит на ff() да вземе адресите на тези членове на класа ще бъде отбелязан като грешка по време на компилация.

Ето реализацията на член функцията repeat(), която разгледахме в началото на този раздел:

typedef Screen& (Screen::*Action)();

Screen& Screen::repeat( Action op, int times )

{ for ( int i = 0; i < times; ++i )

(this->*op)();

return *this;

}

Декларация, която желае да предложи аргумент по подразбиране за repeat() би могла да изглежда така:

class Screen

{ public:

Screen &

repeat( Action=Screen::forward, int=1 );

// ... }

Обръщението към repeat() може да изглежда така:

Screen myScreen;

myScreen.repeat();

// repeat( Screen::forward, 1 );

myScreen.repeat( Screen::down, 20 );

Може също да бъде дефинирана и таблица от указатели към членове на клас. В следващия пример Menu е таблица от указатели към тези член функции на класа Screen, които осигуряват движението на курсора. Освен това е дафиниран един изброим тип CursorMovements, свързан с Menu.

Action Menu[] = {

Screen::home,

Screen::forward;

Screem::back,

Screen::up,

Screen::down,

Screen::bottom };

enum CursorMovements { HOME, FORWARD, BACK, UP, DOWN, BOTTOM };

Ще предложим един презаредим представител на move(), който получава аргумент CursorMovements. Ето реализацията:

Screen& Screen::move( CursorMovements cm )

{ (this->Menu[ cm ])();

return *this;

}

Този представител на move() може да се използува в диалогова програма, в която потребителят избира вида на движението на курсора от меню, показано на екрана.

Упражнение 6-18. Дефинирайте презаредим представител на repeat(), който получава аргумент CursorMovements.

Указатели към статични членове на клас

Статичните членове на даден клас се оказват вън от синтаксиса на указателите към членовете на класа. Статичните членове принадлежат на класа, а не на представителите му. Декларацията на указател към статичен член на клас изглежда така както тази на указател към некласов член. Съотнасянето на указателя не изсква обект клас. Например, нека разгледаме класа CoOp отново:

class CoOp {

friend compareCost( CoOp&, CoOp* );

public:

CoOp( int, char* );

inline double monthlyMaint();

static void raiseCost( double incr );

static double getCost() { return costPerShare;}

private: static double costPerShare;

int shares;

char *owner;

};

void CoOp::raiseCost(double incr) { costPerShare += incr; }

Типът на &costPerShare е double*; той не е double CoOp::*. Дефиницията на указател към costPerShare изглежда така:

// not double CoOp::*pd double *pd = &CoOp::costPerShare;

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

CoOp unit;

double maint = *pd * unit.shares;

По подобие на това типът на getCost() e double (*)(); а не е double(CoOp::*)(). Дефиницията на указател и индиректното обръщение към getCost() се обработват по същия начин както при некласовите указатели:

// not double (CoOp::*pf)() double (*pf)() = CoOp::getCost;

double maint = pf()*unit.shares;



6.8. Обхват на клас

Всеки клас допуска както глобален така и локален обхват. Въпреки, че дефиницията на един клас може да бъде вложена в дефиницията на друг, двата класа се разглеждат като притежаващи един обхват. Вложеният клас не се счита за член на външния клас и няма никакви специални привилегии за достъп до непубличните членове на този клас. Например, в раздел 4.2 се дефинира IntList и личния клас IntItem. Ето една еквивалентна дефиниция, при която дефиницията на IntItem е вложена в дефиницията на IntList:

class IntList

{

class IntItem

{ friend class IntList;

private: IntItem(int v=0) { val = v; next = 0; }

IntItem *next;

int val;

};

public: IntList(int val) { list = new IntItem( val );

}

IntList() { list = 0 }

// ... private:

IntItem *atEnd();

IntItem *list;

};

IntItem не е нито член, нито личен член на Intlist. list, един обект на класа IntItem е личен член на IntList. Както IntItem, така и IntList имат файлов обхват. IntItem е лексикално вложен в IntList за да показва на читателя, че се използува само от IntList.

За спецификации typedef, които се намират в дефиницията на класа също се счита, че имат обхвата на външния клас. Например,

class Foo {

typedef int Bar;

private:

Bar val;

// ok };

Bar val;

// ok: the typedef is visible

Всеки клас поддържа свой собствен обхват. За имената на членовете на класа се казва, че са локални за обхвата на класа си. Ако името на променлива с файлов обхват се използува от член на клас, тази променлива е скрита в обхвата на класа. Например,

int height;

class FooBar

{ public:

// FooBar::height <== 0

FooBar() { height = 0 }

private:

short height;

};

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

int height;

class FooBar;

public: FooBar() { height = ::height; }

private:

short height;

};

Обратното, една променлива, дефинирана във функция, не е видима до откриването на декларацията й. В следващия пример localFunc() работи с два различни представителя на height.

int height = 66;

localFunc() { int hi = height; // ::height

int height = 24; // hides ::height

hi = heigth;// hi <== 24

Следващият пример е още по-объркващ. За кой представител на height мислите, че става дума?

int height = 66;

bdPrac() { int height = ::height; }

Компилаторът вероятно няма да обърка двата представителя на порменливата; читателят на програмата обаче би могъл. Едно предпочитано в този случай решение е да се преименува локалният представител до нещо, различно от heigth. Всяка член функция се явява в обхват на класа си. Тя също поддържа свой собствен локален обхват, който наподобява този на нечлен функциите. Ако едно име на член на клас се използува в локалния обхват на дадена член функция, името на член на класа се скрива в точката, където се дефинира локалния представител. Например,

Screen::barPractice() {

int hi = height; // Screen::heigth

int heigth = height; // heigth local instance }

Достъп до скрития член на класа може да има през оператора за обхват на класа. Например,

Screen::bdPrac() { int height = Screen::height; }

Достъп до скрита глобална променлива е възможен и чрез оператора за обхват. Например,

Screen::bdPractice() {

int height = (Screen::height > ::height) ? ::height : Screen::heeight; }

Съглашения относно обхвата

Когато един идентификатор се явява в член функция на някой клас, алгоритъмът, който свързва идентификатора е следният:

1. Блокът, съдържащ даден идентификатор се претърсва за декларация на идентификатора. Ако такава бъде намерена идентификаторът се свързва; иначе се търси в по-външен обхват.

2. Ако блокът, в който се явява идентификатора, е локален блок, вложен в член функция, тогава по-външен е блока на член функцията. Търси се в обхвата на функцията. Ако се намери декларация, идентификаторът се свързва; иначе се търси по-външен обхват.

3. Ако блокът, в който се явява идентификатора е също член функция, по-външният обхват е този на класа. Ако един член на клас е деклариран със същото име, както и иднтификатора, то идентификатора се свързва така, че да се отнася за този член; иначе се търси по-външен обхват.

4. За свързването на идентификатор в член функция на даден клас като по-външен обхват се приема файловия, ако класът не е произлязъл от друг клас. Ако бъде намерена декларация, идентификаторът се свързва; иначе идентификаторът се отбелязва като недекларирана променлива.

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

extern f( int ), ff(), f3();

int i = 1024;

class Exsample

{ publlic:

f(); // hides ::f( int )

Example( int ii = 0 ){ i = ii; }

private:

int i; // hides ::i

int ff; // hides ::ff() }

Един идентификато е скрит когато името му се използува във вътрешен обхват дори когато типът на локалният представител е различен. Глобалните идентификатори i, ff() и F(int) са скрити в обхвата на класа Example. Във Example::f() обръщението към тези идентификатори трябва да бъде предшествано от файловия оператор за обхват.

#include "Example.h"

Example::f() { int j;

// error: file scope f() is hidden

// Exsample::f() takes no argument

j = f( 1 ); // ok: explicit refernce to ::

f( int ) // i is resolved to Example::i;

j = ::f( i );// ok: explicit reference is unnecessary

// file scope f3() is visible within Example

return( f3() ); }

Example::f() може също да дефинира локални представители на i и f. Тези локални представители ще скрият представителите на членовете на класа. За да работите с членовете на класа трябва да използувате оператора за обхват на клас.

#include "Example.h" Example::f()

{ // hides Example::i

int i = ff ? Example::i : ( ::ff() ? ::i : 0);

// error: Example::f3() undefined

return( Example::f3(); }

Локални класове

Един клас може да бъде дефиниран в локален за даден файл обхват. Името на класа е видимо само в границите на локалния му обхват. Например,

int doString( char *s)

{ // local class visible only within doString()

class String {...};

String str( s ); };

// error: String not visible

String str( "gravity" );

Член функциите на един локален клас трябва да бъдат дефинирани в тялото на класа. Например, недопустимо e следното - една функция да бъде вложена в друга функция.

int doString( char *s )

{

class String

{ public: String& operator=(char*); };

StriingSurprisedperator=(char* s) {} // illegal }

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

int stirng1(char *s)

{ class String { ... }; }

int string2(char *s)

{ class String { ... }; } // error: no Stirng class visible

StringSurprisedperator=(char* s) {}

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

Въпреки, че локалният клас е в обхвата на функцията, която го съдържа, променливите, дефинирани в обхвата на тази функция не са видими в член функциите на локалния клас. Например, bufSize в String се свързва с представителя, дефиниран с файлов обхват, докато bufSize в func() се свързва с локалния представител.

const int bufSize = 1024;

void func() {

const int bufSize = 512;

char *ps = new char[bufSize]; // 512

class String

{

public: String() {

str = new char[ bufSize ]; // 1024 !

} // ...

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



6.9. Обединение: Клас, който пести памет

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

int i = 0;

се преобразува в последователност от пет единици:

1. Ключовата дума за тип int.

2. Идентификаторът i.

3. Операторът =.

4. Цялата константа 0.

5. Разделителят ;

Тези единици се преглеждат от лексичния анализитор по време на синтактичния разбор. Първата стъпка на синтактичния анализатор е да определи последователността от единици. Информацията трябва да бъде представена така, че синтактичният аналазатор да може да разпознава потока от единици като декларации - например, като следната последователност:

Type ID Assign Constant Semicolon

След като синтактичният анализатор вече е разпознал общата последователност от единици той се нуждае от определена информация за всяка единица. В нашия случай той трябва да знае, че

Type <==> int

ID <==> i

Constant <==> 0 Той не се нуждае от друга допълнителна информация относно Assign и Semicolon.

Представянето на една единица, следователно, изисква два члена token и value. token ще съдържа един уникален номер, който се присвоява на всички възможни единици. Например, идентификаторите могат да бъдат представени чрез 85, а точка и и запетая чрез 72. value ще съдържа някаква информация за представителя на единицата. Например, за ID, value ще съдържа низът i; за Type - кодовото представяне на типа int.

Представянето на value е проблематично. Въпреки, че value трябва да съдържа само една стойност за всяка дадена единица, тази стойност може да има различен тип данни. Едно подходящо представяне за тези типове данни, разбира се, е клас. Компилаторът може да декларира value от тип TokenValue като дефинира TokenValue да съдържа член за всички възможни типове данни на value.

Такова представяне решава проблема. value, обаче, може да бъде само от един от възможните типове данни за всяка конкретна единица. TokenValue, обаче, ще отдели необходимата памет за всички възможни типове данни. По-добре би било, обаче, TokenValue да поддържа паметта, необходима само за един от възможните типове данни, а не за всичките заедно. Обединението позволява това. Ето дефиницията на обединението TokenValue:

union TokenValue

{ char cval;

char *sval;

int ival;

double dval; };

Тъй като най-много памет трябва да бъде отделена за типа данни double, размерът на TokenValue е същия като този на double. По подразбиране членовете на обединението са публични. Едно обединение не може да съдържа статични член данни или членове обекти на клас, който дефинира конструктор или деструктор. Ето един пример за това как TokenValue може да бъде използуван:

class Token

{ public:

int tok;

TokenValue val; }

Един обект от тип Token може да бъде използуван по следния начин:

lex() {

Token currToken;

char *curString;

int curIval;

// ...

case ID: // identifier

curToken.tok = ID;

curToken.val.sval = curString;

break;

cаse ICON: // integer constant curToken.tok = ICON;

curToken.val.ival = curIval;

break;

// ...etc. };

Опастността при използуване на обединение се състои в това, че е възможно случайно възстановяване на текущата стойност на обединението чрез неподходящ член данни. Например, ако при последното присвояване на стойност на обединението е използувана цяла стойност ival, програмистът не би желал да възставови тази стойност чрез указател към низ от символи sval. Ако това бъде направено вероятно ще предизвика програмна грешка.

За предотвратяване на този тип грешки, обикновено се дефинира една допълнителна променлива, която да поддържа типа на стойността, текущо съхранена в обединението. Тази допълнителна променлива се нарича дескриптор на обединението. За това служи члена tok на Token. Например,

char * idVal;

if ( curToken.tok == ID ) idVal = curToken.val.sval;

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

char *Token::getSting() {

if ( tok == ID ) idVal = curToken.val.sval;

error( ... );

}

Името на обединението е опционно. Няма никаква причина да бъде предлагано такова име ако то ще бъде използувано само от един представител. Когато такова име не бъде предложено то в програмата има един глобален идентификатор по-малко и съответно намаляват възможностите за възникване на колизии. Например, следната дефиниция на Token е еквивалентна на предходната дефиниция. Единствената разлика е, че обединението няма име:

class Token

{ public:

int tok;

union

{ char cval;

char *sval;

int ival;

double dval;

}

val; }

Съществува един специален представител на обединение, наречен анонимно обединение. Анонимното обединение е обединение без име и не е следвано от дефиниция на обект. Например, ето една дефиниция на класа Token, която съдържа анонимно обединение:

class Token

{ public:

int tok;

union // anonimous union

{ char cval;

char *sval;

int ival;

double dval;

}; };

Член данните на един анонимен клас са достъпни директно. Например, ето фрагмента lex(), написан така, че да използува дефиницията на Token, която съдържа анонимно обединение:

lex() {

Token curToken;

char *curString;

int curIval;

// ... figure out what the token is

// ... now set curToken

case ID: curToken.tok = ID;

curToken.sval = curString;

break;

case ICON: // integer constant curToken.tok = ICON;

curToken.ival = curIval;

break;

// ... etc. }

Всяко анонимно обединение отстранява едно ниво за избор на член понеже имената на членовете на обединението влизат във външния обхват. Едно анонимно обединение не може да има лични или защитени членове. Анонимно обединение, дефинирано с файлов обхват трябва да бъде декларирано като static.



6.10. Разредно поле: член, който пести памет

Един специален член данни на клас, наречен разредно или битово поле, се състои от определен брой битове. Той може да бъде със или без знак. Например,

class File { // ...

unsigned modified : 1; // bit field };

След идентификатора на едно битово поле се записва двуеточие и константен израз, който определя брой на битовете. modified, например, е разредно поле, което се състои от един бит.

Разредни полета, дефинирани в последователен ред в тялото на класа, ще бъдат пакетирани, ако е възможно, в съседни битове като едно цяло число, като чрез това се оситурява компактност на съхраняването. Например, в следната декларация, петте разредни полета са записани на място, отделно за едно unsigned int, което е свързано първо със съвкупността от битови полета.

typedef unsigned int Bit;

class File { public:

// ...

private:

Bit mode: 2;

Bit modified: 1;

Bit prot_owner: 3;

Bit prot_group: 3;

Bit prot_word: 3; // ... };

Едно разредно поле е достъпно по същия начин, както и останалите член данни на даден клас. Например,

File::write() { modified = 1;// ... }

File::close() { if ( modified ) // ... save contents }

Ето един прост пример за това как може да бъде използувано битово поле по-голямо от един бит (Раздел 3.8 операциите за работа с битове, използувани в примера):

enum { READ = 01, WRITE = 02 }; // File modes

main() {

File myFile;

myFile.mode |= READ;

if ( myFile.mode & READ )

cout << "\nmyFile.mode is set to READ";

}

Обикновено се дефинира едно множество от член функции inline за да се проверява стойността на члена разредно поле. Например, File може да дефинира isRead() и isWrite().

inline File::isRead() { return mode & READ; }

inline File::isWrite() { return mode & WRITE; }

if ( myFile.isRead() ) /* ... */

Операторът адрес-на (“&”) не може да се прилага към разредни полета, и поради това не могат да се дефинират указатели към разредни полета на класове. Нито пък битовите полета могат да бъдат декларирани като static.



6.11. Аргумент клас и многоточие

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

extеrn foo( int, ... );

class Screen { public:

Screen( const Screen& );

// ... };

void bar( int ival, Screen scrObj ) {

// error: no Screen argument spesified

foo( ival, scrObj ); }

Възможно е, обаче, да бъде изпратен указател към scrObj за foo().


Дипломен проект на тема "Проектиране на обучаваща система в Web среда" по дисциплината "Програмни езици и системи"
Разработена е от с-на к-т Иван Маринов Калчев, под ръководството на к-н Дойчинов.
За нейната разработка е използван офис пакета на Microsoft – Office 2000.
В обучаващата система е представен програмния език C++. Тя има за цел бързото и лесно запознаване със синтаксиса на този програмен език, като той е описан в осем глави.









/ Трябва да сте регистриран за да напишете коментар /