
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf680 Часть
Такое поведение возможно только общедоступным.
struct Faculty : public Person {
private: |
|
|
// только для преподавателей |
|
char* rank; |
|
|
||
public: |
|
char icl[], const char nm[], const char г []); |
||
Faculty(const |
||||
void write () |
const; |
// отображение записи |
||
-FacultyO; |
} |
; |
// возвращение памяти динамически |
|
struct Student |
: public Person { |
// распределяемой области памяти |
||
// общедоступное наследование |
||||
private: |
|
|
||
char* major; |
|
|
// только для студентов |
|
public: |
|
|
||
Student(const char id[], const |
char nm[], const char m[]); |
|||
void write () const; |
// отображение записи |
|||
-StudentO; |
} |
; |
||
|
|
|
// возвращение памяти динамически |
|
|
|
|
// распределяемой области памяти |
Производные классы Faculty и Student наследуют все элементы данных своего
базового класса Person. Они определяют свои собственные данные, конкретные для каждого вида Person (rank или major).
Конструкторы производных классов Faculty и Student принимают параметры, необходимые для инициализации всех своих полей, независимо от того, опреде лены ли они впроизводном классе или унаследованы из базового класса Person. Задача конструктора производного класса состоитв передачеданных конструктору
базового класса в списке инициализации. Как можно установить из интерфейса конструктора Person, к ним относятся спецификации вида создаваемых объектов.
Faculty или Student.Для многих программистов это означает,что список пара метров конструкторов производных классовдолжен включатьданные для инициа лизации базовой части (три параметра) и данные для инициализации производной части (major для Student, rank для Faculty).
Faculty(const char id[], const char nm[], Kind k, const char r[])
: Person(id,nm, k) |
//список инициализации |
{ rank = new char[strlen(r)+l]; |
|
if (rank == 0) {cout « |
"Out of memory\n"; exit(O); } |
strcpy(rank,r); } |
|
Это типичный пример делегирования обязанности в клиентскую часть производ ного класса. Клиентская программа (функция readO) создает объекты Faculty примерно так:
person = new Faculty(id,name,FACULTY,buf); } |
// объектом является Faculty |
H o это обман, клиентская программа уже объявила о создании объекта Faculty.
Зачем делать бесполезную работу по передаче параметров, объявляя это как
Faculty? Подобная обязанность должна быть передана объекту Faculty. О н
знает, что он является Faculty, и должен сказать об этом своей части Person, не втягивая клиента read() в цикл совместной работы.
Faculty(const char id[], const char nm[], |
const char r[]) |
: Person (id, nm,FACULTY) |
// именно в этом суть ООП |
{ rank = newchar[strlen(r)+l]; |
|
if (rank == 0) {cout « "Out ofmemory\n"; exit(O); } strcpy(rank,r); }
682 |
Чость IV # Роситреииов ыспоАШоваяте C+*t- |
Итак, это не может быть ни указатель Faculty, ни указатель Student. Какой же должен быть тип указателя, способный ссылаться на объекты различных классов? Вспомните, если различные классы не связаны наследованием, то отсутствует указатель, который может указывать на объекты этих классов и выполнять любую работу. Кроме того, если разные классы связываются наследованием, то указатель базового класса может указывать на объекты любого производного класса — А, В или иного. "Старший брат" может указывать на все, что захочет, поскольку класс назначения находится в пределах иерархической структуры наследования.
Следовательно, это должен быть указатель Person. Внутри функции read() объекты разных производных классов создаются и подключаются к указателю базового класса.
void read |
(ifstream& f |
Person*& |
person) |
/ / |
считывание одной записи |
||
{ char kind[8], id[10] |
name[80], |
buf[80]; |
/ / |
распознавание входного типа |
|||
f.getline(kind,80); |
|
|
|||||
f . getline(id,10); |
|
|
/ / |
считывание идентификатора |
|||
f.getlineCname,80); |
|
|
/ / |
считывание имени |
|||
f.getline(buf,80); |
|
|
/ / |
rank или major? |
|||
if (strcmp(kind, "FACULTY") -= 0) |
|
|
|
|
|||
{ person = new Faculty(id,name,buf); } |
/ / |
объект |
- |
Faculty |
|||
else if (strcmp(kind, |
"STUDENT") =- 0) |
|
|
|
|
||
{ person = new Student(id,name, buf); } |
/ / |
объект |
- |
Student |
|||
else |
|
|
|
|
|
|
|
{ cout « |
" Corrupted |
data: unknown type\n" |
; exit(O); |
} |
} |
Функция readO вызывается из main() так же, как и в предыдущем варианте. Отличие состоит в том, что компоненты массива data[] типа Person* теперь ука зывают на объекты других производных классов (Faculty или Student).
int |
mainO |
endl « endl; |
|
|
{ cout « |
// массив указателей |
|||
Person* data[20]; int cnt = 0; |
||||
ifstream from("univ.dat"); |
// файл входных данных |
|||
if (!from) {cout « " Cannot open file\n"; |
return 0; } |
|||
while (! from.eofO) |
|
|
||
{ |
read(from, data[cnt]); |
/ / |
считывание до eof |
|
|
cnt++; |
} |
|
|
. |
. . } |
|
/ / |
остальная часть main() |
Однако базовый указатель не может вызывать операции, которые определены в производных классах. До тех пор, пока базовый указатель ссылается на произ водный объект, всегда суш,ествует способ сообщить компилятору то, что нам из вестно: базовый указатель указывает на производный объект. Для этого следует воспользоваться приведением к производному классу.
Этот процесс принятия решения хотелось бы инкапсулировать в функцию, на пример write(). Подобные функции для выбора решения должны проектировать ся для каждой операции, которые выполняются по-разному для различных типов подобных объектов. Трудно выбрать тип параметра для этой функции. Общая структура данной функции:
void write |
( . . . ? ? р) |
/ / |
отображение записи |
|||||
{ switch (p.getKindO) { |
/ / |
получение типа объекта |
||||||
case |
Person::FACULTY: |
|
|
|
|
|
||
|
. |
. .; |
break; |
/ / |
выполнить |
как |
для |
Faculty |
case |
Person::STUDENT: |
|
|
|
|
|
||
|
. |
. .; |
break; } } |
/ / |
выполнить |
как |
для |
Student |
Глава 15 • Виртуальные функции и использование наследования |
683 |
Тип параметра этой функции должен воспринимать два типа фактических аргументов — объекты Faculty и Student. Если тип объекта — Faculty, то эта функция будет вызывать только Faculty: :write(). Если тип параметра Student, функция будет вызывать только Student: :write().
Именно здесь следует воспользоваться материалом из предыдущего раздела о преобразованиях классов. Можно воспользоваться параметром типа Person, поскольку и объекты Faculty, и объекты Student могут копироваться в объект Person. (Вспомним, что объект производного класса располагает достаточными данными для инициализации объекта базового класса.)
void write |
(Person р) |
/ / |
отображение записи |
||||
{ switch (p.getKindO) { |
/ / |
получение типа объекта |
|||||
case Person::FACULTY: |
/ / |
выполнить |
как |
для |
Faculty |
||
. |
. .; |
break; |
|||||
case Person::STUDENT: |
|
|
|
|
|
||
. |
. .; |
break; } } |
/ / |
выполнить |
как |
для |
Student |
Недостатком такого решения является то, что он передает параметр по значению, а это не слишком хорошая практика, когда объекты управляют своей памятью динамически. Кроме того, тело функции остается на уровне объекта Person и от сутствует способ для преобразования базового объекта обратно в объект произ водного класса. Исходные данные отбрасываются и их невозможно восстановить. Даже если добавить конструктор производного класса, который преобразует ба зовый объект в объект производного класса (см. листинг 15.2), этого будет недо статочно. Конструктор сможет лишь установить значения по умолчанию полей для производного класса. Нам, однако, требуются исходные значения служебного положения преподавателя или специализации студента.
Вы не можете использовать значения базового объекта в качестве параметра функции, но ничто не мешает вам применить базовый указатель как параметр функции. Указатель производного класса (который выполняет все операции про изводного класса) можно преобразовать в указатель базового класса — это безопасное преобразование, поэтому приведение типов не требуется. Данные не отбрасываются и возможно обратное преобразование в указатель производного класса. (Однако это преобразование не является безопасным и, следовательно, требуется приведение типа.)
void write (Person* р) |
|
/ / |
отображение записи |
||||
{ switch (p->getKind()) { |
|
/ / |
получение типа объекта |
||||
case Person::FACULTY: |
|
|
|
|
|
|
|
. . .; |
break; |
|
/ / |
выполнить |
как |
для |
Faculty |
case Person::STUDENT: |
|
|
|
|
|
|
|
. . .; |
break; |
} |
/ / |
выполнить |
как |
для |
Student |
Во время такого преобразования нельзя выполнять операции, определенные для производного класса. Слабый базовый указатель может лишь достичь функций, которые определены в базовом классе. Но преимущество этого решения состоит в том, что этот базовый указатель все еш,е указывает на объект производного класса. В операторе выбора функция writeO выясняет, указывает ли фактиче ский параметр на объект Faculty или на объект Student. Остается только вызвать либо метод writeO из класса Faculty, либо метод write() из класса Student.
void write (const Person* p) |
/ / |
отображение записи |
|||
{ switch (p->getKind()) { |
/ / |
получение типа объекта |
|||
case Person::FACULTY: |
|
|
|
|
|
p->write(); |
break; |
/ / |
выполнить |
как для |
Faculty |
case Person::STUDENT: |
|
|
|
|
|
p->write(); |
break; |
/ / |
выполнить |
как для |
Student |
684 |
Часть IV « Расширенное использование С+"^ |
|
|
|||||
|
|
|
Указатель р является указателем базового класса, поэтому он может добраться |
|||||
|
только до методов базового класса. Следовательно, вызовы write() в обеих ветвях |
|||||||
|
оператора выбора либо будут достигать write() из базового класса (если он есть |
|||||||
|
в классе Person), либо приведут к синтаксической ошибке (если в классе Person |
|||||||
|
метод writeO отсутствует). |
|
|
|
|
|||
|
|
|
Функция writeO уже знает, на объект какого типа указывает ее параметр- |
|||||
|
указатель. Компилятор знает только, что это указатель на класс Person. Значит, |
|||||||
|
функция write() должна сообщить компилятору о том, что она знает. Компилятор |
|||||||
|
должен выполнить приведение базового указателя либо к классу Faculty (первый |
|||||||
|
оператор выбора), либо к классу Student (второй оператор выбора). |
|||||||
|
void write (const Person* p) |
|
/ / |
отображение записи |
||||
|
{ |
switch |
(p->getKind()) { |
|
/ / |
получение типа объекта |
||
|
|
|
case |
Person::FACULTY: |
|
|
|
|
|
|
|
|
((Faculty*)p)->write(); |
break; |
/ / |
выполнить |
как для Faculty |
|
|
|
case Person: .-STUDENT: |
|
/ / |
выполнить |
как для Student |
|
|
|
|
|
((Student*)p)->write(); |
break; |
|||
|
} |
} |
|
|
|
|
|
|
|
Это приведение выглядит внушительным и устрашаюидим. Но оно осуществляет |
|||||||
|
приведение указателя р класса Person* в указатель типа Faculty* или в указатель |
|||||||
|
типа Student*. Скобки используются потому, что операнд выбора в виде стрелки |
|||||||
|
имеет более высокий приоритет, чем операнды приведения. Если опустить скобки |
|||||||
|
и использовать, например, (Faculty*)p->write(), компилятор решит, что нужно |
|||||||
|
преобразовать значение, возвраш.аемое в результате вызова writeO, а не указа |
|||||||
|
тель р. Сохраните здесь эти скобки. |
|
|
|
|
|||
|
|
|
Эта функция writeO будет вызываться в цикле, получая в качестве фактиче |
|||||
|
ских аргументов указатели Person, которые указывают либо на Faculty, либо на |
|||||||
|
Student объекты. |
|
|
|
|
|||
|
for |
(int i=0; i < cnt; i++) |
|
|
|
|
||
|
|
{ |
write(data[i]); } |
/ / |
отображение данных |
|
Полная программа представлена в листинге 15.4.
Листинг 15.4. Обработка неоднородного списка — объектно-ориентированный подход
#include |
<iostream> |
|
|
||
#include |
<fstream> |
|
|
||
using |
namespace std; |
|
|
||
struct |
Person { |
|
|
||
public: |
|
|
|
|
|
|
enum Kind { FACULTY, STUDENT } ; |
|
|
||
protected: |
|
|
|
||
|
Kind |
kind; |
/ / |
FACULTY или STUDENT |
|
|
char |
id[10] ; |
/ / |
данные общие для обоих типов |
|
|
char* |
name; |
/ / |
переменная длина |
public:
Person(const char id[], const char nm[], Kind type)
strcpy(Person::id,id); |
// копирование идентификатора |
name = new char[strlen(nm)+l]; |
// выделение памяти для имени |
if (name ==0) {cout « "Out of memory\n" |
exit(O); } |
strcpy(name,nm); |
// копирование имени |
kind = type; } |
// помните его тип |
Глава 15 • Виртуальные функции и использование носдвАОвания |
| 685 |
|
Kind getKindO const |
|
|
{ return kind; } |
/ / доступ к типу Person |
|
"PersonO |
// возврат памяти динамически распределяемой области памяти |
|
{ delete [] name; } |
||
} ; |
|
|
struct Faculty : public Person { |
|
|
private: |
// только для преподавателей |
|
char* rank; |
|
|
public: |
|
|
Faculty(const char id[], const char nm[], const char r[]) |
|
|
: Person(id,nm,FACULTY) |
// список инициализации |
|
{rank = new char[strlen(r)+l];
if (rank ==0) {cout « "Out of memory\n"; exit(O); } strcpy(rank,r); }
void write () const |
|
|
// отображение записи |
|
{ cout « |
" id: " « id « endl; |
// вывод на печать идентификатора, имени |
||
cout « |
" name: " « |
name « |
endl; |
// только для преподавателей |
cout « |
" rank: " « |
rank «endl «endl; } |
||
"FacultyO |
} |
// возврат памяти динамически распределяемой области памяти |
||
{ delete [] rank; |
||||
} |
|
|
|
|
struct Student : public Person { |
|
|
||
private: |
|
|
|
// для студента |
char* major; |
|
|
public:
Student(const char id[], const char nm[], const char m[])
: Person id,nm,STUDENT) |
// инициализация списка |
|||
{ major = new char[strlen(m)+1]; |
|
|||
if (major =- 0) {cout « |
"Out ofmemory\n"; exit(O); } |
|||
strcpy(major,m); } |
|
|
||
void write () const |
|
|
// отображение записи |
|
{ cout « |
" id: " « id « e n d l ; |
// вывод на печать идентификатора, имени |
||
cout « |
" name: " « |
name « |
endl; |
// только для студента |
cout « |
" major: " « |
major «endl «endl; } |
||
~Student() |
|
// возврат памяти динамически распределяемой области памяти |
||
{ delete [] major; } |
||||
} ; |
|
|
|
|
void read (if stream& f, Person*& person) |
// считывание одной записи |
|||
{ char kind[8], id[10], |
name[80], buf[80]; |
// распознавание входного типа |
||
f .getline(kind,80); |
|
|
||
f.getline(id,10); |
|
|
// считывание идентификатора |
|
f.getline(name,80); |
|
|
// считывание имени |
|
f.getline(buf,80); |
|
|
// rank или major? |
|
if (strcmp(kind, "FACULTY") == 0) |
// объект - Faculty |
|||
{ person = new Faculty(id,name,buf); } |
||||
else if (strcmp(kind, |
"STUDENT") == 0) |
// объект - Student |
||
{ person = new Student(id,name,buf); } |
||||
else |
" Corrupted data: unknown type\n" exit(O); } |
|||
{ cout « |
Глава 15 • Виртуальные функции и использование наследования |
687 |
сгенерированная компилятором, проанализирует тип объекта, на который указы вает базовый указатель р, определит, к какому типу должен относиться вызывае мый метод, и вызовет метод writeO из этого типа. В зависимости от объекта, обозначенного указателем, будет вызван либо метод Faculty, либо метод Student.
Чтобы метод работал, необходимо выполнить несколько ограничений. Вир туальная функция, принадлежащая производному классу, должна вызываться только через базовый указатель или базовую ссылку. Связывания во время выполнения не произойдет, если сообщение посылается базовому объекту или объекту произ водного класса. В каждом случае используется алгоритм для статического связы вания. Сообщение вызывается из того класса, каким является тип объекта.
Например, значение x.writeO зависит от типа, к которому принадлежит объ ект X. Этот тип определяется в процессе компиляции, а не во время выполнения.
Виртуальная функция не может быть статической. Она не может вызваться из оператора области действия класса, но должна вызываться через базовый указа тель (ссылку), которая ссылается на объект производного класса.
Режим наследования для порождения должен быть общедоступным и не может быть защищенным или закрытым. Неявное приведение допускается только для общедоступных порождений.
Функция с этим же именем определяется как виртуальная в базовом классе иерархии наследования. В каждом производном классе должна быть реализована функция с таким же именем, что и базовая виртуальная функция. При переопре делении функции в производном классе она должна совпадать по имени, сигнатуре и типу возвращаемого значения с виртуальной функцией базового класса.
Если имя функции в производном классе другое, это не является ошибкой. Однако такая функция не может вызываться с использованием связывания во время исполнения. Динамическое связывание использует вызов той же самой функции, но с другой ее интерпретацией.
Если в производном классе другая сигнатура, то производный метод скрывает базовый метод и разрушает механизм виртуальной функции. Если производные классы определяют пустую функцию write() без параметров, а базовый класс обозначает пустую функцию write(int), то при использовании динамического связывания отсутствует способ для вызова функций производного класса. В этом случае p->write() вызовет функцию, которая принадлежит к классу указателя р. Если она существует, вы сможете ее вызвать. В противном случае имейте в виду, что допущена синтаксическая ошибка.
Если тип результата виртуальных функций в производных классах другой, это синтаксическая ошибка, даже если сигнатура функций одна и та же.
Зарезервированное слово virtual появляется только в спецификации базового класса. В определении функции базового класса, а также в спецификации произ водного класса его не требуется повторять.
Если иерархия включает классы более чем двух уровней, виртуальные функции можно определять на любом уровне иерархии. Совсем не обязательно реализовать определенную функцию, например, на самом верхнем или на более низком уровне иерархии наследования. Она может наследоваться косвенно.
Если все ограничения удовлетворены, не надо определять поле kind в базовом классе и метод, который возвращает значение поля kind. Для преобразования программы в листинге 15.3 в программу с виртуальными функциями требуется определить функцию в классе Person. У функции должен быть тип результата void и параметры должны отсутствовать.
struct Person { |
|
|
protected: |
|
|
char |
id[10]; |
/ / Kind отсутствует |
char* |
name; |
|
688 |
Часть IV » Расширенное использование С+4- |
|
|||
|
public: |
|
|
|
// Kind отсутствует |
|
Person(const char ici[], const char nm[]); |
||||
|
virtual void write () const; |
|
// const - часть сигнатуры |
||
|
"PersonO; } ; |
|
|
|
|
|
В результате производным классам не требуется передавать базовому классу |
||||
|
информацию поля kind. |
|
|
||
|
struct Faculty : public Person { |
|
|
||
|
private: |
|
|
|
// только для преподавателей |
|
char* rank; |
|
|
||
|
public; |
|
|
|
|
|
Faculty(const char id[], const char nm[], const char r[]) |
||||
|
|
: Person (id, nm) |
|
// FACULTY отсутствует |
|
|
{ rank = new char[strlen(r)+l]; |
|
|||
|
|
if (rank ==0) {cout « |
"Out ofmemory\n"; exit(O); } |
||
|
|
strcpy(rank,r); } |
|
// теперь является виртуальной |
|
|
void write () const |
id « endl; |
|
||
|
{ cout « |
" id: " « |
// вывод на печать идентификатора, имени |
||
|
cout « |
" name: " « |
name « endl; |
} // только для преподавателей |
|
|
cout « |
" rank: " << rank <<endl «endl; |
|||
|
"FacultyO |
|
|
// возврат памяти динамически |
|
|
{ delete [] rank; } } ; |
|
|||
|
|
|
|
|
// распределяемой области памяти |
Что более важно, в клиентской программе не требуется проверять тип объекта. В листинге 15.5приведена программа из листинга 15.4,использующая виртуаль ную функцию writeO для исключения из клиентской программы анализа подтипа. Вывод программы такой же, что идля предыдущего варианта (см.рис. 15.11).
Листинг 15.5. Обработка неоднородного списка сиспользованием виртуальных функций
#inclucle <iostream> #inclucle <fstream> using namespace std;
struct Person { |
|
|
protected: |
|
// данные общие для обоих типов |
char id[10]; |
|
|
char* name; |
|
// переменной длины |
public: |
|
// типа Kind |
Person(const char id[], const char nm[]) |
||
{ strcpy(Person::id,id); |
|
// копирование идентификатора |
name = new char[strlen(nm)+l]; |
// выделение пространства для объекта |
|
if (name == 0) {cout « |
"Out ofmemory\n"; exit(O); } |
|
strcpy(name,nm); |
|
// копирование имени |
} |
|
|
virtual void writeO const |
// работы немного |
|
{ } |
|
|
v~Person() |
// возврат памяти динамически распределяемой области памяти |
|
{ delete[] name; } |
|
// для объекта Person |
} ; |
|
|
struct Faculty : public Person { |
|
|
private: |
|
// только для преподавателей |
char* rank; |
|