Глава 7: Член функции на клас
Съдържание на седма глава :

7.1. Инициализация на клас
7.2. Почленова инициализация
7.3. Презареждане на оператор
7.4. Пример за клас BitVector
7.5. Конвертори, дефинирани от потребителя



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



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

1. Конструктори и деструктори за автоматична инициализация и деинициализация на обекти от класа.

2. Презаредими оператор функции, които могат да бъдат приложени към обекти на класа като се използува синтаксиса за запис на оператор по-скоро, отколкото имена на член функции. Например, вместо явното извикване на член функцията isEqual() на Screen:

if ( myScreen.isEqual(yourScreen) )

презаредимостта на операторите позволява на потребителите на класа Screen да напишат следното еквивалентно извикване:

if ( myScreen == yourScreen )

Освен това един клас може да се заеме и с управлението на собствената си памет като предложи свои членове представители на операторите new и delete.

3. Оператори за конвертиране, които да дефинират множество от разрешени преобразувания на типове за един клас. Тези преобразувания могат да бъдат прилагани неявно от компилатора по същия начин, по който се прилагат и стандартните преобразувания. Извикването на тези специални член функции в общия случай е прозрачно за потребителите на класа. Заедно тези функции правят синтаксиса и използуването на един клас толкова естествени за програмиста, колкото и вградените типове.



7.1. Инициализация на клас

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

class Word { public:

int occurs;

char *string;

};

// explicit member initialization Word

search = { 0, "rosebud" };

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

class Word { public:

Word( char*, int = 0 );

// constructor private:

int occurs;

char *string;

};

#include <string.h>

inline Word::Word( char *str, int cnt )

{ string = new char [ strlen(str) + 1 ];

strcpy( string, str );

occurs = cnt;

}

Конструкторът не трябва да определя тип за връщане или явно да връща стойност. Иначе, дефиницията на един конструктор е същата, както и дефиницията на обикновените член функции. В този случай, конструкторът за Word изисква един аргумент от тип char*. Може да бъде добавен и опционния втори аргумент от тип int. Следват няколко примера за това как един обект на Word може да бъде дефиниран при наличието на този конструктор.

#include "Word.h" // Word::Word( "rosebud", 0 )

Word search = Word("rosebud");

// Word::Word( "sleigh", 1 )

Word *ptrAns = new Word( "sleigh", 1 );

main() { // shorthand constructor notations

// Word::Word( "CitizenKane", 0 )

Word film( "CitizenKane", 0 );

// Word::Word( "Orson Welles", 0 )

Word director = "Orson Welles";

}



Дефиниция на конструктор

Един от най тежко използуваните типове данни е string. В С++, обаче, низовете са произхождащ тип (масив от символи) без вградени оператори за поддръжка. (Няма оператори за присвояване или сравнение, например). Типът string е един неизбежен кандидат за реализация като абстрактен тип данни. Ние ще използуваме проекта на класа Sctring за да илюстрираме синтаксиса и семантиката на конструкторите, деструкторите и презаредимите оператори. Започваме с по-подробно разглеждане на конструкторите.

Един конструктор може да бъде идентифициран като за име му бъде дадено името на класа. Той може да бъде презареждан за да предлага набор от алтернативни инициализациии. Класът String, например, съдържа два члена данни:

1. str, от тип char*, който адресира масива от символи на низа.

2. len, от тип int, който съдържа дължината на символния масив, сочен от str.

Нека да декларираме два конструктора за String, един, който инициализира str и втори, който инициализира len.

class String { public:

String( int );

String( char* );

private:

int len;

char *str;

};

Дефиницията на един обект на класа отделя памет, необходима нестатичните член данни, дефинирани от класа. Конструкторът осигурява инициализацията на тази памет. Ето дефиницията на нашия първи конструктор на String:

String::String( char *s )

{ // #include <string.h>

len = strlen( s );

str = new char[ len + 1 ];

strcpy( str, s );

}

Няма нищо особено сложно в дефиницията на този конструктор. Силата й се дължи на мехонизма на класовете, който я извиква неявно за всеки обект на клас, който потребителят дефинира с получаване на инициализиращ символен низ. strlen() и strcpy() са функции за работа с низове от стандартната С++ библиотека. Ние ще осигурим поддържане на подобни функции за нашия клас String. Добре би било разгледаме защо str не присвоява просто адреса на s:

str = s;

а й отделя динамично памет, в която копира s. Основната причина е, че ние не можем да знаем дали е подходящо разположението на паметта, отделена на низа, който s съдържа:

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

String *readString()

{ // example of a string with local extent

char inBuf[ maxLen ];

cin >> inBuf;

String *ps = new Sting( inBuf );

return ps;

}

String *ps = readString();

- Ако низът е динамично разположен, т.е. отделена му е памет от свободната памет, той трябва да бъде изтрит преди обекта на класа да излезе от обхват. А прилагането на оператора delete върху памет, която не е отделена чрез оператора new може да предизвика сериозни грешки по време на изпълнение на програмата. Изводът, който може да бъде направен от гореизложеното е, че присвояването на стойност на указател, което не е съобразено с обхвата е потенциално опасно и изисква особено внимание от страна на програмиста. Отново ще се върнем на този проблем, относно указателите, когато разглеждаме инициализацията и присвояването на обекти от един и същ клас. Ето дефиницията и на втория конструктор на String:

String::String( int ln ) {

len = ln;

str = new char[ len + 1 ];

str[0] = ‘\0’;

}

Решението да се поддържа дължината на низа като отделно поле може да бъде поставено под въпрос. Трябва да се направи избор между загубата на памет за поддържане на това поле и загубата на време за изчисляване на дължината. Изборът на първия вариант се основава на две неща:

Първо, дължината на един низ се търси достатъчно често, така че спестяването на време се предпочита пред спестяването на място ( това трябва да е доказано от практиката ).

Второ, обектите на String ще бъдат използувани не само за поддържане на низове, но за дефиниране на буфери с фиксирана дължина.

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

int len = 1024;

String myString; // error: no arguments String

inBuf( &len ); // error: bad type:

int* String search("rosebud", 7); //error: two arguments

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

// explicit form: reflects actual invocation

String searchWord = String( "rosebud" );

// abbreviated form: #1

String comonWord( "the" );

// abbreviated form: #2

String inBuf = 1024;

// use of new required explicit form:

String *ptrBuf = new String( 1024 );

Обръщението към оператора new извиква конструктора на класа след като е отделена необходимата памет. Ако new не успее да отдели необходимата памет конструкторът не се изпълнява. Указателят към класа получава стойност 0. Например,

String *ptrBuf = new String( 1024 );

if ( ptrBuf == 0 ) cerr << "free store exhausted\n";

Би било полезно обекти на String да може да бъдат дефинирани и без задаване на аргументи. Например,

String tmpStr;

Това може да бъде направено или чрез задаване на стойности по подразбиране за аргументите на някой от вече дефинираните конструктори, или чрез задаване на конструктор по подразбиране. Конструкторът по подразбиране ще дефинира празен списък от аргументи. Масиви от обекти на клас с конструктори използуват конструкторите при инициализация просто както индивидуалните обекти на клас. Ако са дадени по-малко инициализатори, отколкото са елементите на масива, се използуват конструкторите по подразбиране. Ако такива не бъдат намирени инициализационният списък на масива трябва да бъде пълен. Реализацията на конструктор по подразбиране за класа String може да изглежда така:

#include "String.h"

String::String() {

// default constructor

len = 0;

str = 0;

}

Една често срещана програмна грешка е следната:

String st();

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

String st;

String st = String();

Упражнение 7-1. Дефинирайте един конструктор на String, който да приема следните декларации:

String s1( "rosebud", 7 );

String s1( "rosebud", 8 );

String s2( "", 1024 );

String s3("The Raw and the Cooked" );

String s4;



Конструктори и скриване на информация

Всеки конструктор има такова ниво на дастъп до публичните, личните или защитените раздели, в които е деклариран. Например, за да ограничим използуването на класа String като буфер Stirng::strin(int) може да бъде деклариран като личен:

class Stirng {

friend class Buf;

public:

String( char* );

String();

private:

String( int ); // ... rest of Stirng class };

Единствено член функциите на String и приятелите на класа Buf могат да декларират в програмата обекти на String, които получават един аргумент от тип int. Не са наложени ограничения върху декларирането на обекти на String, които не получават аргументи или пък аргументът им е от тип char*.

f() { // ok: String::String( char* ) is public

String search( "rosebud" ); { // ok: String::String( char* ) is public

private

String inBuf( 1024 );

... }

Buf::in() // error: String::String( int ) is

String search( "rosebud" );

// ok: String::String( int ) is private

// Buf is a friend to String

String inBuf ( 4096 );

... }

Личен клас е клас без публични конструктори. Единствено член функциите и приятелите могат да декларират обекти на класа. Класът IntItem, деклариран в Раздел 5.2 е един пример за личен клас. Обектите на класа IntItem могат да бъдат дефинирани само от класа IntList, който е деклариран като приятелски на IntItem.

Деструктори

С++ поддържа един механизъм, допълващ този на конструкторите, за автоматично деинициализиране на обекти на клас. Една специална, дефинирана от потребителя член функция, наречена деструктор, се извиква, когато обектът на даден клас излиза от обхват или се приложи оператора delete към указател на клас. Когато вън от обхвата излезе псевдоним на обект на клас, обаче, деструкторът не се вика. Това се дължи на факта, че псевдонимът просто дава друго име на вече дефиниран обект; самият той не е обект на клас. Деструкторът на String се дефинира така:

class String { public:

~String();

// destructor

... };

Една член функция може да бъде дефинирана като деструктор на клас, като й се даде за име името на класа и преди него се запише тилда (“~”). Деструкторът не може да получава аргументи (и следователно не може да бъде презареждан). Той не трябва да дефинира тип за връщане или да връща стойност. Деструкторът на класа String се дефинира така:

String::~String() {delete str; }

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

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

String::~String()

{ #ifdef DEBUG

cout << "String() " << len << " " str << "\n";

#endif

delete str; }

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

Деструкторът не се вика автoматично за указатели към обекти на клас, които са излезли от обхват. По-скоро, програмистът трябва явно да приложи операторът delete. Тогава ще бъде извикан деструктора на обекта на класа. Например,

#include "String.h"

String search( "rosebud" );

f() { // would not want destructor applied to p

String *p = &search;

// would want destructor applied to p

String *pp = new String( "sleigth" );

// ... body of f() // Sting::String() invoke for pp

delete pp; }

Ако указателят, към който е приложен операторът delete, не адресира обект на клас (т.е. има стойност 0), деструкторът не се извиква. Не е необходимо да се пише

if ( pp != 0 )

delete pp;

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

#include <string.h>

#include <stream.h>

#include <new.h>

struct inBuf

{ public:

inBuf( char* );

~inBuf();

private: char *st;

int sz;

};

inBuf::inBuf( char *s )

{ st = new char [ sz = strlen(s)+1 ];

strcpy( st, s );

}

inBuf::~inBuf()

{ cout<<"inBuf::~inBuf(): " << st <<"\n";

delete st;

}

char *pBuf = new char[ sizeof( inBuf ) ];

main()

{ inBuf *pb = new (pBuf) inBuf( "free store inBuf #1" );

pb->inBuf::~inBuf();

// explicit destructor call

pb = new (pBuf) inBuf( "free store inBuf #2" );

pb- >inBuf::~inBuf();

// explicit destructor call // ... }

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

inBuf::~inBuf(): free store

inBuf #1 inBuf::~inBuf(): free store

inBuf #2

Явното обръщение към един деструктор изисква задаване на пълното му име. Например,



pb->inBuf::~inBuf();

// correct

pb->~inBuf();

// error



Масиви от класове

Един масив от обекти на клас се дефинира по същия начин както и масив от вградените типове данни. Например, tbl и tbl2 дефинират масиви от по 16 обекта на класа String:

const int size = 16;

String tbl[ size ];

String *tbl2 = new String[size];

Достъпът до отделните елементи се осъществява, като се използува индексния оператор както и при вградените типове данни. За да се осигури достъп да определен елемент на масива се прилага оператора за селекция на член на класа след индексния оператор. Ето един пример:

while ( cin >> tbl[ i ] )

tbl[i].display();

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

Stirng ar1[] = { "phoenix", "crane" };

String ar2[3] = { String(), String(1024),String("string")};

String ar3[2] = { 1024, String( 512 );

};

Screen as[] = { Screen(24, 80,’#’) };

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

Преди tbl2 да излезе от обхват е необходимо явното използване на оператора delete за освобождаване на паметта. Обаче, простото написване на

delete tbl2;

не е достатъчно понеже деструкторът на String се прилага само за началния елемент на tlb2. delete не може да знае, че tbl2 не сочи един обект от String, а масив от такива обекти. Програмистът трябва да приложи операторът delete с размера на масива, който tbl2 адресира. Това може да бъде направено така:

delete [ size ] tbl2;

Сега деструкторът на String ще бъде извикан за всеки от елементите на tbl2.

Членове обекти на клас

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

class Word

{public:

Word();

Word( char*, int = 0 );

Word( String&, int = 0 );

private:

int occurs;

String name;

};

Сега за всеки обект на Word трябва да бъдат викани два конструктора - този на самия клас и другия за члена от клас String. В този случай трябва да бъдат разгледани два въпроса:

1. Дефиниран ли е ред за извикване на конструкторите? И ако е дефиниран, какъв е?

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

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

// first String::String( char * )

// then Word::Word( char * ) Word flower( "iris" );

Аргументите се изпращат до конструкторите чрез инициализационен списък на член - списък, от двойки име/аргумент, отделени със запетаи. Например,

Word::Word( char *s, int cnt ) : name( s )

{ occurs = cnt; }

Инициализационният списък на член съответствува на сигнатурата на конструктора и се отделя чрез двуеточие. Всеки член може да бъде споменат еднократно в списъка. Инициализационният списък на член може да се явява само в дефиницията на един конструктор; той не може да бъде зададен в декларацията му. В предходния пример на name се изпраща указателя към низ s, който на свой ред се подава като аргумент на конструктора на String. За член данните от вградените типове също могат да бъдат зададени стойности в инициализационния списък. Например, в следния пример occurs се инициализира със стойността на cnt:

Word::Word( char *s, int cnt ) : name( s ), occurs( cnt ) {}

Изпълнението на един конструктор преминава през три фази - на инициализация и присвояване. Когато тялото на конструктора е празно няма фаза на присвояване. Например,

class Simple { public:

Simple( int, float );

private:

int i ;

float f;

};

Simple::Simple( int ii, float ff ) : i(ii), f(ff)

// initialization {} // assignment

Фазата на присвояването започва с изпълнението на тялото на конструктора. Неявна инициализация се случва при наличието на член от тип клас, който дефинира конструктор, който не изисква аргументи. Инициализационният списък прави инициализационната фаза явна. Няма да има инициализационна фаза в случай, че конструкторът на Simple е дефиниран така:

Simple::Simple( int ii, float ff )

{ i = ii;

f = ff; // assignment phase }

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

class ConstRef { public:

ConstRef( int ii );

private:

int i;

const int ci;

int &ri;

};

ConstRef::ConstRef( int ii )

{ // assignment

i = ii;// ok

ci = ii; // error: cannot assign to a const

ri = i; // error: ri is uninitialized }

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

ConstRef::ConstRef( int ii ) : ci( ii ), ri( i )

// initialization

{ // assignment

i = ii; }

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

class Random

{ public:

Random( int i ) : val( seed ( i ) ) {}

int seed( int );

private:

int val;

}

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

Word::Word( String &str, int cmt )

: name ( str ), occurs( cnt )

{} String msg( "hello" );

Word greetings( msg );

Всеки член обект на клас трябва да присъствува в инициализационния списък ако конструкторът му изисква списък от аргументи. Ако този списък не бъде зададен ще се издаде съобщение за грешка по време на компилация. Класът SynAntonym, например, съдържа три члена обекти на класове: wd, обект на класа Word, и synonym и antonym, обекти на класа String. Всички обекти на класа Word изискват поне един аргумент от тип char* или от тип String&. Инициализационният списък трябва да предлага списък от аргументи за wd.

class SynAntonym

{ public:

SynAntonym(char* s) : wd(s) {}

SynAntonym(char* s1, char* s2, char* s3) : wd(s1),

synonym(s2), antonym(s3) {}

~SynAntonym();

private:

String synonym;

Word wd;

String antonym;

};

SynAntonym sa1( "repine" );

SynAntonym sa2( "cause", "origin", "effect" );

Редът за извикване на конструкторите за sa1 и sa2 е следният:

1. Според реда на декларациите в тялото на класа се извиква консруктора за всеки член клас:

String(); // synonym String member

String( "repine" ); // String member of wd

Word( "repine" ); // wd Word member

String(); // antonym String member

// sa1( "repine" );

String( "oridin" ); // synonym String member

String( "cause" ); // String member of wd

Word( "cause" ); // wd Word member

String( "effect" ); // antonym String member

// sa2( "cause", "oridin", "effect" );

2. Извиква се конструктора на външния клас.

Когато един член обект на клас съдържа член обект на клас правилата за подреждане на извикването на конструкторите се прилагат рекурсивно. Редът на обръщенията към деструкторите е обратен на този на обръщенията към конструкторите. Т.е. деструкторите за външния клас се извикват преди тези на членовете обекти на клас. Ако има много обекти на класове редът на обръщенията към деструкторите е обратен на реда на декларациите на членовете обекти на клас.

Упражнение 7-2. Ето една схематична дефиниция на класа Buffer:

#inlcude "String.h"

class Buf

{ public: // ...

private: String buf; };

Декларациите могат да приемат някоя от следните форми:

Stirng s1;

Buf();

Buf( 1024 );

Buf( s1 );

Реализирайте множеството на конструкторите и деструкторите.

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

class BinTree;

class INode

{ // private class

friend class BinTree;

int val;

BinTree *left;

BinTree *right;

};

class BinTree

{ public: // ... public intrface

private:

INode *node;

};

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

Упражнение 7-3. Разгледайте предимствата и недостатъците на дефинирането на класа INode като личен клас.

Упражнение 7-4. Дефинирайте конструктор(и) и деструктор на класа INode.

Упражнение 7-5. Дефинирайте конструктор(и) и деструктор на класа BinTree.

Упражнение 7-6. Разгледайте проекта, избран в упражнение 7-4 и 7-5.



7.2. Почленова инициализация

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

String vowel( "a" );

String article = vowel;

Инициализацията на article се извършва чрез последователно копиране на всеки член на vowel в съответния член на article. Това се нарича почленова инициализация. Компилаторът изпълнява почленовото инициализиране чрез дефинирането на един специален конструктор със следната обща форма.

X::X( const X& );

За класа String конструкторът е дефиниран някак така:

Stirng::String( const String& )

{ len = s.len;

str = s.str; }

Реализирането на обект на клас чрез друг обект от същия клас може да бъде необходимо в три случая:

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

// String::String( char* );

String color( "blue" );

// memberwise initialization generated

String mood = color;

2. Изпращане на обект на клас като аргумент на функция. Например,

extern int count( Stirng s, char ch );

// local instance of s <== mood

int occurs = count( mood, ‘e’ );

3. Връщане на обект на клас от функция. Например, extern String sub( String&, char, char );

main()

{ String rriver( "mississippi" );

cout << river << " " << sub( river, ‘i’, ‘I’ ) << "\n"; }

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

Почленовото инициализиране копира всички вградени или произлезли член данни от един обект на клас в друг. Членовете класове, обаче, не се копират; а по-скоро почленовото инициализиране се прилага рекурсивно. Например, класът Word дефинира един член цяло число, occurs, и един член от типа на класа String, name. Ето дефинициите на двата обекта на Word:

Word noun( "book" );

Word vern = noun;

verb се инициализира на следните стъпки:

1. Членът occurs се инициализира със стойността на noun.occurs.

2. Членът name се инициализира почленово чрез вътрешно генерирания конструктор за класа String.

Почленовото инициализиране по подразбиране понякога е недостатъчно. Фиг. 7.1 илюстрира паметта, отделена за count и verb. Съществуват дава проблема:

1. count на noun не трябва да бъде копиран в count на verb. Фактически, двете стойности са разделени. Почленовият механизъм по подразбиране нарушава семантиката на класа Word.

2. Членът str на noon и verb адресира една и съща памет. Това ще създаде сериозни проблеми ако двата обекта на класа не излизат от обхват по едно и също време.

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

Както показва фиг. 7.1, това означава, че паметта, адресирана, от два или повече обекта на класа се “деструктурира” два или повече пъти. В някои случаи могат да се получат висящи указатели. В други случаи може да бъде деструктурирана памет, отделена в последствие за други цели. И в двата случая програмата вероятно няма да работи правилно. Този проблем може да бъде решен, като проектантът на класа създаде представител на конструктор за явна почленова инициализация. Това е тема на следващия подраздел.

Специалният конструктор X(const X&)

Както вече видяхме, при някои обстоятелства, класовете изискват различно управление на инициализирането на обекти чрез други обекти, отколкото се предлага от почленовата инициализация по подразбиране. Един клас допуска това допълнително управление чрез дефинирането на явен представител на конструктора X(const X&). Ако такъв конструктор е явно дефиниран в един клас, той се вика за всяко инициализиране на един обект на клас чрез друг. Например,

String::String( const String& s )

{ len = s.len;

str = new char[ len + 1 ];

strcpy( str, s.str ); }

String(const String& ) се извиква винаги, когато един обект на String се инициализира чрез друг. Всеки член str ще адресира различна област от паметта.

Упражнение 7-7. Реализирайте конструктор Screen(const Screen&) за класа Screen, дефиниран в глава 6. Дайте пример за три случая, в които се вика този конструктор.

Упражнение 7-8. Реализирайте конструктор IntList(const IntList&) за класа IntList, дефиниран в глава 5. Дайте пример за три случая, в които се вика този конструктор.

X(const X&) и обекти членове на клас

В този подраздел, ние ще разгледаме два примера на X(const X&) за класове, съдържащи обекти членове на клас:

1. Външният клас не дефинира представител на X(const X&), но класът член дефинира.

2. Двата класа дефинират представител на X(const X&).

В първия случай, илюстриран чрез дефиницията на класа Word, Word е без Word(const Word&). към члена клас, String, обаче, е добавена дефиниция на String(const String&).

class Word

{ public: Word( char *s, int cnt = 0 ) : name(s), occurs(cnt) {}

Word( String& s, int cnt = 0 ) : name(s), occurs(cnt) {}

private:

int occurs;

String name; };

Инициализацията на един обект на Word чрез друг използува почленовата инициализация по подразбиране. Членът от клас String, обаче, се инициализира чрез извикване на String(const String&). Например,

String mystery( "rosebud" );

Word resolve( mystery );

extern search( Word wd );

search( resolve );

String(const String&) се извиква за инициализиране на члена name от resolve и на члена name от локалното копие на wd. Въобще, почленовата инициализация се прилага рекурсивно за всеки член обект на клас. За всеки член обект на клас, който дефинира представител на X(const X&), обаче, се извиква този представител, а не почленовия инициализатор.

Почленовата инициализация по подразбиране не се прилага по нататък ако явно е зададен представител на Word(const Word&). Представителят на Word отговаря за инициализацията на членовете си. Преди да илюстрираме правилното реализиране на представителя на Word, нека разгледаме следното неправилно решение:

// this implementation is incorrect

Word::Word( const Word& w )

{ occurs = 0;

name = w.name; }

Този представител неправилно инициализира члена name от класа String. Нека да разгледаме следния пример постъпково:

Word weather( "warm" );

Word feeling = wither;

Стъпките по инициализирането са следните:

1. feeling се разпознава като инициализиран с обекта на класа Word(const Word&). Има ли дефиниран представител на Word(const Word&)? Ако има, той се извиква; иначе, се прилага почленовото инициализиране. Word(const Word&) е намерен.

2. Има ли инициализационен списък? Не.

3. Има ли някакви членове обекти на клас? Да. name е обект на класа String.

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

5. Извиква се String() за инициализирането на feeling.name.

6. Извиква се Word(const Word&). Изпълнява се присвояването name = w.name. По подразбиране, както и при инициализацията, присвояването на обект на клас се извършва чрез почленово присвояване (вж. раздел 7.3 за подробности). String(const String&) никога не се извиква.

Това е втори пример, при който е важно разграничаването на фазите на инициализация и присвояване. (Раздел 7.1 разглежда първия - инициализацията на const и на членове псевдоними на клас). За да бъде извикан конструктора на обект на String name трябва да бъде инциализиран с w.name. Това означава, че name трябва да бъде включено в инициализационния списък. Правилната дефиниция на конструктор на обект на Word има вида:

Word::Word( const Word& w ) : name ( w.name )

// initialization

{ // assignment occurs = w.occurs; }

Накратко, ако един външен клас не дефинира представител на X(const X&), всеки член обект на клас се инициализира почленно. Ако членът клас дефинира представетел на X(const X&), този представител ще бъде извикан. Ако външният клас не дефинира представител на X(const X&), обаче, той отговаря за явното инициализиране на члена си обект на клас поради своя инициализационен списък.

Упражнение 7-9. Реализирайте Buf(const Buf&) ( Упр. 7-2).

Упражнение 7-10. Реализирайте представителите на конструкторите INode(const INode&) и BinTree(const BinTree&). Обобщение на конструкторите и деструкторите

Специалният механизъм на конструкторите и деструкторите позволява автоматичната инициализация и освобождаване на обекти на клас. Конструкторите могат да бъдат презареждани за да предлагат набор от опции за инициализация; за по-голяма ефективност те могат да бъдат дефинирани inline. За членовете обекти на класове, които имат собствени конструктори, те се извикват преди конструктора на съдържащия ги клас. Редът на извикване на конструкторите на членовете обекти на клас зависи от реда на декларациите им. Деструкторите се извикват в обратен ред.

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

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

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





7.3. Презареждане на оператор



В предишния раздел дефинирахме член данните и член функциите, необходими за инициализирането и освобождаването на обекти от класа String. Какви други функции трябва да бъдат дефинирани за класа Stirng?

Потребителят трябва да може да проверява обектите на класа Stirng: Дали един низ е празен? Равен ли е един низ на друг? Дали един низ е подниз на друг? Потребителят трябва също да може да въвежда обекти низове, да присвоява един низ на друг, да слепва два низа, да определя дължината на низ, да индексира съдържанието на низ и да преглежда съдържанието му. Кодирането на използуването на един низ може да изглежда така:

String inBuf;

while ( readString( cin, inBuf ))

{ if ( inBuf.isEmpty() ) return;

if ( inBuf.isEquаl( "done" )) return;

switch( inBuf.index(0) ) { /* ... */ }

cout << "String is ";

writeString( cout, inBuf ); }

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

String inBuf;

while ( cin >> inBuf )

{ if ( !inBuf ) return;

if ( inBuf == "done" ) return;

switch( inBuf[ 0 ] ) { /* ... */ }

cout << "String is " << inBuf; }

В останалата част на този раздел ще реализираме набор от оператори, необходими за поддържането на този стил на програмиране за класа String.

Преглед на операторите за презареждане

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

String& StringSurprisedperator=( const String& s )

{ // assign one String object to another

len = s.len;

delete str; // deallocate existing array

str = new char[ len + 1 ];

strcpy( str, s.str );

return *this; }

При всяко присвояване на един обект от тип String на друг ще бъде викан оператора за присвояване на String.

#include "String.h"

String article( "the" );

String common;

main()

{ // StringSurprisedperator=()

common = article; }

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

class String

{

public:

String &operator=( const String& );

String &operator=( const char* );

// ...

};

strcmp() е стандартна библиотечна функция за сравняване на два символни низа за равенство. Ето нашия оператор за равенство на класа String, който работи с два String обекта:

StringSurprisedperator==( String& st )

{ // strcmp return 0 if both strings are equal

// operator== returns 1 for equality

return( strmcp( str, st.str ) == 0 );}



Дефиниране на оператор функции

Могат да бъдат презареждани само предварително дефинирани в С++ оператори. Проектантът на един клас не може да въведе нов оператор (“**”, например, за повдигане на степен). Таблица 7.1 показва операторите, които могат да бъдат презареждани. -Їя


Презаредими оператори

+ - * / % ^ & | ~ ! ‘

= < > <= >= ++ -- << >> == != &&

|| += -= /= %= ^= &= |= <<= >>= []

() -> ->* new delete


Таблица 7.1 Презаредими оператори

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

x == y + z;

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

Предварително дефинираната “арност” на оператора трябва да бъде запазена. Унарният логически оператор NOT (“!”), например, не може да бъде дефиниран като бинарен оператор за неравенство на два обекта от класа String. Неправилна е следната реализация:

// illegal: ! is a unary operator

operator!( String st1, String st2 )

{ return( strcmp(st1.str, st2.str ) != 0 ); }

Четири предварително дефинирани оператора (“+”, “-“, “*” и “&”) притежават както унарни, така и бинарни представители. Могат да бъдат дефинирани оператори с всяка от двете арности.

Може да има само по един представител на унарното увеличаване или намаляване с единица (“++”, “—“). При презареждане не се прави разлика между префиксния и постфиксния представител на тези оператори.

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

String st1( "cobble" );

String st2( "stone" );

String st3 = st1 + st2;

може да бъде дефиниран оператор за събиране или като нечлен приятел на Stirng:

class String { friend String& operator+( String&, String& );

// ... };

или като негова член функция:

class String { public: String& operator+( String& );

// ... };

Дефинирането и на двата представителя вероятно ше предизвика двусмислие:

class String

{ friend String& operator+( String&, String& );

public: String& operator+( String& );

// ... };

String a( "hobby" ), b( "horse" );

String c = a + b; // error: anbiguous

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

st1 + st2

може да бъде дефиниран като член функция със следната форма:

st1.operator+( st2 )

или като нечлен функция с формата:

operator+( st1, st2 )

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

Съществуват четири оператор функции, които трябва да бъдат дефинирани като член функции на клас: оператора за присвояване (“=”), индексния оператор (“[]”), оператора за обръщение (“()”) и оператора указател за избор на член (“->”). Тези оператори се обсъждат индивидуално следващите подраздели на тази глава.

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

class String {

friend ostream& operator<<( ostream& os, String& s )

{ return ( os << s.str ); }

// ... };

Упражнение 7-11. Декларирайте прототипа на унарната оператор фуннкция operator+() - първо като приятел нечлен и след това като член функция на класа String.

Упражнение 7-12. Логическият оператор NOT връща 1 ако обектът на String е празен; иначе връща 0. Реализирайте го като оператор на String.

Упражнение 7-13. Бинарната оператор функция operator+() слепва два обекта на String. Реализирайте този оператор като нечлен.

Упражнение 7-14. Реализирайте представител на operator+() като член функция на String.

Оператор []

Потребителите на нашия клас String желаят да имат достъп за четене и запис до конкретните символи на члена на класа str. Ние трябва да поддържаме следния начин на използуване на обектите на класа String:

String sentence( "Ash on an old man’s sleeve." );

String tempBuf( sentence.getLen() );

for ( int i = 0; i < sentence.getLen(); ++i )

tempBuf[ i ] = sentence[ i ];

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

inline String::getLen() { return len; }

Индексният оператор трябва да може да се записва както от лявата, така и от дясната страна на един ираз. За да бъде постигнато това връщаната от него стойност трябва да бъде стойност за запис. Тази стойност може да баде дефинирана от тип псевдоним:

inline char& StringSurprisedperator[]( int elem )

{ checkBounds( elem );

return str[ elem ]; }

Връщаната от индексния оператор стойност е стойността за запис на индексния елемент. Тя може да се явява и от лявата страна на опрератор за присвояване. Например,

String st( "mauve" );

st[0] = ‘M’;

присвояване на стойността на константа на нулевия елемент на st.str.

checkBounds() проверява дали изпратения чрез индексния оператор индекс е в границите на масива на String. Ако не е се издава съобщениеза грешка и се прекъсва програмата. Реализацията изглежда така:

// pull in exit(int) prototype

#include <stdlib.h>

void String::checkBounds( int elem )

{ // check array bounds

if ( elem < 0 || elem >= len )

{ cerr << "\nString Array Out of Bounds! index: " << elem << " string length (0-" << len-1 << ")\n";

exit( -1 ); } }



Оператор ()

Един повторител позволява на потребителя да преглежда елементите, които се съдържат в обект на клас. Всяко извикване на повторителя връща “следващия” елемент докато не бъде прегледан всеки елемент. Следният програмен фрагмент илюстрира как може да бъде използуван един повторител за класа String:

String inBuf;

// read in a String object

while ( cin >> inBuf ) // iterate over the elements of inBuf

{ char ch;

while ( ch = inBuf() ) // ... do something with ch }

Този подраздел ще разгледа как този програмен фрагмент може да бъде реализиран чрез използуването на две оператор функции: нечлен функцията >>() и член функцията на String operator().

Операторът за вход е презареден за четене от обект на String по следния начин:

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

{ char inBuf[ STRING_SIZE ];

is >> inBuf;

s = inBuf; // StringSurprisedperator=( char * )

return is; }

Входният оператор функция на String трябва да присвоява char* на обект на String. Това може да бъде обработено чрез представител на StringSurprisedperator=(char*).

String& StringSurprisedperator=( const char *s )

{ len = strlen( s );

delete str;

str = new char[ len + 1 ];

strcpy( str, s );

return *this; }

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

while ( ch = inBuf() ) // ... code

За реализирането на повторител на String е необходим един допълнителен член данни - index. index ще адресира следващия елемент, който трябва да бъде върнат. Всеки конструктор на String трябва да инициализира index с 0.

char StringSurprisedperator() ()

{ // provide for an iterator operator

if ( index < len ) return str[ index++ ];

// still here? completed iteration

return ( index = 0 );

}

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

#include "String.h"

const LINESIZE = 40;

enum {PERIOD=’.’,COMMA=’,’,SEMI=’;’,COLON=’:’};

main()

{ String inBuf( STRING_SIZE );

int lineSize = 0;

// operator>>( istream&, String& )

while ( cin >> inBuf )

{ char ch;

int i = 0;

// StringSurprisedperator() ()

while ( ch = inBuf() ) { switch(ch)

{ case PERIOD: case COMMA: case SEMI: case COLON:

// StringSurprisedperator[](int);

inBuf[ i ] = ‘\0’;

break;

}

++i;

++lineSize;

}

if ( lineSize >= LINESIZE )

{ cout << "\n";

lineSize = 0;

}

cout << inBuf << " ";

}

cout << "\n";

}

Програмата има следните входни данни:

We were her pride of ten; she named us: benjamin, phoenix, the prodigal, and perspicacious, pacific Suzanne. Benjamin, hush now. Be still, child. People are never just.

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

We were her pride of ten she named us benjamin phoenix the prodigal and perspicacious pacific Suzanne Benjamin hush now Be still child People are never just

Оператори new и delete

По подразбиране отделянето на свободна памет за обекти на класове се осигурява от предварително дефиниран глобален представител на оператора new (разгледан в Раздел 5.1). Например, следната програма чете последователност от думи от терминала и ги подрежда по дължина. Тя използва класа, наречен StringList.

#include "String.h"

#include "StringList.h"

// maintain a pointer table indexed by length

const maxLen = 25;

StringList *stbl[ maxLen ];

main() {

// read in and sort strings by length

const inBuf = 512;

char st[ inBuf ] StringList *p;

while ( cin >> st )

{ p = new StringList( st );

int sz = p->getLen();

if ( sz >= maxLen ) // issue error message continue;

p->next = stbl[ sz ];

stbl[ sz ] = p;

}

for ( int i = maxLen - 1; i > 0; --i )

{ StringList *tp;

p = stbl[ i ];

while ( p != 0 )

{ cout << *p << "\n";

tp = p;

p = p->next;

delete tp;

}

} }

StringList дефинира два член данни, един обект на класа String, наречен entry и един указател към String, наречен next. Конструкторът получава един аргумент от тип char*. Този аргумент се изпраща на конструктора на String за инициализиране на entry.

StringList::StringList( char *s ) : entry( s ), next( 0 ) { }

getLen() е фуннкция за достъп. Тя връща дължината на entry.

StringList::getLen() { return entry.getLen(); }

Извиква се функцията за достъп на String() getLen() понеже член фнукциите на StringList нямат привилегии за достъп до непубличните членове на класа String. Неправилно би било представител на StringList да напише следното:

StringList::getLen() { // illegal: private member

retrun entry.len }

Операторът за изход е презареден да възприема псевдоним на StringList:

ostream& operator << ostream& os, StringLien& s )

{ retrun ( os << s.entry ); }

Входът на програмата е темата на този подраздел:

A class may provide new and delete operator functions

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

functions operator provide delete class and new may A

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

Един клас допуска собствено управление на паметта чрез задаване на оператори new и delete членове на класа. Ако са дефинирани, те се викат вместо представителите им по подразбиране. Програмите на потребителите не се нуждаят от промени.

Стратегията за управление на паметта, която ще използуваме за StringList, се състои в едновременно отделяне на памет за няколко обекта на StringList. Тези обекти ще бъдат организирани като свързан списък. Ето декларациите на freeStore и stringChunk:

class StringList

{ private:

enum { stringChunk = 24; }

static StringList * freeStore;

// ... };

stringChunk показва броя обекти на StringList, за които се отделя памет едновременно. freeStore е указател към свързания списък от налични обекти на StringList.

Един оператор new представител на член на клас трябва да дефинира тип на връщане void* и да получава първи аргумент от системния typedef size_t, дефиниран в системния заглавен файл stddef.h. Този аргумент автоматично се инициализира от компилатора с размера на типа на класа в байтове. Могат също така да бъдат дефинирани и допълнителни представители на оператора new, като всеки от тях да има уникална сигнатура. Раздел 5.3 разглежда презареждането на оператора new. Когато new се прилага към име на клас компилаторът проверява за наличието на собствен представител на класа. Ако такъв бъде намерен, той се избира; иначе се прилага предварително дефинирания глобален представител. Добавянето или отстраняването на представител new на класа не изисква промяна на текстовете на програмите.

Ето един пример представител на оператор new член на StringList. Той проверява дали има наличен обект от freeStore. Ако такъв има, той се подава от члена представител. Иначе, се извиква глобалния оператор new за отделяне на памет за stringChunk на брой обекти. Ето реализацията:

#include <stddef.h>

StringList *StringList::freeStore = 0;

void *StringListSurprisedperator new( size_t size )

{ register StringList *p;

// if the free store is exhausted

// grab a new chunk of memory

if ( !freeStore )

{ long sz = StringChunk * size;

freeStore = p = new char[ sz ];

// the global new operator (StringList *)

// initialize the StringList freeStore

for ( ; p != &freeStore[ StringChunk-1 ]; p->next = p+1, p++ );

p->next = 0;

}

p = freeStore;

freeStore = freeStore->next;

return p; }

Член операторът delete възстановява обект на StringList към свързания списък от налични обекти. Ето реализацията му:

void StringListSurprisedperator delete( void *p, size_t )

{// restore p to freeStore

((StringList*)p)->next = freestore;

freeStore = (StringList *)p; }

Операторът delete трябва да задава първи аргумент от тип void*. Като втори аргумент може да бъде зададен аргумент от системния тип typrdef size_t (не забравяйте да включите stddef.h). Ако е зададен, той неявно се инициализира с размера в байтове на обекта, адресиран от първия аргумент. Операторът delete трябва да дефинира тип за връщане void.

Представителят на клас на оператора new се извиква само за отделяне на памет за индивидуални обекти, а не за масиви от обекти. Например,

StringList *p = new StringList;

извиква представителя на new на StringList, докато

StringList *pia = new StringList[10];

извиква предварително дефинирания представител, който обработва отделянето на памет за масив от обекти от свободната памет. Забележете също, явното дефиниране на обект на клас, такова като

StringList s;

също не извиква представителя на оператора new.

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

StringList *ps = ::new StringList;

извиква оператора new по подразбиране. Съответно,

::delete ps;

извиква представителя на оператора delete по подразбиране. Оператор функциите new и delete са статични членове на класа си, и се подичняват на обичайните ограничения за статичните член функции. В частност, да припомним, че статичните член функции нямат указате this и следователно могат да имат само директен достъп до статичните член данни на класа си. (Виж раздел 7.6 относно статичните член функции). Тези оператори са направени статични член функции понеже се извикват или преди конструирането на обект на клас (new) или след разрушаването му (delete).

X::Operator=(const X&)

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

Компилаторът генерира представител на оператора за присвояване на класа със следната форма:

X& XSurprisedperator=( const X& );

за да обработва по подразбиране почленовото присвояване на обекти на клас. Например, дадени са следните два обекта на String:

String article( "the" );

String common( "For example" );

и оператора за присвояване common = srticle; който се обработва чрез явния почленов оператор за присвояване:

String& StringSurprisedperator=( const String& s )

{ len = s.len;

str = s.str;

index = s.index;

}

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

1. Както и при почленната инициализация article и common сега адресират една и съща област от свободната памет. Деструкторът на двата обекта на String ще бъде приложен към тази единствена област.

2. Паметта, отделена за да съдържа For example никога не се освобождава. Тя се загубва по време на почленното присвояване.

3. Проектът на значенията на класа String забранява простото копиране на всеки член данни. В раздел 7.3 дефинирахме член данни index на класа String, който позволява на потребителя да преглежда символния масив на String. Стойността на index след почленното копиране трябва да бъде 0 за обекта назначение от класа String - не е така, обаче, за обекта източник. Почленното копиране по подразбиране нарушава семантиката на повторителя за String.

Проектантът на класа може да разреши тези проблеми като предложи явен представител на оператора за почленно присвояване. Представителите на класа String може да бъдат дефинирани по следния начин:

String& StringSurprisedpperator=( const String& s )

{ index = 0;

len = s.len;

delete str;

str = new char[ len + 1 ];

strcpy( str, s.str );

return *this; }

StringList, дефиниран в предишния раздел, съдържа член на класа String, наречен entry. StringList не дефинира оператор за почленно присвояване. Когато един обект на StringList се присвоява на друг обект от класа StringList, явно се извиква опреатора за почленно присвояване за да обработи почленното копиране на entry. Например,

#include "StringList.h"

main()

{ StringList sl1( "horse" );

StringLIst sl2( "carriage" );

// sl1.next = sl2.next

// sl1.entry.StringSurprisedperator=(sl2.entry) sl1 = sl2;

}

Ако StringList дефинира собствен оператор за почленно присвояване, представителят на String ще бъде викан само ако в тялото на оператора явно присъствува присвояване на Stirng. Например,

StringList& StringListSurprisedperator=( const String& s )

{ // StringSurprisedperator=(const String&)

entry = s.entry;

next = 0;

return *this; }

Ако присвояването

entry = s.entry;

не е написано, няма да бъде извикан почленния оператор - entry няма да бъде променен.

Често инициализацията и присвояването не се разграничават адекватно от програмистите, които реализират типовете класове. Това понякога може да предизвика неефективност на реализацията на класовете. Например, дадени са следните две дефиниции на клас:

class X { public:

X();

X( int );

X( const X& );

X& operator=( const X& );

// ... class Y { public:

Y();

private:

X x;

};

като следната проста реализация на конструктора на класа Y:

Y::Y() { x = 0; }

предизивиква явно извикване на два конструктора на X плюс извикване на оператора за присвояване за X:

1. Конструктора по подразбиране

X::X() се

извиква преди конструктора на Y за да инициализира обекта x член на клас. Присвояването на стойност 0 на x не може да бъде извършено директно понеже класът X не дефинира оператор за присвояване, който да получава аргумент от тип int. Присвояването се извършва на два етапа:

2. Извиква се конструктора

X::X( int );

за да преобразува цялото число 0 в обект от тип X. (Използуването на конструктори за преобразуване на типовете се разглежда в раздел 7.5 по нататък в тази глава).

3. Този новосъздаден обект на класа X се присвоява на X чрез извикването на

XSurprisedperator=( const X& )

Второто и третото извикване са ненужни и няма да бъдат направени ако конструкторът на Y инициализира подходящо члена x:

Y::Y() : x( 0 ) {}

Сега ще бъде извикан само X::X( int ) при всяко извикване на конструктора на класа Y.

Оператор ->

Операторът за избор на член (“->”) се обработва като унарен оператор на левия си операнд, който трябва да бъде или обект на клас или псевдоним на такъв обект. Стойността за връщане на оператор функцията трябва да бъде указател към обект на клас или обект на клас, за който операторът -> e дефиниран. В следния пример е деклариран един представител, който връща String*.

#include "String.h"

class Xample

{ public: String *operator->();// ...

private: String *ps; // ... };

Eто една схематична реализация на оператора. Той връща члена ps след като направи някаква обработка на обекта, който сочи.

String* XampleSurprisedperator->()

{ if ( ps == 0 ) // ... initialize ps

// ... process ps

return ps; }

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

void ff( Xample x, Xample &xr, Xample *xp )

{ int i;

// invoke String::getLen()

i = x->getLen(); // ok: x.ps->getLen()

i = xr->getLen(); // ok: xr.ps->getLen()

i = xp->getLen(); // error: no Xample::getLen()

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

Проектиране на презаредим оператор функция

Предварително дефинираните оператори за клас са само оператора за присвояване (“=”) и адресния оператор (“&”). Всеки друг оператор може да има значение когато се прилага към обект на клас ако проектантът на класа явно го дефинира. Кой оператор трябва да бъде избран се определя от потребителя на класа.

Винаги започвайте с дефиниране на публичния интерфейс на класа. Какви оператори трябва да предлага класа на потребителите си? Това ще бъде минималния набор то публични член функции. След като този набор бъде дефиниран е възможно да се разгледа кои оператори трябва да предлага типа клас. Всеки оператор се свързва с някакво значение от предварително дефинираното му използуване. Бинарният +, например, се отнася за събирането. Съпоставянето на + на някоя аналогична операция за типа клас може да предложи удобен кратък запис. Например, конкатенацията на обекти на String е едно подходящо разширение на +.

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

operator!();

isEqual става за оператор за равенство,

operator==();

copy() става за оператор за присвояване,

operator=();

и т.н.

Операторите за еквивалентност трябва също да бъдат дефинирани явно за един клас. Операторите на String, например, поддържат конкатенация и почленово копиране:

String s1( "C" );

String s2( "++" );

s1 = s1 + s2; // s1 <== "C++"

Тези оператори, обаче, не поддържат еквивалентния оператор за присвояване:

s1 += s2;

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

Упражнение 7-15. Определете кои член функции на класа Screen, дефиниран в Глава 6 могат да бъдат презаредени чрез оператори.

Упражнение 7-16. Реализирайт е презаредимите оператор функции, зададени от предишното упражнение.

Упражнение 7-17. В раздел 4.2 беше дефиниран клас - списък от цели числа, IntList. Определете и реализирайте презаредимите оператор функции за този клас.

Един клас бинарно дърво трябва да предлага следните функции: isEmpty(), isEqual(), print(), addNode(), build(), copy() и deleteNode().

Упражнение 7-18. Кои от тези член функции са кандидати за презареждане? Кои от тях са кандидати за презаредими оператори?

Упражнение 7-19. Реализирайте член функциите на бинарното дърво, изброени в горния параграф.

Упражнение 7-20. INodes се генерират доста често при работа с BinTree. Напищете нов начин за управление на паметта, като използувате операторите new и delete.





7.4. Пример за клас BitVector



В Раздел 2.8 бяха показани вектори от битове като един полезен начин за съхраняване на информация да/не. В предишните раздели беше използуван класа String за въвеждане на концепциите за презареждане на оператори. В този раздел ще разгледаме реализацията на класа вектори от битове, който специално набляга върху вариантите за проектиране на презаредими оператор функции. Ще наречем нашия клас BitVector.

typedef unisgned int BitVecType;

typedef BitVecType *BitVec;

class BitVector

{

private:BitVec bv;

unsigned short size;

short wordWidth;

};

size съдържа броя битове, които обектът на класа BitVector представя. bv сочи фактическия вектор от битове, запазен като непрекъсната последователност от едно или повече цели числа без знак. wordWidth съдържа броя цели числа без знак, сочени от bv. Например, ако цялото число без знак се записва в 32 бита BitVector инициализира членовете си със следните стойности:

// number of bits in bit vectorsize = 16;

// number of unsigned ints to hold bit vector wordWidth = 1;

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

size = 107;wordWidth = 4;

И в двата случая bv се инициализира с масив от елементи от тип BitVecType с размерност wordWidth. Например,

bv = new BitVecType[ wordWidth ];

Размерът в битове и байтове на типовете данни са различни за различните машини и поради това ние искаме да дефинираме явни псевдоними за тези машинно зависими стойности в текста на програмата си:

#ifdef vaxconst BITSPERBYTE = 8;

const BYTESPERWORD = 4;

#endif

#ifdef sunconst BITSPERBYTE = 8;

const BYTESPERWORD = 2;

#endif// The size of a machine integer

const WORDSIZE = BITSPERBYTE * BYTESPERWORD;

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

enum { OFF, ON };// default values: sz => WORDSIZE, init => OFF

BitVector::BitVector( int sz, int init )

{

size = sz;wordWidth = (size + WORDSIZE - 1)/ WORDSIZE;

bv = new BitVecType[ wordWidth ];

// now initialize bv to either all 0’s or 1’s

if ( init != OFF ) init = ~0;

// assign 0 or -1 to each word of bv

for ( int i = 0; i < wordWidth; i++ )*(bv + i) = init;

}

Протребителят трябва да може да задава ( set() ) и отменя ( unset() ) стойност на отделни битове. Това са бинарнии операции, които изискват обект на класа BitVector и цяло число, задаващо бита за изменение. Сещаме се за определен брой възможни оператори. Например, за да зададе стойност на определен бит потребителят би могъл да напише един от следните оператори:

BitVector bvec;// possible operator for set()

bvec | 12;

bvec |= 12;

bvec + 12;

bvec += 12;

Задаването на стойност единица за един бит е по-подобна на операцията събиране отколкото побитовата операция OR. Как да направим избор, обаче, между операторите “+” и “+=”? Трябва ли да реализираме и двата? Нека да разгледаме какво фактически значи операцията.

Като функция, която не е оператор set() се извиква така:

bvec.set(12);

bvec се изменя като в 12-я бит получава стойност 1. Това означава, че левият операнд на израза запазва резултата от израза. Това не е естественото поведение на оператора “+”, но отговаря на поведението на оператора “+=”. Това е оператора, който избираме. Ето и реализацията:

void BitVectorSurprisedperator+=( int pos )

{ // turn on bit at position pos

checkBounds( pos );

int index = getIndex( pos );

int offset = getOffset( pos );// turn oon bit offset at word

index*(bv + index) |= (ON << offset);}

getIndex() и getOffset() лични помощни функции. getIndex() връща индекс, показващ кое от целите числа съдържа бита. Например, бит 16 връща индекс 0; бит 33 връща индекс 1; а бит 107 - 3. Ето реализацията й:

inline BitVector::getIndex( int pos )

{// return word bit is positioned in

return( pos / WORDSIZE );}

getOffset() връща позицията на бита в цялото число без знак, което го съдържа. Например, бит 16 връща отместване 16; бит 33 връща отместване 1; а бит 107 - 11. Ето реализацията на функцията:

inline BitVector::getOffset( int pos )

{// return position of bit in word

return( pos % WORDSIZE );}

Реализацията на BitVectorSurprisedperator-=(int) е същото, както и на оператора за събиране, само че битът получава стойност 0.

// turn off the bit

// in worg index at position offset*(bv + index) &= (~(ON<<offset));

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

BitVectro bvec( 16 );bvec += 18; // error: out of bounds

Ето една възможна реализация:

void BitVctor::checkBounds( int index )

{// make sure index is within bounds of BitVector

if ( index < 0 || index >= size )

{

cerr << "\nBitVector Index Out of Bounds: "<< "<< " <<

index<< ", size: " << size << " >>\n";

exit( -1 ); // stdlib.h

}}

Потребителите на класа BitVector трябва да могат да проверяват стойността на определени битове. isOn() и isOff() са бинарни опреации с един ляв операнд - обект на класа BitVector и един десен операнд - цяло число представящо определен бит. За представянето на тези функции сме избрали операторите за равенство ("==") и неравенство ("!=").

Например, за да провери дали бит 17 има стойност 1 потребителят би могъл да напише следното:

BitVector bvec;if (bvec == 17 )// ...

Ето реализацията:

BitVectorSurprisedperator == ( int pos )

{// true if bit at position pos is 1

checkBounds( pos );

int index = getIndex( pos );

int offset = getOffset( pos );

return( *(bv + index) & (ON << offset) );}

BitVectorSurprisedperator != ( int pos )

{ // return the negation of operator==()

return ( !(*this == pos ) );}

Потребителят на BitVector трябва да може да показва обетите на класа на екрана както и да смесва свободно показванетона обекти на BitVector с проказването навградените типове данни. Това може да бъде направено чрез презареждане на оператора за изход (“<<”) като типа за връщане се дефинира като ostream&. Тогава потребителите на BitVector ще могат да пишат следното:

cout << "my BitVector: " << bv << "\n";

Ако битове 7, 12, 19, 27 и 34 имат стойност 1 извеждането на bv ще изглежда така:

< 7 12 19 27 34 >

Оператор функцията може да бъде дефинирана по следния начин:

ostream& operator<<(ostream& os, BitVector& bv )

{

int lineSize = 12;

os << "\n\t< ";

for ( int cnt = 0, i = 0; i < bv.size; ++i )

{ // BitVectorSurprisedperator==(int)if ( bv == i ){// worry about line format of output

int lineCnt = cnt++ % lineSize;

if ( lineCnt == 0 && cnt != 1 ) os << "\n\t ";

os << i << " ";}

}

if ( cnt == 0 ) os << "(none) ";

os << ">\n";

return os;

}

Понякога даден оператор трябва да бъде преобразувана в оператор функция. Ето един пример.

За да използува повторно обект на BitVector програмистът трябва да даде стойност 0 на всички елементи. Може да бъде реализирана функция reset() като унарна операция и да бъде прилагана към обект на класа BitVector. Представя се чрез логическия оператор NOT (“!”):

BitVector& BitVectorSurprisedperator!()

{ // reinitialize all elements to 0

for ( int i = 0; i < wordWidth; ++i )*( bv + i ) = 0;

return *this;

}

Тази реализация на оператора NOT не съответствува на логическата ни представа за него и вероятно ще бъде погрешно употребявана от програмиста. Ето защо.

Според предварителната му дефиниция той връща стойност истина ако операндът му има стойност 0. Той не изменя операнда си.

В реализацията си за BitVector той връща псевдоним на операнда си. На всички елементи битове дава стойност 0.

Ето една много вероятна програмна грешка. В този пример testResults() връща BitVector, в който всеки бит представя резултат от тест. Ако битът има стойност 0 тестът и издържан. Програмистът е объркал употребата на локалното NOT на BitVector с предварително дефинираната му употреба:

if ( !( bvec = testResults() ))cout << "All Tests passed!\n";

elsereroutFailures( bvec );

// ...В нашата реализация reset() e останала като именувана член функция.

Векторите от битове са общо използувани в компилаторите оптимизатори за поддържане информация отнасяща се до анализа на потока от данни в програмата. Две често изпълнявани операции са побитовото AND и OR на два вектора от битове; те са основни за класа BitVector.

За представяне на тези операции се използуват операторите & и |. За простота представената реализация допуска, че двата обекта на BitVector са от един и същ размер.

#include "BitVector.h"

BitVector BitVectorSurprisedperator | (BitVector& b)

{ // simplifying assumption: both have same size

BitVector result ( size, OFF );

BitVec tmp = result.bv;

for ( int i = 0; i < wordWidth; ++i )*(tmp + i) = *(bv + i) | *(b.bv + i);

return result;

}

Отбележете, че реализацията на оператора & е същата както и на оператора |, само че изпълняваната битова операция е AND:

*(tmp + i) = *(bv + i) & *(b.bv + i);

Фиг. 7.2 представя една малка интерактивна програма, която използува обект на BitVector за да съхранява информация за атрибути на типове. Например, ако въведем

unsigned const char * ;

ще зададе стойност 1 на четири бита от обектa typeFlag на BitVector. За oпростяване на представянето на програмата се четат низове, а не отделни символи, като от потребителя се иска да въвежда всеки атрибут, отделяйки го с точка и запетая.

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

Type in a declaration—end with ‘;

‘preceded by white space. For exampleq try ‘unsigned const char * ;

‘Hit ctrl-d to exit programunsigned const char * ;

flags set for declaration are:

unsigned

const

*

char

Фиг. 7.3 използува заглавния файл BitVector на класа BitVector, реализиран в този раздел.

Упражнение 7-21. Променете програмата, описана във фиг. 7.2 така че да обработва входни данни със следния формат:const char * const ;

Упражнение 7-22. Добавете представители на X(const X&) и operator=(const X&) за класа BitVector.

Упражнение 7-23. Дефинирайте оператор за равенство на два обекта на BitVector.

Упражтение 7-24. Преобразувайте оператор функцията AND така че да обработва вектори от битове с различен размер.

Упражнение 7-25. Преобразувайте оператор функцията OR така че да обработва вектори от битове с различен размер.

#include <string.h>

#include "BitVector.h"

const int MAXBITS = 8;

enum { ERROR, CHAR, SHORT, INT, PTR,REFERENCE, CONST,

UNSIGNED };

static char *typeTbl[] = {"OOPS, no type at slot 0", "char",

"short", "int", "*", "&", "const", "unsigned"

};

static char *msg ="Type in a declaration—end width ‘;

‘\ \npreceded by white space. For example, \ try\n\t’

unsigned const char * ;’ \\nHit ctrl-d to exit program\n";

main()

{

BitVector typeFlags( MAXBITS );

char buf[ 1024 ];

cout << msg;

while ( cin >> buf )

{

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

if (strcmp( typeTbl[i], buf ) == 0)

{ // a keyword?

BitVectorSurprisedperator+= typeFlags += i;

break;}

if ( buf[0] == ‘;’ )

{ // end of entry

cout <<"\nflags set for \ declaration are:\n\t";

for ( i = MAXBITS-1; i > 0; i--) //

BitVectorSurprisedperator== if ( typeFlags == i )

cout << typeTbl[i]<< "\n\t";

cout << "\n";// reinitialize:

// BitVector::reset()

typeFlags.reset();}}

}



Фиг. 7.2 Програмен пример за използуване на BitVector

#ifndef BITVECTOR_H

#define BITVECTOR_H

#ifdef vax

const BITSPERBYTE = 8;

const BYTESPERWORD = 4;

#endif

#ifdef sunconst BITSPERBYTE = 8;

const BYTESPERWORD = 2;

#endifconst WORDSIZE = BITSPERBYTE * BYTESPERWORD;

enum { OFF, ON };

typedef unsigned int BitVecType;

typedef BitVecType *BitVec;

#include <iostream.h>

class BitVector

{friend ostream&operator<<( ostream&, BitVector& );

public:

BitVector( int = WORDSIZE, int = OFF );

~BitVector() { delete [wordWidth] bv; }

void operator+=( int pos ); // turn on pos

void operator-=( int pos ); // turn off pos

BitVector operator &( BitVector& );

BitVector operator |( BitVector& );

operator == ( int pos ); // pos is on

operator != ( int pos ); // pos is off

void reset(); // reinit to 0 private: // helping functions

void checkBounds( int );

inline getOffset( int );

inline getIndex( int );

private: // internal representation short wordWidth;short size;

BitVec bv;

};

#indif

Фиг. 7.3 BitVector.h

Упражнение 7-26. Променете представителя на оператора за изход на BitVector така че да сгъстява последователните битове с еднаква стойност. Например, ако текущия изход е:

< 0, 1, 2, 3, 4, 5, 6, 7 >< 0, 2, 3, 4, 7, 8, 9, 12 >

Реализирайте оператора за изход така че да групира последо-вателните стойности като отделена с тире двойка. Например,

< 0 - 7 >

< 0, 2 - 4, 7 - 9, 12 >





7.5. Конвертори, дефинирани от потребителя



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

char ch;

short sh;

int ival;// widout type conversion, each addition

// would require a unique operationch + ival;

ival + ch;ch + sh;

sh + ch;ival + sh;

sh + ival;

Чрез аритметичните конвертори всеки опернд се преобразува към типа int. Необходима е само една операция - за събиране на две цели стойности. Тези преобразувания се обработват неявно от компилатора и следователно са прозрачни за потребителя.

В този раздел ще разгледаме как проектантът на един клас може да предложи набор от конвертори за класа. Тези конвертори се извикват неявно от компилатора когато е необходимо. За да илюстриране на нашето обсъждане ще реализираме класа SmallInt.

Класът SmallInt може да поддържа стойности от един и същ обхват като 8-битови unsigned char - т.е. 0 - 255. Допълнителни възможности са, че се откриват грешки, свързани с малки и големи числа, излизащи извън обхвата. Иначе, искаме класът да се държи по същия начин както и unsigned char. Например, ние бихме желали да добавяме и изваждаме обекти на SmallInt помежду си или с вградените аритметични типове. За поддържането на тези операции трябва да реализираме шест оператор функции на SmallInt:

class SmallInt

{

friend operator+( SmallInt&, int );

friend operator-( SmallInt&, int );

friend operator-( int, SmallInt& );

friend operator+( int, SmallInt& );public:

operator+( SmallInt& );

operator-( SmallInt& );// ...};

Необходими са само шест оператора понеже вградените аритметични типове ще бъдат конвертирани за да съответствуват на операнд от тип int. Например, изразът

SmallInt si( 3 );si + 3.14159

се изчислява на две стъпки:

1. Константата от тип double 3.14159 се преобразува до цялата стойност 3.

2. Извиква се operator+(si,3), който връща стойност 6.

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

С++ предлага механизъм чрез който всеки клас може да дефинира набор от конвертори, които да могат да бъдат прилагани над обектите на класа. За SmallInt ще дефинираме конвертор за обект на SmallInt към тип int. Ето реализацията:

class SmallInt

{

public:// conversion operator:

SmallInt ==> int operator int() { return value; }

// ...private:

int value;};

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

SmallInt si( 3 );si + 3.14159;

сега се изпълнява на следните две стъпки:

1. Извиква се оператора конвертор на SmallInt, който дава цялата стойност 3.

2. Цялата стойност 3 се преобразува до 3.0 е се събира с константата от тип double 3.14159, кото се получава стойност 6.14159.

Слeдната програма илюстрира използуването на класа SmallInt:

#include <stream.h>

#include "SmallInt.h"

SmallInt si1, si2;

main()

{

cout << "enter a SmallInt, please: ";

while ( cin >> si1 )

{ cout << "\n\nThank you.\n";

cout << "The value read is "<< si1 << "\nIt is ";//

SmallIntSurprisedperator int() invoked twice cout << (( si1 > 127 )

? "greater than ": (( si1 < 127 )?

"less than ": "equal to ")) << "127\n";

cout << "\nenter a SmallInt, please \ (ctrl-d to exit): ";

}

cout << "bye now\n";}

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

enter a SmallInt, please: 127

Thank you.

The value read is 127

It is equal to 127 enter a SmallInt, please (ctrl-d to exit): 126

Thank you.

The value read is 126

It is less to 127

enter a SmallInt, please (ctrl-d to exit): 128

Thank you.

The value read is 128

It is greater to 127

enter a SmallInt, please (ctrl-d to exit): 256

***SmallInt range error: 256 ***

Реализацията на класа SmallInt изглежда така:

#include <stream.h>

class SmallInt

{

friend istream&operator>>(istream& is, SmallInt& s);

friend ostream&operator<<(ostream& os, SmallInt& s)

{ return ( os << s.value ); }

public:SmallInt(int i=0) : value(rangeCheck(i) );

}

int operator=(int i)

{ return( value = rangeCheck(i) ); }

operator int() { return value; }

private:

enum { ERRANGE = -1 };

int rangeCheck( int );

int error( int );

int value;

};

Член функциите, дефинирани вън от тялото на класа изглеждат така:

istream& operator>>( istream& is, SmallInt& si )

{

int i;

is >> i;

si = i; // SmallIntSurprisedperator=(int)

return is;}

SmallInt::error( int i )

{

cerr << "\n***SmallInt range error: "<< i << " ***\n";

return ERRANGE;}

SmallInt::rangeCheck( int i )

{// if any bits are set other than the first 8

// value is too large: report, then exit

if ( i & ~0377 )exit( error( i ) ); // stdlib.h

return i;}

Упражнение 7-27. Защо презаредимият оператор за вход не е реализиран по следния начин?

istream& operator>>( istream& is, SmallInt & si )

{return ( is >> si.value );}



Конструктор като оператор конвертор

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

Конструктор, който получава един аргумент като SmallInt(int), служи като оператор за преобразуване между типа на аргумента и класа. SmallInt(int), например, преобразува цели стойности в обекти на

SmallInt.extern f( SmallInt );

int i; // need to convert i into a SmallInt

f( i ); // SmallInt(int) accmplishes this

При обръщението f( i ), i се преобразува в обект на SmallInt чрез извикване на SmallInt(int). Компилаторът конструира временен обект на SmallInt и го изпраща на f().

{SmallInt temp = SmallInt(i);

f(temp);} // a temporary SmallInt Object is created

Фигурните скоби, употребени в примера, показват продължителността на периода на съществуване на генерирания временен обект на SmallInt.

Ако е необходимо преди извикването на SmallInt(int) се прилага стандартен конвертор. Например,

double d;f( d );

става

{// warning: narrowing conversion

SmallInt temp = SmallInt( (int)d );

f(temp);}

Извикването на оператор конвертор се извършва само ако не са възможни други преобразувания. Ако f() беше презаредима функция, както е в следващия пример, SmallInt(int) нямаше да бъде извикана.

f( SmallInt );

f( double );

int ival;// matches f(double) by standard conversion

f( ival );

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

class Token

{public: // ... public membersprivate:

SmallInt tok;// ... rest of Token members};

// create a Smallint object from a Token

SmallInt::SmallInt( Token& t ) { /* ... */ }

extern f( Smallint& );

Token curTok;

main()

{{ // invoke SmallInt( Token& );

f( curTok );}



Оператори конвертори

Операторите за конвертиране, които са специални представители на член функциите на един клас, определят неявното преобразуване на обекти на класа в някякви други типове. SmallInt, например, може да дефинира преобразуване на обект на SmallInt в стойност от тип unsigned int:

SmallIntSurprisedperator unsigned int(){return( (unsigned)value );}

Класът Token, дефиниран по-нататък в текста, може да дефинира много оператори конвертори:

#include "SmallInt.h"

class Token{public:

Token( char *nm, int v1 ): val( v1 ), name( nm ) {}

operator SmallInt() { return val; }

operator char*() { return name; }

operator int() { return val; }// .. rest of public membersprivate:

SmallInt val;

char *name;};

Забележете, че операторите конвертори на Token за SmallInt и int са еднакви. SmallIntSurprisedperator int() е приложен неявно над обекта val на SmallInt в TokenSurprisedperator int(). Например,

#include "Token.h"

void f( int i )

{ cout << "\nf(int) : " << i;}

Token t1( "integer constant", 127 );

main()

{ Token t2( "friend", 255 );

f( t1 ); // t1.operator int()f( t2 ); // t2.operator int()}

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

f(int) : 127f(int) : 255

Един оператор конвертор има следия общ вид:

operator <type> ();

където <type> се замества с определен вграден, произлязъл или класов тип. (Операторите конвертори на масиви или функции не са допустими). Операторите конвертори трябва да бъдат член функции. Те не трябва да определят тип за връщане нито могат да имат списък от аргументи. Всяка от следните декларации, например, е недопустима:

operator int( SmallInt& ); // error:

nonmember class SmallInt{public:int operator int(); // error:

return type operator int( int ); // error:

argument list ...};

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

#include "Token.h"

Token tok( "function", 78 );// function cast notation:

operator SmallInt() SmallInt tokVal = SmallInt( tok );// type cast notation:

operator char*()char *tokString = (char *)tok;

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

#include "Token.h"

extern void f( char *);

Token tok( "enumeration", 8 );

enum { ENUM }; // token constant

main()

{ f( tok ); // tok.operator

char*();// explicit functional cast notation

switch ( int(tok) ) // tok.operator int() {case ENUM:{

// tok.operator SmallInt();

SmallInt si = tok;// ...}

}}

Да допуснем, че желаният тип не съответствува точно на никой от операторите конвертори. Ще бъде ли извикан оператор конвертор?

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

extern void f( double );

Token tok( "constant", 44 );// tok.operator

int() invoked// int ==> double by standard conversionf( tok );

не, ако желаният тип може да бъде получен чрез прилагане на втори опрератор конвертор, дефиниран от потребителя върху резултата от първи, дефиниран от потребителя, оператор конвертор. (Ето защо Token дефинира както operator SmallInt() така и operator int()). Например, ако Token не предлага представител на operator int() следното обръщение би било недопустимо:

extern void f(int);

Token tok( "pointer", 37 );// without

TokenSurprisedperator int() defined,// this call will generate a compile-time error

f( tok );

Ако TokenSurprisedperator int() не е дефиниран, преобразуването на tok към типа int ще изисква извикването на два оператора конвертори, дефинирани от потребителя.

TokenSurprisedperator SmallInt();

ще преобразува tok в обект на SmallInt.

SmallIntSurprisedperator int();

ще завърши преобразуването до типа int.

Според правилото, обаче, могат да бъдат прилагани оператори конвертори, дефинирани от потребителя, само от едно ниво. Ако TokenSurprisedperator int() не е дефиниран, обръщението f(tok), изискващо аргумент от тип int, предизвиква грешка по време на компилация поради нарущение на типовете.

Двусмислие

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

extern void f( int );

extern void f( SmallInt );

Token tok( "string constant", 3 );

f( tok ); // error: ambiguous

Token дефинира оператор конвертор както за SmallInt така и за int: двете преобразувания са еднакво възможни. Обръщението f(tok) е двусмислено, понеже компилаторът не може да избере измежду двата конвертора. Затова обръщението предизвиква грешка по време на компилация. Програмистът може да разреши двусмислието като направи преобразуването явно:

// explicit convertion resolve ambiguityf( int(tok) );

Ако TokenSurprisedperator int() не е дефиниран, обръщението няма да бъде двусмислено. Ще бъде изпълнен TokenSurprisedperator SmallInt(). Фактът, че SmallInt дефинира оператор конвертор int не се разглежда. При определяне на възможните преобразувания на един обект, се преглежда само първото ниво на опрераторите конвертори, дефинирани от потребителя.

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

class Token{public:

operator float();

operator int();

...};

Обаче, ако и двата оператора конвертора са еднакво приложими, те се отбелязват като двусмислени:

// error: both operators float() and int()long lval = tok;

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

SmallInt::SmallInt( Token& );

TokenSurprisedperator SmallInt();

extern void f( SmallInt );

Token tok( "pointer to class member", 197 );// error: two possible user-defined conversionsf( tok );

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

// explicit convertion resolves ambiguity

// TokenSurprisedperator SmallInt() invoked

f( (SmallInt)tok );

Програмистът може да разреши двусмислието и чрез явно извикване на оператора конвертор на класа Token:

// explicit conversion resolves ambiguity

f( tok.operator SmallInt() ):

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

// error: still ambiguous

f( SmallInt(tok) );

SmallInt(Token&) и TokenSurprisedperator SmallInt() са еднакво възможни интерпретации на обръщението.

Една втора форма на двусмисилие се свързва с читателя на програма, която използува оператори конвертори. Когато един обект на SmallInt се преобразува до стойност от тип int, значението на това преобразувание е ясно. Когато идит обект на String се преобразува до типа char*, значението на това преобразувание също е ясно. И в двата случая съществува съответствие едно-към-едно между вградените типове даннии и вътрешното представяне на типа на класа. Когато, обаче няма логическо съответствие между оператора конвертор и типа на класа, използуването на обектите на класа може да стане двусмислено за читателя на програмата. Например,

class Date

{ public:

// guess which member is returned!

operator int();

private:

Int month, day, year;};

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

Упражнение 7-28. Какви оператори конвертори са необходими за поддържане на следното използуване на обекти на класа String?

extern int strlen( const char* );

const maxLen = 32;

String st = "a string";

int len = strlen( st );

if ( st >= maxLen ) ... String st2 = maxLen;

Упражнение 7-29. Разгледайте предимствата и недостатъците от поддържането оператора конвертор, представен чрез

if ( st >= maxLen ) ...

Упражнение 7-30. Един интересен оператор конвертор за BinTree връща обект на класа IntArray от стойности на INode (Раздел 2.8 съдържа дефиницията на класа IntArray). Реализирайте следните два оператора конвертора:

BinTree::BinTree( IntArray& );

BinTreeSurprisedperator IntArray();

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

BinTree::preorder( /* ??? */ )

{

if ( !node ) return;

node->visit( /* ??? *? ); // do work

if ( left ) left->preOrder( /* ??? */ );

if ( right ) right->preOrder( /* ??? */ );

Упражнение 7-31. Предишното упражнение предлага обработване на изрази, включващи обекти от двата класа IntArray и BinTree. Например,

extern BinTree bt;

extern IntArray ia;

if ( ia == bt ) ...

Редът на операциите е следният: Извиква се оператора конвертор на BinTree за да приобразува bt в обект на класа IntArray. Тогава се извиква оператора за равенство на IntArray за да сравни двата обекта на класа IntArray. Какво ще се случи, обаче, ако класа IntArray дефинира следните два оператора конвертори?

IntArray::IntArray( BinTree& );

IntArraySurprisedperator BinTree();



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





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