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

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; }

 // ошибка: закрытые члены недоступны производным классам

 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 {

 // ошибка: 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). Обычные пользователи пишут код, который использует объекты типа класса; такой код может обращаться только к открытым членам класса (интерфейсу). Разработчики пишут код, содержащийся в членах и друзьях класса. Члены и друзья класса могут обращаться и к открытым, и к закрытым разделам (реализации).

При наследовании появляется третий вид пользователей, а именно производные классы. Базовый класс делает защищенными те части своей реализации, которые позволено использовать его производным классам. Защищенные члены остаются недоступными обычному пользовательскому коду; закрытые члены остаются недоступными производным классам и их друзьям.

Подобно любому другому классу, базовый класс объявляет члены своего интерфейса открытыми. Класс, используемый как базовый, может разделить свою реализацию на члены, доступные для производных классов и доступные только для базового класса и его друзей. Член класса, относящийся к реализации, должен быть защищен, если он предоставляет функцию или данные, которые производный класс должен будет использовать в собственной реализации. В противном случае члены реализации должны быть закрытыми.

Дружественные отношения и наследование

Подобно тому, как дружественные отношения не передаются (см. раздел 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 объектам классов, производных от него.

Когда класс объявляет другой класс дружественным, это относится только к данному классу, ни его базовые, ни производные классы никаких специальных прав доступа не имеют:

// у 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, смогут обратиться к переменной 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

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. Укажите имена некоторых из наиболее вероятных виртуальных функций, а также открытых и защищенных членов для класса, выбранного в предыдущем упражнении.

Более 800 000 книг и аудиокниг! 📚

Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением

ПОЛУЧИТЬ ПОДАРОК