На предыдущих уроках мы говорили о том, как работает наследование в языке C++. Во всех наших примерах мы использовали открытое наследование.
На этом уроке мы рассмотрим детально этот тип наследования, а также два других типа: private и protected. Также поговорим о том, как эти типы наследований взаимодействуют со спецификаторами доступа для разрешения или ограничения доступа к членам.
Спецификатор доступа protected
Мы уже рассматривали спецификаторы доступа private и public, которые определяют, кто может иметь доступ к членам класса. В качестве напоминания: доступ к public-членам открыт для всех, к private-членам доступ имеют только члены того же класса, в котором находится private-член. Это означает, что дочерние классы не могут напрямую обращаться к private-членам родительского класса!
1 2 3 4 5 6 7 |
class Parent { private: int m_private; // доступ к этому члену есть только у других членов класса Parent и у дружественных классов/функций (но не у дочерних классов) public: int m_public; // доступ к этому члену открыт для всех объектов }; |
Всё просто.
Примечание: public = «открытый», private = «закрытый», protected = «защищенный».
В языке C++ есть третий спецификатор доступа, о котором мы еще не говорили, так как он полезен только в контексте наследования. Спецификатор доступа protected открывает доступ к членам класса дружественным и дочерним классам. Доступ к protected-члену вне тела класса закрыт.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class Parent { public: int m_public; // доступ к этому члену открыт для всех объектов private: int m_private; // доступ к этому члену открыт только для других членов класса Parent и для дружественных классов/функций (но не для дочерних классов) protected: int m_protected; // доступ к этому члену открыт для других членов класса Parent, дружественных классов/функций, дочерних классов }; class Child: public Parent { public: Child() { m_public = 1; // разрешено: доступ к открытым членам родительского класса из дочернего класса m_private = 2; // запрещено: доступ к закрытым членам родительского класса из дочернего класса m_protected = 3; // разрешено: доступ к защищенным членам родительского класса из дочернего класса } }; int main() { Parent parent; parent.m_public = 1; // разрешено: доступ к открытым членам класса извне parent.m_private = 2; // запрещено: доступ к закрытым членам класса извне parent.m_protected = 3; // запрещено: доступ к защищенным членам класса извне } |
В примере, приведенном выше, вы можете видеть, что член m_protected
класса Parent напрямую доступен дочернему классу Child, но доступ к нему для членов извне — закрыт.
Когда следует использовать спецификатор доступа protected?
К protected-членам родительского класса доступ открыт для членов дочернего класса, а это означает, что если вы позже измените что-либо в protected-члене (тип данных, значение и т.д.), то вам придется внести изменения как в родительский, так и во все дочерние классы. Поэтому использование спецификатора доступа protected наиболее полезно, когда вы будете наследовать только свои же классы и количество дочерних классов будет небольшое. Таким образом, если вы внесете изменения в реализацию родительского класса, и вам понадобится обновить все дочерние классы, то вы сможете сделать эти обновления сами и это не займет много времени (так как дочерних классов будет немного).
Создание private-членов предоставляет лучшую инкапсуляцию и изолирует родительские классы от изменений, вызванных дочерними классами. Но цена этому — дополнительное создание открытого или защищенного интерфейса (способа взаимодействия других объектов с классами и их членами, т.е. геттеры и сеттеры). Это дополнительная работа, которая не стоит того, если вы сами работаете со своими же классами (чужие классы не обращаются к вашему классу) и количество дочерних классов небольшое.
Типы наследований. Доступ к членам
Существует три типа наследований классов:
public;
private;
protected.
Для определения типа наследования нужно просто указать нужное ключевое слово возле наследуемого класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Открытое наследование class Pub: public Parent { }; // Закрытое наследование class Pri: private Parent { }; // Защищенное наследование class Pro: protected Parent { }; class Def: Parent // по умолчанию язык C++ устанавливает закрытое наследование { }; |
Если вы сами не определили тип наследования, то в языке C++ по умолчанию будет выбран тип наследования private (аналогично и для членов класса, которые по умолчанию являются private, если не указано иначе).
Это дает нам 9 комбинаций: 3 спецификатора доступа (public, private и protected) и 3 типа наследования (public, private и protected).
Так в чем же разница между ними? Если вкратце, то при наследовании спецификатор доступа члена родительского класса может быть изменен в дочернем классе (в зависимости от типа наследования). Другими словами, члены, которые были public- или protected- в родительском классе, могут стать private- в дочернем классе.
Это может показаться немного запутанным, но всё не так уж плохо. Мы сейчас со всем этим разберемся, но перед этим вспомним следующие правила:
Класс всегда имеет доступ к своим (не наследуемым) членам.
Доступ к члену класса основывается на его спецификаторе доступа.
Дочерний класс имеет доступ к унаследованным членам родительского класса на основе спецификатора доступа этих членов в родительском классе.
Наследование типа public
Открытое наследование является одним из наиболее используемых типов наследования. Очень редко вы увидите или будете использовать другие типы, поэтому основной упор следует сделать на понимание именно этого типа наследования. К счастью, открытое наследование является самым легким и простым из всех типов. Когда вы открыто наследуете родительский класс, то унаследованные public-члены остаются public, унаследованные protected-члены остаются protected, а унаследованные private-члены остаются недоступными для дочернего класса. Ничего не меняется.
Спецификатор доступа в родительском классе | Спецификатор доступа при наследовании типа public в дочернем классе |
public | public |
private | Недоступен |
protected | protected |
Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
class Parent { public: int m_public; private: int m_private; protected: int m_protected; }; class Pub: public Parent // открытое наследование { // Открытое наследование означает, что: // - public-члены остаются public в дочернем классе; // - protected-члены остаются protected в дочернем классе; // - private-члены остаются недоступными в дочернем классе. public: Pub() { m_public = 1; // разрешено: доступ к m_public открыт m_private = 2; // запрещено: доступ к m_private в дочернем классе из родительского класса закрыт m_protected = 3; // разрешено: доступ к m_protected в дочернем классе из родительского класса открыт } }; int main() { Parent parent; parent.m_public = 1; // разрешено: m_public доступен извне через родительский класс parent.m_private = 2; // запрещено: m_private недоступен извне через родительский класс parent.m_protected = 3; // запрещено: m_protected недоступен извне через родительский класс Pub pub; pub.m_public = 1; // разрешено: m_public доступен извне через дочерний класс pub.m_private = 2; // запрещено: m_private недоступен извне через дочерний класс pub.m_protected = 3; // запрещено: m_protected недоступен извне через дочерний класс } |
Правило: Используйте открытое наследование, если у вас нет веских причин делать иначе.
Наследование типа private
При закрытом наследовании все члены родительского класса наследуются как закрытые. Это означает, что private-члены остаются недоступными, а protected- и public-члены становятся private в дочернем классе.
Обратите внимание, это не влияет на то, как дочерний класс получает доступ к членам родительского класса! Это влияет только на то, как другими объектами осуществляется доступ к этим членам через дочерний класс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
class Parent { public: int m_public; private: int m_private; protected: int m_protected; }; class Priv: private Parent // закрытое наследование { // Закрытое наследование означает, что: // - public-члены становятся private (m_public теперь private) в дочернем классе; // - protected-члены становятся private (m_protected теперь private) в дочернем классе; // - private-члены остаются недоступными (m_private недоступен) в дочернем классе. public: Priv() { m_public = 1; // разрешено: m_public теперь private в Priv m_private = 2; // запрещено: дочерние классы не имеют доступ к закрытым членам родительского класса m_protected = 3; // разрешено: m_protected теперь private в Priv } }; int main() { Parent parent; parent.m_public = 1; // разрешено: m_public доступен извне через родительский класс parent.m_private = 2; // запрещено: m_private недоступен извне через родительский класс parent.m_protected = 3; // запрещено: m_protected недоступен извне через родительский класс Priv priv; priv.m_public = 1; // запрещено: m_public недоступен извне через дочерний класс priv.m_private = 2; // запрещено: m_private недоступен извне через дочерний класс priv.m_protected = 3; // запрещено: m_protected недоступен извне через дочерний класс } |
Итого:
Спецификатор доступа в родительском классе | Спецификатор доступа при наследовании типа private в дочернем классе |
public | private |
private | Недоступен |
protected | private |
Закрытое наследование может быть полезно, когда дочерний класс не имеет очевидной связи с родительским классом, но использует его в своей реализации. В таком случае мы не хотим, чтобы открытый интерфейс родительского класса был доступен через объекты дочернего класса (как это было, когда мы использовали открытый тип наследования).
На практике наследование типа private используется редко.
Наследование типа protected
Этот тип наследования почти никогда не используется, за исключением особых случаев. С защищенным наследованием, public- и protected-члены становятся protected, а private-члены остаются недоступными.
Поскольку этот тип наследования очень редко используется, то мы пропустим пример на практике и сразу перейдем к таблице:
Спецификатор доступа в родительском классе | Спецификатор доступа при наследовании типа protected в дочернем классе |
public | protected |
private | Недоступен |
protected | protected |
Финальный пример
1 2 3 4 5 6 7 8 9 |
class Parent { public: int m_public; private: int m_private; protected: int m_protected; }; |
Класс Parent может обращаться к своим членам беспрепятственно. Доступ к m_public
открыт для всех. Дочерние классы могут обращаться как к m_public
, так и к m_protected
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class D2 : private Parent // закрытое наследование { // Закрытое наследование означает, что: // - public-члены становятся private в дочернем классе; // - protected-члены становятся private в дочернем классе; // - private-члены недоступны для дочернего класса. public: int m_public2; private: int m_private2; protected: int m_protected2; }; |
Класс D2 может беспрепятственно обращаться к своим членам. D2 имеет доступ к членам m_public
и m_protected
класса Parent, но не к m_private
. Поскольку D2 наследует класс Parent закрыто, то m_public
и m_protected
теперь становятся закрытыми при доступе через D2. Это означает, что другие объекты не смогут получить доступ к этим членам через использование объекта D2, а также любые другие классы, которые будут дочерними классу D2, не будут иметь доступ к этим членам:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class D3 : public D2 { // Открытое наследование означает, что: // - унаследованные public-члены остаются public в дочернем классе; // - унаследованные protected-члены остаются protected в дочернем классе; // - унаследованные private-члены остаются недоступными в дочернем классе. public: int m_public3; private: int m_private3; protected: int m_protected3; }; |
Класс D3 может беспрепятственно обращаться к своим членам. D3 имеет доступ к членам m_public2
и m_protected2
класса D2, но не к m_private2
. Поскольку D3 наследует D2 открыто, то m_public2
и m_protected2
сохраняют свои спецификаторы доступа и остаются public и protected при доступе через D3. D3 не имеет доступ к m_private
класса Parent. Он также не имеет доступ к m_protected
или m_public
класса Parent, оба из которых стали закрытыми, когда D2 унаследовал их.
Заключение
Способ взаимодействия спецификаторов доступа, типов наследования и дочерних классов может вызывать путаницу. Чтобы это устранить, проясним всё еще раз:
Во-первых, класс всегда имеет доступ к своим собственным не унаследованным членам (и дружественные ему классы также имеют доступ). Спецификаторы доступа влияют только на то, могут ли объекты вне класса и дочерние классы обращаться к этим членам.
Во-вторых, когда дочерние классы наследуют члены родительских классов, то члены родительского класса могут изменять свои спецификаторы доступа в дочернем классе. Это никак не влияет на собственные (не наследуемые) члены дочерних классов (которые определены в дочернем классе и имеют свои собственные спецификаторы доступа). Это влияет только на то, могут ли объекты извне и классы, дочерние нашим дочерним классам, получить доступ к унаследованным членам родительского класса.
Общая таблица спецификаторов доступа и типов наследования:
Спецификатор доступа в родительском классе | Спецификатор доступа при наследовании типа public в дочернем классе | Спецификатор доступа при наследовании типа private в дочернем классе | Спецификатор доступа при наследовании типа protected в дочернем классе |
public | public | private | protected |
private | Недоступен | Недоступен | Недоступен |
protected | protected | private | protected |
Хотя в вышеприведенных примерах мы рассматривали использование переменных-членов, эти правила выполняются для всех членов классов (и для методов, и для типов, объявленных внутри класса).
Спасибо. все понятно и логически интуитивно. Очень редко можно встретить понятное с первого раза объяснение урока.
Очень редко можно встретить понятное с первого раза объяснение урока. У Вас все понятно и просто, а значит, применимо на практике. Спасибо
Отличное объяснение, как и большинство уроков на вашем сайте. Прошу вас продолжайте в таком же стиле. У вас определенно есть методический талант. Всем рекомендую!
Спасибо, очень приятно 🙂
Я еще никогда не видел настолько подробного, хорошего урока(кроме остальных на данном сайте). Я не знал, что protected открыт для дружественных классов и много чего другого. Блин, вы заморачиваетесь над картинками, которые у вас отменно получаются. Может, они и не много занимают времени и сил(я думаю, это не так), но все таки это старание.
Спасибо! Приятно, что Равесли вас не разочаровывает 🙂
Спасибо. Ничего запутанного, все понятно и логически интуитивно.