Язык программирования C++. Пятое издание - Стенли Липпман
Шрифт:
Интервал:
Закладка:
Связанные наследованием (inheritance) классы формируют иерархию. В корне иерархии обычно находится базовый класс (base class), от которого прямо или косвенно происходят другие классы. Эти унаследованные классы известны как производные классы (derived class). В базовом классе определяют те члены, которые будут общими у всех типов в иерархии. В производных классах определяются те члены, которые будут специфическими для данного производного класса.
Для моделирования разных стратегий расценок определим класс Quote, который будет базовым классом нашей иерархии. Объект класса Quote представит книгу без скидок. От него унаследуем второй класс, Bulk_quote, представляющий книги, которые могут быть проданы со скидкой за опт.
У этих классов будут две функции-члена.
• Функция isbn() будет возвращать ISBN. Она никак не зависит от специфических особенностей производных классов; поэтому будет определена только в классе Quote.
• Функция net_price(size_t) будет возвращать цену при покупке определенного количества экземпляров книги. Эта операция специфична для типа; классы Quote и Bulk_quote определят собственные версии этой функции.
В языке С++ базовый класс отличает функции, специфические для типа, от тех, которые предполагается наследовать в производных классах без изменений. Те функции, которые производные классы должны определять самостоятельно, базовый класс определяет как virtual. Исходя из этого, класс Quote можно первоначально написать так:
class Quote {
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
};
Производный класс должен указать класс (классы), который он намеревается унаследовать. Для этого используется находящийся после двоеточия список наследования класса (class derivation list), представляющий собой разделяемый запятыми список базовых классов, у каждого из которых может быть необязательный спецификатор доступа:
class Bulk_quote : public Quote { // Bulk_quote наследуется от Quote
public:
double net_price(std::size_t) const override;
};
Поскольку класс Bulk_quote использует в списке наследования спецификатор public, его объекты можно использовать так, как будто они являются объектами класса Quote.
Тело производного класса должно включать объявления всех виртуальных функций (virtual function), которые он намеревается определить для себя. Производный класс может включить в эти функции ключевое слово virtual, но не обязательно. По причинам, рассматриваемым в разделе 15.3, новый стандарт позволяет производному классу явно указать, что функция-член предназначена для переопределения (override) унаследованной виртуальной функции. Для этого после списка ее параметров располагают ключевое слово override.
Динамическое связываниеДинамическое связывание (dynamic binding) позволяет взаимозаменяемо использовать тот же код для обработки объектов как типа Quote, так и Bulk_quote. Например, следующая функция выводит общую стоимость при покупке заданного количества экземпляров указанной книги:
// вычислить и отобразить цену за указанное количество экземпляров
// с применением всех скидок
double print_total(ostream &os,
const Quote &item, size_t n) {
// в зависимости от типа, связанного с параметром item объекта,
// вызвать функцию Quote::net_price() или Bulk_quote::net_price()
double ret = item.net_price(n);
os << "ISBN: " << item.isbn() // вызов Quote::isbn()
<< " # sold: " << n << " total due: " << ret << endl;
return ret;
}
Эта функция довольно проста — она выводит результаты вызова функций isbn() и net_price() для своего параметра и возвращает значение, вычисленное вызовом функции net_price().
Однако у этой функции есть два интересных момента: по описанным в разделе 15.2.3 причинам, поскольку параметр item является ссылкой на тип Quote, эту функцию можно вызвать как для объекта класса Quote, так и для объекта класса Bulk quote. По причинам, описанным в разделе 15.2.1, поскольку функция net_price() является виртуальной, а функция print_total() вызывает ее через ссылку, выполняемая версия функции net_price() будет зависеть от типа объекта, переданного функции print_total():
// basic имеет тип Quote; bulk имеет тип Bulk_quote
print_total(cout, basic, 20); // вызов версии net_price() класса Quote
print_total(cout, bulk, 20); // вызов версии net_price()
// класса Bulk_quote
Первый вызов передает функции print_total() объект класса Quote. Когда функция print_total() вызовет функцию net_price(), будет выполнена ее версия из класса Quote. В следующем вызове, где аргумент имеет тип Bulk_quote, будет выполнена версия функции net_price() из класса Bulk_quote (применяющая скидку). Поскольку решение о выполняемой версии зависит от типа аргумента, оно может быть принято до времени выполнения. Поэтому динамическое связывание иногда называют привязкой во время выполнения (run-time binding).
В языке С++ динамическое связывание происходит тогда, когда обращение к виртуальной функции осуществляется при помощи ссылки (или указателя) на базовый класс.
15.2. Определение базовых и производных классов
Во многих, но не всех случаях базовые и производные классы определяются, как и другие классы, но отличия все же имеются. В этом разделе рассматриваются основные возможности, используемые при определении классов, связанных наследованием.
15.2.1. Определение базового класса
Для начала завершим определение класса Quote:
class Quote {
public:
Quote() = default; // = default см. раздел 7.1.4
Quote(const std::string &book, double sales_price):
bookNo(book), price(sales_price) { }
std::string isbn() const { return bookNo; }
// возвращает общую цену за определенное количество проданных
// экземпляров, а различные системы скидок определяют и
// применяют производные классы
virtual double net_price(std::size_t n) const
{ return n * price; }
virtual ~Quote() = default; // динамическое связывание для
// деструктора
private:
std::string bookNo; // идентификатор экземпляра
protected:
double price = 0.0; // стандартная цена (без скидки)
};
Новым в этом классе являются использование ключевого слова virtual в функции net_price() и деструкторе, а также спецификатора доступа protected. Виртуальные деструкторы рассматриваются в разделе 15.7.1, а пока следует заметить, что корневой класс иерархии наследования почти всегда определяет виртуальный деструктор.
Базовые классы обычно должны определять виртуальный деструктор. Виртуальные деструкторы необходимы, даже если они не делают ничего.
Функции-члены и наследованиеПроизводные классы наследуют члены своих базовых классов. Но производный класс должен быть в состоянии обеспечить собственное определение таких зависимых от типа операций, как net_price(). В таких случаях производный класс должен переопределить унаследованное от базового класса определение, обеспечив собственное определение.
В языке С++ базовый класс должен отличать функции, которые предполагается переопределить в производных классах, от тех, которые производные классы, вероятно, наследуют без изменений. Функции, переопределение которых предполагается в производных классах, базовый класс определяет как virtual. Когда вызов виртуальной функции происходит через указатель или ссылку, он будет привязан динамически. В зависимости от типа объекта, с которым связана ссылка или указатель, будет выполнена версия базового или одного из его производных классов.
Базовый класс определяет, что функция-член должна быть привязана динамически, предваряя ее объявление ключевым словом virtual. Любая нестатическая функция-член (см. раздел 7.6), кроме конструктора, может быть виртуальной. Ключевое слово virtual присутствует только в объявлении в классе и не может использоваться в определении функции вне тела класса. Функция, объявленная виртуальной в базовом классе, неявно является виртуальной и в производных классах. Более подробная информация о виртуальных функциях приведена в разделе 15.3.