17.5 – Наследование и спецификаторы доступа
В предыдущих уроках этой главы вы немного узнали о том, как работает базовое наследование. До сих пор во всех наших примерах мы использовали открытое (публичное) наследование. То есть наш производный класс открыто наследовал базовый класс.
В этом уроке мы более подробно рассмотрим открытое (public
) наследование, а также два других вида наследования: закрытое (частное, private
) и защищенное (protected
). Мы также рассмотрим, как различные виды наследования взаимодействуют со спецификаторами доступа, чтобы разрешить или ограничить доступ к членам.
К этому моменту вы уже видели спецификаторы доступа public
и private
, которые определяют, кто может получить доступ к членам класса. Напоминаем, что к открытым (public
) членам может получить доступ кто угодно. Доступ к закрытым (private
) членам могут получить только функции-члены этого же класса или друзей. Это означает, что производные классы не могут напрямую обращаться к закрытым членам базового класса!
class Base
{
private:
// доступна только членам и друзьям Base (не производным классам)
int m_private;
public:
// может получить доступ кто угодно
int m_public;
};
Это довольно просто, и вы уже должны были к этому привыкнуть.
Спецификатор доступа protected
При работе с наследованными классами всё становится немного сложнее.
В C++ есть третий спецификатор доступа, о котором мы еще не говорили, потому что он полезен только в контексте наследования. Спецификатор доступа protected
позволяет получить доступ к члену классу, к которому принадлежит член, друзьям и производным классам. Однако защищенные члены недоступны извне класса.
class Base
{
public:
// может получить доступ кто угодно
int m_public;
protected:
// доступна для членов Base, друзей и производных классов
int m_protected;
private:
// доступна только членам и друзьям Base (но не производным классам)
int m_private;
};
class Derived: public Base
{
public:
Derived()
{
// допустимо: можно получить доступ к открытым членам
// базового класса из производного класса
m_public = 1;
// допустимо: можно получить доступ к защищенным членам
// базового класса из производного класса
m_protected = 2;
// не допустимо: нет доступа к закрытым членам
// базового класса из производного класса
m_private = 3;
}
};
int main()
{
Base base;
// допустимо: можно получить доступ к открытым членам извне класса
base.m_public = 1;
// не допустимо: нет доступа к защищенным членам извне класса
base.m_protected = 2;
// не допустимо: нет доступа к закрытым членам извне класса
base.m_private = 3;
return 0;
}
В приведенном выше примере вы можете видеть, что защищенный член базового класса m_protected
напрямую доступен производному классу, но не внешней функции.
Итак, когда мне следует использовать спецификатор доступа protected
?
К защищенному члену в базовом классе производные классы могут обращаться напрямую. Это означает, что если вы позже измените что-либо в этом защищенном члене (тип, значение и т.д.), вам, вероятно, придется изменить как базовый класс, так и все производные классы.
Следовательно, использование спецификатора доступа protected
наиболее полезно, когда вы (или ваша команда) собираетесь наследоваться от ваших собственных классов, и количество производных классов не слишком велико. Таким образом, если вы вносите изменения в реализацию базового класса и в результате необходимы обновления производных классов, вы можете вносить эти обновления самостоятельно (и это не будет длиться вечно, поскольку количество производных классов ограничено).
Делая ваши члены закрытыми, вы лучше инкапсулируете и изолируете производные классы от изменений в базовом классе. Но в этом случае появляются затраты на создание открытого или защищенного интерфейсов для поддержки всех методов или возможностей доступа, которые необходимы сторонним и/или производным классам. Это дополнительная работа, которая, вероятно, не стоит того, если только вы не ожидаете, что кто-то другой будет наследоваться от вашего класса, или у вас есть огромное количество производных классов, и обновление их всех будет стоить дорого.
Различные виды наследования и их влияние на доступ
Во-первых, классы могут наследоваться от других классов тремя разными способами: открытый (public
), защищенный (protected
) и закрытый (private
).
Для этого при выборе класса для наследования просто укажите, какой тип доступа вам нужен:
// Открытое наследование от Base
class Pub: public Base
{
};
// Защищенное наследование от Base
class Pro: protected Base
{
};
// Закрытое наследование от Base
class Pri: private Base
{
};
class Def: Base // По умолчанию закрытое наследование
{
};
Если вы не выбрали тип наследования, C++ по умолчанию использует закрытое наследование (точно так же, как члены по умолчанию имеют закрытый доступ, если вы не укажете иное).
Это дает нам 9 комбинаций: 3 спецификатора доступа к членам (public
, private
и protected
) и 3 типа наследования (public
, private
и protected
).
Так в чем разница между ними? Вкратце, когда члены наследуются, спецификатор доступа для унаследованного члена может быть изменен (только в производном классе) в зависимости от типа используемого наследования. Другими словами, члены, которые были открытыми или защищенными в базовом классе, могут изменять спецификаторы доступа в производном классе.
Это может показаться немного запутанным, но всё не так уж и плохо. Мы посвятим оставшуюся часть урока подробному изучению этого.
При рассмотрении примеров помните о следующих правилах:
- Класс всегда может получить доступ к своим собственным (ненаследуемым) членам.
- Сторонний код получает доступ к членам класса на основе спецификаторов доступа класса, к которому он обращается.
- Производный класс обращается к унаследованным членам на основе спецификатора доступа, унаследованного от родительского класса. Это зависит от спецификатора доступа и типа наследования.
Открытое наследование
Открытое наследование – это, безусловно, наиболее часто используемый тип наследования. Фактически, вы очень редко увидите или будете использовать другие типы наследования, поэтому ваше основное внимание должно быть сосредоточено на понимании этого раздела. К счастью, открытое наследование также легче всего для понимания. Когда вы наследуете базовый класс открыто, унаследованные открытые члены остаются открытыми, а унаследованные защищенные члены остаются защищенными. Унаследованные закрытые члены, которые были недоступны, потому что они были закрытыми в базовом классе, остаются недоступными.
Спецификатор доступа в базовом классе | Спецификатор доступа при открытом наследовании |
---|---|
public | public |
protected | protected |
private | не доступен |
Ниже приведен пример, показывающий, как всё работает:
class Base
{
public:
int m_public;
protected:
int m_protected;
private:
int m_private;
};
class Pub: public Base // обратите внимание: открытое наследование
{
// Открытое наследование означает:
// Открытые унаследованные члены остаются открытыми
//(поэтому m_public считается открытым).
// Защищенные унаследованные члены остаются защищенными
// (поэтому m_protected рассматривается как защищенный)
// Закрытые унаследованные члены остаются недоступными
// (поэтому m_private недоступен)
public:
Pub()
{
m_public = 1; // ok: m_public был унаследован как public
m_protected = 2; // ok: m_protected был унаследован как protected
m_private = 3; // нет: m_private недоступен из производного класса
}
};
int main()
{
// Внешний доступ использует спецификаторы доступа класса,
// к которому осуществляется доступ.
Base base;
base.m_public = 1; // ok: m_public открыт в Base
base.m_protected = 2; // нет: m_protected защищен в Base
base.m_private = 3; // нет: m_private закрыт в Base
Pub pub;
pub.m_public = 1; // ok: m_public открыт в Pub
pub.m_protected = 2; // нет: m_protected защищен в Pub
pub.m_private = 3; // нет: m_private недоступен Pub
return 0;
}
Это то же самое, что и в приведенном выше примере, где мы ввели спецификатор защищенного доступа, за исключением того, что мы также создали экземпляр производного класса, просто чтобы показать, что с открытым наследованием в базовом и производном классах всё работает одинаково.
Если у вас нет особой причины делать иначе, то вы должны использовать открытое наследование.
Лучшая практика
Используйте открытое наследование, если у вас нет особых причин делать иначе.
Защищенное наследование
Защищенное наследование – наименее распространенный метод наследования. Практически никогда не используется, за исключением очень особых случаев. При защищенном наследовании открытые и защищенные члены становятся защищенными, а закрытые члены остаются недоступными.
Поскольку эта форма наследования встречается очень редко, мы пропустим этот пример и подведем итоги в виде таблицы:
Спецификатор доступа в базовом классе | Спецификатор доступа при защищенном наследовании |
---|---|
public | protected |
protected | protected |
private | не доступен |
Закрытое наследование
При закрытом наследовании все члены базового класса наследуются как закрытые. Это означает, что закрытые члены остаются закрытыми, а защищенные и открытые члены становятся закрытыми.
Обратите внимание, что это не влияет на способ доступа производного класса к членам, унаследованным от его родителя! Это влияет только на код, пытающийся получить доступ к этим членам через производный класс.
class Base
{
public:
int m_public;
protected:
int m_protected;
private:
int m_private;
};
class Pri: private Base // обратите внимание: закрытое наследование
{
// Закрытое наследование означает:
// Открытые унаследованные члены становятся закрытыми
// (поэтому m_public рассматривается как закрытый).
// Защищенные унаследованные члены становятся закрытыми
// (поэтому m_protected рассматривается как закрытый)
// Закрытые унаследованные члены остаются недоступными
// (поэтому m_private недоступен)
public:
Pri()
{
m_public = 1; // ok: m_public теперь закрытый в Pri
m_protected = 2; // ok: m_protected теперь закрытый в Pri
m_private = 3; // нет: производные классы не могут получить доступ
// к закрытым членам базового класса
}
};
int main()
{
// Внешний доступ использует спецификаторы доступа класса,
// к которому осуществляется доступ.
// В этом случае спецификаторы доступа base.
Base base;
base.m_public = 1; // ok: m_public открытый в Base
base.m_protected = 2; // нет: m_protected защищенный в Base
base.m_private = 3; // нет: m_private закрытый в Base
Pri pri;
pri.m_public = 1; // нет: m_public теперь закрытый в Pri
pri.m_protected = 2; // нет: m_protected теперь закрытый в Pri
pri.m_private = 3; // нет: m_private недоступен в Pri
return 0;
}
Подведем итоги в виде таблицы:
Спецификатор доступа в базовом классе | Спецификатор доступа при закрытом наследовании |
---|---|
public | private |
protected | private |
private | не доступен |
Закрытое наследование может быть полезно, когда производный класс не имеет очевидной связи с базовым классом, но использует базовый класс для внутренней реализации. В таком случае мы, вероятно, не хотим, чтобы открытый интерфейс базового класса предоставлялся через объекты производного класса (как это было бы, если бы мы наследовали открыто).
На практике закрытое наследование используется редко.
Последний пример
class Base
{
public:
int m_public;
protected:
int m_protected;
private:
int m_private;
};
Base
может получить доступ к своим членам без ограничений. Внешний код может получить доступ только к m_public
. Производные классы могут обращаться к m_public
и m_protected
.
class D2 : private Base // обратите внимание: закрытое наследование
{
// Закрытое наследование означает:
// Открытые унаследованные члены становятся закрытыми.
// Защищенные унаследованные члены становятся закрытыми.
// Закрытые унаследованные члены остаются недоступными
public:
int m_public2;
protected:
int m_protected2;
private:
int m_private2;
};
D2
может получить доступ к своим членам без ограничений. D2
может получить доступ к членам Base m_public
и m_protected
, но не к m_private
. Поскольку D2
наследуется от Base
закрыто, при доступе через D2
члены m_public
и m_protected
теперь считаются закрытыми. Это означает, что внешний код не может получить доступ к этим переменным при использовании объекта D2
, равно как и никакие классы, производные от D2
.
class D3 : public D2
{
// Открытое наследование означает:
// Открытые унаследованные члены остаются открытыми.
// Защищенные унаследованные члены остаются защищенными
// Закрытые унаследованные члены остаются недоступными
public:
int m_public3;
protected:
int m_protected3;
private:
int m_private3;
};
D3
может получить доступ к своим собственным членам без ограничений. D3
может получить доступ к членам m_public2
и m_protected2
класса D2
, но не к m_private2
. Поскольку D3
наследуется от D2
открыто, при доступе через D3
члены m_public2
и m_protected2
сохраняют свои спецификаторы доступа. D3
не имеет доступа к m_private
класса Base
, который в Base
уже был закрытым. У него также нет доступа к членам m_protected
или m_public
класса Base
, которые стали закрытыми, когда D2
унаследовал их.
Резюме
Способ взаимодействия спецификаторов доступа, типов наследования и производных классов вызывает большую путаницу. Чтобы попытаться прояснить ситуацию как можно больше:
Во-первых, класс (и его друзья) всегда могут получить доступ к своим собственным ненаследуемым членам. Спецификаторы доступа влияют только на то, могут ли посторонние и производные классы получить доступ к этим членам.
Во-вторых, когда производные классы наследуют члены, эти члены могут изменять спецификаторы доступа в производном классе. Это не влияет на собственные (ненаследуемые) члены производных классов (которые имеют свои собственные спецификаторы доступа). Это влияет только на то, могут ли посторонний код и классы, производные от производного класса, получить доступ к этим унаследованным членам.
Вот таблица всех комбинаций спецификаторов доступа и типов наследования:
Спецификатор доступа в базовом классе | Спецификатор доступа при открытом наследовании | Спецификатор доступа при закрытом наследовании | Спецификатор доступа при защищенном наследовании |
---|---|---|---|
public | public | private | protected |
protected | protected | private | protected |
private | не доступен | не доступен | не доступен |
В заключение, хотя в приведенных выше примерах мы показали только примеры с использованием переменных-членов, эти правила доступа верны для всех членов (например, функций-членов и типов, объявленных внутри класса).