
- •1.2 Философские замечания
- •1.3 Процедурное программирование
- •1.4 Модульное программирование
- •1.5 Абстракция данных
- •1.6 Пределы абстракции данных
- •1.7 Объектно-ориентированное программирование
- •1.8 Концепции объектно-ориентированного программирования
- •1.8.1 Инкапсуляция
- •1.8.2 Полиморфизм
- •1.8.3 Наследование
- •1.10 Несколько полезных советов
- •2.2 Перегрузка функций
- •2.3 Перегрузка операторов
- •2.4 Наследование
- •2.5 Конструкторы и деструкторы
- •2.7 Два новых типа данных
- •Глава 3. Классы и объекты
- •3.1 Параметризованные конструкторы
- •3.2 Дружественные функции
- •3.3 Значения аргументов функции по умолчанию
- •3.3.1 Корректное использование аргументов по умолчанию
- •3.4 Взаимосвязь классов и структур
- •3.5 Связь объединений и классов
- •3.6 Анонимные объединения
- •3.7 Inline-функции
- •3.7.1 Создание inline-функций внутри класса
- •3.8 Передача объектов в функции
- •3.9 Возвращение объектов функциями
- •3.10 Присваивание объектов
- •3.11 Конструктор копирования
- •3.12 Массивы объектов
- •3.12.1 Инициализация массивов объектов
- •3.12.2 Создание инициализированных и неинициализированных массивов
- •3.13 Указатели на объекты
- •3.14 Статические члены класса
- •Глава 4. Перегрузка функций и операторов
- •4.1 Перегрузка конструкторов
- •4.2 Локализация переменных
- •4.3 Локализация создания объектов
- •4.4 Перегрузка функций и неопределенность
- •4.5 Определение адреса перегруженной функции
- •4.6 Указатель this
- •4.7 Перегрузка операторов
- •4.8 Дружественная функция-оператор
- •4.9 Ссылки
- •4.9.1 Параметры-ссылки
- •4.9.2 Передача ссылок на объекты
- •4.9.3 Возврат ссылок
- •4.9.4 Независимые ссылки
- •4.9.5 Использование ссылок для перегрузки унарных операторов
- •4.10 Перегрузка оператора []
- •4.11 Создание функций преобразования типов
- •Глава 5. Наследование, виртуальные функции и полиморфизм
- •5.1 Наследование и спецификаторы доступа
- •5.1.1 Спецификаторы доступа
- •5.1.2 Спецификатор доступа при наследовании базового класса
- •5.1.3 Дополнительная спецификация доступа при наследовании
- •5.2 Конструкторы и деструкторы производных классов
- •5.3 Множественное наследование
- •5.4 Передача параметров в базовый класс
- •5.5 Указатели и ссылки на производные типы
- •5.6 Ссылки на производные классы
- •5.7 Виртуальные функции
- •5.8 Для чего нужны виртуальные функции?
- •5.9 Чисто виртуальные функции и абстрактные типы
- •5.10 Виртуальный базовый класс
- •5.11 Раннее и позднее связывание
- •Глава 6. Подсистема динамического выделения памяти
- •6.1 Введение в обработку исключений
- •6.1.1 Перехват всех исключений
- •6.2 Работа с памятью с помощью new и delete
- •6.3 Размещение объектов
- •6.4 Перегрузка new u delete
- •7.1.1 Потоки
- •7.3 Создание собственных операторов вставки и извлечения
- •7.3.1 Создание операторов вставки
- •7.3.2 Перегрузка операторов извлечения
- •7.4 Форматирование ввода/вывода
- •7.4.1 Форматирование с помощью функций-членов класса ios
- •7.4.2 Использование манипуляторов
- •7.5 Создание собственных функций-манипуляторов
- •7.5.1 Создание манипуляторов без параметров
- •7.5.2 Создание манипуляторов с параметрами
- •7.6 Файловый ввод/вывод
- •7.6.1 Открытие и закрытие файлов
- •7.6.2 Чтение и запись в текстовые файлы
- •7.6.3 Двоичный ввод/вывод
- •7.6.4 Определение конца файла
- •7.6.5 Произвольный доступ
- •Глава 8. Ввод/вывод в массивы
- •8.1 Классы ввода/вывода в массивы
- •8.2 Создание потока вывода
- •8.3 Ввод из массива
- •8.4 Использование функций-членов класса ios
- •8.5 Потоки ввода/вывода в массивы
- •8.6 Произвольный доступ в массив
- •8.7 Использование динамических массивов
- •8.8 Манипуляторы и ввод/вывод в массив
- •8.9 Собственные операторы извлечения и вставки
- •8.10 Форматирование на основе массивов
- •Глава 9. Шаблоны и библиотека stl
- •9.1 Функции-шаблоны
- •9.2 Функции с двумя типами-шаблонами
- •9.3 Ограничения на функции-шаблоны
- •9.4 Классы-шаблоны
- •9.5 Пример с двумя типами-шаблонами
- •9.6 Обзор библиотеки stl
- •9.7 Класс vector
- •9.7 Класс string
- •9.8 Класс list
5.8 Для чего нужны виртуальные функции?
Как отмечалось в начале главы, виртуальные функции в комбинации с производными типами позволяют языку C++ поддерживать полиморфизм времени исполнения. Этот полиморфизм важен для объектно-ориентированного программирования, поскольку он позволяет переопределять функции базового класса в классах-потомках с тем, чтобы иметь их версию применительно к данному конкретному классу. Таким образом, базовый класс определяет общий интерфейс, который имеют все производные от него классы, и вместе с тем полиморфизм позволяет производным классам иметь свои собственные реализации методов. Благодаря этому полиморфизм часто определяют фразой «один интерфейс — множество методов».
Успешное применение полиморфизма связано с пониманием того, что базовые и производные классы образуют иерархию, в которой переход от базового класса к производному отвечает переходу от большей к меньшей общности. Поэтому при корректном использовании базовый класс задает все элементы, которые производные классы могут непосредственно использовать, плюс набор функций, которые производные классы должны реализовать путем их переопределения.
Наличие общего интерфейса и его множественной реализации является важным, поскольку помогает программистам разрабатывать сложные программы. Например, доступ ко всем объектам, порожденным от некоторого базового класса, осуществляется одинаковым способом, даже если реальные действия этих объектов отличаются при переходе от одного производного класса к другому. Это означает, что необходимо запомнить только один интерфейс, а не несколько. Более того, отделение интерфейса от реализации позволяет создавать библиотеки классов, поставляемые независимыми разработчиками. Если эти библиотеки реализованы корректно, то они обеспечивают общий интерфейс, и их можно использовать для вывода своих собственных специфических классов.
Чтобы понять всю мощь идеи «один интерфейс — множество методов», рассмотрим следующую короткую программу. Она создает базовый класс figure. Этот класс используется для хранения размеров различных двумерных объектов и для вычисления их площадей. Функция set_dim() является стандартной функцией-членом, поскольку ее действия являются общими для всех производных классов. Однако функция show_area() объявляется как виртуальная функция, поскольку способ вычисления площади каждого объекта является специфическим. Программа использует класс figure для вывода двух специфических классов square и triangle.
#include <iostream.h>
class figure
{
protected:
double x, y;
public:
void set_dim(double i, double j) { x = i; У = j; }
virtual void show_area() { cout << "No area computation defined for this class.\n"; }
};
class triangle: public figure
{
public:
void show_area()
{
cout << "Triangle with height " << x << " and base " << y << " has an area of " << x * 0.5 * у;
}
};
class square: public figure
{
public:
void show_area()
{
cout << "Square with dimensions “ << x << "x" << y << " has an area of " << x * у;
}
};
int main()
{
figure *p; /* создание указателя базового типа */
triangle t; /* создание объектов порожденных типов */
square s;
p = &t; p->set_dim(10.0, 5.0); p->show_area();
p = &s; p->set_dim(10.0, 5.0); p->show_area() ;
return 0;
}
Как можно видеть на основе анализа этой программы, интерфейс классов square и triangle является одинаковым, хотя оба обеспечивают свои собственные методы для вычисления площади каждой из фигур. На основе объявления класса figure можно вывести класс circle, вычисляющий площадь, ограниченную окружностью заданного радиуса. Для этого необходимо создать новый производный класс, в котором реализовано вычисление площади круга. Вся сила виртуальной функции основана на том факте, что можно легко вывести новый класс, разделяющий один и тот же общий интерфейс с другими родственными классами. В качестве примера здесь показан один из способов реализации:
class circle: public figure
{
public:
void show_area()
{
cout << "Circle with radius " << x << " has an area of " << 3.14 * x * x;
}
};
Прежде чем использовать класс circle, посмотрим внимательно на определение функции show_area(). Обратим внимание, что она использует только величину х, которая выражает радиус. Как известно, площадь круга вычисляется по формуле R2. Однако функция set_dim(), определенная в базовом классе figure, требует не одного, а двух аргументов. Поскольку класс circle не нуждается во второй величине, то как нам быть в данной ситуации?
Имеются два пути для решения этой проблемы. Первый заключается в том, чтобы вызвать set_dim(), используя в качестве второго параметра фиктивный параметр, который не будет использован. Недостатком такого подхода служит необходимость запомнить этот исключительный случай, что по существу нарушает принцип «один интерфейс — множество методов».
Лучшее решение данной проблемы связано с использованием параметра y в set_dim() со значением по умолчанию. В таком случае при вызове set_dim() для круга необходимо указать только радиус. При вызове set_dim() для треугольника или прямоугольника укажем обе величины:
#include <iostream.h>
class figure
{
protected:
double x, y;
public:
void set_dim(double i, double j=0) { x = i; У = j; }
virtual void show_area() { cout << "No area computation defined for this class.\n"; }
};
int main()
{
figure *p; /* создание указателя базового типа */
circle с;
p = &с;
p->set_dim(9.0); p->show_area();
return 0;
}
Этот пример также показывает, что при определении базового класса важно проявлять максимально возможную гибкость. Не следует налагать на программу какие-то ненужные ограничения.