Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
234
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

892

17.5.4. Виртуальные функции и аргументы по умолчанию

#include <iostream>

class base { public:

virtual int foo( int ival = 1024 ) {

cout << "base::foo() -- ival: " << ival << endl; return ival;

}

// ...

};

class derived : public base { public:

virtual int foo( int ival = 2048 ) {

cout << "derived::foo() -- ival: " << ival << endl; return ival;

}

// ...

Рассмотрим следующую простую иерархию классов:

};

Проектировщик класса хотел, чтобы при вызове без параметров реализации foo() из

base b;

base *pb = &b;

//вызывается base::foo( int )

//предполагалось, что будет возвращено 1024

базового класса по умолчанию передавался аргумент 1024: pb->foo();

Кроме того, разработчик хотел, чтобы при вызове его реализации foo() без параметров

derived d; base *pb = &d;

//вызывается derived::foo( int )

//предполагалось, что будет возвращено 2048

использовался аргумент по умолчанию 2048: pb->foo();

Однако в C++ принята другая семантика механизма виртуализации. Вот небольшая программа для тестирования нашей иерархии классов:

С++ для начинающих

893

int main()

{

derived *pd = new derived; base *pb = pd;

int val = pb->foo();

cout << "main() : val через base: " << val << endl;

val = pd->foo();

cout << "main() : val через derived: " << val << endl;

}

После компиляции и запуска программа выводит следующую информацию:

derived::foo() -- ival: 1024 main() : val через base: 1024 derived::foo() -- ival: 2048 main() : val через derived: 2048

При обоих обращениях реализация foo() из производного класса вызывается корректно,

поскольку фактически вызываемый экземпляр определяется во время выполнения на основе типа класса, адресуемого pd и pb. Но передаваемый foo() аргумент по умолчанию определяется не во время выполнения, а во время компиляции на основе типа объекта, через который вызывается функция. При вызове foo() через pb аргумент по умолчанию извлекается из объявления base::foo() и равен 1024. Если же foo() вызывается через pd, то аргумент по умолчанию извлекается из объявления derived::foo() и равен 2048.

Если реализации из производного класса при вызове через указатель или ссылку на базовый класс по умолчанию передается аргумент, указанный в базовом классе, то зачем задавать аргумент по умолчанию для реализации из производного класса?

Нам могут понадобиться различные аргументы по умолчанию в зависимости не от реализации foo() в конкретном производном классе, а от типа указателя или ссылки, через которые функция вызвана. Например, значения 1024 и 2048 – это размеры изображений. Когда нужно получить менее детальное изображение, вызываем foo() через класс base, а когда более детальное через derived.

Но если мы все-таки хотим, чтобы аргумент по умолчанию, передаваемый foo(), зависел от фактически вызванного экземпляра? К сожалению, механизм виртуализации такую возможность не поддерживает. Однако разрешается задать такой аргумент по умолчанию, который для вызванной функции означает, что пользователь не передал никакого значения. Тогда реальное значение, которое функция хотела бы видеть в качестве аргумента по умолчанию, объявляется локальной переменной и используется, если ничего другого не передано:

С++ для начинающих

894

void base::

foo( int ival = base_default_value )

{

int real_default_value = 1024;

// настоящее значение по умолчанию

if ( ival == base_default_value ) ival = real_default_value;

// ...

}

Здесь base_default_value значение, согласованное между всеми классами иерархии, которое явно говорит о том, что пользователь не передал никакого аргумента.

void derived::

foo( int ival = base_default_value )

{

int real_default_value = 2048;

if ( ival == base_default_value ) ival = real_default_value;

// ...

Производный класс может быть реализован аналогично:

}

17.5.5. Виртуальные деструкторы

void doit_and_bedone( vector< Query* > *pvec )

{

// ...

for ( ; it != end_it; ++it )

{

Query *pq = *it; // ...

delete pq;

}

Вданной функции мы применяем оператор delete:

}

Чтобы функция выполнялась правильно, применение delete должно вызывать деструктор того класса, на который указывает pq. Следовательно, необходимо объявить деструктор Query виртуальным:

С++ для начинающих

895

class Query { public:

virtual ~Query() { delete _solution; } // ...

};

Деструкторы всех производных от Query классов автоматически считаются виртуальными. doit_and_bedone() выполняется правильно.

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

delete pq;

приводит к вызову деструктора класса AndQuery за счет механизма виртуализации. После этого статически вызывается деструктор BinaryObject, а затем снова статически деструктор Query.

class Query { public: // ...

protected:

virtual ~Query(); // ...

};

class NotQuery : public Query { public:

~NotQuery();

//...

Вследующей иерархии классов

};

уровень доступа к конструктору NotQuery открытый при вызове через объект NotQuery, но защищенный при вызове через указатель или ссылку на объект Query. Таким образом, виртуальная функция подразумевает уровень доступа того класса, через объект

int main()

{

Query *pq = new NotQuery;

// ошибка: деструктор является защищенным delete pq;

которого вызывается:

}

С++ для начинающих

896

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

17.5.6. Виртуальная функция eval()

В основе иерархии классов Query лежит виртуальная функция eval() (но с точки зрения возможностей языка она наименее интересна). Как и для других функций-членов, разумной реализации eval() в абстрактном классе Query нет, поэтому мы объявляем ее

class Query { public:

virtual void eval() = 0; // ...

чисто виртуальной:

};

Реальное разрешение имени eval() происходит при построении отображения слов на вектор позиций. Если слово есть в тексте, то в отображении будет его вектор позиций. В нашей реализации вектор позиций, если он имеется, передается конструктору NameQuery вместе с самим словом. Поэтому в классе NameQuery функция eval() пуста.

Однако мы не можем унаследовать чисто виртуальную функцию из Query. Почему? Потому что NameQuery это конкретный класс, объекты которого разрешается создавать в приложении. Если бы мы унаследовали чисто виртуальную функцию, то он стал бы абстрактным классом, так что создать объект такого типа не удалось бы. Поэтому мы

class NameQuery : public Query { public:

virtual void eval() {} // ...

объявим eval() пустой функцией:

};

Для запроса NotQuery отыскиваются все строки текста, где указанное слово отсутствует. Для таких строк в член _loc класса NotQuery помещаются все пары (строка, колонка). Наша реализация выглядит следующим образом:

С++ для начинающих

897

void NotQuery::eval()

{

//вычислим операнд

_op->eval();

//_all_locs - это вектор, содержащий начальные позиции всех слов,

//он является статическим членом NotQuery:

//static const vector<locations>* _all_locs

vector< location >::const_iterator iter = _all_locs->begin(), iter_end = _all_locs->end();

//получить множество строк, в которых операнд встречается set<short> *ps = _vec2set( _op->locations() );

//для каждой строки, где операнд не найден,

//скопировать все позиции в _loc

for ( ; iter != iter_end; ++iter )

{

if ( ! ps->count( (*iter).first )) { _loc.push_back( *iter );

}

}

}

Ниже приводится трассировка выполнения запроса NotQuery. Операнд встречается в 0, 3 и 5 строках текста. (Напомним, что внутри программы строки текста в векторе нумеруются с 0; а когда мы предъявляем строки пользователю, мы нумеруем их с единицы.) Поэтому при вычислении ответа создается вектор, содержащий начальные позиции слов в строках 1,2 и 4. (Мы отредактировали вектор позиций, чтобы он занимал меньше места.)

==> ! daddy

 

daddy (

3 ) lines match

display_location_vector:

 

first: 0

second: 8

 

first: 3

second: 3

 

first: 5

second: 5

! daddy ( 3 ) lines match

display_location_vector:

 

first: 1

second: 0

 

first: 1

second: 1

 

first: 1

second: 2

 

...

second: 10

 

first: 1

 

first: 2

second: 0

 

first: 2

second: 1

 

...

second: 12

 

first: 2

 

first: 4

second: 0

 

first: 4

second: 1

 

...

second: 12

 

first: 4

Requested query:

! daddy

( 2 ) when the wind blows through her hair, it looks

almost alive,

(

3 )

like a fiery bird

in flight. A beautiful fiery

bird, he

tells her,

(

5 )

she tells him, at

the same time wanting him to

tell her

more.

При обработке запроса OrQuery векторы позиций обоих операндов объединяются. Для этого применяется обобщенный алгоритм merge(). Чтобы merge() мог упорядочить

С++ для начинающих

898

пары (строка, колонка),

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

class less_than_pair { public:

bool operator()( location loc1, location loc2 )

{

return (( loc1.first < loc2.first ) || ( loc1.first == loc2.first ) && ( loc1.second < loc2.second ));

}

};

void OrQuery::eval()

{

// вычислить левый и правый операнды

_lop->eval(); _rop->eval();

// подготовиться к объединению двух векторов позиций vector< location, allocator >::const_iterator

riter = _rop->locations()->begin(), liter = _lop->locations()->begin(), riter_end = _rop->locations()->end(), liter_end = _lop->locations()->end();

merge( liter, liter_end, riter, riter_end, inserter( _loc, _loc.begin() ), less_than_pair() );

приведена наша реализация:

}

А вот трассировка выполнения запроса OrQuery, в которой мы выводим вектор позиций каждого из двух операндов и результат их объединения. (Напомним еще раз, что для пользователя строки нумеруются с 1, а внутри программы с 0.)

==> fiery || untamed

fiery (

1 ) lines match

display_location

vector:

 

first: 2

second: 2

 

first: 2

second: 8

untamed ( 1 ) lines match

display_location

vector:

 

first: 3

second: 2

fiery || untamed

( 2 ) lines match

display_location

vector:

 

first: 2

second: 2

 

first: 2

second: 8

 

first: 3

second: 2

Requested query: fiery || untamed

( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her, ( 4 ) magical but untamed. "Daddy, shush, there is no such thing,"

При обработке запроса AndQuery мы обходим векторы позиций обоих операндов и ищем соседние слова. Каждая найденная пара вставляется в вектор _loc. Основная трудность связана с тем, что эти векторы нужно просматривать синхронно, чтобы можно было установить соседство слов.

С++ для начинающих

899

void AndQuery::eval()

{

// вычислить левый и правый операнды

_lop->eval(); _rop->eval();

// установить итераторы

vector< location, allocator >::const_iterator riter = _rop->locations()->begin(), liter = _lop->locations()->begin(), riter_end = _rop->locations()->end(), liter_end = _lop->locations()->end();

// продолжать цикл, пока есть что сравнивать while ( liter != liter_end &&

riter != riter_end )

{

// пока номер строки в левом векторе больше, чем в правом while ( (*liter).first > (*riter).first )

{

++riter;

if ( riter == riter_end ) return;

}

// пока номер строки в левом векторе меньше, чем в правом while ( (*liter).first < (*riter).first )

{

//если соответствие найдено для последнего слова

//в одной строке и первого слова в следующей

//_max_col идентифицирует последнее слово в строке

if ( ((*liter).first == (*riter).first-1 ) && ((*riter).second == 0 ) &&

((*liter).second == (*_max_col)[ (*liter).first ] ))

{

_loc.push_back( *liter ); _loc.push_back( *riter ); ++riter;

if ( riter == riter_end ) return;

}

++liter;

if ( liter == liter_end ) return;

}

// пока оба в одной и той же строке

while ( (*liter).first == (*riter).first )

{

if ( (*liter).second+1 == ((*riter).second) )

{// соседние слова

_loc.push_back( *liter ); ++liter; _loc.push_back( *riter ); ++riter;

}

else

if ( (*liter).second <= (*riter).second ) ++liter;

else ++riter;

if ( liter == liter_end || riter == riter_end ) return;

}

}

}

С++ для начинающих

900

А так выглядит трассировка выполнения запроса AndQuery, в которой мы выводим векторы позиций обоих операндов и результирующий вектор:

==> fiery && bird fiery ( 1 ) lines match

display_location

vector:

first: 2

second: 2

first: 2

second: 8

bird ( 1 ) lines

match

display_location

vector:

first: 2

second: 3

first: 2

second: 9

fiery && bird ( 1 ) lines match

display_location

vector:

first: 2

second: 2

first: 2

second: 3

first: 2

second: 8

first: 2

second: 9

Requested query: fiery && bird

( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her,

Приведем трассировку выполнения составного запроса, включающего как И, так и ИЛИ. Показаны векторы позиций каждого операнда, а также результирующий вектор:

==> fiery && ( bird || untamed )

 

fiery (

1 ) lines match

 

 

display_location

vector:

second:

3

 

first: 2

 

 

first: 2

match

second:

8

bird ( 1 ) lines

 

 

display_location

vector:

second:

3

 

first: 2

 

 

first: 2

 

second:

9

untamed ( 1 ) lines match

 

 

display_location

vector:

second: 2

 

 

first: 3

 

 

( bird || untamed ) ( 2 ) lines match

display_location

vector:

second:

3

 

first: 2

 

 

first: 2

 

second:

9

 

first: 3

 

second:

2

fiery && ( bird || untamed

) ( 1 )

lines match

display_location

vector:

second:

2

 

first: 2

 

 

first: 2

 

second:

3

 

first: 2

 

second:

8

 

first: 2

 

second:

9

Requested query: fiery && ( bird || untamed )

( 3 ) like a fiery bird in

flight. A beautiful fiery bird, he tells her,

17.5.7. Почти виртуальный оператор new

Если дан указатель на один из конкретных подтипов запроса, то разместить в хипе

NotQuery *pnq;

//установить pnq ...

//оператор new вызывает

//копирующий конструктор NotQuery ...

дубликат объекта несложно:

NotQuery *pnq2 = new NotQuery( *pnq );

С++ для начинающих

901

Если же у нас есть только указатель на абстрактный класс Query, то задача создания

const Query *pq = pnq->op();

дубликата становится куда менее тривиальной:

// как получить дубликат pq?

Если бы позволялось объявить виртуальный экземпляр оператора new, то проблема была бы решена, поскольку автоматически вызывался бы нужный экземпляр. К сожалению, это невозможно: new статическая функция-член, которая применяется к неструктурированной памяти еще до конструирования объекта класса (см. раздел 15.8).

Но хотя оператор new нельзя сделать виртуальным, разрешается создать его суррогат,

class Query { public:

virtual Query *clone() = 0; // ...

который будет выделять память из хипа и копировать туда объекты, – clone():

};

class NameQuery : public Query { public:

virtual Query *clone()

// вызывается копирующий конструктор класса NameQuery

{ return new NameQuery( *this ); }

//...

Вот как он может быть реализован в классе NameQuery:

};

Query *pq = new NameQuery( "valery" );

Это работает правильно, если тип целевого указателя Query*:

Query *pq2 = pq->clone();

Если же его тип равен NameQuery*, нужно привести возвращенный указатель типа

NameQuery *pnq = new NameQuery( "Rilke" );

NameQuery *pnq2 =

Query* назад к типу NameQuery*: static_cast<NameQuery*>( pnq->clone() );

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