15.1. Краткий обзор ООП
Ключевыми концепциями объектно-ориентированного программирования являются абстракция данных, наследование и динамическое связывание. Используя абстракцию данных, можно определить классы, отделяющие интерфейс от реализации (см. главу 7). Наследование позволяет определять классы, моделирующие отношения между подобными типами. Динамическое связывание позволяет использовать объекты этих типов, игнорируя незначительные различия между ними.
Наследование
Связанные наследованием (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).
В языке С++ динамическое связывание происходит тогда, когда обращение к виртуальной функции осуществляется при помощи ссылки (или указателя) на базовый класс.
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОК