Главная страница

Язык программирования C++. Вводный курс. С для начинающих


Скачать 5.41 Mb.
НазваниеС для начинающих
Дата24.08.2022
Размер5.41 Mb.
Формат файлаpdf
Имя файлаЯзык программирования C++. Вводный курс.pdf
ТипДокументы
#652350
страница91 из 93
1   ...   85   86   87   88   89   90   91   92   93
976
}
Так выглядит реализация двоичного поиска в функции-члене find() класса Array_Sort:
}
Протестируем нашу реализацию класса Array_Sort с помощью функции try_array().
Показанная ниже программа тестирует шаблон этого класса для конкретизаций типами int и string: template void Array_Sort::grow()
{
Array::grow(); sort( 0, Array::_size-1 ); clear_bit(); template int Array_Sort::find( const Type &val )
{ int low = 0; int high = Array::_size-1; check_bit(); while ( low <= high ) { int mid = ( low + high )/2; if ( val == ia[ mid ] ) return mid; if ( val < ia[ mid ] ) high = mid-1; else low = mid+1;
} return -1;

С++ для начинающих
977
}
При конкретизации типом string после компиляции и запуска программа печатает следующий текст (обратите внимание, что попытка вывести элемент с индексом -1 заканчивается крахом): конкретизация класса Array_Sort try_array: начальные значения массива
( 7 )< Eeyore, Gopher, Heffalump, Owl, Piglet, Pooh
Tigger > try_array: после присваиваний
( 7 )< Eeyore, Gopher, Owl, Piglet, Pooh, Pooh
Pooh > try_array: почленная инициализация
( 7 )< Eeyore, Gopher, Owl, Piglet, Pooh, Pooh
Pooh > try_array: после почленного копирования
( 7 )< Eeyore, Piglet, Owl, Piglet, Pooh, Pooh
Pooh > try_array: после вызова grow
( 7 )< , , , , Eeyore, Owl
Piglet, Piglet, Pooh, Pooh, Pooh > искомое значение: Tigger возвращенный индекс: -1
Memory fault (coredump)
После почленного копирования массив не отсортирован, поскольку виртуальная функция вызывалась через объект, а не через указатель или ссылку. Как было сказано в разделе
17.5, в таком случае вызывается экземпляр функции из класса именно этого объекта, а не того подтипа, который может находиться в переменной. Поэтому функция sort() никогда не будет вызвана через объект Array. (Разумеется, мы реализовали такое поведение только в целях демонстрации.)
#include "Array_S.C"
#include "try_array.C"
#include main()
{ static int ia[ 10 ] = { 12,7,14,9,128,17,6,3,27,5 }; static string sa[ 7 ] = {
"Eeyore", "Pooh", "Tigger",
"Piglet", "Owl", "Gopher", "Heffalump"
};
Array_Sort iA( ia,10 );
Array_Sort SA( sa,7 ); cout << "
êîíêðåòèçàöèÿ êëàññà Array_Sort"
<< endl; try_array( iA ); cout << "
êîíêðåòèçàöèÿ êëàññà Array_Sort"
<< endl; try_array( SA ); return 0;

С++ для начинающих
978
18.6.3.
Класс массива с множественным наследованием
Определим отсортированный массив с контролем выхода за границы. Для этого можно применить множественное наследование от Array_RC и Array_Sort. Вот как выглядит наша реализация (напомним еще раз, что мы ограничились тремя конструкторами и оператором взятия индекса). Определение находится в заголовочном файле
Array_RC_S.h
:
#endif
Этот класс наследует две реализации каждой интерфейсной функции Array: из
Array_Sort и из виртуального базового класса Array через Array_RC (за исключением оператора взятия индекса, для которого из обоих базовых классов наследуется замещенный экземпляр). При невиртуальном наследовании вызов find() был бы помечен компилятором как неоднозначный, поскольку он не знает, какой из унаследованных экземпляров мы имели в виду. В нашем случае замещенным в
Array_Sort экземплярам отдается предпочтение по сравнению с экземплярами, унаследованными из виртуального базового класса через Array_RC (см. раздел 18.5.4).
Таким образом, при виртуальном наследовании неквалифицированный вызов find() разрешается в пользу экземпляра, унаследованного из класса Array_Sort.
Оператор взятия индекса переопределен в классах Array_RC и Array_Sort, и обе реализации имеют равный приоритет.
Поэтому внутри
Array_RC_S
неквалифицированное обращение к оператору взятия индекса неоднозначно. Класс
Array_RC_S
должен предоставить собственную реализацию, иначе пользователи не смогут напрямую применять такой оператор к объектам этого класса. Но какова
#ifndef ARRAY_RC_S_H
#define ARRAY_RC_S_H
#include "Array_S.C"
#include "Array_RC.C" template class Array_RC_S : public Array_RC, public Array_Sort
{ public:
Array_RC_S( int sz = Array::ArraySize )
: Array( sz )
{ clear_bit(); }
Array_RC_S( const Array_RC_S &rca )
: Array( rca )
{ sort( 0,Array::_size-1 ); clear_bit(); }
Array_RC_S( const Type* arr, int sz )
: Array( arr, sz )
{ sort( 0,Array::_size-1 ); clear_bit(); }
Type& operator[]( int index )
{ set_bit(); return Array_RC::operator[]( index );
}
};

С++ для начинающих
979
семантика его вызова в Array_RC_S? При учете отсортированности массива он должен установить в true унаследованный член dirty_bit. А чтобы учесть наследование от класса с контролем выхода за границы массива – проверить указанный индекс. После этого можно возвращать элемент массива с данным индексом. Последние два шага выполняет унаследованный из Array_RC оператор взятия индекса. При обращении return Array_RC::operator[]( index ); он вызывается явно, и механизм виртуализации не применяется. Поскольку это встроенная функция, то при статическом вызове компилятор подставляет ее код в место вызова.
Теперь протестируем нашу реализацию с помощью функции try_array(), передавая ей по очереди классы, конкретизированные из шаблона Array_RC_S типами int и string:
}
Вот что печатает программа для класса, конкретизированного типом string (теперь ошибка выхода за границы массива перехватывается): конкретизация класса Array_Sort try_array: начальные значения массива
( 7 )< Eeyore, Gopher, Heffalump, Owl, Piglet, Pooh
Tigger > try_array: после присваиваний
( 7 )< Eeyore, Gopher, Owl, Piglet, Pooh, Pooh
Pooh > try_array: почленная инициализация
( 7 )< Eeyore, Gopher, Owl, Piglet, Pooh, Pooh
Pooh > try_array: после почленного копирования
( 7 )< Eeyore, Piglet, Owl, Piglet, Pooh, Pooh
Pooh >
#include "Array_RC_S.h"
#include "try_array.C"
#include int main()
{ static int ia[ 10 ] = { 12,7,14,9,128,17,6,3,27,5 }; static string sa[ 7 ] = {
"Eeyore", "Pooh", "Tigger",
"Piglet", "Owl", "Gopher", "Heffalump"
};
Array_RC_S iA( ia,10 );
Array_RC_S SA( sa,7 ); cout << "
êîíêðåòèçàöèÿ êëàññà Array_RC_S"
<< endl; try_array( iA ); cout << "
êîíêðåòèçàöèÿ êëàññà Array_RC_S"
<< endl; try_array( SA ); return 0;

С++ для начинающих
980
try_array: после вызова grow
( 7 )< , , , , Eeyore, Owl
Piglet, Piglet, Pooh, Pooh, Pooh > искомое значение: Tigger возвращенный индекс: -1
Assertion failed: ix >= 0 && ix < size
Представленная в этой главе реализация иерархии класса Array иллюстрирует применение множественного и виртуального наследования. Детально проектирование класса массива описано в [NACKMAN94]. Однако, как правило, достаточно класса vector из стандартной библиотеки.
Упражнение 18.16
Добавьте в Array функцию-член spy(). Она запоминает операции, примененные к объекту класса: число доступов по индексу; количество вызовов каждого члена; какой элемент искали с помощью find() и сколько было успешных поисков. Поясните свои проектные решения. Модифицируйте все подтипы Array так, чтобы spy() можно было использовать и для них тоже.
Упражнение 18.17
Стандартный библиотечный класс map (отображение) называют еще ассоциативным массивом, поскольку он поддерживает индексирование значением ключа. Как вы думаете, является ли ассоциативный массив кандидатом на роль подтипа нашего класса
Array
? Почему?
Упражнение 18.18
Перепишите иерархию Array, пользуясь контейнерными классами из стандартной библиотеки и применяя обобщенные алгоритмы.

С++ для начинающих
981
19
19.
Применение наследования в C++
При использовании наследования указатель или ссылка на тип базового класса способен адресовать объект любого производного от него класса. Возможность манипулировать такими указателями или ссылками независимо от фактического типа адресуемого объекта называется полиморфизмом. В этой главе мы рассмотрим три функции языка, обеспечивающие специальную поддержку полиморфизма.
Сначала мы познакомимся с идентификацией типов во время выполнения (RTTI –
Run-time Type Identification), которая позволяет программе узнать истинный производный тип объекта, адресованного ссылкой или указателем на тип базового класса. Затем расскажем о влиянии наследования на обработку исключений: покажем, как можно определять их в виде иерархии классов и как обработчики для типа базового класса могут перехватывать исключения производных типов. В конце главы мы вернемся к правилам разрешения перегрузки функций и посмотрим, как наследование влияет на то, какие преобразования типов можно применять к аргументам функции, и на выбор наилучшей из устоявших.
19.1.
Идентификация типов во время выполнения
RTTI позволяет программам, которые манипулируют объектами через указатели или ссылки на базовые классы, получить истинный производный тип адресуемого объекта.
Для поддержки RTTI в языке C++ есть два оператора:

оператор dynamic_cast поддерживает преобразования типов во время выполнения, обеспечивая безопасную навигацию по иерархии классов. Он позволяет трансформировать указатель на базовый класс в указатель на производный от него, а также преобразовать l-значение, ссылающееся на базовый класс, в ссылку на производный, но только в том случае, если это завершится успешно;

оператор typeid позволяет получить фактический производный тип объекта, адресованного указателем или ссылкой.
Однако для получения информации о типе производного класса операнд любого из операторов dynamic_cast или typeid должен иметь тип класса, в котором есть хотя бы одна виртуальная функция. Таким образом, операторы RTTI – это события времени выполнения для классов с виртуальными функциями и события времени компиляции для всех остальных типов. В данном разделе мы более подробно познакомимся с их возможностями.
Использование RTTI оказывается необходимым при реализации таких приложений, как отладчики или объектные базы данных, когда тип объектов, которыми манипулирует программа, становится известен только во время выполнения путем исследования RTTI- информации, хранящейся вместе с типами объектов. Однако лучше пользоваться статической системой типов C++, поскольку она безопаснее и эффективнее.

С++ для начинающих
982
19.1.1.
Оператор dynamic_cast
Оператор dynamic_cast можно применять для преобразования указателя, ссылающегося на объект типа класса в указатель на тип класса из той же иерархии. Его также используют для трансформации l-значения объекта типа класса в ссылку на тип класса из той же иерархии. Приведение типов с помощью оператора dynamic_cast, в отличие от других имеющихся в C++ способов, осуществляется во время выполнения программы.
Если указатель или l-значение не могут быть преобразованы в целевой тип, то dynamic_cast завершается неудачно. В случае приведения типа указателя признаком неудачи служит возврат нулевого значения. Если же l-значение нельзя трансформировать в ссылочный тип, возбуждается исключение. Ниже мы приведем примеры неудачного выполнения этого оператора.
Прежде чем перейти к более детальному рассмотрению dynamic_cast, посмотрим, зачем его нужно применять. Предположим, что в программе используется библиотека классов для представления различных категорий служащих компании. Входящие в иерархию классы поддерживают функции-члены для вычисления зарплаты:
}
В компании есть разные категории служащих. Параметром функции-члена payroll() класса company является указатель на объект employee, который может адресовать один из типов manager или programmer. Поскольку payroll() обращается к виртуальной функции-члену salary(), то вызывается подходящая замещающая функция, определенная в классе manager или programmer, в зависимости от того, какой объект адресован указателем.
Допустим, класс employee перестал удовлетворять нашим потребностям, и мы хотим его модифицировать, добавив еще одну функцию-член bonus(), используемую совместно с salary()
при расчете платежной ведомости. Для этого нужно включить новую функцию-член в классы, составляющие иерархию employee: class employee { public: virtual int salary();
}; class manager : public employee { public: int salary();
}; class programmer : public employee { public: int salary();
}; void company::payroll( employee *pe ) {
// используется pe->salary()

С++ для начинающих
983
}
Если параметр pe функции payroll() указывает на объект типа manager, то вызывается виртуальная функция-член bonus() из базового класса employee, поскольку в классе manager она не замещена. Если же pe указывает на объект типа programmer, то вызывается виртуальная функция-член bonus() из класса programmer.
После добавления новых виртуальных функций в иерархию классов придется перекомпилировать все функции-члены. Добавить bonus() можно, если у нас есть доступ к исходным текстам функций-членов в классах employee, manager и programmer.
Однако если иерархия была получена от независимого поставщика, то не исключено, что в нашем распоряжении имеются только заголовочные файлы, описывающие интерфейс библиотечных классов и объектные файлы с их реализацией, а исходные тексты функций-членов недоступны. В таком случае перекомпиляция всей иерархии невозможна.
Если мы хотим расширить функциональность библиотеки классов, не добавляя новые виртуальные функции-члены, можно воспользоваться оператором dynamic_cast.
Этот оператор применяется для получения указателя на производный класс, чтобы иметь возможность работать с теми его элементами, которые по-другому не доступны.
Предположим, что мы расширяем библиотеку за счет добавления новой функции-члена bonus()
в класс programmer. Ее объявление можно включить в определение programmer
, находящееся в заголовочном файле, а саму функцию определить в одном из своих исходных файлов: class employee { public: virtual int salary(); //
çàðïëàòà virtual int bonus(); //
ïðåìèÿ
}; class manager : public employee { public: int salary();
}; class programmer : public employee { public: int salary(); int bonus();
}; void company::payroll( employee *pe ) {
//
èñïîëüçóåòñÿ pe->salary() è pe->bonus()

С++ для начинающих
984
};
Напомним, что payroll() принимает в качестве параметра указатель на базовый класс employee
. Мы можем применить оператор dynamic_cast для получения указателя на производный programmer и воспользоваться им для вызова функции-члена bonus():
}
Оператор dynamic_cast< programmer* >( pe ) приводит свой операнд pe к типу programmer*. Преобразование будет успешным, если pe ссылается на объект типа programmer, и неудачным в противном случае: тогда результатом dynamic_cast будет 0.
Таким образом, оператор dynamic_cast осуществляет сразу две операции. Он проверяет, выполнимо ли запрошенное приведение, и если это так, выполняет его. Проверка производится во время работы программы. dynamic_cast безопаснее, чем другие операции приведения типов в C++, поскольку проверяет возможность корректного преобразования.
Если в предыдущем примере pe действительно указывает на объект типа programmer, то операция dynamic_cast завершится успешно и pm будет инициализирован указателем на объект типа programmer. В противном случае pm получит значение 0. Проверив значение class employee { public: virtual int salary();
}; class manager : public employee { public: int salary();
}; class programmer : public employee { public: int salary(); int bonus(); void company::payroll( employee *pe )
{ programmer *pm = dynamic_cast< programmer* >( pe );
//
åñëè pe óêàçûâàåò íà îáúåêò òèïà programmer,
//
òî dynamic_cast âûïîëíèòñÿ óñïåøíî è pm áóäåò
//
óêàçûâàòü íà íà÷àëî îáúåêòà programmer if ( pm ) {
//
èñïîëüçîâàòü pm äëÿ âûçîâà programmer::bonus()
}
//
åñëè pe íå óêàçûâàåò íà îáúåêò òèïà programmer,
//
òî dynamic_cast âûïîëíèòñÿ íåóäà÷íî
//
è pm áóäåò ñîäåðæàòü 0 else {
//
èñïîëüçîâàòü ôóíêöèè-÷ëåíû êëàññà employee
}

С++ для начинающих
985
pm
, функция company::payroll() может узнать, указывает ли pm на объект programmer.
Если это так, то она вызывает функцию-член programmer::bonus() для вычисления премии программисту. Если же dynamic_cast завершается неудачно, то pe указывает на объект типа manager, а значит, необходимо применить более общий алгоритм расчета, не использующий новую функцию-член programmer::bonus().
Оператор dynamic_cast употребляется для безопасного приведения указателя на базовый класс к указателю на производный. Такую операцию часто называют
понижающим приведением (downcasting). Она применяется, когда необходимо воспользоваться особенностями производного класса, отсутствующими в базовом.
Манипулирование объектами производного класса с помощью указателей на базовый обычно происходит автоматически, с помощью виртуальных функций. Однако иногда использовать виртуальные функции невозможно. В таких ситуациях dynamic_cast предлагает альтернативное решение, хотя этот механизм в большей степени подвержен ошибкам, чем виртуализация, и должен применяться с осторожностью.
Одна из возможных ошибок – это работа с результатом dynamic_cast без предварительной проверки на 0: нулевой указатель нельзя использовать для адресации объекта класса. Например:
}
Результат, возвращенный dynamic_cast, всегда следует проверять, прежде чем использовать в качестве указателя. Более правильное определение функции company::payroll()
могло бы выглядеть так:
}
Результат операции dynamic_cast используется для инициализации переменной pm внутри условного выражения в инструкции if. Это возможно, так как объявления в условиях возвращают значения. Ветвь, соответствующая истинности условия, выполняется, если pm не равно нулю: мы знаем, что операция dynamic_cast завершилась успешно и pe указывает на объект programmer. В противном случае результатом объявления будет 0 и выполняется ветвь else. Поскольку теперь оператор и проверка его результата находятся в одной инструкции программы, то невозможно случайно вставить void company::payroll( employee *pe )
{ programmer *pm = dynamic_cast< programmer* >( pe );
//
ïîòåíöèàëüíàÿ îøèáêà: pm èñïîëüçóåòñÿ áåç ïðîâåðêè çíà÷åíèÿ static int variablePay = 0; variablePay += pm->bonus();
// ... void company::payroll( employee *pe )
{
//
âûïîëíèòü dynamic_cast è ïðîâåðèòü ðåçóëüòàò if ( programmer *pm = dynamic_cast< programmer* >( pe ) ) {
//
èñïîëüçîâàòü pm äëÿ âûçîâà programmer::bonus()
} else {
//
èñïîëüçîâàòü ôóíêöèè-÷ëåíû êëàññà employee
}

С++ для начинающих
1   ...   85   86   87   88   89   90   91   92   93


написать администратору сайта