
- •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.9 Чисто виртуальные функции и абстрактные типы
Когда виртуальная функция не переопределена в производном классе, то при вызове ее в объекте производного класса вызывается версия из базового класса. Однако во многих случаях невозможно ввести содержательное определение виртуальной функции в базовом классе. Например, при объявлении базового класса figure в предыдущем примере определение функции show_area() не несет никакого содержания. Она не вычисляет и не выводит на экран площадь объекта какого-либо типа. Имеется два способа действий в подобной ситуации. Первый, подобно предыдущему примеру, заключается в выводе какого-нибудь предупреждающего сообщения. Хотя такой подход полезен в некоторых ситуациях, он не является универсальным. Могут быть такие виртуальные функции, которые обязательно должны быть определены в производных классах, без чего эти производные классы не будут иметь никакого значения. Например, класс triangle бесполезен, если не определена функция show_area(). В таких случаях необходим метод, гарантирующий, что производные классы действительно определят все необходимые функции. Язык C++ предлагает в качестве решения этой проблемы чисто виртуальные функции.
Чисто виртуальная функция (pure virtual function) является функцией, которая объявляется в базовом классе, но не имеет в нем определения. Поскольку она не имеет определения, то есть тела в этом базовом классе, то всякий производный класс обязан иметь свою собственную версию определения. Для объявления чисто виртуальной функции используется следующая общая форма:
virtual тип имя_функции(список параметров) = 0;
Здесь тип обозначает тип возвращаемого значения, а имя_функции является именем функции. Например, следующая версия функции show_area() класса figure является чисто виртуальной функцией.
class figure
{
double x, у;
public:
void set_dim(double i, double j=0) { x = i; у = j; }
virtual void show_area() = 0; // чисто виртуальная
};
При введении чисто виртуальной функции в производном классе обязательно необходимо определить свою собственную реализацию этой функции. Если класс не будет содержать определения этой функции, то компилятор выдаст ошибку.
Если какой-либо класс имеет хотя бы одну чисто виртуальную функцию, то такой класс называется абстрактным (abstract). Важной особенностью абстрактных классов является то, что не существует ни одного объекта данного класса. Вместо этого абстрактный класс служит в качестве базового для других производных классов. Причина, по которой абстрактный класс не может быть использован для объявления объекта, заключается в том, что одна или несколько его функций-членов не имеют определения. Тем не менее, даже если базовый класс является абстрактным, все равно можно объявлять указатели или ссылки на него, с помощью которых затем поддерживается полиморфизм времени выполнения.
5.10 Виртуальный базовый класс
В C++ ключевое слово virtual используется для объявления виртуальных функций, которые будут переопределены в производных классах. Однако ключевое слово virtual также имеет другое использование, позволяющее определить виртуальный базовый класс. Для того чтобы понять, что собой представляет виртуальный базовый класс и почему ключевое слово virtual имеет второе значение, давайте начнем с короткой некорректной программы:
#include <iostream.h>
class base { public: int i; };
class d1 : public base { public: int j };
class d2 : public base { public: int k; };
/* d3 наследует как d1, так и d2. Это означает, что в d3 имеется две копии base! */
class d3 : public d1, public d2
{
public:
int m;
};
int main()
{
d3 d;
d.i = 10; // неопределенность, какое i???
d.j = 20; d.k = 30; d.m = 40;
// также неопределенность, какое i???
cout << d.i << “ “ << d.j << " " << d.k << " " << d.m;
return 0;
}
Как показывает комментарий в данной программе, оба класса d1 и d2 наследуют класс base. Однако класс d3 наследует оба класса dl и d2. Это означает, что в классе d3 представлены две копии класса base. Поэтому в выражении типа
d.i = 20;
не ясно, какое именно i имеется в виду — относящееся к d1 или же относящееся к d2? Поскольку имеется две копии класса base в объекте d, то там имеются также две переменные d.i. Как видно, инструкция является двусмысленной в силу описанного наследования.
Имеется два способа исправить программу. Первый заключается в использовании оператора области видимости для переменной i с дальнейшим выбором вручную одного из i. Например, следующая версия программы компилируется и исполняется так, как это необходимо:
#include <iostream.h>
class base { public: int i; };
class d1 : public base { public: int j; };
class d2 : public base { public: int k; };
class d3 : public d1, public d2
{
public:
int m;
};
int main()
{
d3 d;
d.d2::i = 10; // область видимости определена, используется i для d2
d.j = 20; d.k = 30; d.m = 40;
// область видимости определена, используется i для d2
cout << d.d2::i << " “ << d.j << " " << d.k << “ “ << d.m;
return 0;
}
Как можно видеть, используя оператор области видимости ::, в программе вручную выбирается версия i класса d2. Тем не менее, данное решение порождает более глубокие вопросы: что если требуется только одна копия класса base? Имеется ли какой-либо способ предотвратить включение двух копий в класс d3? Как можно было догадаться, ответ на этот вопрос положительный. Решение достигается путем использования виртуального базового класса.
Когда два или более класса порождаются от одного общего базового класса, можно предотвратить включение нескольких копий базового класса в объект-потомок этих классов путем объявления базового класса виртуальным при его наследовании. Например, ниже приведена другая версия предыдущей программы, в которой d3 содержит только одну копию класса base:
#include <iostream.h>
class base { public: int i; };
class d1 : virtual public base { public: int j; };
class d2 : virtual public base ( public: int k; };
/* d3 наследует как d1 так и d2. Тем не менее в d3 имеется только одна копия base! */
class d3 : public d1, public d2 { public: int m; };
int main()
{
d3 d;
d.i = 10; // неопределенности больше нет
d.j = 20; d.k = 30; d.m = 40;
cout << d.i << " " << d.j << " " << d.k << " " << d.m;
return 0;
}
Как видно, ключевое слово virtual предшествует спецификации наследуемого класса. Теперь оба класса d1 и d2 наследуют класс base как виртуальный. Любое множественное наследование с их участием порождает теперь включение только одной копии класса base. Поэтому в классе d3 имеется только одна копия класса base, и, следовательно, d.i = 10 теперь не является двусмысленным выражением.
Необходимо иметь в виду еще одно обстоятельство: хотя оба класса d1 и d2 используют класс base как виртуальный, тем не менее, всякий объект класса d1 или d2 будет содержать в себе base. Например, следующий код абсолютно корректен:
// определение объекта класса d1
d1 myclass;
myclass.i = 100;
Обычные и виртуальные базовые классы отличаются друг от друга только тогда, когда какой-либо объект наследует базовый класс более одного раза. При использовании виртуального базового класса только одна копия базового класса содержится в объекте. В случае использования обычного базового класса в объекте могут содержаться несколько копий.