Введение в язык Си++

         

Виртуальные функции


Предположим, что мы пишем программу для изображения фигур на экране. Общие атрибуты фигуры представлены классом shape, а специальные атрибуты - специальными классами:

class shape { point center; color col; //... public: void move(point to) { center=to; draw(); } point where() { return center; } virual void draw(); virtual void rotate(int); //... };

Функции, которые можно определить не зная точно определенной фигуры (например, move и where, то есть, "передвинуть" и "где"), можно описать как обычно. Остальные функции описываются как virual, то есть такие, которые должны определяться в производном классе. Например:

class circle: public shape { int radius; public: void draw(); void rotatte(int i) {} //... };

Теперь, если shape_vec - вектор фигур, то можно написать:

for (int i = 0; i

*1

Программирующим на C

*2 одномерный массив. Это принятый термин (например, вектора прерываний), и мы сочли, что стандартный перевод его как "массив" затуманит изложение. (прим. перев.)

*3 англ. dereference - получить значение объекта, на который указывает данный указатель. (прим. перев.)

*4 в оригинале expression-oriented (expression - выразительность и выражение). (прим. перев.)

[] [] []


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

struct employee { employee* next; char* name; short department; // ... virtual void print(); };

Ключевое слово virtual указывает, что могут быть различные варианты функции print() для разных производных классов, и что поиск среди них подходящей для каждого вызова print() является задачей компилятора. Тип функции описывается в базовом классе и не может переписываться в производном классе. Виртуальная функция должна быть определена для класса, в котором она описана впервые. Например:



void employee::print() { cout name department

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

struct manager : employee { employee* group; short level; // ... void print(); };

void manager::print() { employee::print(); cout

Функция print_employee() теперь не нужна, поскольку ее место заняли функции члены print(), и теперь со списком служащих можно работать так:

void f(employee* ll) { for (; ll; ll=ll-next) ll-print(); }

Каждый служащий будет печататься в соответствии с его типом. Например:

main() { employee e; e.name = "Дж.Браун"; e.department = 1234; e.next = 0; manager m; m.name = "Дж.Смит"; e.department = 1234; m.level = 2; m.next = e f(m); }

выдаст

Дж.Смит 1234 уровень 2 Дж.Браун 1234

Заметьте, что это будет работать даже в том случае, если f() была написана и откомпилирована еще до того, как производный класс manager был задуман! Очевидно, при реализации этого в каждом объекте класса employee сохраняется некоторая информация о типе. Занимаемого для этого пространства (в текущей реализации) как раз хватает для хранения указателя. Это пространство занимается только в объектах классов с виртуальными функциями, а не во всех объектах классов и даже не во всех объектах производных классов. Вы платите эту пошлину только за те классы, для которых описали виртуальные функции.

Вызов функции с помощью операции разрешения области видимости ::, как это делается в manager::print(), гарантирует, что механизм виртуальных функций применяться не будет. Иначе manager::print() подвергалось бы бесконечной рекурсии. Применение уточненного имени имеет еще один эффект, который может оказаться полезным: если описанная как virtual функция описана еще и как inline (в чем ничего необычного нет), то там, где в вызове применяется :: может применяться inline-подстановка. Это дает программисту эффективный способ справляться с теми важными специальными случаями, когда одна виртуальная функция вызывает другую для того же объекта. Поскольку тип объекта был определен при вызове первой виртуальной функции, обычно его не надо снова динамически определять другом вызове для того же объекта.




Если базовый класс base содержит (виртуальную) virtual (#8.1) функцию vf, а производный класс derived также содержит функцию vf, то вызов vf для объекта класса derived вызывает derived::vf. Например:

struct base { virtual void vf (); void f (); };

struct derived : public base { void vf (); void f (); };

derived d; base* bp = d

bp-vf (); bp-f ();

Вызовы вызывают, соответственно, derived::vf и base::f для объекта класса derived, именованного d. Так что интерпретация вызова виртуальной функции зависит от типа объекта, для которого она вызвана, в то время как интерпретация вызова невиртуальной функции зависит только от типа указателя, обозначающего объект.

Из этого следует, что тип объектов классов с виртуальными функциями и объектов классов, выведенных из таких классов, могут быть определены во время выполнения.

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



Содержание раздела