Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология Образование Политология Производство Психология Стандартизация Технологии |
Глава 23. Разложение классов 249
virtual void write(char classThreedVGA:publicHWVGA virtual void initialized; }; void SVGA VGAvga; //все
Класс Display, описывающий дисплеи персонального компьютера, содержит две чисто виртуальные функции: initializ e () и writef] . Вы не можете ввести эти функции в общем виде. Разные типы видеоадаптеров инициализируются и осуществ- ляют вывод по-разному. Один из подклассов SVGA — не абстрактный. Это отдельный тип видеоадаптера, и программист точно знает, как его реализовать. Таким образом, класс SVGA переоп- ределяет обе функции— initialize d и — именно так, как необходимо для данного адаптера. Еще один подкласс — HWVGA . Программисту известно, как программировать уско- ренный VGA-адаптер. Поэтому между общим классом Display и его частным случа- ем, ThreedVGA, который представляет собой специальный тип карт 3-D, находится еще один уровень абстракции. В нашем обсуждении предположим, что запись во все аппаратно ускоренные кар- ты VGA происходит одинаково (это не соответствует истине, но представим что это так). Чтобы правильно выразить общее свойство записи, вводится класс HWVGA, реализующий функцию () (и другие общие для HWVGA свойства). При этом функция initializ e () не переопределяется, поскольку для разных типов карт HWVGA она реализуется по-разному. Поэтому, несмотря на то что функция writ e () переопределена в классе HWVGA, ОН все равно остается абстрактным, поскольку функция initializ e () все еще не пере- определена. Поскольку ThreedVGA наследуется от он переопределить только одну функцию, initializ e {), для того чтобы окончательно определить адаптер дис- плея. Таким образом, функция f n () может свободно реализовать и использовать объ- ект класса ThreedVGA. Замещение нормальной функцией последней чисто виртуальной функции делает класс завершенным (т.е. неабстрактным). Только неабстрактные классы могут быть реализованы в виде объектов.
Изначально требовалось, чтобы каждая чисто виртуальная функция была переопределена в каждом подклассе другой, хотя бы и снова чисто вирту- альной функцией. В конечном счете люди решили, что это глупое требо- вание, и исключили его. Однако старые компиляторы могут требовать вы- полнения этого условия.
250 Часть Наследование Передача абстрактных классов Поскольку вы не можете реализовать абстрактный класс, упоминание о возможно- сти создавать указатели на абстрактные классы звучит несколько странно. Однако ес- ли вспомнить о полиморфизме, то станет ясно, что это не так уж глупо, как кажется поначалу. Рассмотрим следующий фрагмент кода: void //это допустимо void otherFn ()
s; c;
//Savings ЯВЛЯЕТСЯ Account fn ; //Checking — тоже fn(sc);
В этом примере объявлен как указатель на Account. Разумеется, при вызове функции ей будет передаваться адрес какого-то объекта неабстрактного класса, например Checking или Savings. Все объекты, полученные функцией fn () , будут объектами либо класса Checking, либо Savings (или другого неабстрактного подкласса Account). Можно с уверенно- стью заявить, что вы никогда не передадите этой функции объект класса Account, поскольку никогда не сможете создать объект этого класса. Нужны ли чисто виртуальные функции Если нельзя определить функцию withdrawal;) , почему бы просто не опустить ее? Почему бы не объявить ее в классах Savings и Checking, где она может быть оп- ределена, оставив в покое класс Account? Во многих объектно-ориентированных языках вы могли бы именно так и сделать. Но C++ предпочитает иметь возможность убедиться в вашем понимании того, что вы делаете. Не забывайте, что объявление функции — это указание полного имени функции, включающего ее аргументы. Определение же функции вклю- чает в себя и код, который будет выполняться в результате вызова этой функции. Чтобы продемонстрировать суть сказанного, можно внести следующие незначи- тельные изменения В класс Account: class Account
//то же, что и раньше, но //нет функции
class Savings : public Account {
virtual void
void { некоторую сумму
Глава 23. Разложение классов 251 //этот вызов недопустим, //поскольку //не является членом класса Account
int main ! Savings s; //открыть счет
//продолжение программы } Представьте себе, что вы открываете сберегательный счет s. Затем вы передаете адрес этого счета функции f n (}, которая пытается выполнить функцию withdrawal (). Однако, поскольку функция () не член класса Account, компилятор сгенерирует сообщение об ошибке. Некоторые языки выполняют такую проверку, когда функция уже вызвана, во время выполнения программы. В таком случае приведенный выше фрагмент кода бу- дет работать: raain() будет вызывать fn () и передавать ей объект s. Когда в свою очередь, вызовет функцию withdrawal (), программа увидит, что withdrawal! ) действительно определена в переданном ей объекте. Цена такой гиб- кости снижение скорости выполнения программы, поскольку язык должен прово- дить множество проверок во время ее выполнения. Это также чревато некоторыми ошибками. Например, если кто-то передаст объект, который является счетом, но не содержит определенной в нем функции withdrawal О, то программа аварийно пре- кратит работу, так как не сможет определить, что делать с этим вызовом. Я думаю, пользователи будут не очень рады этому. Взгляните, как чисто виртуальная функция помогает решить эту проблему. Вот та же ситуация с абстрактным классом Account: class Account { //почти то же, что и в предыдущей программе, //однако функция определена virtual void 0; }; class Savings : public Account { virtual void amnt);
void *pAcc) i //снять некоторую сумму //теперь этот код будет работать
int main() { Savings s; //открыть счет
//продолжениепрограммы
Часть IV. Наследование Ситуация та же, но теперь класс Account содержит функцию-член withdrawal {). Поэтому, когда компилятор проверяет, ли функция (), он видит ожидаемое определение Компилятор счастлив. Вы счастливы. А значит, и я тоже счастлив. (Честно говоря, для того чтобы сделать меня счастливым, достаточно футбола и холодного пива.) Чисто виртуальная функция занимает место в базовом классе для функции с тем, чтобы позже быть переопределенной в подклассе, который будет знать, как ее реализовать. Если место не будет занято в базовом классе, не будет и переоп- ределения.
Рационализация В этой части продолжается превращение исключительно функциональной вер- сии программы 1, приведенной в конце части 2, которая затем прошла че- рез объектно-основанный этап своей эволюции — Budget2, представленную в конце части 3. Теперь она наконец превратится в объектно-ориентированную программу Budget3. Программа Budget осуществляет вложение денег на счет и снятие со счета в воображаемом банке. Пользователь поочередно вводит номера банковских сче- тов и суммы вкладов на этот счет и снятий с него. После того как пользователь выполнил все транзакции, программа показывает баланс каждого счета и общий баланс. Обе программы — и Budget3 — эмулируют Checking (чековый) и Savings (сберегательный) счета. Чековые счета взимают небольшой гонорар за обслуживание при каждом снятии, если баланс упал ниже 500 долларов, тогда как сберегательный счет взимает большой гонорар за обслуживание при первом сня- тии, независимо от баланса. Программа Budget2 превосходит 3udgetl только в одном: она изолирует особен- ности классов, описывающих счет, от внешних функций, которые манипулировали счетами. сожалению, Budget2 содержала большое количество дублированного кода в классах Savings и Checking, и именно от него мы и хотим избавиться, используя принципы наследования. Программа Budget3 обладает следующими преимуществами: использует наследование, чтобы выделить общие для чековых и сбере- гательных счетов свойства, избегая избыточности; использует виртуальные функции-члены для улучшения читаемости и гибкости программы; создает чисто виртуальные классы для выделения общих черт, прису- щих чековым и счетам; вместо массива использует связанный список во избежание ограниче- ния на количество счетов, которые может поддерживать банк. С помощью такого "супергероя" объектно-ориентированного программирования, как наследование, и его верного бокового удара полиморфизма мы смогли оптими- зировать два класса счетов, объединив в один класс Account все то общее, что при- суще этим двум классам. В результате получилась гораздо более короткая и простая программа. |
Последнее изменение этой страницы: 2019-04-19; Просмотров: 202; Нарушение авторского права страницы