Язык программирования C Пятое издание
![]()
|
Упражнение 15.1. Что такое виртуальный член класса? Упражнение 15.2. Чем спецификатор доступа protected отличается от private? Page 745/1103 Упражнение 15.3. Определите собственные версии класса Quote и функции print_total(). 15.2.2. Определение производного класса Производный класс должен определить, от какого класса (классов) он происходит. Для этого используется находящийся после двоеточия список наследования класса (class derivation list), представляющий собой разделяемый запятыми список имен определенных ранее классов. Каждому имени базового класса может предшествовать необязательный спецификатор доступа: public, protected или private. Производный класс должен объявить каждую унаследованную функцию-член, которую он намеревается переопределить. Поэтому класс Bulk_quote должен включать функцию-член net_price(): class Bulk_quote : public Quote { // Bulk_quote происходит от Quote Bulk_quote() = default; Bulk_quote(const std::string&, double, std::size_t, double); // переопределить базовую версию и реализовать политику // скидок при оптовых закупках double net_price(std::size_t) const override; private: std::size_t min_qty = 0; // минимальная покупка для скидки double discount = 0.0; // доля применяемой скидки }; Класс Bulk_quote унаследовал функцию isbn(), а также переменные-члены bookNo и price из своего базового класса Quote. Он определяет собственную версию функции net_price() и имеет две дополнительные переменные-члена — min_qty и discount, которые определяют минимальное количество экземпляров и скидку, применяемую при его покупке. Более подробная информация об используемых в списке наследования спецификаторах доступа приведена в разделе 15.5, а пока достаточно знать, что спецификатор доступа определяет, разрешено ли пользователям производного класса знать, что он унаследован от базового класса. Page 746/1103 При открытом наследовании открытые члены базового класса становятся частью интерфейса производного. Кроме того, объект открытого производного типа можно привязать к указателю или ссылке на базовый тип. Поскольку в списке наследования использован спецификатор public, интерфейс класса Bulk_quote неявно содержит функцию isbn(), объект класса Bulk_quote можно использовать там, где ожидается указатель или ссылка на объект класса Quote. Большинство классов непосредственно происходит только от одного базового класса. Эта форма наследования, известная как "одиночное наследование", и является темой данной главы. В разделе 18.3 будут описаны классы, у которых в списке наследования больше одного базового класса. Виртуальные функции в производном классе Производные классы часто, но не всегда, переопределяют унаследованные виртуальные функции. Если производный класс не переопределяет виртуальную функцию своего базового класса, то, подобно любому другому члену, производный класс наследует версию, определенную в его базовом классе. Производный класс может применять к переопределяемым функциям ключевое слово virtual, но не обязательно. По причинам, рассматриваемым в разделе 15.3, новый стандарт позволяет производному классу явно указывать, что функция-член предназначена для переопределения унаследованной виртуальной функции. Для этого применяется спецификатор override в определении после списка параметров, либо после ключевого слова const, либо квалификатора ссылки, если член класса константен (см. раздел 7.1.2), или ссылки на функцию (см. раздел 13.6.3). Объекты производного класса и преобразование производного в базовый Объект производного класса состоит из несколькими частей: нестатических членов, определенных в самом производном классе, а также объекта, состоящего из нестатических членов каждого его базового класса, от которых он происходит. Таким образом, объект класса Bulk_quote будет содержать четыре части данных: переменные-члены bookNo и price, унаследованные от класса Quote, и переменные-члены min_qty и discount, определенные в классе Bulk_quote. Хотя стандарт не определяет расположение в памяти производных объектов, объект Bulk_quote можно считать состоящим из двух частей (рис. 15.1). Рис. 15.1. Концептуальная структура объекта класса Bulk_quote Базовые и производные части объекта вовсе не обязательно будут располагаться рядом. Рис. 15.1 — это концептуальное, не физическое представление работы класса. Поскольку производная часть объекта соответствует его базовому классу (классам), объект производного типа можно использовать так, как будто это объект его базового класса (классов). В частности, ссылку или указатель на базовый класс можно связать с частью базового класса производного объекта. Quote item; // объект базового типа Bulk_quote bulk; // объект производного типа Quote *p = &item; // p указывает на объект Quote Page 747/1103 p = &bulk; // p указывает на часть bulk объекта Quote Quote &r = bulk; // r связан с частью bulk объекта Quote Это преобразование обычно называют преобразованием производного в базовый (derived-to-base conversion). Подобно любому другому преобразованию, компилятор применяет его неявно (см. раздел 4.11). Факт неявного преобразования производного в базовый означает возможность использования объекта производного типа или ссылки на него там, где нужна ссылка на базовый тип. Точно так же можно использовать указатель на производный тип там, где требуется указатель на базовый тип. Факт наличия в объекте производного класса частей объектов его базовых классов является основой работы наследования. Конструкторы производного класса Хотя объект производного класса содержит члены, унаследованные им от базового, он не может инициализировать их непосредственно. Как и любой другой код, создающий объект базового класса, производный класс должен использовать конструктор базового класса для инициализации своей части базового класса. Каждый класс сам контролирует инициализацию своих членов. Часть базового класса объекта, наряду с переменными-членами производного класса, инициализируется на этапе инициализации конструктора (см. раздел 7.5.1). Аналогично инициализации переменных-членов, для передачи аргументов конструктору базового класса конструктор производного класса использует свой список инициализации. Рассмотрим конструктор Bulk_quote() с четырьмя параметрами: Bulk_quote(const std::string& book, double p, std::size_t qty, double disc) : Quote(book, p), min_qty(qty), discount(disc) { } // как прежде }; Для инициализации переменных-членов конструктору класса Quote передаются его первые два параметра (представляющие ISBN и цену). Этот конструктор инициализирует базовую часть класса Bulk_quote (т.е. переменные-члены bookNo и price). Когда (пустое) тело конструктора класса Quote закончит работу, часть базового класса создаваемого объекта будет инициализирована. Затем инициализируются прямые переменные-члены min_qty и discount. И наконец, выполняется (пустое) тело конструктора класса Bulk_quote. Подобно переменной-члену, если не определено иное, базовая часть производного объекта инициализируется по умолчанию. Чтобы использовать другой конструктор базового класса, следует предоставить список инициализации конструктора, используя имя базового класса, сопровождаемое, как обычно, заключенным в скобки списком аргументов. Эти аргументы Page 748/1103 используются для выбора конкретного конструктора базового класса для инициализации базовой части объекта производного класса. Сначала инициализируются члены базового класса, а затем члены производного класса в порядке их объявления. Использование членов базового класса из производного Производный класс может обращаться к открытым и защищенным членам своего базового класса: // если приобретено достаточное количество экземпляров, // использовать цену со скидкой double Bulk_quote::net_price(size_t cnt) const { if (cnt >= min_qty) return cnt * (1 - discount) * price; else return cnt * price; } Эта функция вычисляет цену со скидкой: если приобретенное количество экземпляров превышает значение переменной min_qty, к цене (price) применяется скидка (discount). Более подробная информация об областях видимости приведена в разделе 15.6, а пока достаточно знать, что область видимости производного класса вкладывается в область видимости его базового класса. В результате нет никакого различия между тем, как член производного класса использует члены, определенные в его собственном классе (например, min_qty и discount), и как он использует члены, определенные в его базовом классе (например, price). Ключевая концепция. Соблюдение интерфейса базового класса Важно понимать, что каждый класс определяет собственный интерфейс. Для взаимодействия с объектом типа класса следует использовать интерфейс этого класса, даже если он — часть базового класса в объекте производного. В результате конструкторы производного класса не могут непосредственно инициализировать члены своего базового класса. Тело конструктора производного класса может присваивать значения его открытых или защищенных членов базового класса. Хотя он может присвоить значения этим членам, обычно это не применяется . Как и любой другой пользователь базового класса, производный класс должен соблюдать интерфейс своего базового класса, используя для инициализации своих унаследованных членов его конструктор.Наследование и статические члены Если в базовом классе определен статический (static) член (см. раздел 7.6), для всей иерархии существует только один его экземпляр. Независимо от количества классов, производных от базового класса, существовать будет только один экземпляр каждого статического члена. Page 749/1103 class Base { public: static void statmem(); }; class Derived : public Base { void f(const Derived&); }; Статические члены подчиняются обычным правилам управления доступом: если член класса объявлен в базовом классе закрытым, производные классы не получат к нему доступа. Когда статический член класса доступен, к нему можно обращаться как из базового, так и из производного класса: void Derived::f(const Derived &derived_obj) { Base::statmem(); // ok: statmem() определена в Base Derived::statmem(); // ok: Derived наследует statmem() // ok: объект производного класса применим для доступа к // статическому члену базового derived_obj.statmem(); // доступ в объекте класса Derived statmem(); // доступ в объекте этого класса } Объявления производных классов Производный класс объявляется как любой другой класс (см. раздел 7.3.3). Объявление содержит имя класса, но не включает его список наследования: class Bulk_quote : public Quote; // ошибка: здесь не может быть списка // наследования class Bulk_quote; // Page 750/1103 ok: правильный способ объявления // производного класса Задача объявления в том, чтобы сообщить о существовании имени и какую сущность он обозначает: класс, функцию или переменную. Список наследования и все другие подробности определения должны присутствовать в теле класса. Классы, используемые как базовые Класс должен быть определен, а не только объявлен, прежде чем его можно будет использовать как базовый класс: class Quote; // объявлен, но не определен // ошибка: класс Quote следует определить class Bulk_quote : public Quote { ... }; Причина этого ограничения очевидна: каждый производный класс содержит и может использовать члены, унаследованные от его базового класса. Чтобы использовать эти члены, производный класс должен знать, что они из себя представляют. Одним из следствий этого правила является невозможность наследования класса от себя самого. Базовый класс сам может быть производным классом: class Base { /* ... */ }; class D1: public Base { /* ... */ }; class D2: public D1 { /*...*/ }; В этой иерархии класс Base является прямым базовым (direct base class) для класса D1 и косвенным базовым (indirect base class) для класса D2. Прямой базовый класс указывают в списке наследования. Косвенный базовый класс наследуется производным через его прямой базовый класс. Каждый класс наследует все члены своего прямого базового класса. Большинство производных классов наследует члены своего прямого базового класса. Члены прямого базового класса включают унаследованные из его базового класса и т.д. по цепи наследования. Фактически самый последний производный объект содержит часть его прямого базового класса и каждого из его косвенных базовых классов. Предотвращение наследования Иногда определяют класс, от которого не следует получать другие производные классы. Либо может быть определен класс, который не предусматривается как подходящий на роль базового. По новому стандарту можно воспрепятствовать использованию класса как базового, расположив за его именем спецификатор final: class NoDerived final { /* */ }; // Page 751/1103 класс NoDerived // не может быть базовым class Base { /* */ }; // класс Last финальный; нельзя наследовать класс Last class Last final : Base { /* */ }; // класс Last не может быть базовым class Bad : NoDerived { /* */ }; // ошибка: класс NoDerived финальный class Bad2 : Last { /* */ }; // ошибка: класс Last финальный Упражнения раздела 15.2.2 Упражнение 15.4. Какие из следующих объявлений (если они есть) некорректны? Объясните, почему. class Base { ... }; (a) class Derived : public Derived { ... }; (b) class Derived : private Base { ... }; (c) class Derived : public Base; Упражнение 15.5. Напишите собственную версию класса Bulk_quote. Упражнение 15.6. Проверьте свою функцию print_total() из упражнения раздела 15.2.1, передав ей объекты класса Quote и Bulk_quote. Упражнение 15.7. Определите класс, реализующий ограниченную стратегию скидок, которая применяет скидку только к покупкам до заданного предела. Если количество экземпляров превышает этот предел, к остальным применяется обычная цена. 15.2.3. Преобразования и наследование Понимание того, как происходит преобразование типов между базовыми и производными классами, очень важно для освоения принципов объектно-ориентированного программирования на языке С++. Обычно ссылку или указатель можно связать только с тем объектом, тип которого либо совпадает с типом ссылки или указателя (см. раздел 2.3.1 и раздел 2.3.2), либо допускает константное преобразование в него (см. раздел 4.11.2). Классы, связанные наследованием, являются важным исключением: с объектом производного типа можно связать указатель или ссылку на тип базового класса. Например, ссылку Quote& можно использовать для Page 752/1103 обращения к объекту Bulk_quote, а адрес объекта Bulk_quote можно сохранить в указателе Quote*. У факта возможности привязки ссылки (или указателя) на тип базового класса к объекту производного есть очень важное следствие: при использовании ссылки (или указателя) на тип базового класса неизвестен фактический тип объекта, с которым он связан. Этот объект может быть как объектом базового класса, так и производного. Подобно встроенным указателям, классы интеллектуальных указателей (см. раздел 12.1) обеспечивают преобразование производного в базовый, позволяя хранить указатель на объект производного типа в интеллектуальном указателе на базовый. Статический и динамический типы При использовании связанных наследованием типов нередко приходится отличать статический тип (static type) переменной или выражения от динамического типа (dynamic type) объекта, который представляет выражение. Статический тип выражения всегда известен на момент компиляции — это тип, с которым переменная объявляется или возвращает выражение. Динамический тип — это тип объекта в области памяти, которую представляет переменная или выражение. Динамический тип не может быть известен во время выполнения. Рассмотрим пример, когда функция print_total() вызывает функцию net_price() (см. раздел 15.1): double ret = item.net_price(n); Известно, что статическим типом параметра item является Quote&. Динамический тип зависит от типа аргумента, с которым связан параметр item. Этот тип не может быть известен, пока не произойдет вызов во время выполнения. Если функции print_total() передать объект класса Bulk_quote, то статический тип параметра item будет отличаться от его динамического типа. Как уже упоминалось, статический тип параметра item — это Quote&, но в данном случае динамическим типом будет Bulk_quote. Динамический тип выражения, которое не является ни ссылкой, ни указателем, всегда будет совпадать со статическим типом этого выражения. Например, переменная типа Quote всегда будет объектом класса Quote; нельзя сделать ничего, что изменит тип объекта, которому соответствует эта переменная. Крайне важно понять, что статический тип указателя или ссылки на базовый класс может отличаться от его динамического типа. Не существует неявного преобразования из базового типа в производный… Преобразование из производного в базовый существует благодаря тому, что каждый объект производного класса содержит часть базового класса, с которой и могут быть связаны указатели или ссылки на тип базового класса. Для объектов базового класса подобной гарантии нет. Объект базового класса может существовать либо как независимый объект, либо как часть объекта производного класса. У объекта базового класса, не являющегося частью объекта производного, есть только те члены, которые определены базовым классом; в нем не определены члены производного класса. Поскольку объект базового класса может быть, а может и не быть частью производного объекта, нет никаких автоматических преобразований из базового класса в класс (классы), производный от него: Page 753/1103 Quote base; Bulk_quote* bulkP = &base; // ошибка: нельзя преобразовать базовый в // производный Bulk_quote& bulkRef = base; // ошибка: нельзя преобразовать базовый в // производный Если бы эти присвоения были допустимы, то можно было бы попытаться использовать указатель bulkP или ссылку bulkRef для доступа к членам, которые не существуют в объекте base. Немного удивительно то, что невозможно преобразование из базового в производный, даже когда с объектом производного класса связан указатель или ссылка на базовый класс: Bulk_quote bulk; Quote * itemP = &bulk; // ok: динамический тип Bulk quote Bulk_quote *bulkP = itemP; // ошибка: нельзя преобразовать базовый в // производный У компилятора нет никакого способа узнать (во время компиляции), что некое преобразование окажется безопасно во время выполнения. Компилятор рассматривает только статические типы указателей или ссылок, определяя допустимость преобразования. Если у базового класса есть одна или несколько виртуальных функций, для запроса преобразования, проверяемого во время выполнения, можно использовать оператор dynamic_cast (рассматриваемый в разделе 19.2.1). В качестве альтернативы, когда известно, что преобразование из базового в производный безопасно, для обхода запрета компилятора можно использовать оператор static_cast (см. раздел 4.11.3). …и нет преобразований между объектами Автоматическое преобразование производного класса в базовый применимо только для ссылок и указателей. Нет способа преобразования типа производного класса в тип базового класса. Однако нередко вполне возможно преобразовать объект производного класса в тип базового класса. Но такие преобразования не всегда ведут себя так, как хотелось бы. Помните, что при инициализации или присвоении объекта типа класса фактически происходит вызов функции. При инициализации происходит вызов конструктора (см. раздел 13.1.1 и раздел 13.6.2), а при присвоении — вызов оператора присвоения (см. раздел 13.1.2 и раздел 13.6.2). У этих функций-членов обычно есть параметр, являющийся ссылкой на |