Главная страница

Язык программирования C Пятое издание


Скачать 1.85 Mb.
НазваниеЯзык программирования C Пятое издание
Дата15.07.2019
Размер1.85 Mb.
Формат файлаpdf
Имя файла620354-www.libfox.ru.pdf
ТипДокументы
#84130
страница41 из 54
1   ...   37   38   39   40   41   42   43   44   ...   54

Page 754/1103
константную версию типа класса.
Поскольку эти функции-члены получают ссылки, преобразование производного класса в базовый позволяет передавать функциям копирования и перемещения базового класса объект производного класса. Эти функции не являются виртуальными. При передаче объекта производного класса конструктору базового выполняется конструктор, определенный в базовом классе. Этому конструктору известно только о членах самого базового класса. Точно так же, если объект производного класса присваивается объекту базового, выполняется оператор присвоения, определенный в базовом классе. Этот оператор также знает только о членах самого базового класса.
Например, классы приложения книжного магазина используют синтезируемые версии операторов копирования и присвоения (см. раздел 13.1.1 и раздел 13.1.2). Более подробная информация об управлении копированием и наследовании приведена в разделе 15.7.2, а пока достаточно знать, что синтезируемые версии осуществляют почленное копирование или присвоение переменных-членов класса тем же способом, что и у любого другого класса:
Bulk_quote bulk; // объект производного типа
Quote item(bulk); // используется конструктор
//
Quote::Quote(const Quote&) item = bulk; // вызов Quote::operator=(const Quote&)
При создании объекта item выполняется конструктор копий класса Quote. Этот конструктор знает только о переменных-членах bookNo и price. Он копирует эти члены из части Quote объекта bulk и игнорирует члены, являющиеся частью Bulk_quote объекта bulk. Аналогично при присвоении объекта bulk объекту item ему присваивается только часть Quote объекта bulk.
Поскольку часть Bulk_quote игнорируется, говорят, что она была отсечена (sliced down).
При инициализации объекта базового типа (или присвоении) объектом производного типа копируется, перемещается или присваивается только часть базового класса производного объекта. Производная часть объекта игнорируется. Ключевая концепция. Преобразования между типами, связанными наследованием
Есть три правила преобразования связанных наследованием классов, о которых следует помнить.
• Преобразование из производного класса в базовый применимо только к указателю или ссылке.
• Нет неявного преобразования из типа базового класса в тип производного.
Page 755/1103

• При преобразовании производного в базовый член класса может быть недоступен из за спецификатора управления доступом. Доступность рассматривается в разделе 15.5.
Хотя автоматическое преобразование применимо только к указателям и ссылкам,
большинство классов в иерархии наследования (явно или неявно) определяют функции-члены управления копированием (см. главу 13). В результате зачастую вполне можно копировать, перемещать и присваивать объекты производного типа объектам базового. Однако копирование, перемещение или присвоение объекта производного типа объекту базового копирует, перемещает или присваивает только члены части базового класса объекта.Упражнения раздела 15.2.3
Упражнение 15.8. Определите статический и динамический типы.
Упражнение 15.9. Когда может возникнуть отличие статического типа выражения от его динамического типа? Приведите три примера, в которых статический и динамический типы отличаются.
Упражнение 15.10. Возвращаясь к обсуждению в разделе 8.1, объясните, как работает программа из раздела 8.2.1, где функции read() класса Sales_data передавался объект ifstream.
15.3. Виртуальные функции
Как уже упоминалось, в языке С++ динамическое связывание происходит при вызове виртуальной функции-члена через ссылку или указатель на тип базового класса (см. раздел
15.1). Поскольку до времени выполнения неизвестно, какая версия функции вызывается,
виртуальные функции следует определять всегда . Обычно, если функция не используется, ее определение предоставлять необязательно (см. раздел 6.1.2). Однако следует определить каждую виртуальную функцию,
независимо от того, будет ли она использована, поскольку у компилятора нет никакого способа определить, используется ли виртуальная функция.Вызовы виртуальной функции могут быть распознаны во время выполнения
Когда виртуальная функция вызывается через ссылку или указатель, компилятор создает код распознавания во время выполнения (decide at run time) вызываемой функции. Вызывается та функция, которая соответствует динамическому типу объекта, связанного с этим указателем или ссылкой.
В качестве примера рассмотрим функцию print_total() из раздела 15.1. Она вызывает функцию net_price() своего параметра item типа Quote&. Поскольку параметр item — это ссылка и функция net_price() является виртуальной, какая именно из ее версий будет вызвана во время выполнения, зависит от фактического (динамического) типа аргумента, связанного с параметром item:
Quote base("0-201-82470-1", 50); print_total(cout, base, 10); // вызов Quote::net_price()
Page 756/1103

Bulk_quote derived("0-201-82470-1", 50, 5, .19); print_total(cout, derived, 10); // вызов Bulk_quote::net_price()
В первом вызове параметр item связан с объектом типа Quote. В результате, когда функция print_total() вызовет функцию net_price(), выполнится ее версия, определенная в классе
Quote. Во втором вызове параметр item связан с объектом класса Bulk_quote. В этом вызове функция print_total() вызывает версию функции net_price() класса Bulk_quote.
Крайне важно понимать, что динамическое связывание происходит только при вызове виртуальной функции через указатель или ссылку. base = derived; // копирует часть Quote производного в базовый base.net_price(20); // вызов Quote::net_price()
Когда происходит вызов виртуальной функции в выражении с обычным типом (не ссылкой и не указателем), такой вызов привязывается во время компиляции. Например, когда происходит вызов функции net_price() объекта base, нет никаких вопросов о выполняемой версии. Можно изменить значение (т.е. содержимое) объекта, который представляет base, но нет никакого способа изменить тип этого объекта. Следовательно, этот вызов распознается во время компиляции как версия Quote::net_price(). Ключевая концепция. Полиморфизм в языке С++
Одной из ключевых концепций ООП является полиморфизм (polymorphism). В переводе с греческого языка "полиморфизм" означает множество форм. Связанные наследованием типы считаются полиморфными, поскольку вполне можно использовать многообразие форм этих типов, игнорируя различия между ними.
Краеугольным камнем поддержки полиморфизма в языке С++ является тот факт, что статические и динамические типы ссылок и указателей могут отличаться.
Когда при помощи ссылки или указателя на базовый класс происходит вызов функции,
определенной в базовом классе, точный тип объекта, для которого будет выполняться функция, неизвестен. Это может быть объект базового класса, а может быть и производного.
Если вызываемая функция не виртуальна, независимо от фактического типа объекта,
выполнена будет та версия функции, которая определена в базовом классе. Если функция виртуальна, решение о фактически выполняемой версии функции откладывается до времени выполнения. Она определяется на основании типа объекта, с которым связана ссылка или указатель.
С другой стороны, вызовы невиртуальных функций связываются во время компиляции. Точно так же вызовы любой функции (виртуальной или нет) для объекта связываются во время компиляции. Тип объекта фиксирован и неизменен — никак нельзя заставить динамический тип объекта отличаться от его статического типа. Поэтому вызовы для объекта связываются во время компиляции с версией, определенной типом объекта.
Виртуальные функции распознаются во время выполнения, только если вызов осуществляется через ссылку или указатель. Только в этих случаях динамический тип объекта может отличаться от его статического типа.Виртуальные функции
Page 757/1103
в производном классе
При переопределении виртуальной функции производный класс может, но не обязан,
повторить ключевое слово virtual. Как только функция объявляется виртуальной, она остается виртуальной во всех производных классах.
У функции производного класса, переопределяющей унаследованную виртуальную функцию,
должны быть точно такие же типы параметров, как и у функции базового класса, которую она переопределяет.
За одним исключением тип возвращаемого значения виртуальной функции в производном классе также должен соответствовать типу возвращаемого значения функции в базовом классе. Исключение относится к виртуальным функциям, возвращающим ссылку (или указатель) на тип, который сам связан наследованием. Таким образом, если тип D
происходит от типа В, то виртуальная функция базового класса может возвратить указатель на тип B*, а ее версия в производном классе может возвратить указатель на тип D*. Но такие типы возвращаемого значения требуют, чтобы преобразование производного класса в базовый из типа D в тип В было доступно. Доступность базового класса рассматривается в разделе 15.5. Пример такого вида виртуальной функции рассматривается в разделе 15.8.1.
Функция, являющаяся виртуальной в базовом классе, неявно остается виртуальной в его производных классах. Когда производный класс переопределяет виртуальную функцию, ее параметры в базовом и производных классах должны точно совпадать. Спецификаторы final и override
Как будет продемонстрировано в разделе 15.6, производный класс вполне может определить функцию с тем же именем, что и виртуальная функция в его базовом классе, но с другим списком параметров. Компилятор полагает, что такая функция независима от функции базового класса. В таких случаях версия в производном классе не переопределяет версию в базовом. На практике такие объявления зачастую являются ошибкой — автор класса намеревался переопределить виртуальную функцию базового класса, но сделал ошибку в определении списка параметров.
Поиск таких ошибок может быть на удивление трудным. По новому стандарту можно задать переопределение виртуальной функции в производном классе. Это дает ясно понять наше намерение и (что еще более важно) позволяет компилятору самому находить такие проблемы. Компилятор отвергнет программу, если функция, отмеченная как override, не переопределит существующую виртуальную функцию: struct В { virtual void f1(int) const; virtual void f2(); void f3();
}; struct D1 : B { void f1(int) const override; // ok: f1() соответствует f1() базового void f2(int) override; //
Page 758/1103
ошибка: В не имеет функции f2(int) void f3() override; // ошибка: f3() не виртуальная функция void f4() override; // ошибка: В не имеет функции f4()
};
В структуре D1 спецификатор override для функции f1() вполне подходит; и базовые, и производные версии функции-члена f1() константы, они получают тип int и возвращают void.
Версия f1() в структуре D1 правильно переопределяет виртуальную функцию, которую она унаследовала от структуры B.
Объявление функции f2() в структуре D1 не соответствует объявлению функции f2() в структуре B — она не получает никаких аргументов, а определенная в структуре D1 получает аргумент типа int. Поскольку объявления не совпадают, функция f2() в структуре D1 не переопределяет функцию f2() структуры В; это новая функция со случайно совпавшим именем. Как уже упоминалось, это объявление должно было быть переопределено, но этого не произошло и компилятор сообщил об ошибке.
Поскольку переопределена может быть только виртуальная функция, компилятор отвергнет также функцию f3() в структуре D1. Эта функция не виртуальна в структуре В, поэтому нечего и переопределять.
Точно так же ошибочна и функция f4(), поскольку в структуре В даже нет такой функции.
Функцию можно также определить как final. Любая попытка переопределения функции,
которая была определена со спецификатором final, будет помечена как ошибка: struct D2 : В {
// наследует f2() и f3() из В и переопределяет f1(int) void f1(int) const final; // последующие классы не могут
// переопределять f1(int)
}; struct D3 : D2 { void f2(); // ok: переопределение f2() унаследованной от косвенно
// базовой структуры В
Page 759/1103
void f1(int) const; // ошибка: D2 объявила f2() как final
};
Спецификаторы final и override располагаются после списка параметров (включая квалификаторы ссылки или const) и после замыкающего типа (см. раздел 6.3.3). Виртуальные функции и аргументы по умолчанию
Подобно любой другой функции, виртуальная функция может иметь аргументы по умолчанию
(см. раздел 6.5.1). Если вызов использует аргумент по умолчанию, то используемое значение определяется статическим типом, для которого вызвана функция.
Таким образом, при вызове через ссылку или указатель на базовый класс аргумент
(аргументы) по умолчанию будет определен в базовом классе. Аргументы базового класса будут использоваться даже тогда, когда выполняется версия функции производного класса. В
данном случае функции производного класса будут переданы аргументы по умолчанию,
определенные для версии функции базового класса. Если функция производного класса будет полагаться на передачу других аргументов, то программа не будет выполняться, как ожидалось.
Виртуальные функции с аргументами по умолчанию должны использовать те же значения аргументов в базовом и производных классах. Хитрость виртуального механизма
В некоторых случаях необходимо предотвратить динамическое связывание вызова виртуальной функции; нужно вынудить вызов использовать конкретную версию этой виртуальной функции. Для этого используется оператор области видимости. Рассмотрим,
например, этот код:
// вызов версии базового класса независимо от динамического типа baseP double undiscounted = baseP->Quote::net_price(42);
Здесь происходит вызов версии функции net_price() класса Quote независимо от типа объекта, на который фактически указывает baseP. Этот вызов будет распознан во время компиляции.
Обычно только код функций-членов (или друзей) должен использовать оператор области видимости для обхода виртуального механизма.
Зачем обходить виртуальный механизм? Наиболее распространен случай, когда виртуальная функция производного класса вызывает версию базового класса. В таких случаях версия базового класса могла бы выполнять действия, общие для всей иерархии типов. Версии,
определенные в производных классах, осуществляли бы любые дополнительные действия,
специфичные для их собственного типа.
Если виртуальная функция производного класса, намереваясь вызвать свою версию из базового класса, пропустит оператор области видимости, то вызов будет распознан во время выполнения как вызов самой версии производного класса, что приведет к бесконечной рекурсии. Упражнения раздела 15.3
Упражнение 15.11. Добавьте в иерархию класса Quote виртуальную функцию debug(),
отображающую переменные-члены соответствующих классов.
Page 760/1103

Упражнение 15.12. Возможен ли случай, когда полезно объявить функцию-член и как override,
и как final? Объясните, почему.
Упражнение 15.13. С учетом следующих классов объясните каждую из функций print(): class base { public: string name() { return basename; } virtual void print(ostream &os) { os << basename; } private: string basename;
}; class derived : public base { public: void print(ostream &os) { print(os); os << " " << i; } private: int i;
};
Если в этом коде имеются ошибки, устраните их.
Упражнение 15.14. С учетом классов из предыдущего упражнения и следующих объектов укажите, какие из версий функций будут применены во время выполнения: base bobj; base *bp1 = &bobj; base &br1 = bobj; derived dobj; base *bp2 = &dobj; base &br2 = dobj;
(a) bobj.print(); (b) dobj.print(); (c) bp1->name();
(d) bp2->name(); (e) br1.print(); (f) br2.print();
15.4. Абстрактные базовые классы
Предположим, что классы приложения книжного магазина необходимо дополнить поддержкой нескольких стратегий скидок. Кроме оптовой скидки, можно было бы предоставить скидку за покупку до определенного количества, а свыше применять полную цену. Либо можно было бы предоставить скидку за покупку свыше одного предела, но не выше другого.
Для всех этих стратегий необходимы одинаковые средства: количество экземпляров и объем скидки. Для поддержки этих столь разных стратегий можно определить новый класс по имени
Disc_quote, позволяющий хранить количество экземпляров и объем скидки. Такие классы как
Bulk_item, предоставляющие определенную стратегию скидок, наследуются от класса
Page 761/1103

Disc_quote. Каждый из производных классов реализует собственную стратегию скидок,
определяя собственную версию функции net_price().
Прежде чем определять собственный класс Disc_quote, следует решить, что будет делать функция net_price(). Класс Disc_quote не будет соответствовать никакой конкретной стратегии скидок; для этого класса нет никакого смысла создавать функцию net_price().
Класс Disc_quote можно было бы определить без его собственной версии функции net_price().
В данном случае класс Disc_quote наследовал бы функцию net_price() от класса Quote.
Однако такой проект позволил бы пользователям писать бессмысленный код. Пользователь мог бы создать объект типа Disc_quote, предоставив количество и объем скидки. Передача объекта класса Disc_quote такой функции, как print_total(), задействовала бы версию функции net_price() из класса Quote. Вычисляемая цена не включила бы скидку, предоставляемую при создании объекта. Такое поведение не имеет никакого смысла. Чистые виртуальные функции
Тщательный анализ этого вопроса показывает, что проблема не только в том, что неизвестно,
как определить функцию net_price(). Практически следовало бы запретить пользователям создавать объекты класса Disc_quote вообще. Этот класс представляет общую концепцию скидки на книги, а не конкретную стратегию скидок.
Для воплощения этого намерения (и однозначного уведомления о бессмысленности функции net_price()) определим функцию net_price() как чистую виртуальную функцию (pure virtual). В отличие от обычных виртуальных функций,
чистая виртуальная функция не должна быть определена. Для определения виртуальной функции как чистой вместо ее тела используется часть = 0 (т.е. как раз перед точкой с запятой, завершающей объявление). Часть = 0 может присутствовать только в объявлении виртуальной функции в теле класса:
// класс для содержания объема скидки и количества экземпляров
// используя эти данные, производные классы реализуют стратегии скидок class Disc_quote : public Quote { public:
Disc_quote() = default;
Disc_quote(const std::string& book, double price, std::size t qty, double disc):
Quote(book, price), quantity(qty), discount(disc) { } double net_price(std::size_t) const = 0; protected: std::size_t quantity = 0; // минимальная покупка для скидки
Page 762/1103
double discount = 0.0; // доля применяемой скидки
};
Подобно прежнему классу Bulk_item, класс Disc_quote определяет стандартный конструктор и конструктор, получающий четыре параметра. Хотя объекты этого типа нельзя создавать непосредственно, конструкторы в классах, производных от класса Disc_quote, будут использовать конструкторы Disc_quote() для построения части Disc_quote своих объектов.
Конструктор с четырьмя параметрами передает первые два конструктору Quote(), а двумя последними непосредственно инициализирует собственные переменные-члены discount и quantity. Стандартный конструктор инициализирует эти члены значениями по умолчанию.
Следует заметить, что определение для чистой виртуальной функции предоставить нельзя.
Однако тело функции следует определить вне класса. Поэтому нельзя предоставить в классе тело функции, для которой использована часть = 0. Классы с чистыми виртуальными функциями являются абстрактными
Класс, содержащий (или унаследовавший без переопределения) чистую виртуальную функцию, является абстрактным классом (abstract base class). Абстрактный класс определяет интерфейс для переопределения последующими классами. Нельзя (непосредственно) создавать объекты абстрактного класса. Поскольку класс Disc_quote определяет функцию net_price() как чистую виртуальную, нельзя определить объекты типа Disc_quote. Можно определить объекты классов, производных от Disc_quote, если они переопределят функцию net_price():
//
Disc_quote объявляет чистые виртуальные функции, которые
// переопределит Bulk_quote
Disc_quote discounted; // ошибка: нельзя определить объект Disc_quote
Bulk_quote bulk; // ok: у Bulk_quote нет чистых виртуальных функций
Классы, унаследованные от класса Disc_quote, должны определить функцию net_price(),
иначе они также будут абстрактными.
Нельзя создать объекты абстрактного класса. Конструктор производного класса инициализирует только свой прямой базовый класс
Теперь можно повторно реализовать класс Bulk_quote так, чтобы он происходил от класса
Disc_quote, а не непосредственно от класса Quote:
// скидка прекращается при продаже определенного количества экземпляров
//
Page 763/1103
скидка выражается как доля сокращения полной цены class Bulk_quote : public Disc_quote { public:
Bulk_quote() = default;
Bulk_quote(const std::string& book, double price, std::size_t qty, double disc):
Disc_quote(book, price, qty, disc) { }
// переопределение базовой версии для реализации политики скидок double net_price(std::size_t) const override;
};
У этой версии класса Bulk_quote есть прямой базовый класс (direct base class), Disc_quote, и косвенный базовый класс (indirect base class), Quote. У каждого объекта класса Bulk_quote есть три внутренних объекта: часть Bulk_quote (пустая), часть Disc_quote и часть Quote.
Как уже упоминалось, каждый класс контролирует инициализацию объектов своего типа.
Поэтому, даже при том, что у класса Bulk_quote нет собственных переменных-членов, он предоставляет тот же конструктор на четыре аргумента, что и первоначальный класс. Новый конструктор передает свои аргументы конструктору класса Disc_quote. Этот конструктор, в свою очередь, запускает конструктор Quote(). Конструктор Quote() инициализирует переменные-члены bookNo и price объекта bulk. Когда конструктор Quote() завершает работу,
начинает работу конструктор Disc_quote(), инициализирующий переменные-члены quantity и discount. Теперь возобновляет работу конструктор Bulk_quote(). Он не делает ничего и ничего не инициализирует. Ключевая концепция. Рефакторинг
Добавление класса Disc_quote в иерархию Quote является примером рефакторинга (refactoring). Рефакторинг подразумевает переделку иерархии классов с передачей некоторых функций и/или данных из одного класса в другой. Рефакторинг весьма распространен в объектно-ориентированных приложениях.
Примечательно, что, несмотря на изменение иерархии наследования, код, который использует классы Bulk_quote и Quote, изменять не придется. Но после рефакторинга классов
(или любых других измененный) следует перекомпилировать весь код, который использует эти классы. Упражнения раздела 15.4
Упражнение 15.15. Определите собственные версии классов Disc_quote и Bulk_quote.
Упражнение 15.16. Перепишите класс из упражнения 15.2.2 раздела 12.1.6, представляющий ограниченную стратегию скидок, так, чтобы он происходил от класса Disc_quote.
Упражнение 15.17. Попытайтесь определить объект типа Disc_quote и посмотрите, какие сообщения об ошибке выдал компилятор.
Page 764/1103

15.5. Управление доступом и наследование
Подобно тому, как каждый класс контролирует инициализацию своих переменных-членов (см.
раздел 15.2.2), каждый класс контролирует также доступность (accessible) своих членов для производного класса.Защищенные члены
Как уже упоминалось, класс использует защищенные члены в тех случаях, когда желает предоставить к ним доступ из производных классов, но защитить их от общего доступа.
Спецификатор доступа protected можно считать гибридом спецификаторов private и public.
• Подобно закрытым, защищенные члены недоступны пользователям класса.
• Подобно открытым, защищенные члены доступны для членов и друзей классов,
производных от данного класса.
Кроме того, защищенный член имеет еще одно важное свойство.
• Производный член класса или дружественный класс может обратиться к защищенным членам базового класса только через объект производного. У производного класса нет никакого специального способа доступа к защищенным членам объектов базового класса.
Чтобы лучше понять это последнее правило, рассмотрим следующий пример: class Base { protected: int prot_mem; // защищенный член
}; class Sneaky : public Base { friend void clobber(Sneaky&); // есть доступ к Sneaky::prot_mem friend void clobber(Base&); // нет доступа к Base::prot_mem int j; // j по умолчанию закрытая
};
// ok: clobber может обращаться к закрытым и защищенным членам Sneaky
Page 765/1103
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }
// ошибка: clobber не может обращаться к защищенным членам Base void clobber(Base &b) { b.prot_mem = 0; }
Если производные классы (и друзья) смогут обращаться к защищенным членам в объекте базового класса, то вторая версия функции clobber (получающая тип Base&) будет корректна. Хоть эта функция и не дружественна классу Base, она все же сможет изменить объект типа Base; для обхода защиты спецификатором protected любого класса достаточно определить новый класс по линии Sneaky.
Для предотвращения такого способа применения члены и друзья производного класса могут обращаться к защищенным членам только тех объектов базового класса, которые встроены в объект производного; к обычным объектам базового типа у них никакого доступа нет.Открытое, закрытое и защищенное наследование
Доступ к члену наследуемого класса контролируется комбинацией спецификатора доступа этого члена в базовом классе и спецификатором доступа в списке наследования производного класса. Для примера рассмотрим следующую иерархию: class Base { public: void pub_mem(); // открытый член protected: int prot_mem; // защищенный член private: char priv_mem; // закрытый член
}; struct Pub_Derv : public Base {
// ok: производный класс имеет доступ к защищенным членам int f() { return prot_mem; }
// ошибка: закрытые члены недоступны производным классам
Page 766/1103
char g() { return priv_mem; }
}; struct Priv_Derv : private Base {
// закрытое наследование не затрагивает доступ в производном классе int f1() const { return prot_mem; }
};
Спецификатор доступа наследования никак не влияет на возможность членов (и друзей)
производного класса обратиться к членам его собственного прямого базового класса. Доступ к членам базового класса контролируется спецификаторами доступа в самом базовом классе.
Структуры Pub_Derv и Priv_Derv могут обращаться к защищенному члену prot_mem, но ни одна из них не может обратиться к закрытому члену priv_mem.
Задача спецификатора доступа наследования — контролировать доступ пользователей производного класса, включая другие классы, производные от него, к членам,
унаследованным от класса Base:
Pub_Derv d1; // члены, унаследованные от Base, являются открытыми
Priv_Derv d2; // члены, унаследованные от Base, являются закрытыми d1.pub_mem(); // ok: pub_mem является открытой в производном класс d2.pub_mem(); // ошибка: pub_mem является закрытой в производном классе
Структуры Pub_Derv и Priv_Derv унаследовали функцию pub_mem(). При открытом наследовании члены сохраняют свой спецификатор доступа. Таким образом, объект d1
может вызвать функцию pub_mem(). В структуре Priv_Derv члены класса Base являются закрытыми; пользователи этого класса не смогут вызвать функцию pub_mem().
Спецификатор доступа наследования, используемый производным классом, также контролирует доступ из классов, унаследованных от этого производного класса: struct Derived_from_Public : public Pub_Derv {
// ok: Base::prot_mem остается защищенной в Pub_Derv int use_base() { return prot_mem; }
}; struct Derived_from_Private : public Priv_Derv {
Page 767/1103

// ошибка: Base::prot_mem является закрытой в Priv_Derv int use_base() { return prot_mem; }
};
Классы, производные от структуры Pub_Derv, могут обращаться к переменной-члену prot_mem класса Base, поскольку она остается защищенным членом в структуре Pub_Derv. У
классов, производных от структуры Priv_Derv, напротив, такого доступа нет. Все члены,
которые структура Priv_Derv унаследовала от класса Base, являются закрытыми.
Если бы был определен другой класс, скажем, Prot_Derv, использующий защищенное наследование, открытые члены класса Base в этом классе будут защищенными. У
пользователей структуры Prot_Derv не было бы никакого доступа к функции pub_mem(), но ее члены и друзья могли бы обратиться к унаследованному члену. Доступность преобразования производного класса в базовый класс
Будет ли доступно преобразование производного класса в базовый класс (см. раздел 15.2.2),
зависит от того, какой код пытается использовать преобразование, а также от спецификатора доступа, используемого при наследовании производного класса. С учетом, что класс D
происходит от класса B:
• Пользовательский код может использовать преобразование производного класса в базовый,
только если класс D открыто наследует класс B. Пользовательский код не может использовать преобразование, если наследование было защищенным или закрытым.
• Функции-члены и друзья класса D могут использовать преобразование в В независимо от вида наследования D от B. Преобразование производного в прямой базовый класс всегда доступно для членов и друзей производного класса.
• Функции-члены и друзья классов, производных от класса D, могут использовать преобразование производного класса в базовый, если наследование было открытым или защищенным. Такой код не сможет использовать преобразование, если наследование классом D класса В было закрытым.
В любом месте кода, где доступен открытый член базового класса, будет доступно также преобразование производного класса в базовый, но не наоборот. Ключевая концепция.
Проект класса и защищенные члены
Без наследования у класса будет два разных вида пользователей: обычные пользователи и разработчики (implementor). Обычные пользователи пишут код, который использует объекты типа класса; такой код может обращаться только к открытым членам класса (интерфейсу).
Разработчики пишут код, содержащийся в членах и друзьях класса. Члены и друзья класса могут обращаться и к открытым, и к закрытым разделам (реализации).
При наследовании появляется третий вид пользователей, а именно производные классы.
Базовый класс делает защищенными те части своей реализации, которые позволено использовать его производным классам. Защищенные члены остаются недоступными обычному пользовательскому коду; закрытые члены остаются недоступными производным классам и их друзьям.
Подобно любому другому классу, базовый класс объявляет члены своего интерфейса
Page 768/1103
открытыми. Класс, используемый как базовый, может разделить свою реализацию на члены,
доступные для производных классов и доступные только для базового класса и его друзей.
Член класса, относящийся к реализации, должен быть защищен, если он предоставляет функцию или данные, которые производный класс должен будет использовать в собственной реализации. В противном случае члены реализации должны быть закрытыми. Дружественные отношения и наследование
Подобно тому, как дружественные отношения не передаются (см. раздел 7.3.4), они также не наследуются. У друзей базового класса нет никаких специальных прав доступа к членам его производных классов, а у друзей производного класса нет специальных прав доступа к базовому классу: class Base {
// добавлено объявление; другие члены, как прежде friend class Pal; // у Pal нет доступа к классам, производным от Base
}; class Pal { public: int f(Base b) { return b.prot_mem; } // ok: Pal дружествен Base int f2(Sneaky s) { return s.j; } // ошибка: Pal не
// дружествен Sneaky
// доступ к базовому классу контролируется базовым классом, даже в
// объекте производного int f3(Sneaky s) { return s.prot_mem; } // ok: Pal дружествен
};
Факт допустимости функции f3() может показаться удивительным, но он непосредственно следует из правила, что все классы контролируют доступ к собственным членам. Класс Pal —
друг класса Base, поэтому класс Pal может обращаться к членам объектов класса Base. Это относится и к встроенным в объект класса Base объектам классов, производных от него.
Page 769/1103

Когда класс объявляет другой класс дружественным, это относится только к данному классу,
ни его базовые, ни производные классы никаких специальных прав доступа не имеют:
// у D2 нет доступа к закрытым или защищенным членам Base class D2 : public Pal { public: int mem(Base b)
{ return b.prot_mem; } // ошибка: дружба не наследуется
};
Дружественные отношения не наследуются; каждый класс сам контролирует доступ к своим членам. Освобождение индивидуальных членов
Иногда необходимо изменить уровень доступа к имени, унаследованному производным классом. Для этого можно использовать объявление using (см. раздел 3.1): class Base { public: std::size_t size() const { return n; } protected: std::size_t n;
}; class Derived : private Base { // заметьте, наследование закрытое public:
// обеспечить уровни доступа для членов, связанных с размером объекта using Base::size; protected: using Base::n;
};
Поскольку класс Derived использует закрытое наследование, унаследованные члены size() и n по умолчанию будут закрытыми членами класса Derived. Объявления using корректируют доступность этих членов. Пользователи класса Derived могут обращаться к функции-члену size(), а классы, впоследствии произошедшие от класса Derived, смогут обратиться к
Page 770/1103
переменной n.
Объявление using в классе может использовать имя любого доступного (не закрытого) члена прямого или косвенного базового класса. Доступность имени, указанного в объявлении using,
зависит от спецификатора доступа, предшествующего объявлению using. Таким образом,
если объявление using расположено в разделе private класса, то имя будет доступно только для членов и друзей. Если объявление находится в разделе public, имя доступно для всех пользователей класса. Если объявление находится в разделе protected, имя доступно только для членов, друзей и производных классов.
Производный класс может предоставить объявление using только для тех имен, доступ к которым разрешен. Уровни защиты наследования по умолчанию
В разделе 7.2 упоминалось о том, что у классов, определенных с использованием ключевых слов struct, и class разные спецификаторы доступа по умолчанию. Точно так же заданный по умолчанию спецификатор наследования зависит от ключевого слова, используемого при определении производного класса. По умолчанию у производного класса, определенного с ключевым словом class, будет закрытое наследование (private inheritance), а с ключевым словом struct — открытое (public inheritance): class Base { /* ... */ }; struct D1 : Base { /* ... */ }; // открытое наследование по умолчанию class D2 : Base { /* ... */ }; // закрытое наследование по умолчанию
Весьма распространенно заблуждение, что между классами и структурами есть иные, более глубокие различия. Единственное различие — заданные по умолчанию спецификаторы доступа для членов и наследования. Никаких других различий нет.
Для закрытого наследования производный класс должен быть явно определен как private, не следует полагаться на поведение по умолчанию. Это ясно дает понять, что закрытое наследование применено преднамеренно, а не по оплошности. Упражнения раздела 15.5
Упражнение 15.18. С учетом классов Base и производных от него, и типов объектов,
приведенных в комментариях, укажите, какие из следующих присвоений допустимы.
Объясните, почему некорректны недопустимые.
Base *p = &d1; // d1 имеет тип Pub_Derv p = &d2; // d2 имеет тип Priv_Derv p = &d3; // d3 имеет тип Prot_Derv p = &dd1; // dd1 имеет тип Derived_from_Public
Page 771/1103
p = &dd2; // dd2 имеет тип Derived_from_Private p = &dd3; // dd3 имеет тип Derived_from_Protected
Упражнение 15.19. Предположим, у каждого из классов: Base и производных от него, есть функция-член в формате void memfcn(Base &b) { b = *this; }
Укажите, была ли эта функция допустима для каждого класса.
Упражнение 15.20. Напишите код проверки ответов на предыдущие два упражнения.
Упражнение 15.21. Выберите одну из следующих общих абстракций, содержащих семейство типов (или любую собственную). Организуйте типы в иерархию наследования.
(a) Форматы графических файлов (например: gif, tiff, jpeg, bmp)
(b) Геометрические примитивы (например: box, circle, sphere, cone)
(c) Типы языка С++ (например: class, function, member function)
Упражнение 15.22. Укажите имена некоторых из наиболее вероятных виртуальных функций, а также открытых и защищенных членов для класса, выбранного в предыдущем упражнении.
15.6. Область видимости класса при наследовании
Каждый класс определяет собственную область видимости (scope) (см. раздел 7.4), в рамках которой определены его члены. При наследовании область видимости производного класса (см. раздел 2.2.4) вкладывается в области видимости его базовых классов. Если имя не найдено в области видимости производного класса, поиск его определения продолжается в областях видимости базовых классов.
Тот факт, что область видимости производного класса вложена в область видимости его базовых классов, может быть удивителен. В конце концов, базовые и производные классы определяются в разных частях текста программы. Но именно это иерархическое вложение областей видимости класса позволяет членам производного класса использовать члены его базового класса, как будто они являются частью производного класса. Рассмотрим пример:
Bulk_quote bulk; cout << bulk.isbn();
В этом коде поиск определения имени isbn() осуществляется следующим образом.
• Поскольку вызывается функция isbn() объекта типа Bulk_quote, поиск начинается в классе
Bulk_quote. В этом классе имя isbn() не найдено.
Page 772/1103

• Поскольку класс Bulk_quote происходит от класса Disc_quote, в нем и продолжается поиск.
Имя все еще не найдено.
• Поскольку класс Disc_quote происходит от класса Quote, поиск продолжается в нем. В этом классе находится определение имени isbn(); таким образом, вызов isbn() распознается как вызов функции isbn() класса Quote. Поиск имен осуществляется во время компиляции
Статический тип (см. раздел 15.2.3) объекта, ссылки или указателя определяет, какие члены этого объекта будут видимы. Даже когда статический и динамический типы отличаются (это бывает в случае, когда используется ссылка или указатель на базовый класс), именно статический тип определяет применимые члены. Например, в класс Disc_quote можно было бы добавить функцию-член, которая возвращает пару (тип pair) (см. раздел 11.2.3),
содержащую минимальное (или максимальное) количество и цену со скидкой. class Disc_quote : public Quote { public: std::pair<size_t, double> discount_policy() const
{ return {quantity, discount}; }
// другие члены как прежде
};
Функцию discount_policy() можно использовать только через объект, указатель, или ссылку на тип Disc_quote, или класс, производный от него:
Bulk_quote bulk;
Bulk_quote *bulkP = &bulk; // статический и динамический типы совпадают
Quote *itemP = &bulk; // статический и динамический типы отличаются bulkP->discount_policy(); // ok: bulkP имеет тип Bulk_quote* itemP->discount_policy(); // ошибка: itemP имеет тип Quote*
Хотя объект bulk имеет функцию-член discount_policy(), она недоступна через указатель itemP. Тип itemP — указатель на тип Quote, а это значит, что поиск имени discount_policy()
начнется в классе Quote. У класса Quote нет члена по имени discount_policy(), поэтому вызов этой функции-члена объекта, ссылки или указателя на тип Quote невозможен. Конфликт имен и наследование
Как и любая другая, область видимости производного класса позволяет повторно использовать имя, определенное в его прямом или косвенном базовом классе. Как обычно,
имена, определенные во внутренней области видимости (например, в производном классе),
Page 773/1103
скрывают имена во внешней области видимости (например, в базовом классе) (см. раздел
2.2.4): struct Base {
Base() : mem(0) { } protected: int mem;
}; struct Derived : Base {
Derived(int i): mem(i) { } //
Derived::mem инициализируется i
//
Base::mem инициализируется по умолчанию int get_mem() { return mem; } // возвращает Derived::mem protected: int mem; // скрывает mem в Base
};
Ссылка на переменную mem в функции get_mem() распознается как имя в классе Derived.
Таким образом, код
Derived d(42); cout << d.get_mem() << endl; // выводит 42 выведет значение 42.
Член производного класса, имя которого совпадает с именем члена базового класса,
скрывает член базового класса и предотвращает прямой доступ к нему. Применение оператора области видимости для доступа к скрытым членам
Для доступа к скрытому члену базового класса можно использовать оператор области видимости. struct Derived : Base { int get_base_mem() { return Base::mem; }
};
Page 774/1103

Оператор области видимости изменяет нормальный порядок поиска и заставляет компилятор начинать поиск имени mem с класса Base. Если бы код выше был выполнен с этой версией класса Derived, то результатом вызова d.get_mem() был бы 0.
Кроме переопределения унаследованных виртуальных функций, производный класс обычно не должен повторно использовать имена, определенные в его базовом классе. Ключевая концепция. Поиск имени и наследование
Для понимания наследования в языке С++ крайне важно знать, как распознаются вызовы функций. Процесс распознавания вызова p->mem() (или obj.mem()) проходит в четыре этапа.
• Сначала определяется статический тип объекта p (или obj). Поскольку это вызов члена класса, тип будет классом.
• Поиск имени mem осуществляется в классе, который соответствует статическому типу объекта p (или obj). Если функция mem() не найдена, поиск продолжается в прямом базовом классе и далее по цепи классов, пока имя mem не будет найдено или пока не будет осмотрен последний класс. Если функция mem() не будет найдена ни в самом классе, ни в его базовых классах, вызов откомпилирован не будет.
• Как только имя mem будет найдено, осуществляется обычная проверка соответствия типов
(см. раздел 6.1), гарантирующая допустимость найденного определения для данного вызова.
• Если вызов допустим, компилятор создает код, зависящий от того, является ли вызываемая функция виртуальной или нет:
- Если функция mem() виртуальная и вызов осуществляется через ссылку или указатель, то компилятор создает код, который во время выполнения определяет на основании динамического типа объекта выполняемую версию функции.
- В противном случае, если функция не является виртуальной или если вызов осуществляется для объекта (а не ссылки или указателя), то компилятор создает код обычного вызова функции. Как обычно, поиск имени осуществляется перед проверкой соответствия типов
Как уже упоминалось, функции, объявленные во внутренней области видимости, не перегружают функции, объявленные во внешней области видимости (см. раздел 6.4.1). В
результате функции, определенные в производном классе, не перегружают функции-члены,
определенные в его базовом классе (классах). Подобно любой другой области видимости,
если имя члена производного класса (т.е. определенное во внутренней области видимости)
совпадает с именем члена базового класса (т.е. именем во внешней области видимости), то в рамках производного класса имя, определенное в производном классе, скрывает имя в базовом классе. Имя функции-члена базового класса скрывается, даже если у функций будут разные списки параметров: struct Base { int memfcn();
}; struct Derived : Base { int memfcn(int); // скрывает memfcn() в базовом классе
Page 775/1103

};
Derived d; Base b; b.memfcn(); // вызов Base::memfcn() d.memfcn(10); // вызов Derived::memfcn() d.memfcn(); // ошибка: memfcn() без аргументов скрывается d.Base::memfcn(); // ok: вызов Base::memfcn()
Объявление функции memfcn() в классе Derived скрывает объявление функции memfcn() в классе Base. Не удивительно, что первый вызов через объект b класса Base вызывает версию в базовом классе. Точно так же второй вызов (через объект d) вызывает версию класса Derived. Удивительно то, что третий вызов, d.memfcn(), некорректен.
Чтобы распознать этот вызов, компилятор ищет имя memfcn в классе Derived. Этот класс определяет член по имени memfcn, и поиск на этом останавливается. Как только имя будет найдено, компилятор далее не ищет. Версия функции memfcn() в классе Derived ожидает аргумент типа int. Поскольку данный вызов такого аргумента не предоставляет, вызов ошибочен. Виртуальные функции и область видимости
Теперь можно разобраться, почему у виртуальных функций должен быть одинаковый список параметров в базовом и производном классах (см. раздел 15.3). Если функции-члены в базовом и производном классах будут получать разные аргументы, не будет никакого способа вызвать версию производного класса через ссылку или указатель на базовый. Например: class Base { public: virtual int fcn();
}; class D1 : public Base { public:
// скрывает fcn() в базовом; функция fcn() не виртуальна
//
D1 наследует определение из Base::fcn() int fcn(int); // список параметров fcn() в Base другой
Page 776/1103
virtual void f2(); // новая виртуальная функция,
// не существующая в Base
}; class D2 : public D1 { public: int fcn(int); // невиртуальная функция скрывает D1::fcn(int) int fcn(); // переопределяет виртуальную функцию fcn() из Base void f2(); // переопределяет виртуальную функцию f2() из D1
};
Функция fcn() в классе D1 не переопределяет виртуальную функцию fcn() из класса Base,
поскольку у них разные списки параметров. Вместо этого она скрывает функцию fcn() из базового класса. Фактически у класса D1 есть две функции по имени fcn(): класс D1 унаследовал виртуальную функцию fcn() от класса Base, а также определяет собственную невиртуальную функцию-член по имени fcn(), получающую параметр типа int.Вызов скрытой виртуальной функции через базовый класс
С учетом классов, описанных выше, рассмотрим несколько разных способов вызова этих функций:
Base bobj; D1 d1obj; D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj; bp1->fcn(); // виртуальный вызов Base::fcn() во время выполнения bp2->fcn(); // виртуальный вызов Base::fcn() во время выполнения bp3->fcn(); // виртуальный вызов D2::fcn() во время выполнения
D1 *d1p = &d1obj; D2 *d2p = &d2obj; bp2->f2(); //
Page 777/1103
ошибка: Base не имеет члена по имени f2() d1p->f2(); // виртуальный вызов D1::f2() во время выполнения d2p->f2(); // виртуальный вызов D2::f2() во время выполнения
Все три первых вызова сделаны через указатели на базовый класс. Поскольку функция fcn()
является виртуальной, компилятор создает код, способный во время выполнения решить,
какую версию вызвать.
Это решение будет принято на основании фактического типа объекта, с которым связан указатель. В случае указателя bp2 основной объект имеет тип D1. Этот класс не переопределит функцию fcn() без параметров. Таким образом, вызов через указатель bp2
распознается (во время выполнения) как версия, определенная в классе Base.
Следующие три вызова осуществляются через указатели с отличными типами. Каждый указатель указывает на один из типов в этой иерархии. Первый вызов некорректен, так как в классе Base нет функции f2(). Тот факт, что указатель случайно указывает на производный объект, является несущественным.
И наконец, рассмотрим вызовы невиртуальной функции fcn(int):
Base *p1 = &d2obj; D1 *p2 = &d2obj; D2 *p3 = &d2obj; p1->fcn(42); // ошибка: Base не имеет версии fcn(), получающей int p2->fcn(42); // статическое связывание, вызов D1::fcn(int) p3->fcn(42); // статическое связывание, вызов D2::fcn(int)
В каждом вызове указатель случайно указывает на объект типа D2. Но динамический тип не имеет значения, когда происходит вызов невиртуальной функции. Вызываемая версия зависит только от статического типа указателя. Переопределение перегруженных функций
Подобно любой другой функции, функция-член (виртуальная или нет) может быть перегружена. Производный класс способен переопределить любое количество экземпляров перегруженных функций, которые он унаследовал. Если производный класс желает сделать все перегруженные версии доступными через свой тип, то он должен переопределить их все или ни одну из них.
Иногда класс должен переопределить некоторые, но не все функции в наборе перегруженных. В таких случаях было бы весьма утомительно переопределять каждую версию базового класса, чтобы переопределить только те, которые должен специализировать класс.
Вместо переопределения каждой версии базового класса, которую он унаследовал,
производный класс может предоставить объявление using (см. раздел 15.5) для перегруженного члена. Объявление using определяет только имя; оно не может определить
Page 778/1103
список параметров. Таким образом, объявление using для функции-члена базового класса добавляет все перегруженные экземпляры этой функции в область видимости производного класса. Перенеся все имена в свою область видимости, производный класс должен определить только те функции, которые действительно зависят от его типа.
Обычные правила объявления using в классе относятся и к именам перегруженных функций
(см. раздел 15.5); каждый перегруженный экземпляр функции в базовом классе должен быть доступен в производном классе. Доступ к перегруженным версиям, которые в противном случае не переопределяются производным классом, будет возможен в точке объявления using. Упражнения раздела 15.6
Упражнение 15.23. Предположим, что класс D1 намеревается переопределить свою унаследованную функцию fcn(). Как исправить этот класс? Предположим, что класс исправлен так, что функция fcn() соответствует определению в классе Base. Как бы распознавались вызовы в этом разделе?
15.7. Конструкторы и функции управления копированием
Подобно любому другому классу, класс в иерархии наследования контролирует происходящее при создании, копировании, перемещении, присвоении или удалении объектов его типа. Как и у любого другого класса, если класс (базовый или производный) сам не определяет одну из функций управления копированием, ее синтезирует компилятор. Кроме того, как обычно, синтезируемая версия любой из этих функций-членов может быть удаленной функцией.
15.7.1. Виртуальные деструкторы
Основное воздействие, которое наследование оказывает на управление копированием для базового класса, заключается в том, что базовый класс обычно должен определять виртуальный деструктор (см. раздел 15.2.1). Деструктор должен быть виртуальной функцией,
чтобы обеспечить объектам в иерархии наследования возможность динамического создания.
Помните, что деструктор выполняется при удалении указателя на динамически созданный объект (см. раздел 13.1.3). Если это указатель на тип в иерархии наследования, вполне возможно, что статический тип указателя может отличаться от динамического типа удаляемого объекта (см. раздел 15.2.2). Например, при удалении указателя типа Quote*
может оказаться, что он указывал на объект класса Bulk_quote. Если он указывает на объект типа Bulk_quote, компилятор должен знать, что следует выполнить деструктор именно класса
Bulk_quote. Подобно любой другой функции, чтобы был выполнен надлежащий деструктор, в базовом классе его следует определить как виртуальную функцию: class Quote { public:
// виртуальный деструктор необходим при удалении указателя на
Page 779/1103

// базовый тип, указывающего на объект производного virtual

Quote() = default; // динамическое связывание для
// деструктора
};
Подобно любой другой виртуальной функции, виртуальный характер деструктора наследуется. Таким образом, у классов, производных от класса Quote, окажутся виртуальные деструкторы, будь то синтезируемый деструктор или собственный. Пока деструктор базового класса остается виртуальной функцией, при удалении указателя на базовый класс будет выполнен соответствующий деструктор:
Quote *itemP = new Quote; // статический и динамический типы совпадают delete itemP; // вызов деструктора для Quote itemP = new Bulk_quote; // статический и динамический типы разные delete itemP; // вызов деструктора для Bulk_quote
Выполнение оператора delete для указателя на базовый класс, который указывает на объект производного класса, приведет к непредсказуемым последствиям, если деструктор базового класса не будет виртуальным.
Деструкторы базовых классов — важное исключение из эмпирических правил, согласно которым, если класс нуждается в деструкторе, то он также нуждается в функциях копирования и присвоения (см. раздел 13.6). Базовый класс почти всегда нуждается в деструкторе, поэтому он может сделать деструктор виртуальным. Если базовый класс обладает пустым деструктором, только чтобы сделать его виртуальным, то наличие у класса деструктора вовсе не означает, что также необходим оператор присвоения или конструктор копий. Виртуальный деструктор отменяет синтез функций перемещения
Тот факт, что базовый класс нуждается в виртуальном деструкторе, имеет важное косвенное последствие для определения базовых и производных классов: если класс определит деструктор (даже с использованием синтаксиса = default, чтобы использовать синтезируемую версию), то компилятор не будет синтезировать функцию перемещения для этого класса (см.
раздел 13.6.2). Упражнения раздела 15.7.1

1   ...   37   38   39   40   41   42   43   44   ...   54


написать администратору сайта