Язык программирования C++. Пятое издание - Стенли Липпман
Шрифт:
Интервал:
Закладка:
Производный класс может предоставить объявление 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
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() не найдено.
• Поскольку класс 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 невозможен.
Конфликт имен и наследованиеКак и любая другая, область видимости производного класса позволяет повторно использовать имя, определенное в его прямом или косвенном базовом классе. Как обычно, имена, определенные во внутренней области видимости (например, в производном классе), скрывают имена во внешней области видимости (например, в базовом классе) (см. раздел 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; }
};
Оператор области видимости изменяет нормальный порядок поиска и заставляет компилятор начинать поиск имени mem с класса Base. Если бы код выше был выполнен с этой версией класса Derived, то результатом вызова d.get_mem() был бы 0.
Кроме переопределения унаследованных виртуальных функций, производный класс обычно не должен повторно использовать имена, определенные в его базовом классе.
Ключевая концепция. Поиск имени и наследованиеДля понимания наследования в языке С++ крайне важно знать, как распознаются вызовы функций. Процесс распознавания вызова p->mem() (или obj.mem()) проходит в четыре этапа.
• Сначала определяется статический тип объекта p (или obj). Поскольку это вызов члена класса, тип будет классом.
• Поиск имени mem осуществляется в классе, который соответствует статическому типу объекта p (или obj). Если функция mem() не найдена, поиск продолжается в прямом базовом классе и далее по цепи классов, пока имя mem не будет найдено или пока не будет осмотрен последний класс. Если функция mem() не будет найдена ни в самом классе, ни в его базовых классах, вызов откомпилирован не будет.