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

Штерн В. - Основы C++. Методы программной инженерии - 2003

.pdf
Скачиваний:
267
Добавлен:
13.08.2013
Размер:
28.32 Mб
Скачать

690

 

Часть iV # Растьлу

cout

«

" Total

records

read: " « cnt « endl « endl;

for

(int

i=0; i

< cnt;

i++)

{ write(data[i]); }

// отображение данных

for (int j=0; j < cnt;

j++)

{ delete data[j]; }

// удаление записи

return 0 ;

 

 

}

 

 

 

 

Полиморфизм (интерпретация сообщений объектам во время исполнения) основывается на допустимости неявного приведения типов объектов из производ­ ного в базовый класс. Базовый указатель (в примере — Person) может указывать на производный объект (Faculty или Student) без применения явного приведения.

Person *р, *pf, *ps;

 

/ / указатели типа Person

р = new Person("U12345678", "Smith");

 

pf = new Faculty("U12345689",

"Black",

"Assistant Professor");

ps = new Student("U12345622",

"Green",

"Astronomy");

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

ps = (Person*) new Student("U12345622", "Green", "Astronomy");

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

Student* S = (Student*) ps;

/ /

приведение типов обязательно

s->write();

/ /

указатель производного класса

В виртуальных функциях использование базовых указателей приводит в результа­ те к вызовам функций-членов производного класса.

ps->write(); / / указатель базового класса

Однако все эти улучшения влияют только на внешний вид клиентской программы. По своей сути программа, представленная в листинге 15.5, выполняет то же са­ мое, что и программа в листинге 15.4. Поле kind происходит из класса Person, но фактически находится здесь. Причем оно доступно программе, сгенерированной компилятором, а не исходной программе, написанной программистом. Оператор выбора происходит из клиентской программы, но он также находится здесь. Он реализован программой, сгенерированной компилятором, а не исходной програм­ мой, написанной программистом.

Программа в листинге 15.4 явно выделяет дополнительную память для анали­ за типа объектов Person и тратит время на принятие решения, какую функцию write() вызвать. Программа в листинге 15.5 выделяет такую же дополнительную память и тратит дополнительное время.

Некоторые программисты, разрабатывающие системы управления реального времени, говорят, что виртуальные функции неэкономны. Это несправедливо. Для полиморфного алгоритма требуется выделить дополнительную память и за­ тратить время. А реализован ли он явно, как в листинге 15.4, или с виртуальными функциями, как в листинге 15,5, не имеет большого значения.

Глава 15 • Виртуальные функции и использование наследования

691

Динамическое и статическое связывание

Динамическое связывание предлагает программисту новый и увлекательный способ структурирования обрабатывающих алгоритмов. Можно создать семейство связанных производных классов в общем открытом базовом классе, снабдить ка>вдый производный класса функцией, которая выполняет обработку способом, конкретным для этого производного класса, удостовериться, что у всех функций одинаковое имя и интерфейс, а затем вызвать эту функцию через указатель базо­ вого класса. В результате вы увидите, что вызываемая функция не зависит от типа объекта, обозначенного указателем.

Но динамическое связывание не уменьшает значения традиционного статиче­ ского связывания. В большинстве случаев программирования на C++ вызываемый метод зависит от типа указателя, ссылающегося на объект, а не от типа объекта, на который указывает указатель. Появляются дополнительные проблемы.

Для статического связывания при анализе вызова функции необходимо рас­ смотреть тип цели сообщения и сигнатуру вызываемого метода. При возможном использовании динамического связывания необходимо принять во внимание не­ сколько дополнительных факторов.

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

Во-вторых, следует определить, к какой точке иерархии наследования принад­ лежит указатель. Ес*ли указатель базового типа, то динамическое связывание воз­ можно — оно зависит от типа объекта, на который он указывает, и от того, как определена функция. Если указатель относится к одному из производных типов, возможно только статическое связывание. Однако результат вызова также зави­ сит от типа указываемого объекта и от способа определения функции.

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

Функции, определенные в базовом классе и наследованные

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

Функции, определенные в производном классе без прототипа

вбазовом классе

Функции, определенные в базовом классе и переопределенные

впроизводном классе с тем же самым именем

и с той же или другой сигнатурой

Функции, определенные в базовом классе и переопределенные

впроизводном классе как виртуальные с тем же самым именем и с той же сигнатурой

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

692

одьзование С^+

Попытка вызвать функцию, определенную в производном классе без прототипа

вбазовом классе, является синтаксической ошибкой. Попытка вызвать функцию, переопределенную в производном классе, несерьезна — функция, определенная

вбазовом классе, вызывается везде.

Для производного указателя, ссылающегося на производный объект (того же класса) базовые функции недоступны, за исключением тех, которые определены в базовом классе и наследуются без изменений. Этот указатель может вызывать методы, которые добавляются в производный класс и переопределяются в произ­ водном классе. Функции, переопределенные в производном классе, вызываются статически независимо от того, как они определены — с той же сигнатурой или с другой, как виртуальные функции или нет.

Обратите внимание, что базовые функции, переопределенные в производном классе, недоступны для указателя производного класса, ссылающеегося на объект производного класса. Они скрыты соответствующими функциями производного класса. Попытка достичь базовой функции приведет к статическому вызову функ­ ции, определенной в производном классе (виртуальном или не виртуальном), если сигнатуры совпадают, или к синтаксической ошибке, если нет.

Указатель базового класса, ссылающийся на объект производного класса, мо­ жет вызывать базовые методы, унаследованные (но не переопределенные) произ­ водным классом. Он не может достичь методов, определенных в производном классе и не имеющих прототипа в базовом классе. Ес/ш производный класс пере­ определяет базовый метод как не виртуальную функцию (либо с той же самой, либо с другой сигнатурой), этот производный метод также не может быть вызван через базовый указатель. Вместо этого соответствующий базовый метод будет вызываться статически. Если производный класс переопределяет базовый метод как виртуальную функцию, то через указатель базового класса вызывается метод производного класса, а не базового класса. Это единственный случай, когда воз­ можно динамическое связывание.

Указатель производного класса, указывающий на базовый объект, является аномалией. Он может вызывать методы, определенные в базовом классе и уна­ следованные в базовом классе без переопределения. Он не может вызвать методы базового класса, переопределенные в производном классе, поскольку они скрыты от этого указателя. Он не может вызвать методы производного класса, которые переопределяют методы базового класса (как виртуальные, так и не виртуальные с той же самой или с другой сигнатурой), поскольку они не поддерживаются базо­ вым объектом, а при попытке сделать это возникает ошибка.

Описание основывается на двух принципах:

Производный указатель, ссылающийся на производный объект, может достичь методов, определенных в производном классе,

и методов, унаследованных из базового класса без изменений. Методы, переопределенные в производном классе, скрывают методы, определенные в базовом классе, от указателя производного класса.

• Базовый указатель, указывающий на производный объект, может достичь этих методов, определенных в базовом классе. Но есть одно исключение. Если функция переопределена в производном классе как виртуальная, то базовый указатель, используя динамическое связывание, вызывает функцию производного класса, а не базового класса.

Это очень просто, но для выполнения такой операции может потребоваться время. Эти правила представлены в графическом виде (см. рис. 15.12 и таблицу 15.1).

На рис. 15.12 представлены указатели базового класса (узкие прямоугольни­ ки) и указатели производного класса (более широкие прямоугольники из двух частей), которые указывают на объекты базового класса (часть, показанная пунктирной линией, представляет собой пропущенную производную часть) и производного класса (левая часть представляет базовую часть, правая часть производную часть).

 

Глава 15 • Виртуальные функции и использование наследования

693 )

А)

'

' i

l

l

41

 

1 "

Вертикальные линии внутри каждой части обозначают функ­

 

I

ции-члены четырех типов. Тип 1 определяется в базовом классе

 

 

I

I

I

 

I

I

 

и наследуется в производном классе без изменений. Тип 2 добав­

 

 

J_3 4

 

2

3

 

4

Q-

^' 1 1

1-J-JJ-J

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

классе. Тип 3 определяется в базовом классе и переопределяется

 

 

1

1 1

 

n

i

l

 

в производном

классе с тем же

самым именем. Тип 4 задается

 

 

1

3

4

 

2

3 4

в базовом классе (как виртуальный) и переопределяется в произ­

 

 

 

 

 

 

 

 

 

 

О-

^' 1 1

1

I

I

 

I '

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

 

Методы, которые могут вызываться через указатель, подчерк­

 

 

1

1 1

 

1

1 1

нуты. В случае А функции типа 3 и 4, определенные в базовом

 

 

1

3

4

 

2

3 ^

D)

L-H-+-I. j _ j . j j

классе,

скрываются

функциями,

определенными

в производном

классе.

В случае В

доступны только функции,

определенные

 

 

 

 

 

 

 

 

 

 

 

 

 

1

3 4

 

 

 

 

 

в базовом классе. В случае С разрешаются только функции, опре­

Рис. 15.12.

 

 

 

 

 

 

 

 

деленные в базовом классе, но функции, переопределенные в про­

и

динамическое

изводном

классе как виртуальные, скрывают свои прототипы

Статическое

базового

класса

и могут вызываться динамически. В случае D

связывание

для

указателей

 

 

могут вызываться только функции, определенные в базовом клас­

базового и производного

класса

 

 

 

 

 

 

 

 

 

 

се и не переопределенные в производном классе.

 

 

 

 

 

 

 

В таблице 15.1 перечислены эти же правила. Столбцы показывают типы

 

 

 

 

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

 

 

 

 

различные виды функций-членов.

 

 

 

Краткое

перечисление

правил

статического

и динамического

связывания

Таблица 15.1

 

Виды функций-членов

 

 

Базовые указатели

Производные указатели

 

 

 

 

 

 

 

 

 

 

Базовый

Производный

Базовый

Производный

 

 

 

 

 

 

 

 

 

 

объект

 

объект

объект

объект

Функции,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

определенные в классе Base

 

 

 

 

 

 

 

 

 

 

Наследованы в классе Derived

доступны

 

доступны

доступны

доступны

без изменений

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Переопределены в Derived

 

 

доступны

 

доступны

недоступны

скрыты

(не виртуальные)

 

 

 

 

 

 

 

 

 

 

 

 

 

Переопределены в Derived

 

 

доступны

 

скрыты

недоступны

скрыты

(виртуальные)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Функции,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

определенные в классе Derived

 

 

 

 

 

 

 

Определены только

 

 

 

 

 

синтаксическая

синтаксическая

аварийная

доступны

в классе Derived

 

 

 

 

 

 

ошибка

 

ошибка

ситуация

 

Переопределены в Derived

 

 

недоступны

недоступны

аварийная

доступны

(не виртуальные)

 

 

 

 

 

 

 

 

 

 

 

ситуация

 

Переопределены в Derived

 

 

недоступны

динамическое

аварийная

доступны

(виртуальные)

 

 

 

 

 

 

 

 

 

 

связывание

ситуация

 

Чисто виртуальные функции

Базовые виртуальные функции могут не выполнять никаких действий, потому что они не имеют значения в рамках приложения. Их задача — определить на­ следование как стандарт для всех производных классов. Именно поэтому вир­ туальные функции представляются в первую очередь.

Например, метод writeO в классе Person ничего не содержит. В нем нет кода. Обратите внимание, что он никогда не вызывается. Все вызовы метода writeO в клиентской программе (глобальная функция writeO) разрешаются либо в классе Faculty, либо в методе write() класса Student.

I

694

Часть IV • Расширенное использование C++

 

 

 

 

Фактически класс Person является обобщением. В приложении отсутствуют

 

 

объекты Person. Все объекты создаются оператором new в глобальной функции

 

 

readO и относятся либо к классу Student, либо к Faculty. Описание проблемы

 

 

в начале этой части

свидетельствует о том, что существуют два вида запи­

 

 

сей — одна доя студентов и одна доя преподавателей. Класс Person первоначально

 

 

был введен в приложение как абстракция, которая объединяет характеристики

 

 

объектов профессорско-преподавательского состава и объектов студентов в один

 

 

обобщенный класс (листинг 15.3). Позднее он использовался доя определения

 

 

иерархии производных классов (листинг 15.4). В последней версии программы

 

 

(листинг 15.4) класс Person применялся доя определения интерфейса виртуальной

 

 

функции write().

 

 

 

 

 

В реальной жизни класс Person может быть очень полезным. В нем могут быть

 

 

не только идентификатор и наименование университета, но и дата рождения, ад­

 

 

рес, номер телефона

и другие характеристики,

обычные доя объектов Faculty

 

 

и Student. Кроме того, класс Person может определять такие многочисленные ме­

 

 

тоды, как изменение имени, адреса или номера телефона, извлечение идентифика­

 

 

тора университета и других данных, обычных доя объектов Faculty и Student.

 

 

Производные классы могут наследовать все эти полезные функции. Клиенты про­

 

 

изводных классов используют подобные функции, посылая сообщения, определен­

 

 

ные в классе Person, объектам классов Faculty и Student. Снова не было сказано,

 

 

что класс Person — бесполезен. Отмечалось, что объекты класса Person являются

 

 

бесполезными доя этого приложения. Приложению требуются только объекты

 

 

классов, производных от Person. Помните об этом.

 

 

Проектировщик класса Person знает, что приложение не создает объекты клас­

 

 

са и что доя объектов класса нет задания доя выполнения. Было бы прекрасно

 

 

передать эту информацию программисту клиентской части и лицам, осуществляю­

 

 

щим сопровождение, не через комментарии, а в самой программе. Язык С+ +

 

 

позволяет определять базовый класс таким образом, что попытка создания объек­

 

 

та этого типа будет недопустимой и приведет к синтаксической ошибке.

 

 

Язык C+-F делает это возможным через использование чистых виртуальных

 

 

функций и абстрактных классов. Не совсем ясно, почему два термина — "чистый"

 

 

и "абстрактный" — используются доя описания одной и той же идеи. Чистой

 

 

виртуальной функцией является виртуальная функция, которая не должны вызы­

 

 

ваться (подобно writeO в классе Person). Если программа пытается ее вызвать,

 

 

возникает синтаксическая ошибка. Абстрактный класс — это класс с не менее

 

 

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

 

 

добного класса. Если программа пытается создать объект этого класса либо ди­

 

 

намически, либо в стеке, появляется синтаксическая ошибка.

 

 

Для чистых виртуальных функций и абстрактных классов в C + + отсутствуют

 

 

ключевые слова. Вместо них чистая виртуальная функция распознается (компи­

 

 

лятором, клиентской

программой и лицом,

осуществляющим сопровождение)

 

 

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

 

 

класс Person, функция-член wrlte() которой определяется как чистая виртуальная

 

 

функция.

 

 

 

 

 

struct Person {

 

/ /

абстрактный класс

 

 

protected:

 

 

 

 

 

char id[10];

 

/ /

данные, общие для обоих типов

 

 

char* name;

 

 

 

 

 

public:

 

 

 

 

 

Person(const char id[], const char nm[]);

 

 

 

virtual void write () const = 0;

// чистая виртуальная функция

"PersonO ;

} ;

Глава 15 • Виртуальные функции и использование наследования

695

Оператор присваивания не обозначает присваивание. Это еще один пример придания символу дополнительного значения в следующем контексте. Добавление другого ключевого слова, такого как чистый или абстрактный, возможно, было бы лучшим решением.

Чистая виртуальная функция не имеет реализации. Фактически предоставле­ ние реализации чистой виртуальной функции (или вызов функции) является син­ таксической ошибкой. Именно присутствие виртуальных функций делает класс абстрактным (или частичным) классом.

Абстрактный класс должен иметь хотя бы один производный класс. В нем не должно быть объектов с приписанными значениями. Если производный класс реализует эту функцию, он становится регулярным классом. В противном случае он становится абстрактным классом. Создание объекта этого производного класса не допускается, и такой класс должен иметь не менее одного производного класса.

Производные классы реализуют чистые виртуальные функции так же, как и обычные виртуальные функции. Это означает, что производный класс должен использовать то же самое имя, сигнатуру и возвращаемый тип, которые будут применяться чистой виртуальной функцией. Режим порождения должен быть общедоступным. Ниже приводится пример класса Faculty, который реализует виртуальную функцию write(). Это регулярный неабстрактный класс.

struct Faculty : public Person {

/ /

регулярный

класс

private:

 

/ /

только для

преподавателей

char* rank;

 

public:

 

 

 

 

Paculty(const

char icl[], const

char nm[],

const char

r [ ] ) ;

void writeO

const;

/ /

регулярная

виртуальная функция

"FacultyO;

 

 

 

 

} ;

Этот же производный класс использовался в листинге 15.5. Рассматривая регу­ лярный неабстрактный класс, можно видеть, является ли он производным от абстрактного класса или от регулярного класса. Пользователю класса Faculty совершенно все равно, как реализован базовый класс Person, в той мере, в которой клиентская программа не пытается приписать значения объектам абстрактного класса.

Для регулярного класса с виртуальными функциями клиентская программа мо­ жет создать объекты, отправить им сообщения и, earn требуется, использовать полиморфизм.

Абстрактный класс является классом языка C+ + . Он может включать элемен­ ты набора данных и регулярные, не чистые функции, даже виртуальные функции.

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

class Base {

public:

 

v i r t u a l

void memberO = 0;

. . . . }

;

class Derived : public Base { public:

void memberO

{ }

//абстрактный класс

//чистая виртуальная функция

//оставшаяся часть класса Base

//регулярный класс

//виртуальная функция

//пустое тело: поор

//оставшаяся часть класса Derived

6У6 I

Часть !V # Расширенное использование С-^-^

швшшшшшшшшшшшшшшшшшшшшшяишшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшшяшшшшшшшшшшшшшя^^

Класс Base является абстрактным классом. Его объекты не могут быть созда­ ны. Класс Derived представляет собой регулярный класс. Его объектам могут быть присвоены значения в стеке (как именованные переменные) или в динами­ чески распределяемой области памяти (как неименованные переменные). Функ­ ция memberO в классе Base является чистой виртуальной функцией. Вызвать ее невозможно. Функция memberO в классе Derived является регулярной виртуаль­ ной функцией. Однако ее вызов приводит в результате к отсутствию операций.

Base *b; Derived *d;

/ /

указатели Base и Derived

b = new Base;

/ /

синтаксическая ошибка, абстрактный класс

d = new Derived;

/ /

OK, регулярный класс, объект динамически

 

/ /

распределяемой области памяти

b = new Derived;

/ /

OK, неявное преобразование указателя

d->member();

/ /

OK, связывание на этапе компиляции,

 

/ /

холостая команда

b->member();

/ /

OK, связывание во время выполнения

d->Base::member();

/ /

ошибка компоновщика: реализация отсутствует

Переопределение с другой сигнатурой делает функцию в производном классе не виртуальной. Здесь класс Derivedl является классом, который наследуется из абстрактного класса Base, но не переопределяет чистую функцию memberO без параметров. Вместо этого он задает функцию member(int) с одним параметром.

class Derivedl : public Base {

/ /

также абстрактный класс

public:

 

 

 

void

member(int)

/ /

не виртуальная функция

{

}

/ /

пустое тело: холостая команда

. . . .

} ;

/ /

оставшаяся часть класса Derived

Это означает, что класс Derivedl является абстрактным классом. Создание его объектов является синтаксической ошибкой. Поскольку этот класс не использу­ ется как базовый класс для порождения других классов, он бесполезен.

class Derived2 : public Derivedl {

/ /

регулярный класс

public:

 

 

 

void memberO

/ /

виртуальная функция

{

}

/ /

пустое тело: холостая команда

. . . .

} ;

/ /

оставшаяся часть класса Derived

Класс Derived2 наследуется из класса Derivedl. Он реализует виртуальную функцию-член memberO, следовательно, допускается создание объектов этого класса. Его объекты могут отвечать на сообидение memberO как со связыванием на этапе выполнения, так и со статическим связыванием. Объекты этого класса не могут отвечать на сообщение member(int), потому что функция скрывается функцией-членом memberO, определенной в классе Derived2.

Derived2 *d2 = new Derived2;

/ /

OK,

регулярный класс, объект динамически

 

/ /

распределяемой области памяти

d2->member();

//OK,

статическое связывание

b = new Derived2;

/ /

OK для виртуальной функции

b->member();

/ /

OK, динамическое связывание

b->member(0);

/ /

синтаксическая ошибка

d2->member(0);

/ /

неверное количество параметров

Обратите внимание, что указатель b базового класса при обозначении объекта производного класса может вызвать:

Не чистые функции-члены (виртуальные или не виртуальные), определенные в базовом классе

Виртуальные функции, заданные в производном классе

Глава 15 • Виртуальные функции и использование наследования

697

Он вызывает только виртуальные функции-члены, определенные в производ­ ном классе. Это указатель ближнего действия. Он использует виртуальную функ­ цию Д/1Я расширения ее области видения до производной части объекта, на который он указывает. В противном случае она сможет видеть только базовую часть произ­ водного объекта. Указатель производного класса используется для осуществления доступа как к базовой части, так и к производной части производного объекта.

Виртуальные функции: деструкторы

При вызове оператора удаления называется деструктор и объект уничтожается. Вы вызываете деструктор, определенный в классе указателей, ссылающихся на объект, или деструктор, определенный в классе, к которому принадлежит объект, на который указывает указатель.

Когда указатель и объект принадлежат к одному классу, деструктор принадле­ жит этому классу.

DerivecJ2 *62 - new Derivecl2 ;

/ /

OK, регулярный класс, объект динамически

 

 

/ /

распределяемой области памяти

cl2->member();

/ /

OK, статическое связывание

b = new Derivecl2;

/ /

OK для виртуальных функций

b->member();

/ /

OK, динамическое

связывание

delete

CJ2;

/ /

деструктор класса

Deriveci2

delete

b;

. / /

??

 

Деструкторы C++ являются регулярными, не виртуальными функциями-членами. Когда используется оператор delete, компилятор находит определение операнда указателя, затем класса, к которому принадлежит указатель, и вызывает деструк­ тор. Все это происходит во время компиляции. Компилятор не обращает внима­ ния на класс объекта, на который указывает указатель.

Когда указатель и объект принадлежат одному классу, проблемы отсутствуют. При выполнении кода деструктора динамическая память и другие ресурсы, выде­ ленные объекту, возвращаются. Когда указатель производного класса ссылается на базовый объект, это делать не требуется. Большой и мощный указатель про­ изводного класса потребует от небольшого базового класса выполнить то, что он не может.

Person р; Faculty f;

/ /

базовый и производный указатели

р = new Person("U12345678",

"Smith");

 

f

= р;

/ /

синтаксическая ошибка: следует избегать

f

= (Faculty*)р;

/ /

именно это нужно выполнить

delete f;

/ /

деструктор Faculty

В этом примере деструктор Faculty вызывается в объекте Person. Оператор уда­ ления delete вызывается для компонента набора данных rank, т. е. не в объекте. Результаты не определены.

Когда базовый указатель ссылается на объект производного класса, вызыва­ ется деструктор базового класса. Если динамическая память обрабатывается в базовом, а не в производном классе, то проблем не возникает. Память, выде­ ленная из динамически распределяемой области памяти, будет возвращена ей базовым деструктором. Если управление памятью динамически распределяемой области памяти осуществляется в производном классе, она не будет возвращена базовым деструктором. В результате происходит "утечка" памяти.

Person

*р; Faculty* f;

/ /

базовый и производный указатели

f = new Faculty("U12345689", "Black",

"Assistant Professor");

p = f;

 

/ /

или p = (Person*) f;

delete

p;

/ /

"утечка" памяти

698Чость IV # Расширенное ыспом>$овонт€

Вданном примере оператор delete вызывает деструктор Person, который удаляет динамическую память, выделенную для имени. Деструктор Faculty не вызывается, а память, выделенная в динамически распределяемой области памяти для rank, не возвращается.

Влистинге 15.5 клиентская программа использует цикл для перехода к массиву базовых указателей и удаляет каждый объект, выделенный динамически в начале выполнения. Для каждого объекта в структуре данных выполняется деструктор Person.

for (int

j=0; j < cnt;

j++)

 

{ delete

data[j]; }

/ /

возвращение памяти, выделенной для Person,

 

 

/ /

динамически распределяемой области памяти

Для Faculty и Student их память возврандается [юлностью. Оператор delete удаляет объект независимо от его типа. Проблема связана с памятью динамически распределяемой области памяти, выделенной для объектов производного класса (см. рис. 15.10). Деструктор Person удаляет память динамически распределяемой области памяти, выделенную для имени, но не память динамически распределяе­ мой области памяти, выделенную для rank и major. Когда производный объект уничтожается через базовый указатель, вызывается только базовый деструктор.

Для решения подобной проблемы С 4-+ предлагает объявить деструктор Base виртуальным. Условно говоря, деструктор каждого производного класса также станет виртуальным. Когда оператор delete применяется к базовому указателю, деструктор класса назначения вызывается полиморфным способом (а затем, если имеется, деструктор базового класса).

struct Person {

// абстрактный класс

protected:

// данные, общие для обоих типов

char id [10];

char* name;

 

 

public:

 

 

Person(const char id[], const char nm[]);

// чистая виртуальная функция

virtual void write() const = 0;

virtual "PersonO ;

// в этом весь фокус!

} ;

 

 

struct Faculty : public Person {

// регулярный класс

private:

// только для преподавателей

char* rank;

public :

 

 

Faculty(const char id[], const charnm[],

const char r [ ] ) ;

void write () const;

/ /

регулярная виртуальная функция

"FacultyO ;

/ /

теперь также виртуальный

Это решение громоздкое. Первое, что следует помнить о виртуальных функциях, это то, что и в базовом классе, и во всех производных классах используется одно и то же имя. С деструкторами это не так, поскольку каждый из них имеет то же имя, что и имя класса. Следовательно, деструкторы нарушают правила для вир­ туальных функций. Это подобно конструкторам, и в С4-+ виртуальные конст­ рукторы отсутствуют.

Однако с "утечками" памяти опасно мириться. Именно поэтому С-+-4- поддер­ живает виртуальные деструкторы.

Глава 15 • Виртуальные функции и использование наследования

699

Множественное наследование: несколько базовых классов

в СН-+ производный класс может иметь более одного базового класса. При простом наследовании классы расположены следующим образом: на вершине иерархии находится базовый класс, ниже — производный класс.

При множественном наследовании иерархия классов может стать скорее гра­ фом, чем деревом, как в простом наследовании. Вопросы, связанные с множест­ венным наследованием, трудны для понимания.

Множественное наследование — это методика, позволяюндая облегчить созда­ ние серверной программы. В отличие от простого наследования множественное разрешает проектировщику серверного класса смешивать характеристики различ­ ных классов в одном классе.

Рассмотрим простой пример. Предположим, что класс В1 предоставляет клиен­ там открытый сервис f1(), а класс В2 — открытый сервис f2(). Это почти то же самое, что требуется клиентской программе. Дополнительно для этих двух серви­ сов клиентской программы необходим открытый сервис f3(). Одной из возмож­ ных методик для обслуживания клиента должно быть объединение характеристик классов В1 и В2 в одном классе с помощью множественного наследования.

class В1

 

 

 

 

{

public:

 

 

 

 

 

 

void

f1();

/ /

открытый сервис

f l ( )

 

. . .

};

 

/ /

оставшаяся часть

класса В1

class В2

 

 

 

 

{

public:

 

 

 

 

 

 

void

f2();

/ /

открытый сервис

f2()

 

. . .

};

 

/ /

оставшаяся часть

класса В2

При открытом наследовании от классов В1 и В2 класс Derived способен обеспе­ чить своих клиентов объединенными сервисами, предоставляемыми каждому из базовых классов (в данном случае методы fl() и f2()). Это означает, что для обес­ печения своих клиентов всеми тремя сервисами (f 1(), f2() и f3()) проектировщик класса Derived должен реализовать только одну функцию f3().

Class Derived : public В1, public В2

/ /

два базовых

класса

{ public:

/ /

f l ( ) ,

f2()

наследуются

void f3();

/ /

f3()

добавляется

к сервисам

. . . } ;

/ /

оставшаяся

часть

класса В2

Теперь клиентская программа может присвоить значения объектам Derived

иотправить им сообщения, которые они наследовали из обоих базовых классов,

исообщения, которые добавляются классом Derived.

Derived d;

/ /

присваивание значения объекту Derived

d . f1();

d . f2();

/ /

унаследованные сервисы (В1, В2)

d . f3();

 

/ /

сервисы добавляются в класс Derived

Класс Derived предоставляет клиентам возможности всех базовых классов, плюс их собственные данные и поведение.

Первоначально язык СН-+ не располагал множественным наследованием. Но Страуструп, разработчик С4-+, жаловался, что программисты "требовали множе­ ственное наследование", и теперь оно в C++ есть.

Множественное наследование хорошо подходит для настройки существующих библиотек классов, например, для добавления или переопределения членов имею­ щихся классов. Производные классы представляют комбинацию базовых классов,

Соседние файлы в предмете Программирование на C++