- Виртуальные функции и полиморфизм
- Указатели на производные типы
- Ссылки на производные типы
- Виртуальные функции
- Наследование виртуальных функций
- Зачем нужны виртуальные функции
- Простое приложение виртуальных функций
- Чисто виртуальные функции и абстрактные классы
- Сравнение раннего связывания с поздним
- Полиморфизм и пуризм
Виртуальные функции и полиморфизм
Одной из трех основных граней объектно-ориентированного программирования является полиморфизм. Применительно к C++ полиморфизм представляет собой термин, используемый для описания процесса, в котором различные реализации функции могут быть доступны посредством одного и того же имени. По этой причине полиморфизм иногда характеризуется фразой "один интерфейс, много методов". Это означает, что ко всем функциям-членам общего класса можно получить доступ одним и тем же способом, несмотря на возможное различие в конкретных действиях, связанных с каждой отдельной операцией.
В C++ полиморфизм поддерживается как во время выполнения, так в период компиляции программы. Перегрузка операторов и функций — это примеры полиморфизма, относящегося ко времени компиляции. Но, несмотря на могущество механизма перегрузки операторов и функций, он не в состоянии решить все задачи, которые возникают в реальных приложениях объектно-ориентированного языка программирования. Поэтому в C++ также реализован полиморфизм периода выполнения на основе использования производных классов и виртуальных функций, которые и составляют основные темы этой главы
Начнем же мы эту главу с краткого описания указателей на производные типы, поскольку именно они обеспечивают поддержку динамического полиморфизма.
Указатели на производные типы
Указатель на базовый класс может ссылаться на любой объект, выведенный из этого базового класса.
Фундаментом для динамического полиморфизма служит указатель на базовый класс. Указатели на базовые и производные классы связаны такими отношениями, которые не свойственны указателям других типов. Как было отмечено выше в этой книге, указатель одного типа, как правило, не может указывать на объект другого типа. Однако указатели на базовые классы и объекты производных классов — исключения из этого правила. В C++ указатель на базовый класс также можно использовать для ссылки на объект любого класса, выведенного из базового. Например, предположим, что у нас есть базовый класс B_class и класс D_class, который выведен из класса B_class. В C++ любой указатель, объявленный как указатель на класс B_class, может быть также указателем на класс D_class. Следовательно, после этих объявлений
B_class *р; // указатель на объект типа B_class
B_class B_ob; // объект типа B_class
D_class D_ob; // объект типа D_class
обе следующие инструкции абсолютно законны:
р = &В_ob; // р указывает на объект типа B_class
р = &D_ob; /* р указывает на объект типа D_class, который
является объектом, выведенным из класса B_class. */
В этом примере указатель р можно использовать для доступа ко всем элементам объекта D_ob, выведенным из объекта В_ob. Однако к элементам, которые составляют специфическую "надстройку" (над базой, т.е. над базовым классом B_class) объекта D_ob, доступ с помощью указателя р получить нельзя.
В качестве более конкретного примера рассмотрим короткую программу, которая определяет базовый класс B_class и производный класс D_class. В этой программе простая иерархия классов используется для хранения имен авторов и названий их книг.
// Использование указателей на базовый класс для доступа к
объектам производных классов.
#include <iostream>
#include <cstring>
using namespace std;
class B_class {
char author[80];
public:
void put_author(char *s) { strcpy(author, s); }
void show_author() { cout << author << "\n"; }
};
class D_class : public B_class {
char title [80];
public:
void put_title(char *num) { strcpy(title, num);}
void show_title() {
cout << "Название: ";
cout << title << "\n";
}
};
int main()
{
B_class *p;
B_class B_ob;
D_class *dp;
D_class D_ob;
p = &B_ob; // адрес объекта базового класса
// Доступ к классу B_class через указатель.
p->put_author("Эмиль Золя");
// Доступ к классу D_class через "базовый" указатель.
р = &D_ob;
p->put_author("Уильям Шекспир");
// Покажем, что каждый автор относится к соответствующему
объекту.
B_ob.show_author();
D_ob.show_author();
cout << "\n";
/* Поскольку функции put_title() и show_title() не являются
частью базового класса, они недоступны через "базовый" указатель
р, и поэтому к ним нужно обращаться либо непосредственно, либо,
как показано здесь, через указатель на производный тип.
*/
dp = &D_ob;
dp->put_title("Буря");
p->show_author(); // Здесь можно использовать либо указатель
р, либо указатель dp.
dp->show_title();
return 0;
}
При выполнении эта программа отображает следующие результаты.
Эмиль Золя
Уильям Шекспир
Уильям Шекспир
Название: Буря
В этом примере указатель р определяется как указатель на класс B_class. Но он может также ссылаться на объект производного класса D_class, причем его можно использовать для доступа только к тем элементам производного класса, которые унаследованы от базового. Однако следует помнить, что через "базовый" указатель невозможно получить доступ к тем членам, которые специфичны для производного класса. Вот почему к функции show_title() обращение реализуется с помощью указателя dp, который является указателем на производный класс.
Если вам нужно с помощью указателя на базовый класс получить доступ к элементам, определенным производным классом, необходимо привести этот указатель к типу указателя на производный тип. Например, при выполнении этой строки кода действительно будет вызвана функция show_title() объекта D_ob:
((D_class *)р)->show_title();
Внешний набор круглых скобок используется для связи операции приведения типа с указателем р, а не с типом, возвращаемым функцией show_title(). Несмотря на то что в использовании такой операции формально нет ничего некорректного, этого по возможности следует избегать, поскольку подобные приемы попросту вносят в код программы путаницу. (На самом деле большинство С++-программистов считают такой стиль программирования неудачным.)
Кроме того, необходимо понимать, что хотя "базовый" указатель можно использовать для доступа к объектам любого производного типа, обратное утверждение неверно. Другими словами, используя указатель на производный класс, нельзя получить доступ к объекту базового типа.
Указатель инкрементируется и декрементируется относительно своего базового типа. Следовательно, если указатель на базовый класс используется для доступа к объекту производного типа, инкрементирование или декрементирование не заставит его ссылаться на следующий объект производного класса. Вместо этого он будет указывать на следующий объект базового класса. Таким образом, инкрементирование или декрементирование указателя на базовый класс следует расценивать как некорректную операцию, если этот указатель используется для ссылки на объект производного класса.
Тот факт, что указатель на базовый тип можно использовать для ссылки на любой объект, выведенный из базового, чрезвычайно важен и принципиален для C++. Как будет показано ниже, эта гибкость является ключевым моментом для способа реализации динамического полиморфизма в C++.
Ссылки на производные типы
Подобно указателям, ссылку на базовый класс также можно использовать для доступа к объекту производного типа. Эта возможность особенно часто применяется при передаче аргументов функциям. Параметр, который имеет тип ссылки на базовый класс, может принимать объекты базового класса, а также объекты любого другого типа, выведенного из него.
Виртуальные функции
Динамический полиморфизм возможен благодаря сочетанию двух средств: наследования и виртуальных функций. О механизме наследования вы узнали в предыдущей главе. Здесь же вы познакомитесь с виртуальными функциями.
Виртуальная функция — это функция, которая объявляется в базовом классе с использованием ключевого слова virtual и переопределяется в одном или нескольких производных классах. Таким образом, каждый производный класс может иметь собственную версию виртуальной функции. Интересно рассмотреть ситуацию, когда виртуальная функция вызывается через указатель (или ссылку) на базовый класс. В этом случае C++ определяет, какую именно версию виртуальной функции необходимо вызвать, по типу объекта, адресуемого этим указателем. Причем следует иметь в виду, что это решение принимается во время выполнения программы. Следовательно, при указании на различные объекты будут вызываться и различные версии виртуальной функции. Другими словами, именно по типу адресуемого объекта (а не по типу самого указателя) определяется, какая версия виртуальной функции будет выполнена. Таким образом, если базовый класс содержит виртуальную функцию и если из этого базового класса выведено два (или больше) других класса, то при адресации различных типов объектов через указатель на базовый класс будут выполняться и различные версии виртуальной функции. Аналогичный механизм работает и при использовании ссылки на базовый класс.
Чтобы объявить функцию виртуальной, достаточно предварить ее объявление ключевым словом virtual.
Функция объявляется виртуальной в базовом классе с помощью ключевого слова virtual. При переопределении виртуальной функции в производном классе ключевое слово virtual повторять не нужно (хотя это не будет ошибкой).
Класс, который включает виртуальную функцию, называется полиморфным классом.
Класс, который включает виртуальную функцию, называется полиморфным классом.
Этот термин также применяется к классу, который наследует базовый класс, содержащий
виртуальную функцию.
Рассмотрим следующую короткую программу, в которой демонстрируется использование виртуальных функций.
// Пример использования виртуальных функций.
#include <iostream>
using namespace std;
class base {
public:
virtual void who() {
// объявление виртуальной функции
cout << "Базовый класс.\n";
}
};
class first_d : public base {
public:
void who() {
// Переопределение функции who() для
// класса first_d.
cout << "Первый производный класс.\n";
}
};
class second_d : public base {
public:
void who() {
// Переопределение функции who() для
// класса second_d.
cout << "Второй производный класс.\n";
}
};
int main()
{
base base_obj;
base *p;
first_d first_obj;
second_d second_obj;
p = &base_obj;
p->who(); // доступ к функции who() класса base
p = &first_obj;
p->who(); // доступ к функции who() класса first_d
p = &second_obj;
p->who(); // доступ к функции who() класса second_d
return 0;
}
При выполнении эта программа генерирует такие результаты.
Базовый класс.
Первый производный класс.
Второй производный класс.
Теперь рассмотрим код этой программы подробно, чтобы понять, как она работает. В классе base функция who() объявлена виртуальной. Это означает, что ее можно переопределить в производном классе (в классе, выведенном из base). И она действительно переопределяется в обоих производных классах first_d и second_d. В функции main() объявляются четыре переменные: base_obj (объект типа base), p (указатель на объект класса base), а также два объекта first_obj и second_obj двух производных классов first_d и second_d соответственно. Затем указателю p присваивается адрес объекта base_obj, и вызывается функция who(). Поскольку функция who() объявлена виртуальной, C++ во время выполнения программы определяет, к какой именно версии функции who() здесь нужно обратиться, причем решение принимается путем анализа типа объекта, адресуемого указателем p. В данном случае р указывает на объект типа base, поэтому сначала выполняется та версия функции who(), которая объявлена в классе base. Затем указателю р присваивается адрес объекта first_obj. Вспомните, что с помощью указателя на базовый класс можно обращаться к объекту любого его производного класса. Поэтому, когда функция who() вызывается во второй раз, C++ снова выясняет тип объекта, адресуемого указателем р, и, исходя из этого типа, определяет, какую версию функции who() нужно вызвать. Поскольку р здесь указывает на объект типа first_d, то выполняется версия функции who(), определенная в классе first_d. Аналогично после присвоения р адреса объекта second_obj вызывается версия функции who(), объявленная в классе second_d.
Узелок на память. То, какая версия виртуальной функции действительно будет
вызвана, определяется во время выполнения программы. Решение основывается
исключительно на типе объекта, адресуемого указателем на базовый класс.
Виртуальную функцию можно вызывать обычным способом (не через указатель), используя оператор "точка" и задавая имя вызывающего объекта. Это означает, что в предыдущем примере было бы синтаксически корректно обратиться к функции who() с помощью следующей инструкции:
first_obj.who();
Однако при вызове виртуальной функции таким способом игнорируются ее полиморфные атрибуты. И только при обращении к виртуальной функции через указатель на базовый класс достигается динамический полиморфизм.
Если виртуальная функция переопределяется в производном классе, ее называют переопределенной.
Поначалу может показаться, что переопределение виртуальной функции в производном классе представляет собой специальную форму перегрузки функций. Но это не так. В действительности мы имеем дело с двумя принципиально разными процессами. Прежде всего, версии перегруженной функции должны отличаться друг от друга типом и/или количеством параметров, в то время как тип и количество параметров у версий переопределенной функции должны в точности совпадать. И в самом деле, прототипы виртуальной функции и ее переопределений должны быть абсолютно одинаковыми. Если прототипы будут различными, то такая функция будет попросту считаться перегруженной, и ее "виртуальная сущность" утратится. Кроме того, виртуальная функция должна быть членом класса, для которого она определяется, а не его "другом". Но в то же время виртуальная функция может быть "другом" другого класса. И еще: функциям деструкторов разрешается быть виртуальными, а функциям конструкторов — нет.
Наследование виртуальных функций
Атрибут virtual передается "по наследству".
Если функция объявляется как виртуальная, она остается такой независимо от того,
через сколько уровней производных классов она может пройти. Например, если бы класс
second_d был выведен из класса first_d, а не из класса base, как показано в следующем
примере, то функция who() по-прежнему оставалась бы виртуальной, и механизм выбора
соответствующей версии по-прежнему работал бы корректно.
// Этот класс выведен из класса first_d, а не из base.
class second_d : public first_d {
public:
void who() {
// Переопределение функции who() для класса second_d.
cout << "Второй производный класс.\n";
}
};
Если производный класс не переопределяет виртуальную функцию, то используется функция, определенная в базовом классе. Например, проверим, как поведет себя версия предыдущей программы, если в классе second_d не будет переопределена функция who().
#include <iostream>
using namespace std;
class base {
public:
virtual void who() {
cout << "Базовый класс.\n";
}
};
class first_d : public base {
public:
void who() {
cout << "Первый производный класс.\n";
}
};
class second_d : public base {
// Функция who() здесь не определена вообще.
};
int main()
{
base base_obj;
base *p;
first_d first_obj;
second_d second_obj;
p = &base_obj;
p->who(); // доступ к функции who() класса base
р = &first_obj;
p->who(); // доступ к функции who() класса first_d
р = &second_obj;
p->who(); /* Здесь выполняется обращение к функции who()
класса base, поскольку в классе second_d она не переопределена. */
return 0;
}
Теперь при выполнении этой программы на экран выводится следующее.
Базовый класс.
Первый производный класс.
Базовый класс.
Как подтверждают результаты выполнения этой программы, поскольку функция who() не переопределена классом second_d, то при ее вызове с помощью инструкции p->who() (когда член р указывает на объект second_obj) выполняется та версия функции who(), которая определена в классе base.
Следует иметь в виду, что наследуемые свойства спецификатора virtual являются иерархическими. Поэтому, если предыдущий пример изменить так, чтобы класс second_d был выведен из класса first_d, а не из класса base, то при обращении к функции who() через объект типа second_d будет вызвана та ее версия, которая объявлена в классе first_d, поскольку этот класс является "ближайшим" (по иерархическим "меркам") к классу second_d, а не функция who() из тела класса base. Эти иерархические зависимости демонстрируются на примере следующей программы.
#include <iostream>
using namespace std;
class base {
public:
virtual void who() {
cout << "Базовый класс.\n";
}
};
class first_d : public base {
public:
void who() {
cout << "Первый производный класс.\n";
}
};
// Класс second_d теперь выведен из класса first_d, а не из
класса base.
class second_d : public first_d {
// Функция who() не определена.
};
int main()
{
base base_obj;
base *p;
first_d first_obj;
second_d second_obj;
р = &base_obj;
p->who(); // доступ к функции who() класса base
р = &first_obj;
p->who(); // доступ к функции who() класса first_d
р = &second_obj;
p->who(); /* Здесь выполняется обращение к функции who()
класса first_d, поскольку в классе second_d она не переопределена.
*/
return 0;
}
Эта программа генерирует такие результаты.
Базовый класс.
Первый производный класс.
Первый производный класс.
Как видите, класс second_d теперь использует версию функции who(), которая определена в классе first_d, поскольку она ближе всех в иерархической цепочке классов.
Зачем нужны виртуальные функции
Как отмечалось в начале этой главы, виртуальные функции в сочетании с производными типами позволяют C++ поддерживать динамический полиморфизм. Полиморфизм существенен для объектно-ориентированного программирования по одной важной причине: он обеспечивает возможность некоторому обобщенному классу определять функции, которые будут использовать все производные от него классы, причем производный класс может определить собственную реализацию некоторых или всех этих функций. Иногда эта идея выражается следующим образом: базовый класс диктует общий интерфейс, который будет иметь любой объект, выведенный из этого класса, но позволяет при этом производному классу определить метод, используемый для реализации этого интерфейса. Вот почему для описания полиморфизма часто используется фраза "один интерфейс, множество методов".
Для успешного применения полиморфизма необходимо понимать, что базовый и производный классы образуют иерархию, развитие которой направлено от большей к меньшей степени обобщения (т.е. от базового класса к производному). При корректной разработке базовый класс обеспечивает все элементы, которые производный класс может использовать напрямую. Он также определяет функции, которые производный класс должен реализовать самостоятельно. Это дает производному классу гибкость в определении собственных методов, но в то же время обязывает использовать общий интерфейс. Другими словами, поскольку формат интерфейса определяется базовым классом, любой производный класс должен разделять этот общий интерфейс. Таким образом, использование виртуальных функций позволяет базовому классу определять обобщенный интерфейс, который будет использован всеми производными классами.
Теперь у вас может возникнуть вопрос: почему же так важен общий интерфейс со множеством реализаций? Ответ снова возвращает нас к основной побудительной причине возникновения объектно-ориентированного программирования: такой интерфейс позволяет программисту справляться со все возрастающей сложностью программ. Например, если корректно разработать программу, то можно быть уверенным в том, что ко всем объектам, выведенным из базового класса, можно будет получить доступ единым (общим для всех) способом, несмотря на то, что конкретные действия у одного производного класса могут отличаться от действий у другого. Это означает, что программисту придется помнить только один интерфейс, а не великое их множество. Кроме того, производный класс волен использовать любые или все функции, предоставленные базовым классом. Другими словами, разработчику производного класса не нужно заново изобретать элементы, уже имеющиеся в базовом классе. Более того, отделение интерфейса от реализации позволяет создавать библиотеки классов, написанием которых могут заниматься сторонние организации. Корректно реализованные библиотеки должны предоставлять общий интерфейс, который программист может использовать для выведения классов в соответствии со своими конкретными потребностями. Например, как библиотека базовых классов Microsoft (Microsoft Foundation Classes — MFC), так и более новая библиотека классов .NET Framework Windows Forms поддерживают Windows-программирование. Использование этих классов позволяет писать программы, которые могут унаследовать множество функций, нужных любой Windows-программе. Вам понадобится лишь добавить в нее средства, уникальные для вашего приложения. Это — большое подспорье при программировании сложных систем.
Простое приложение виртуальных функций
Чтобы вы могли получить представление о силе принципа "один интерфейс, множество методов", рассмотрим следующую короткую программу. Она создает базовый класс figure, предназначенный для хранения размеров различных двумерных объектов и вычисления их площадей. Функция set_dim() является стандартной функцией-членом, поскольку эта операция подходит для всех производных классов. Однако функция show_area() объявлена как виртуальная, так как методы вычисления площади различных объектов будут разными. Программа использует базовый класс figure для выведения двух специальных классов rectangle и triangle.
#include <iostream>
using namespace std;
class figure {
protected:
double x, y;
public:
void set_dim(double i, double j) {
x = i;
у = j;
}
virtual void show_area() {
cout << "Для этого класса выражение вычисления ";
cout << "площади не определено.\n";
}
};
class triangle : public figure {
public:
void show_area() {
cout << "Треугольник с высотой ";
cout << x << " и основанием " << у;
cout << " имеет площадь ";
cout << х * 0.5 * у << ".\n";
}
};
class rectangle : public figure {
public:
void show_area() {
cout << "Прямоугольник с размерами ";
cout << x << " x " << у;
cout << " имеет площадь ";
cout << х * у << ".\n";
}
};
int main()
{
figure *р; // создаем указатель на базовый тип
triangle t; // создаем объекты производных типов
rectangle r;
р = &t;
p->set_dim(10.0, 5.0);
p->show_area();
р = &r;
p->set_dim(10.0, 5.0);
p->show_area();
return 0;
}
Вот как выглядят результаты выполнения этой программы.
Треугольник с высотой 10 и основанием 5 имеет площадь 25.
Прямоугольник с размерами 10 х 5 имеет площадь 50.
В этой программе обратите внимание на то, что при работе с классами rectangle и triangle используется одинаковый интерфейс, несмотря на то, что в них реализованы собственные методы вычисления площади соответствующих объектов.
Как вы думаете, используя объявление класса figure, можно вывести класс circle для вычисления площади круга по заданному значению радиуса? Ответ: да. Для этого достаточно создать новый производный тип, который бы вычислял площадь круга. Могущество виртуальных функций опирается на тот факт, что программист может легко вывести новый тип, который будет разделять общий интерфейс с другими "родственными" объектами. Вот, например, как это можно сделать в нашем случае:
class circle : public figure {
public:
void show_area() {
cout << "Круг с радиусом ";
cout << x;
cout << " имеет площадь ";
cout << 3.14 * x * x;
}
};
Прежде чем опробовать класс circle в "деле", рассмотрим внимательно определение функции show_area(). Обратите внимание на то, что в нем используется только одно значение переменной x, которая должна содержать радиус круга. (Вспомните, что площадь круга вычисляется по формуле пR 2 .) Однако согласно определению функции set_dim() в классе figure ей передается два значения, а не одно. Поскольку классу circle не нужно второе значение, то что мы можем предпринять?
Есть два способа решить эту проблему. Первый (и одновременно наихудшим) состоит в том, что мы могли бы, работая с объектом класса circle, просто вызывать функцию set_dim(), передавая ей в качестве второго параметра фиктивное значение. Основной недостаток этого метода — отсутствие четкости в задании параметров и необходимость помнить о специальном исключении, которое нарушает действие принципа: "один интерфейс, множество методов".
Есть более удачный способ решения этой проблемы, который заключается в предоставлении параметру функции set_dim() значения, действующего по умолчанию. В этом случае при вызове функции set_dim() для круга нужно задавать только радиус. При вызове же функции set_dim() для треугольника или прямоугольника задаются оба значения. Ниже показана программа, в которой реализован этот метод.
#include <iostream>
using namespace std;
class figure {
protected:
double x, y;
public:
void set_dim(double i, double j=0) {
x = i;
у = j;
}
virtual void show_area() {
cout << "Для этого класса выражение вычисления ";
cout << "площади не определено.\n";
}
};
class triangle : public figure {
public:
void show_area() {
cout << "Треугольник с высотой ";
cout << x << " и основанием " << у;
cout << " имеет площадь ";
cout << х * 0.5 * у << ".\n";
}
};
class rectangle : public figure {
public:
void show_area() {
cout << "Прямоугольник с размерами ";
cout << x << " x " << у;
cout << " имеет площадь ";
cout << х * у << ".\n";
}
};
class circle : public figure {
public:
void show_area() {
cout << "Круг с радиусом ";
cout << x;
cout << " имеет площадь ";
cout << 3.14 * x * x << ".\n";
}
};
int main()
{
figure *p; // создаем указатель на базовый тип
triangle t; // создаем объекты производных типов
rectangle r;
circle с;
р = &t;
p->set_dim(10.0, 5.0);
p->show_area();
р = &r;
p->set_dim(10.0, 5.0);
p->show_area();
р = &с;
p->set_dim(9.0);
p->show_area();
return 0;
}
При выполнении эта программа генерирует такие результаты.
Треугольник с высотой 10 и основанием 5 имеет площадь 25.
Прямоугольник с размерами 10 х 5 имеет площадь 50.
Круг с радиусом 9 имеет площадь 254.34.
Важно! Несмотря на то что виртуальные функции синтаксически просты для понимания, их настоящую силу невозможно продемонстрировать на коротких примерах. Как правило, могущество полиморфизма проявляется в больших сложных системах. По мере освоения C++ вам еще не раз представится случай убедиться в их полезности.
Чисто виртуальные функции и абстрактные классы
Как вы могли убедиться, если виртуальная функция, которая не переопределена в производном классе, вызывается объектом этого производного класса, то используется версия, определенная в базовом классе. Но во многих случаях вообще нет смысла давать определение виртуальной функции в базовом классе. Например, в базовом классе figure (из предыдущего примера) определение функции show_area() — это просто заглушка. Она не вычисляет и не отображает площадь ни одного из объектов. Как вы увидите при создании собственных библиотек классов, в том, что виртуальная функция не имеет значащего определения в контексте базового класса, нет ничего необычного.
Чисто виртуальная функция — это виртуальная функция, которая не имеет определения в базовом классе.
Существует два способа обработки таких ситуаций. Первый (он показан в предыдущем примере программы) заключается в обеспечении функцией вывода предупреждающего сообщения. Возможно, такой подход и будет полезен в определенных ситуациях, но в большинстве случаев он попросту неприемлем. Например, можно представить себе виртуальные функции, без определения которых в существовании производного класса вообще нет никакого смысла. Рассмотрим класс triangle. Он абсолютно бесполезен, если в нем не определить функцию show_area(). В этом случае имеет смысл создать метод, который бы гарантировал, что производный класс действительно содержит все необходимые функции. В C++ для решения этой проблемы и предусмотрены чисто виртуальные функции.
Чисто виртуальная функция — это функция, объявленная в базовом классе, но не имеющая в нем никакого определения. Поэтому любой производный тип должен определить собственную версию этой функции, ведь у него просто нет никакой возможности использовать версию из базового класса (по причине ее отсутствия). Чтобы объявить чисто виртуальную функцию, используйте следующий общий формат:
virtual тип имя_функции(список_параметров) = 0;
Здесь под элементом тип подразумевается тип значения, возвращаемого функцией, а элемент имя_функции— ее имя. Обозначение = 0 является признаком того, что функция здесь объявляется как чисто виртуальная. Например, в следующей версии определения класса figure функция show_area() уже представлена как чисто виртуальная.
class figure {
double х, у;
public:
void set_dim(double i, double j =0) {
x = i;
У = j;
}
virtual void show_area() =0; // чисто виртуальная функция
};
Объявив функцию чисто виртуальной, программист создает условия, при которых производный класс просто вынужден иметь определение собственной ее реализации. Без этого компилятор выдаст сообщение об ошибке. Например, попытайтесь скомпилировать эту модифицированную версию программы вычисления площадей геометрических фигур, в которой из класса circle удалено определение функции show_area().
/* Эта программа не скомпилируется, поскольку в классе circle
нет переопределения функции show_area().
*/
#include <iostream>
using namespace std;
class figure {
protected:
double x, y;
public:
void set_dim(double i, double j) {
x = i;
у = j;
}
virtual void show_area() = 0; // чисто виртуальная функция
};
class triangle : public figure {
public:
void show_area() {
cout << "Треугольник с высотой ";
cout << x << " и основанием " << у;
cout << " имеет площадь ";
cout << х * 0.5 * у << ".\n";
}
};
class rectangle : public figure {
public:
void show_area() {
cout << "Прямоугольник с размерами ";
cout << x << "x" << у;
cout << " имеет площадь ";
cout << x * у << ".\n";
}
};
class circle : public figure {
// Отсутствие определения функции show_area()
// вызовет сообщение об ошибке.
};
int main()
{
figure *р; // создаем указатель на базовый тип
triangle t; // создаем объекты производных классов
rectangle r;
circle с; // Ошибка: создание этого объекта невозможно!
р = & t;
p->set_dim(10.0, 5.0);
p->show_area();
р = & r;
p->set_dim(10.0, 5.0);
p->show_area();
return 0;
}
Класс, который содержит хотя бы одну чисто виртуальную функцию, называется
абстрактным.
Если класс имеет хотя бы одну чисто виртуальную функцию, его называют абстрактным. Абстрактный класс характеризуется одной важной особенностью: у такого класса не может быть объектов. Абстрактный класс можно использовать только в качестве базового, из которого будут выводиться другие классы. Причина того, что абстрактный класс нельзя использовать для создания объектов, лежит, безусловно, в том, что его одна или несколько функций не имеют определения. Но даже если базовый класс является абстрактным, его все равно можно использовать для объявления указателей и ссылок, которые необходимы для поддержки динамического полиморфизма.
Сравнение раннего связывания с поздним
При обсуждении объектно-ориентированных языков обычно используются два термина: раннее связывание (early binding) и позднее связывание (late binding). В C++ эти термины связывают с событиями, которые происходят во время компиляции и в период выполнения программы соответственно.
При раннем связывании вызов функции подготавливается во время компиляции, а при позднем — во время выполнения программы.
Раннее связывание означает, что вся информация, необходимая для вызова функции, известна при компиляции программы. Примерами раннего связывания могут служить вызовы стандартных функций и вызовы перегруженных функций (обычных и операторных). Из принципиальных достоинств раннего связывания можно назвать эффективность: оно работает быстрее позднего и часто требует меньших затрат памяти. Его основной недостаток — отсутствие гибкости
Позднее связывание означает, что точное решение о вызове функции будет принято во время выполнения программы. Позднее связывание в C++ достигается за счет использования виртуальных функций и производных типов. Преимущество позднего связывания состоит в том, что оно обеспечивает большую степень гибкости. Его можно применять для поддержки общего интерфейса и разрешать при этом различным объектам, которые используют этот интерфейс, определять их собственные реализации. Более того, позднее связывание может помочь программисту в создании библиотек классов, характеризующихся многократным использованием и возможностью расширяться. Но к его недостаткам можно отнести, хотя и незначительное, но все же понижение скорости выполнения программ
Чему отдать предпочтение — раннему или позднему связыванию, зависит от назначения вашей программы. (В действительности в большинстве крупных программ используются оба вида связывания.) Позднее связывание (его еще называют динамическим) — это одно из самых мощных средств C++. Однако за это могущество приходится расплачиваться потерями в скорости выполнения программ. Поэтому позднее связывание лучше всего использовать только в случае, когда оно существенно улучшает структуру и управляемость программой. Как и все сильные средства, позднее связывание, конечно, стоит использовать, но не злоупотребляя им. Вызванные им потери в производительности весьма незначительны, поэтому, когда ситуация требует позднего связывания, смело берите его на вооружение.
Полиморфизм и пуризм
На протяжении всей книги (и в частности, в этой главе) мы отмечаем различия между динамическим и статическим полиморфизмом. Статический полиморфизм (полиморфизм времени компиляции) реализуется в перегрузке функций и операторов. Динамический (полиморфизм времени выполнения программы) достигается за счет виртуальных функций. Самое общее определение полиморфизма заключено во фразе "один интерфейс, множество методов", и все упомянутые выше "орудия" полиморфизма отвечают этому определению. Однако при использовании самого термина полиморфизм все же существуют некоторые разногласия.
Некоторые пуристы (в данном случае — борцы за чистоту терминологии объектноориентированного программирования) настаивают, чтобы этот термин использовался только для событий, которые происходят во время выполнения программ. Они утверждают, что полиморфизм поддерживается только виртуальными функциями. Частично эта точка зрения основывается на том факте, что самыми первыми полиморфическими языками программирования были интерпретаторы (для них характерно то, что все события относятся ко времени выполнения программы). Появление транслируемых полиморфических языков программирования расширило концепцию полиморфизма. Однако все еще не утихают заявления о том, что термин полиморфизм должен применяться исключительно к событиям периода выполнения. Большинство С++-программистов не согласны с этой точкой зрения и считают, что этот термин применим к обоим видам средств. Поэтому вы не должны удивляться, если кто-то в один прекрасный день станет спорить с вами на предмет использования этого термина!