С помощью виртуальных функций можно преодолеть трудности, возникающие
при использовании поля типа. В базовом классе описываются функции,
которые могут переопределяться в любом производном классе. Транслятор
и загрузчик обеспечат правильное соответствие между объектами и
применяемыми к ним функциями:
class employee {
char* name;
short department;
// ...
employee* next;
static employee* list;
public:
employee(char* n, int d);
// ...
static void print_list();
virtual void print() const;
};
Служебное слово virtual (виртуальная) показывает, что функция print()
может иметь разные версии в разных производных классах, а выбор нужной
версии при вызове print() - это задача транслятора.
Тип функции указывается в базовом классе и не может быть
переопределен в производном классе. Определение виртуальной функции
должно даваться для того класса, в котором она была впервые
описана (если только она не является чисто виртуальной функцией,
см. $$6.3). Например:
void employee::print() const
{
cout << name << '\t' << department << '\n';
// ...
}
Мы видим, что виртуальную функцию можно использовать, даже если
нет производных классов от ее класса. В производном же классе
не обязательно переопределять виртуальную функцию, если она там
не нужна. При построении производного класса надо определять
только те функции, которые в нем действительно нужны:
class manager : public employee {
employee* group;
short level;
// ...
public:
manager(char* n, int d);
// ...
void print() const;
};
Место функции print_employee() заняли функции-члены print(), и она
стала не нужна. Список служащих строит конструктор employee ($$6.2.2).
Напечатать его можно так:
void employee::print_list()
{
for ( employee* p = list; p; p=p->next) p->print();
}
Данные о каждом служащем будут печататься в соответствии с типом
записи о нем. Поэтому программа
int main()
{
employee e("J.Brown",1234);
manager m("J.Smith",2,1234);
employee::print_list();
}
напечатает
J.Smith 1234
level 2
J.Brown 1234
Обратите внимание, что функция печати будет работать даже в том случае,
если функция employee_list() была написана и оттранслирована еще до того,
как был задуман конкретный производный класс manager! Очевидно, что для
правильной работы виртуальной функции нужно в каждом объекте класса
employee хранить некоторую служебную информацию о типе. Как правило,
реализации в качестве такой информации используют просто указатель.
Этот указатель хранится только для объектов класса с виртуальными
функциями, но не для объектов всех классов, и даже для не для всех
объектов производных классов. Дополнительная память отводится только
для классов, в которых описаны виртуальные функции. Заметим, что
при использовании поля типа, для него все равно нужна дополнительная
память.
Если в вызове функции явно указана операция разрешения
области видимости ::, например, в вызове manager::print(),
то механизм вызова виртуальной функции не действует. Иначе подобный
вызов привел бы к бесконечной рекурсии. Уточнение имени функции
дает еще один положительный эффект: если виртуальная функция
является подстановкой (в этом нет ничего необычного), то в вызове
с операцией :: происходит подстановка тела функции. Это
эффективный способ вызова, который можно применять
в важных случаях, когда одна виртуальная функция
обращается к другой с одним и тем же объектом. Пример такого
случая - вызов функции manager::print(). Поскольку тип объекта
явно задается в самом вызове manager::print(), нет нужды определять
его в динамике для функции employee::print(), которая и будет
вызываться.
6.3 Абстрактные классы
Многие классы сходны с классом employee тем, что в них можно дать
разумное определение виртуальным функциям. Однако, есть и другие
классы. Некоторые, например, класс shape, представляют
абстрактное понятие (фигура), для которого нельзя создать объекты.
Класс shape приобретает смысл только как базовый класс в некотором
производном классе. Причиной является то, что невозможно дать
осмысленное определение виртуальных функций класса shape:
class shape {
// ...
public:
virtual void rotate(int) { error("shape::rotate"); }
virtual void draw() { error("shape::draw"): }
// нельзя ни вращать, ни рисовать абстрактную фигуру
// ...
};
Создание объекта типа shape (абстрактной фигуры) законная, хотя
совершенно бессмысленная операция:
shape s; // бессмыслица: ``фигура вообще''
Она бессмысленна потому, что любая операция с объектом s приведет
к ошибке.
Лучше виртуальные функции класса shape описать как чисто
виртуальные. Сделать виртуальную функцию чисто виртуальной можно,
добавив инициализатор = 0:
class shape {
// ...
public:
virtual void rotate(int) = 0; // чисто виртуальная функция
virtual void draw() = 0; // чисто виртуальная функция
};
Класс, в котором есть виртуальные функции, называется абстрактным.
Объекты такого класса создать нельзя:
shape s; // ошибка: переменная абстрактного класса shape
Абстрактный класс можно использовать только в качестве базового
для другого класса:
class circle : public shape {
int radius;
public:
void rotate(int) { } // нормально:
// переопределение shape::rotate
void draw(); // нормально:
// переопределение shape::draw
circle(point p, int r);
};
Если чисто виртуальная функция не определяется в производном
классе, то она и остается таковой, а значит производный класс тоже
является абстрактным. При таком подходе можно реализовывать
классы поэтапно:
class X {
public:
virtual void f() = 0;
virtual void g() = 0;
};
X b; // ошибка: описание объекта абстрактного класса X
class Y : public X {
void f(); // переопределение X::f
};
Y b; // ошибка: описание объекта абстрактного класса Y
class Z : public Y {
void g(); // переопределение X::g
};
Z c; // нормально
Абстрактные классы нужны для задания интерфейса без уточнения
каких-либо конкретных деталей реализации. Например, в операционной
системе детали реализации драйвера устройства можно скрыть
таким абстрактным классом:
class character_device {
public:
virtual int open() = 0;
virtual int close(const char*) = 0;
virtual int read(const char*, int) =0;
virtual int write(const char*, int) = 0;
virtual int ioctl(int ...) = 0;
// ...
};
Настоящие драйверы будут определяться как производные от класса
character_device.
После введения абстрактного класса у нас есть все основные
средства для того, чтобы написать законченную программу.
Достарыңызбен бөлісу: |