Наследование и cпецификатор доступа protected в С++ | Уроки С++
Обновл. 14 Авг 2020 |
На предыдущих уроках мы говорили о том, как работает наследование в языке C++. Во всех наших примерах мы использовали открытое наследование.
На этом уроке мы рассмотрим детально этот тип наследования, а также два других типа: private и protected. Также поговорим о том, как эти типы наследований взаимодействуют со спецификаторами доступа для разрешения или ограничения доступа к членам.
Спецификатор доступа protected
Мы уже рассматривали спецификаторы доступа private и public, которые определяют, кто может иметь доступ к членам класса. В качестве напоминания: доступ к членам public открыт для всех, к членам private доступ имеют только члены того же класса, в котором находится член private. Это означает, что дочерние классы не могут напрямую обращаться к членам private родительского класса!
class Parent { private: int m_private; // доступ к этому члену есть только у других членов класса Parent и у дружественных классов/функций (но не у дочерних классов) public: int m_public; // доступ к этому члену открыт для всех объектов };
class Parent { private: int m_private; // доступ к этому члену есть только у других членов класса Parent и у дружественных классов/функций (но не у дочерних классов) public: int m_public; // доступ к этому члену открыт для всех объектов }; |
Всё просто.
Примечание: public = «открытый», private = «закрытый», protected = «защищенный».
В языке C++ есть третий спецификатор доступа, о котором мы еще не говорили, так как он полезен только в контексте наследования. Спецификатор доступа protected открывает доступ к членам класса дружественным и дочерним классам. Доступ к члену protected вне тела класса закрыт.
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; // запрещено: доступ к защищенным членам класса извне }
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.
Для определения типа наследования нужно просто указать нужное ключевое слово возле наследуемого класса:
// Открытое наследование class Pub: public Parent { }; // Закрытое наследование class Pri: private Parent { }; // Защищенное наследование class Pro: protected Parent { }; class Def: Parent// по умолчанию язык C++ устанавливает закрытое наследование { };
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 |
Например:
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 недоступен извне через дочерний класс }
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 в дочернем классе.
Обратите внимание, это не влияет на то, как дочерний класс получает доступ к членам родительского класса! Это влияет только на то, как другими объектами осуществляется доступ к этим членам через дочерний класс:
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 недоступен извне через дочерний класс }
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 |
Финальный пример
class Parent { public: int m_public; private: int m_private; protected: int m_protected; };
class Parent { public: int m_public; private: int m_private; protected: int m_protected; }; |
Класс Parent может обращаться к своим членам беспрепятственно. Доступ к m_public
открыт для всех. Дочерние классы могут обращаться как к m_public
, так и к m_protected
:
class D2 : private Parent // закрытое наследование { // Закрытое наследование означает, что: // — члены public становятся private в дочернем классе; // — члены protected становятся private в дочернем классе; // — члены private недоступны для дочернего класса. public: int m_public2; private: int m_private2; protected: int m_protected2; };
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, не будут иметь доступ к этим членам:
class D3 : public D2 { // Открытое наследование означает, что: // — унаследованные члены public остаются public в дочернем классе; // — унаследованные члены protected остаются protected в дочернем классе; // — унаследованные члены private остаются недоступными в дочернем классе. public: int m_public3; private: int m_private3; protected: int m_protected3; };
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 |
Хотя в вышеприведенных примерах мы рассматривали использование переменных-членов, эти правила выполняются для всех членов классов (и для методов, и для типов, объявленных внутри класса).
Оценить статью:
Загрузка…Поделиться в социальных сетях:
Наследование (программирование) — это… Что такое Наследование (программирование)?
Насле́дование — механизм объектно-ориентированного программирования (наряду с инкапсуляцией, полиморфизмом и абстракцией), позволяющий описать новый класс на основе уже существующего (родительского), при этом свойства и функциональность родительского класса заимствуются новым классом.
Другими словами, класс-наследник реализует спецификацию уже существующего класса (базовый класс). Это позволяет обращаться с объектами класса-наследника точно так же, как с объектами базового класса.
Типы наследования
Простое наследование
Класс, от которого произошло наследование, называется базовым или родительским (англ. base class). Классы, которые произошли от базового, называются потомками, наследниками или производными классами (англ. derived class).
В некоторых языках используются абстрактные классы. Абстрактный класс — это класс, содержащий хотя бы один абстрактный метод, он описан в программе, имеет поля, методы и не может использоваться для непосредственного создания объекта. То есть от абстрактного класса можно только наследовать. Объекты создаются только на основе производных классов, наследованных от абстрактного. Например, абстрактным классом может быть базовый класс «сотрудник вуза», от которого наследуются классы «аспирант», «профессор» и т. д. Так как производные классы имеют общие поля и функции (например, поле «год рождения»), то эти члены класса могут быть описаны в базовом классе. В программе создаются объекты на основе классов «аспирант», «профессор», но нет смысла создавать объект на основе класса «сотрудник вуза».
Множественное наследование
При множественном наследовании у класса может быть более одного предка. В этом случае класс наследует методы всех предков. Достоинства такого подхода в большей гибкости. Множественное наследование реализовано в C++. Из других языков, предоставляющих эту возможность, можно отметить Python и Эйфель. Множественное наследование поддерживается в языке UML.
Множественное наследование — потенциальный источник ошибок, которые могут возникнуть из-за наличия одинаковых имен методов в предках. В языках, которые позиционируются как наследники C++ (Java, C# и др.), от множественного наследования было решено отказаться в пользу интерфейсов. Практически всегда можно обойтись без использования данного механизма. Однако, если такая необходимость все-таки возникла, то, для разрешения конфликтов использования наследованных методов с одинаковыми именами, возможно, например, применить операцию расширения видимости — «::» — для вызова конкретного метода конкретного родителя.
Попытка решения проблемы наличия одинаковых имен методов в предках была предпринята в языке Эйфель, в котором при описании нового класса необходимо явно указывать импортируемые члены каждого из наследуемых классов и их именование в дочернем классе.
Большинство современных объектно-ориентированных языков программирования (C#, Java, Delphi и др.) поддерживают возможность одновременно наследоваться от класса-предка и реализовать методы нескольких интерфейсов одним и тем же классом. Этот механизм позволяет во многом заменить множественное наследование — методы интерфейсов необходимо переопределять явно, что исключает ошибки при наследовании функциональности одинаковых методов различных классов-предков.
Единый базовый класс
В ряде языков программирования все классы явно или неявно наследуются от некого базового класса. Smalltalk был одним из первых языков, в которых использовалась эта концепция. К таким языкам относятся Objective-C (NSObject
), Perl (UNIVERSAL
), Eiffel (ANY
), Java (java.lang.Object
), C# (System.Object
), Delphi (TObject
).
Наследование в языках программирования
Visual Basic
Наследование в Visual Basic:
Class A 'базовый класс End Class Class B : Inherits A 'наследование от A End Class Noninheritable Class C 'Класс, который нельзя наследовать (final в Java) End Class MustInherit Class Z 'Класс, который обязательно наследовать (абстрактный класс) End Class
C++
Наследование в C++:
class A{ //базовый класс }; class B : public A{ //public наследование }; class C : protected A{ //protected наследование }; class Z : private A{ //private наследование };
В C++ существует три типа наследования: public, protected, private. Спецификаторы доступа членов базового класса меняются в потомках следующим образом:
ANSI ISO IEC 14882 2003
Если класс объявлен как базовый для другого класса со спецификатором доступа public, тогда public члены базового класса доступны как public члены производного класса, protected члены базового класса доступны как protected члены производного класса.
Если класс объявлен как базовый для другого класса со спецификатором доступа protected, тогда public и protected члены базового класса доступны как protected члены производного класса.
Если класс объявлен как базовый для другого класса со спецификатором доступа private, тогда public и protected члены базового класса доступны как private члены производного класса.
\ANSI ISO IEC 14882 2003
Одним из основных преимуществ public-наследования является то, что указатель на классы-наследники может быть неявно преобразован в указатель на базовый класс, то есть для примера выше можно написать:
Эта интересная особенность открывает возможность динамической идентификации типа (RTTI).
Delphi (Object Pascal)
Для использования механизма наследования в Delphi необходимо в объявлении класса справа от слова class
указать класс предок:
Предок:
TAncestor = class private protected public // Виртуальная процедура procedure VirtualProcedure; virtual; abstract; procedure StaticProcedure; end;
Наследник:
TDescendant = class(TAncestor) private protected public // Перекрытие виртуальной процедуры procedure VirtualProcedure; override; procedure StaticProcedure; end;
Абсолютно все классы в Delphi являются потомками класса TObject
. Если класс-предок не указан, то подразумевается, что новый класс является прямым потомком класса TObject
.
Множественное наследование в Delphi частично поддерживается за счёт использования классов-помощников (Сlass Helpers).
Python
Python поддерживает как одиночное, так и множественное наследование. При доступе к атрибуту порядок просмотра производных классов называется порядком разрешения метода (англ. method resolution order)[1].
class Ancestor1(object): # Предок 1 def m1(self): pass class Ancestor2(object): # Предок 2 def m1(self): pass class Descendant(Ancestor1, Ancestor2): # Наследник def m2(self): pass d = Descendant() # инстанциация print d.__class__.__mro__ # порядок разрешения метода:
(<class '__main__.Descendant'>, <class '__main__.Ancestor1'>, <class '__main__.Ancestor2'>, <type 'object'>)
С версии Python 2.2 в языке сосуществуют «классические» классы и «новые» классы. Последние являются наследниками object
. «Классические» классы будут поддерживаться вплоть до версии 2.6, но удалены из языка в Python версии 3.0.
Множественное наследование применяется в Python, в частности, для введения в основной класс классов-примесей (англ. mix-in).
PHP
Для использования механизма наследования в PHP необходимо в объявлении класса после имени объявляемого класса-наследника указать слово extends
и имя класса-предка:
class Descendant extends Ancestor { }
В случае перекрытия классом-наследником свойств и методов предка, доступ к свойствам и методам предка можно получить с использованием ключевого слова parent
:
class A { function example() { echo "Вызван метод A::example().<br />\n"; } } class B extends A { function example() { echo "Вызван метод B::example().<br />\n"; parent::example(); } }
Objective-C
@interface MyNumber : NSObject { int num; } - (int) num; - (void) setNum: (int) theNum; @end @implementation - (id) init { self = [super init]; return self; } - (int) num { return num; } - (void) setNum: (int) theNum { num = theNum; } @end
Переопределенные методы не нужно объявлять в интерфейсе.
Java
Пример наследования от одного класса и двух интерфейсов:
public class A { } public interface I1 { } public interface I2 { } public class B extends A implements I1, I2 { }
Директива final в объявлении класса делает наследование от него невозможным.
C#
Пример наследования от одного класса и двух интерфейсов:
public class A { } public interface I1 { } public interface I2 { } public class B : A, I1, I2 { }
Наследование от типизированных классов можно осуществлять, указав фиксированный тип, либо путем переноса переменной типа в наследуемый класс:
public class A<T> { } public class B : A<int> { } public class B2<T> : A<T> { }
Допустимо также наследование вложенных классов от классов, их содержащих:
class A { public class B : A { } }
Директива sealed в объявлении класса делает наследование от него невозможным.[2]
Ruby
class Parent def public_method "Public method" end private def private_method "Private method" end end class Children < Parent def public_method "Redefined public method" end def call_private_method "Ancestor's private method: " + private_method end end
Класс Parent является предком для класса Children, у которого переопределен метод public_method.
children = Children.new children.public_method #=> "Redefined public method" children.call_private_method #=> "Ancestor's private method: Private method"
Приватные методы предка можно вызывать из наследников.
JavaScript
var Parent = function( data ) { this.data = data || false; this.public_method = function() { return 'Public Method'; } } var Child = function() { this.public_method = function() { return 'Redefined public method'; } this.getData = function() { return 'Data: ' + this.data; } } Child.prototype = new Parent('test'); var Test = new Child(); Test.getData(); // => "Data: test" Test.public_method(); // => "Redefined public method" Test.data; // => "test"
Класс Parent является предком для класса Children, у которого переопределен метод public_method. В JavaScript используется прототипное наследование.
Конструкторы и деструкторы
В С++ конструкторы при наследовании вызываются последовательно от самого раннего предка до самого позднего потомка, а деструкторы наоборот — от самого позднего потомка до самого раннего предка.
class First { public: First() { cout << ">>First constructor" << endl; } ~First() { cout << ">>First destructor" << endl; } }; class Second: public First { public: Second() { cout << ">Second constructor" << endl; } ~Second() { cout << ">Second destructor" << endl; } }; class Third: public Second { public: Third() { cout << "Third constructor" << endl; } ~Third() { cout << "Third destructor" << endl; } }; // выполнение кода Third *th = new Third(); delete th; // результат вывода /* >>First constructor >Second constructor Third constructor Third destructor >Second destructor >>First destructor */
См. также
Примечания
Ссылки
Композиция или наследование: как выбрать? / Хабр
В начале…
… не было ни композиции, ни наследования, только код.
И был код неповоротливым, повторяющимся, нераздельным, несчастным, избыточным и измученным.
Основным инструментом для повторного использования кода была копипаста. Процедуры и функции были редкостью, подозрительными новомодными штучками. Вызов процедур был дорогим удовольствием. Части кода, отделенные от основной логики, вызывали недоумение!
Мрачные были времена.
Но вот лучик ООП воссиял над миром… Правда, несколько десятилетий1 никто этого не замечал. Покуда не появился графический интерфейс2, которому, как выяснилось, очень-очень не хватало ООП. Когда нажимаешь на кнопку в окне, что может быть проще, чем отправить кнопке (или ее представителю) сообщение «Нажатие»3 и получить результат?
И вот тут ООП взлетел. Было написано множество4 книг, расплодились бесчисленные5 статьи. Так что сегодня-то каждый может в объектно-ориентированное программирование, так?
Увы, код (и интернет) говорит, что не так
Самые жаркие споры и наибольшее непонимание, похоже, вызывает выбор между композицией и наследованием, зачастую выраженный мантрой «предпочитайте композицию наследованию». Вот об этом и поговорим.
Когда мантры вредят
В житейском плане «предпочитать композицию наследованию» в целом нормально, хоть я и не любитель мантр. Несмотря на то, что они зачастую и несут зерно истины, слишком легко поддаться соблазну и бездумно следовать лозунгу, не понимая, что за ним скрывается. А это всегда выходит боком.
Желтушные статьи с заголовками вроде «Наследование — зло»6 тоже не по мне, особенно если автор пытается обосновать свои набросы, сначала неправильно применяя наследование, а потом делая вывод, что оно во всем виновато. Ну типа «молотки — отстой, потому что ими нельзя завинтить шуруп.»
Начнем с основ.
Определения
Далее в статье я буду понимать под ООП «классический» объектный язык, который поддерживает классы со свойствами, методами и простое (одиночное) наследование. Никаких вам интерфейсов, примесей, аспектов, множественного наследования, делегатов, замыканий, лямбд, — ничего, кроме самых простых вещей:
- Класс: именованная сущность из предметной области, возможно, имеющая предка (суперкласс), определенная как набор полей и методов.
- Поле: именованное свойство с определенным типом, которое может, в частности, ссылаться на другой объект (см. композиция).
- Метод: именованная функция или процедура, с параметрами или без них, реализующая какое-то поведение класса.
- Наследование: класс может унаследовать — использовать по умолчанию — поля и методы своего предка. Наследование транзитивно: класс может наследоваться от другого класса, который наследуется от третьего, и так далее вплоть до базового класса (обычно —
Object
), возможно, неявного. Наследник может переопределить какие-то методы и поля чтобы изменить поведение по умолчанию. - Композиция: если поле у нас имеет тип Класс, оно может содержать ссылку на другой объект этого класса, создавая таким образом связь между двумя объектами. Не влезая в дебри различий между простой ассоциацией, агрегированием и композицией, давайте «на пальцах» определим: композиция — это когда один объект предоставляет другому свою функциональность частично или полностью.
- Инкапсуляция: мы обращаемся с объектами как с единой сущностью, а не как с набором отдельных полей и методов, тем самым скрываем и защищаем реализацию класса. Если клиентский код не знает ничего, кроме публичного интерфейса, он не может зависеть от деталей реализации.
Наследование фундаментально
Наследование — это фундаментальное понятие ООП. В языке программирования могут быть объекты и сообщения, но без наследования он не будет объектно-ориентированным (только основанным на объектах, но все еще полиморфным).
… как и композиция
Композиция это тоже фундаментальное свойство, причем любого языка. Даже если язык не поддерживает композицию (что редкость в наши дни), люди все равно будут мыслить категориями частей и компонентов. Без композиции было бы невозможно решить сложные задачи по частям.
(Инкапсуляция тоже вещь фундаментальная, но сейчас речь не о ней)
Так от чего весь сыр-бор?
Ну хорошо, и композиция, и наследование фундаментальны, в чем дело-то?
А дело в том, что можно подумать, что одно всегда может заменить другое, или что первое лучше или хуже второго. Разработка ПО — это всегда выбор разумного баланса, компромисс.
С композицией все более-менее просто, мы с ней постоянно сталкиваемся в жизни: у стула есть ножки, стена состоит из кирпичей и цемента и тому подобное. А вот наследование, несмотря на свое простое определение, может все усложнить и запутать, если хорошенько не поразмыслить над тем, как его применять. Наследование это весьма абстрактная штука, о нем можно рассуждать, но так просто его не потрогаешь. Мы, конечно, можем сымитировать наследование, используя композицию, но это, как правило, слишком много возни. Для чего нужна композиция — очевидно: из частей собрать целое. А вот с наследованием сложнее, потому что оно сразу о двух вещах: о смысле и о механике.
Наследование смысловое
Как в биологии классификация таксонов организует их в иерархии, так наследование отражает иерархию понятий из предметной области. Упорядочивает их от общего к частному, собирает родственные идеи в ветви иерархического древа. Смысл (семантика) класса по большей части выражен в его интерфейсе — наборе сообщений, которые класс способен понять, но также определяется и теми сообщениями, которыми класс отвечает. Унаследовался от предка — будь добр не только понять все сообщения, которые мог понять предок, но также и уметь ответить как он (сохранить поведение предка — прим. пер.) И поэтому наследование связывает наследника с предком гораздо сильнее, чем если бы мы взяли просто экземпляр предка как компонент. Обратите внимание, даже если класс делает что-то совсем простое, почти не имеет логики, его имя несет существенную смысловую нагрузку, разработчик делает из него важные выводы о предметной области.
Наследование механическое
Говоря о наследовании в механическом плане, мы имеем в виду, что наследование берет данные (поля) и поведение (методы) базового класса и позволяет использовать их повторно или же дополнить в наследниках. С точки зрения механики, если потомок унаследует реализацию (код) предка, то неизбежно получит и его интерфейс.
Я уверен, что в недопонимании виновата именно эта двойственная природа наследования7 в большинстве ОО-языков. Многие считают, что наследование — это чтобы повторно использовать код, хотя оно не только для этого. Если придавать повторному использованию чрезмерное значение — жди беды в архитектуре. Вот пара примеров.
Как не надо наследовать. Пример 1
class Stack extends ArrayList {
public void push(Object value) { … }
public Object pop() { … }
}
Казалось бы, класс Stack
, все хорошо. Но посмотрите внимательно на его интерфейс. Что должно быть в классе с именем Stack? Методы push()
и pop()
, что же еще. А у нас? У нас есть get()
, set()
, add()
, remove()
, clear()
и еще куча барахла, доставшегося от ArrayList
, которое стеку ну вообще не нужно.
Можно было бы переопределить все нежелательные методы, а некоторые (например, clear()
) даже и адаптировать под наши нужды, но не многовато ли работы из-за одной ошибки в дизайне? На самом деле трех: одной смысловой, одной механической и одной комбинированной:
- Утверждение «Stack это ArrayList» ложно.
Stack
не является подтипомArrayList
. Задача стека — обеспечить выполнение правила LIFO (последним пришел, первым ушел), которое легко удовлетворяется интерфейсом push/pop, но никак не соблюдается интерфейсомArrayList
. - Механически наследование от
ArrayList
нарушает инкапсуляцию. Клиентскому коду не должно быть известно, что мы решили использоватьArrayList
для хранения элементов стека. - Ну и наконец, реализуя стек через
ArrayList
мы смешиваем две разные предметные области:ArrayList
— это коллекция с произвольным доступом, а стек — это понятие из мира очередей, со строго ограниченным (а не произвольным)8 доступом.
Последний пункт — незначительная на первый взгляд, но важная вещь. Посмотрим на нее пристальнее.
Как не надо наследовать. Пример 2
Частая ошибка при наследовании — это создать модель из предметной области, унаследовав ее от готовой реализации. Вот, скажем, нам надо выделить некоторых наших клиентов (класс Customer
) в определенное подмножество. Легко! Наследуемся от ArrayList<Customer>
, называем это CustomerGroup
и понеслась.
Не тут-то было. Поступив так мы опять спутаем две предметные области. Старайтесь избегать этого:
ArrayList<Customer>
это уже наследник списка, утилиты типа «коллекция», готовой реализации.CustomerGroup
это совсем другая штука — класс из предметной области (домена).- Классы из предметной области должны использовать реализации, а не наследовать их.
Слой предметной области не должен знать, как у нас там все внутри сделано. Рассуждая о том, что делает наша программа, мы оперируем понятиями из предметной области, и мы не хотим отвлекаться на нюансы внутреннего устройства. Если видеть в наследовании только инструмент повторного использования кода, мы раз за разом будем попадаться в эту ловушку.
Дело не в одиночном наследовании
Одиночное наследование пока остается самой популярной моделью ООП. Оно неизбежно влечет наследование реализации, которое приводит к сильному зацеплению (coupling — прим. пер.) между классами. Может показаться, что беда в том, что ветка наследования у нас только одна на обе потребности: и смысловую и механическую. Если использовали для одного, то для другого уже нельзя. А раз так, может быть множественное наследование все исправит?
Нет. Отношение наследования не должно пересекать границы между предметными областями: инструментальной (структуры данных, алгоритмы, сети) и прикладной (бизнес-логика). Если CustomerGroup будет наследовать ArrayList<Customer>
и одновременно, скажем, DemographicSegment, то две предметные области переплетутся между собой, а «видовая принадлежность» объектов станет неочевидна.
Предпочтительно (по крайней мере, с моей точки зрения) делать так. Наследуемся от имеющихся в языке инструментальных классов по минимуму, ровно настолько, чтобы реализовать «механическую» часть вашей логики. Потом соединяем получившиеся части композицией, но не наследованием. Иными словами:
От инструментов можно наследовать только другие инструменты.
Это очень частая ошибка новичков. Что не удивительно, ведь так просто взять и унаследовать. Редко где встретишь обсуждения, почему именно это неправильно. Еще раз: бизнес-сущности должны пользоваться инструментами, а не быть ими. Мухи (инструменты) — отдельно, котлеты (бизнес-модели) — отдельно.
Так когда же нужно наследование?
Наследуемся как надо
Чаще всего — и при этом с наибольшей отдачей — наследование применяют для описания объектов, незначительно отличающихся друг от друга (в оригинале используется термин «differential programming» — прим. пер.) Например, нам нужна особенная кнопка с небольшими дополнениями. Нормально, наследуемся от существующего класса Кнопка. Потому что наш новый класс, это все еще кнопка, а мы полностью наследуем API класса Кнопка, его поведение и реализацию. Новая функциональность только добавляется к существующему. А вот если в наследнике часть функциональности убирается, это повод задуматься, а нужно ли наследование.
Наследование полезнее всего для группировки сходных сущностей и понятий, определения семейств классов, и вообще для организации терминов и понятий, описывающих предметную область. Зачастую, когда значительная часть предметной логики уже реализована, исходно выбранные иерархии наследования перестают работать. Если всё к тому идет, не бойтесь разобрать и заново сложить эти иерархии9 так, чтобы они лучше соответствовали и работали друг с другом.
Композиция или наследование: что выбрать?
В ситуации, когда вроде бы подходит и то и другое, взгляните на дизайн в двух плоскостях:
- Структура и механическое исполнение бизнес-объектов.
- Что они обозначают по смыслу и как взаимодействуют.
Пока наследование остается внутри одной плоскости, все нормально. Но если иерархия проходит через две плоскости сразу, это плохой симптом.
Например, у вас есть один объект внутри другого. Внутренний объект реализует значительную часть поведения внешнего. У внешнего объекта куча прокси-методов, которые тупо пробрасывают параметры во внутренний объект и возвращают от него результат. В этом случае посмотрите, а не стоит ли унаследоваться от внутреннего объекта, хотя бы частично.
Разумеется, никакие инструкции не заменят голову на плечах. Когда строишь объектную модель, вообще полезно думать. Но если вам хочется конкретных правил, то пожалуйста.
Наследуем, если:
- Оба класса из одной предметной области
- Наследник является корректным подтипом (в терминах LSP — прим. пер.) предка
- Код предка необходим либо хорошо подходит для наследника
- Наследник в основном добавляет логику
Иногда все эти условия выполняются одновременно:
- в случае моделирования высокоуровневой логики из предметной области
- при разработке библиотек и расширений для них
- при дифференциальном программировании (автор снова использует термин «differential programming», очевидно, понимая под ним нечто, отличное от DDP — прим. пер.)
Если это не ваш случай, то и наследование вам, скорее всего, будет нужно не часто. Но не потому, что надо «предпочитать» композицию наследованию, и не потому что она «лучше». Выбирайте то, что подходит наилучшим образом для конкретно вашей задачи.
Надеюсь, эти правила помогут вам понять разницу между двумя подходами.
Приятного кодинга!
Послесловие
Отдельная благодарность сотрудникам ThoughtWorks за их ценный вклад и замечания: Питу Хогсону, Тиму Брауну, Скотту Робинсону, Мартину Фаулеру, Минди Ор, Шону Ньюхэму, Сэму Гибсону и Махендре Кария.
1
Первый официальный ОО-язык, SIMULA 67, появился в 1967 году.
2
Системные и прикладные программисты приняли на вооружение C++ в середине 1980-х, но перед тем, как ООП стал общепринятым, прошел еще десяток лет.
3
Я намеренно упрощаю, не говорю про паб/саб, делегатов и тому подобное, чтобы не раздувать статью.
4
На момент написание этого текста Амазон предлагает 24777 книг по ООП.
5
Поиск в гугле по фразе «объектно-ориентированное программирование» дает 8 млн результатов.
6
Поиск в гугле выдает 37600 результатов по запросу «наследование это зло».
7
Смысл (интерфейс) и механику (исполнение) можно разделить за счет усложнения языка. См. пример из спецификации языка D.
8
С грустью замечу, что в Java Stack
унаследован от Vector
.
9
Проектирование для повторного использования через наследования выходит за рамки темы статьи. Просто имейте в виду, что ваш дизайн должен удовлетворить потребности и тех, кто пользуется базовым классом, и тех, кому нужен наследник.
Переводчик выражает благодарность ООП-чату в Telegram, без которого этот текст не смог бы появиться.
Наследование реализаций: закопайте стюардессу / Хабр
Как известно, классическое ООП покоится на трех китах:
- Инкапсуляция
- Наследование
- Полиморфизм
Классическая же реализация по умолчанию:
- Инкапсуляция — публичные и приватные члены класса
- Наследование — реализация функционала за счет расширения одного класса-предка, защищенные члены класса.
- Полиморфизм — виртуальные методы класса-предка.
Но еще в 1986 году была обозначена серьезнейшая проблема, кратко формулируемая так:
Наследование ломает инкапсуляцию
- Классу-потомку доступны защищенные члены класса-предка. Всем остальным доступен только публичный интерфейс класса. Предельный случай взлома —
антипаттерн Паблик Морозов; - Реально изменить поведение предка можно только с помощью перекрытия виртуальных методов;
- Принцип подстановки Лисков обязывает класс-потомок удовлетворять всем требованиям к классу-предку;
- Для выполнения пункта 2 в точном соответствии с пунктом 3 классу-потомку необходима полная информация о времени вызова и реализации перекрытого виртуального метода;
- Информация из пункта 4 зависит от реализации класса-предка, включая приватные члены и их код.
В теории мы уже имеем былинный отказ, но как насчет практики?
- Зависимость, создаваемая наследованием, чрезвычайно сильна;
- Наследники гиперчувствительны к любым изменениям предка;
- Наследование от чужого кода добавляет адскую боль при сопровождении: разработчики библиотеки рискуют получить обструкцию из-за поломанной обратной совместимости при малейшем изменении базового класса, а прикладники — регрессию при любом обновлении используемых библиотек.
Все, кто используют фреймворки, требующие наследования от своих классов (WinForms, WPF, WebForms, ASP.NET), легко найдут подтверждения всем трем пунктам в своем опыте.
Неужели все так плохо?
Влияние проблемы можно ослабить принятием некоторых конвенций:
1. Защищенные члены не нужны
Это соглашение ликвидирует пабликов морозовых как класс.
2. Виртуальные методы предка ничего не делают
Это соглашение позволяет сочетать знание о реализации предка с независимостью от нее реализации уже в потомке.
3. Виртуальные методы предка никогда не вызываются в его коде
Это соглашение позволяет потомкам не зависеть от внутренней реализации предка, а также требует публичности всех виртуальных методов.
4. Экземпляры предка никогда не создаются
Это соглашение позволяет избавиться от несоответствия требований к виртуальными методам (публичный контракт класса) с одной стороны и обязанностью ничего не делать (защищенный контракт класса) с другой. Теперь принцип подстановки Лисков можно соблюсти, не вступая в порочную связь с закрытым содержимым предка.
5. Невиртуальных членов у предка нет
С учетом предыдущих соглашений невиртуальные члены предка становятся бесполезными и подлежат ликвидации.
Результат: если класс-предок состоит из публичных виртуальных пустых методов и требований к ним для потомков, то наследование уже не ломает инкапсуляцию. Что и требовалось доказать.
Попутно получаем возможность решение проблемы ромба для случая множественного наследования от конвенционных предков. Но это все теория, а нам нужны…
- Виртуальные методы-пустышки уже есть во многих языках и носят гордое звание абстрактных.
- Классы, экземпляры которых создавать нельзя, тоже есть во многих языках и даже имеют то же звание.
- Полное соблюдение указанных соглашений в языке C++ использовалось как паттерн для проектирования и реализации Component Object Model.
- Ну и самое приятное: в C# и многих других языках соглашения реализованы как первоклассный элемент «интерфейс».
Происхождение названия очевидно — в результате соблюдения соглашений от класса остается только его публичный интерфейс. И если множественное наследование от обычных классов — редкость, то от интерфейсов оно доступно без всяких ограничений.
- Языки, где нет наследования от классов, но есть — от интерфейсов (например, Go), нельзя лишать звания объектно-ориентированных. Более того, такая реализация ООП правильнее теоретически и безопаснее практически.
- Наследование от обычных классов (имеющих реализацию) — чрезвычайно специфический и крайне опасный архаизм.
- Избегайте наследования реализаций без крайней необходимости.
- Используйте модификатор sealed (для .NET) или его аналог для всех классов, кроме специально спроектированных для наследования реализации.
- Избегайте публичных незапечатанных классов: пока наследование не выходит за рамки своих сборок, из него еще можно извлечь пользу и ограничить вред.
PS: Дополнения и критика традиционно приветствуются.
Наследованиев Java OOP с примером
- Home
Testing
- Back
- Agile Testing
- BugZilla
- Cucumber
- Database Testing 9000 9000 J4000
- 9000 J4 Назад
- JUnit
- LoadRunner
- Ручное тестирование
- Мобильное тестирование
- Mantis
- Почтальон
- QTP
- Назад
- Центр качества (ALM)
- Центр качества SAPU
- Управление тестированием
- TestLink
SAP
- Назад 9 0004 ABAP
- APO
- Начинающий
- Basis
- BODS
- BI
- BPC
- CO
- Назад
- CRM
- Crystal Reports
- Crystal Reports
- FICO
- Заработная плата
- Назад
- PI / PO
- PP
- SD
- SAPUI5
- Безопасность
- Менеджер решений
- Successfactors
- SAP Back Tutorials
- Назад
- Java
- JSP
- Kotlin
- Linux Botlin
- Linux js
- Perl
- Назад
- PHP
- PL / SQL
- PostgreSQL
- Python
- ReactJS
- Ruby & Rails
- Scala
- SQL 0000004 SQL
- UML
- VB.Net
- VBScript
- Веб-службы
- WPF
Обязательно учите!
- Назад
- Бухгалтерский учет
- Алгоритмы
- Android
- Блокчейн
- Business Analyst
- Веб-сайт сборки
- CCNA
- Облачные вычисления
- COBOL 9000 Compiler
- 0005
- Ethical Hacking
- Учебные пособия по Excel
- Программирование на Go
- IoT
- ITIL
- Jenkins
- MIS
- Сетевые подключения
- Операционная система
- Назад Управление проектами Обзоры
- 9000 Встроенный COBOL 9000 Дизайн 9000
- Salesforce
- SEO
- Разработка программного обеспечения
- VBA
- 0005
Big Data
- Назад
- AWS
- BigData
- Cassandra
- Cognos
- Хранилище данных 0005
11.2 — Базовое наследование в C ++
Теперь, когда мы поговорили о том, что такое наследование в абстрактном смысле, давайте поговорим о том, как оно используется в C ++.
В C ++ наследование происходит между классами. В отношении наследования (is-a) класс, от которого происходит наследование, называется родительским классом , базовым классом или суперклассом , а класс, выполняющий наследование, называется дочерним классом , производным классом . , или подкласс .
На приведенной выше диаграмме Fruit является родительским элементом, а Apple и Banana — дочерними элементами.
На этой диаграмме Triangle является дочерним элементом (для Shape) и родительским (для Right Triangle).
Дочерний класс наследует и поведение (функции-члены), и свойства (переменные-члены) от родительского (с учетом некоторых ограничений доступа, которые мы рассмотрим в следующем уроке).
Эти переменные и функции становятся членами производного класса.
Поскольку дочерние классы являются полноценными классами, они могут (конечно) иметь свои собственные члены, специфичные для этого класса.Мы скоро увидим пример этого.
A Персональный класс
Вот простой класс для представления обычного человека:
1 2 3 4 5 6 7 8 9 10 11 12 13 140002 14000 18 | #include class Person { // В этом примере мы делаем наших членов общедоступными для простоты public: std :: string m_name; int m_age; Человек (std :: string name = «», int age = 0) : m_name (имя), m_age (возраст) { } std :: string getName () const {return m_name; } int getAge () const {return m_age; } }; |
Поскольку этот класс Person разработан для представления общего человека, мы определили только члены, которые будут общими для любого типа человека.У каждого человека (независимо от пола, профессии и т. Д.) Есть имя и возраст, поэтому они здесь представлены.
Обратите внимание, что в этом примере мы сделали все наши переменные и функции общедоступными. Это сделано исключительно для того, чтобы сейчас эти примеры были простыми. Обычно мы делаем переменные приватными. Мы поговорим об элементах управления доступом и о том, как они взаимодействуют с наследованием, позже в этой главе.
Бейсбол Класс игрока
Допустим, мы хотели написать программу, которая отслеживает информацию о некоторых бейсболистах.Бейсболисты должны содержать информацию, относящуюся к бейсболистам — например, мы можем захотеть сохранить средний результат игрока и количество хоумранов, которые он совершил.
Вот наш неполный класс бейсболиста:
class BaseballPlayer { // В этом примере мы делаем наших участников общедоступными для простоты public: double m_battingAverage; int m_homeRuns; BaseballPlayer (среднее значение двойного удара = 0.0, int homeRuns = 0) : m_battingAverage (battingAverage), m_homeRuns (homeRuns) { } }; |
Теперь мы также хотим отслеживать имя и возраст бейсболиста, и у нас уже есть эта информация как часть нашего класса Person.
У нас есть три варианта добавления имени и возраста в BaseballPlayer:
1) Добавить имя и возраст в класс BaseballPlayer непосредственно в качестве членов. Вероятно, это худший вариант, поскольку мы дублируем код, который уже существует в нашем классе Person.Любые обновления для Person также должны быть сделаны в BaseballPlayer.
2) Добавить человека в качестве члена BaseballPlayer, используя композицию. Но мы должны спросить себя: «Есть ли у BaseballPlayer личность»? Нет, это не так. Так что это неправильная парадигма.
3) Заставьте BaseballPlayer унаследовать эти атрибуты от Person. Помните, что наследование представляет собой отношения. Бейсболист — это человек? Да, это так. Так что наследование здесь — хороший выбор.
Создание производного класса BaseballPlayer
Чтобы BaseballPlayer унаследовал от нашего класса Person, синтаксис довольно прост.После объявления класса BaseballPlayer
мы используем двоеточие, слово «public» и имя класса, который мы хотим унаследовать. Это называется публичное наследство . Мы поговорим больше о том, что означает публичное наследование, на следующем уроке.
// BaseballPlayer публично наследует Person class BaseballPlayer: public Person { public: double m_battingAverage; int m_homeRuns; BaseballPlayer (среднее значение двойного удара = 0.0, int homeRuns = 0) : m_battingAverage (battingAverage), m_homeRuns (homeRuns) { } }; |
Используя диаграмму деривации, наше наследование выглядит так:
Когда BaseballPlayer наследуется от Person, BaseballPlayer получает функции-члены и переменные от Person. Кроме того, BaseballPlayer определяет два собственных члена: m_battingAverage и m_homeRuns.Это имеет смысл, поскольку эти свойства специфичны для BaseballPlayer, а не для любого человека.
Таким образом, объекты BaseballPlayer будут иметь 4 переменных-члена: m_battingAverage и m_homeRuns из BaseballPlayer, а также m_name и m_age из Person.
Это легко доказать:
1 2 3 4 5 6 7 8 9 10 11 12 13 140002 14000 18 19 20 21 22 23 24 25 26 27 28 29 30 000 3435 36 37 38 39 40 41 42 43 | #include #include class Person { public: std :: string m_name; int m_age; Человек (std :: string name = «», int age = 0) : m_name (имя), m_age (возраст) { } std :: string getName () const {return m_name; } int getAge () const {return m_age; } }; // BaseballPlayer, публично наследующий Person class BaseballPlayer: public Person { public: double m_battingAverage; int m_homeRuns; BaseballPlayer (среднее значение двойного удара = 0.0, int homeRuns = 0) : m_battingAverage (battingAverage), m_homeRuns (homeRuns) { } }; int main () { // Создайте новый объект BaseballPlayer BaseballPlayer joe; // Присвойте ему имя (мы можем сделать это напрямую, потому что m_name публично) joe.m_name = «Joe»; // Распечатайте имя std :: cout << joe.getName () << '\ n'; // используем функцию getName (), полученную из базового класса Person return 0; } |
Что печатает значение:
Джо
Это компилируется и выполняется, потому что joe — это BaseballPlayer, а все объекты BaseballPlayer имеют переменную-член m_name и функцию-член getName (), унаследованную от класса Person.
Производный класс Employee
Теперь напишем еще один класс, который также наследуется от Person.На этот раз мы напишем класс Employee. Сотрудник «является» человеком, поэтому использование наследования уместно:
1 2 3 4 5 6 7 8 9 10 11 12 13 140002 14 | // Сотрудник публично наследует от Person class Employee: public Person { public: double m_hourlySalary; длинный m_employeeID; Сотрудник (двойная почасовая зарплата = 0.0, long employeeID = 0) : m_hourlySalary (hourlySalary), m_employeeID (employeeID) { } void printNameAndSalary () const { 9000 << std_name <<: "<< m_hourlySalary << '\ n';} }; |
Employee наследует m_name и m_age от Person (а также две функции доступа) и добавляет еще две переменные-члены и собственную функцию-член.Обратите внимание, что printNameAndSalary () использует переменные как из класса, к которому он принадлежит (Employee :: m_hourlySalary), так и из родительского класса (Person :: m_name).
Это дает нам диаграмму вывода, которая выглядит следующим образом:
Обратите внимание, что Employee и BaseballPlayer не имеют прямых отношений, даже если они оба наследуются от Person.
Вот полный пример использования Employee:
1 2 3 4 5 6 7 8 9 10 11 12 13 140002 14000 18 19 20 21 22 23 24 25 26 27 28 29 30 000 3435 36 37 38 39 40 41 42 43 44 45 | #include #include class Person { public: std :: string m_name; int m_age; std :: string getName () const {return m_name; } int getAge () const {return m_age; } Человек (std :: string name = «», int age = 0) : m_name (имя), m_age (возраст) { } }; // Сотрудник публично наследует от Person class Employee: public Person { public: double m_hourlySalary; длинный m_employeeID; Сотрудник (двойная почасовая зарплата = 0.0, long employeeID = 0) : m_hourlySalary (hourlySalary), m_employeeID (employeeID) { } void printNameAndSalary () const { 9000 << std_name <<: "<< m_hourlySalary << '\ n';} }; int main () { Франк сотрудника (20.25, 12345); frank.m_name = «Франк»; // мы можем это сделать, потому что m_name публично frank.printNameAndSalary (); возврат 0; } |
Это отпечатки:
Фрэнк: 20,25
Цепи наследования
Возможно наследование от класса, который сам является производным от другого класса. В этом нет ничего примечательного или особенного — все происходит, как в примерах выше.
Например, давайте напишем класс Supervisor. Супервизор — это сотрудник, то есть человек.Мы уже написали класс Employee, поэтому давайте используем его в качестве базового класса для наследования Supervisor:
.class Supervisor: public Employee { public: // Этот Supervisor может контролировать максимум 5 сотрудников long m_overseesIDs [5]; Руководитель () { } }; |
Теперь наша диаграмма вывода выглядит так:
Все объекты Supervisor наследуют функции и переменные как Employee, так и Person, и добавляют свои собственные переменные-члены m_overseesIDs.
Создавая такие цепочки наследования, мы можем создать набор многократно используемых классов, которые являются очень общими (вверху) и постепенно становятся более конкретными на каждом уровне наследования.
Чем полезен такой вид наследования?
Наследование от базового класса означает, что нам не нужно переопределять информацию из базового класса в наших производных классах. Мы автоматически получаем функции-члены и переменные-члены базового класса через наследование, а затем просто добавляем дополнительные функции или переменные-члены, которые нам нужны.Это не только экономит работу, но также означает, что если мы когда-либо обновим или изменим базовый класс (например, добавим новые функции или исправим ошибку), все наши производные классы автоматически унаследуют изменения!
Например, если мы когда-нибудь добавим новую функцию в Person, и Employee, и Supervisor автоматически получат к ней доступ. Если мы добавим новую переменную в Employee, Supervisor также получит к ней доступ. Это позволяет нам создавать новые классы простым, интуитивно понятным и не требующим обслуживания способом!
Заключение
Наследование позволяет нам повторно использовать классы, заставляя другие классы наследовать их члены.В будущих уроках мы продолжим изучать, как это работает.
.Документация
— Какой хороший пример наследования классов?
Переполнение стека- Около
- Товары
- Для команд
- Переполнение стека Общественные вопросы и ответы
- Переполнение стека для команд Где разработчики и технологи делятся частными знаниями с коллегами
- работы Программирование и связанные с ним технические возможности карьерного роста
- Талант Нанимайте технических специалистов и создавайте свой бренд работодателя
- реклама Обратитесь к разработчикам и технологам со всего мира
- О компании
11.5 — Спецификаторы наследования и доступа
Автор Alex, 14 января 2008 г. | последнее изменение: nascardriver: 15 марта 2020 г.
В предыдущих уроках этой главы вы немного узнали о том, как работает наследование по базе. До сих пор во всех наших примерах мы использовали публичное наследование. То есть наш производный класс публично наследует базовый класс.
В этом уроке мы более подробно рассмотрим публичное наследование, а также два других вида наследования (частное и защищенное).Мы также рассмотрим, как различные виды наследования взаимодействуют со спецификаторами доступа, чтобы разрешить или ограничить доступ к членам.
К этому моменту вы видели спецификаторы частного и общего доступа, которые определяют, кто может получить доступ к членам класса. Напоминаем, что к публичным членам может получить доступ кто угодно. Доступ к закрытым членам могут получить только функции-члены того же класса или друзей. Это означает, что производные классы не могут напрямую обращаться к закрытым членам базового класса!
class Base { private: int m_private; // доступны только членам и друзьям Базы (не производным классам) public: int m_public; // может получить доступ кто угодно }; |
Это довольно просто, и вы уже должны к этому привыкнуть.
Спецификатор защищенного доступа
При работе с унаследованными классами все становится немного сложнее.
C ++ имеет третий спецификатор доступа, о котором мы еще не говорили, потому что он полезен только в контексте наследования. Спецификатор доступа protected позволяет классу, к которому принадлежит член, друзьям и производным классам получить доступ к члену. Однако защищенные члены недоступны извне класса.
1 2 3 4 5 6 7 8 9 10 11 12 13 140002 14 18 19 20 21 22 23 24 25 26 27 28 | class Base { public: int m_public; // может получить доступ кто угодно protected: int m_protected; // могут получить доступ члены Базы, друзья и производные классы private: int m_private; // доступны только членам и друзьям Базы (но не производным классам) }; class Derived: public Base { public: Derived () { m_public = 1; // разрешено: может получить доступ к открытым базовым элементам из производного класса m_protected = 2; // разрешено: может получить доступ к защищенным базовым членам из производного класса m_private = 3; // не разрешено: нет доступа к закрытым базовым элементам из производного класса } }; int main () { Базовая база; база.m_public = 1; // разрешено: доступ к публичным членам извне class base.m_protected = 2; // не разрешено: нет доступа к защищенным членам извне class base.m_private = 3; // не разрешено: нет доступа к закрытым членам извне класса } |
В приведенном выше примере вы можете видеть, что защищенный базовый член m_protected напрямую доступен производному классу, но не публике.
Итак, когда мне следует использовать спецификатор защищенного доступа?
С атрибутом protected в базовом классе производные классы могут напрямую обращаться к этому члену.Это означает, что если вы позже измените что-либо в этом защищенном атрибуте (тип, значение и т. Д.), Вам, вероятно, потребуется изменить как базовый класс, так и все производные классы.
Следовательно, использование спецификатора защищенного доступа наиболее полезно, когда вы (или ваша команда) собираетесь быть производными от ваших собственных классов, и количество производных классов является разумным. Таким образом, если вы вносите изменения в реализацию базового класса, и в результате необходимы обновления производных классов, вы можете вносить обновления самостоятельно (и это не займет много времени, поскольку количество производных классов ограничено. ).
Делая ваши члены закрытыми, вы лучше инкапсулируете и изолируете производные классы от изменений базового класса. Но есть также затраты на создание общедоступного или защищенного интерфейса для поддержки всех методов или возможностей доступа, которые необходимы общедоступным и / или производным классам. Это дополнительная работа, которая, вероятно, того не стоит, если только вы не ожидаете, что кто-то другой будет производным от вашего класса, или у вас есть огромное количество производных классов, и их обновление всех будет стоить дорого.
Различные виды наследования и их влияние на доступ
Во-первых, классы могут наследовать от других классов тремя разными способами: общедоступный, защищенный и частный.
Для этого просто укажите, какой тип доступа вы хотите при выборе класса для наследования:
1 2 3 4 5 6 7 8 9 10 11 12 13 140002 14000 18 | // Наследование от Base публично class Pub: public Base { }; // Защищенное наследование от базы class Pro: protected Base { }; // Наследование от Base в частном порядке class Pri: private Base { }; class Def: Base // По умолчанию используется частное наследование { }; |
Если вы не выбрали тип наследования, C ++ по умолчанию использует частное наследование (точно так же, как члены по умолчанию имеют частный доступ, если вы не укажете иное).
Это дает нам 9 комбинаций: 3 спецификатора доступа к членам (общедоступный, частный и защищенный) и 3 типа наследования (общедоступный, частный и защищенный).
Так в чем разница между ними? В двух словах, когда члены наследуются, спецификатор доступа для унаследованного члена может быть изменен (только в производном классе) в зависимости от типа используемого наследования. Другими словами, члены, которые были общедоступными или защищенными в базовом классе, могут изменять спецификаторы доступа в производном классе.
Это может показаться немного запутанным, но не так уж и плохо. Мы проведем оставшуюся часть урока, подробно исследуя это.
При рассмотрении примеров не забывайте о следующих правилах:
- Класс всегда может получить доступ к своим собственным (ненаследуемым) членам.
- Публика получает доступ к членам класса на основе спецификаторов доступа класса, к которому она обращается.
- Класс обращается к унаследованным членам на основе спецификатора доступа, унаследованного от родительского класса.Это зависит от спецификатора доступа и типа используемого наследования.
Государственное наследство
Публичное наследование — это, безусловно, наиболее часто используемый тип наследования. Фактически, вы очень редко увидите или будете использовать другие типы наследования, поэтому ваше основное внимание должно быть сосредоточено на понимании этого раздела. К счастью, публичное наследование также легче всего понять. Когда вы наследуете базовый класс публично, унаследованные общедоступные члены остаются общедоступными, а унаследованные защищенные члены остаются защищенными.Унаследованные закрытые члены, которые были недоступны, потому что они были закрытыми в базовом классе, остаются недоступными.
Спецификатор доступа в базовом классе | Спецификатор доступа при публичном наследовании |
---|---|
Общественный | Общественный |
Защищено | Защищено |
Частный | Недоступно |
Вот пример, показывающий, как все работает:
1 2 3 4 5 6 7 8 9 10 11 12 13 140002 14000 18 19 20 21 22 23 24 25 26 27 28 29 30 000 3435 36 37 | class Base { public: int m_public; защищено: int m_protected; частный: int m_private; }; class Pub: общедоступная база // примечание: общедоступное наследование { // Средства общедоступного наследования: // Публичные унаследованные члены остаются общедоступными (поэтому m_public рассматривается как общедоступные) // Защищенные унаследованные члены оставаться защищенным (поэтому m_protected рассматривается как защищенный) // Частные унаследованные члены остаются недоступными (поэтому m_private недоступен) public: Pub () { m_public = 1; // хорошо: m_public был унаследован как public m_protected = 2; // хорошо: m_protected был унаследован как protected m_private = 3; // не нормально: m_private недоступен из производного класса } }; int main () { // Внешний доступ использует спецификаторы доступа класса, к которому осуществляется доступ. Базовая база; base.m_public = 1; // хорошо: m_public общедоступен в Base base.m_protected = 2; // не нормально: m_protected защищен в Base base.m_private = 3; // не нормально: m_private является частным в базе Pub pub; pub.m_public = 1; // хорошо: m_public общедоступен в Pub pub.m_protected = 2; // не нормально: m_protected защищен в Pub pub.m_private = 3; // не нормально: m_private недоступен в Pub |
Это то же самое, что и в приведенном выше примере, где мы ввели описатель защищенного доступа, за исключением того, что мы также создали экземпляр производного класса, просто чтобы показать, что с открытым наследованием все работает одинаково в базовом и производном классе.
Публичное наследование — это то, что вам следует использовать, если у вас нет особой причины не делать этого.
Используйте публичное наследование, если у вас нет особой причины поступить иначе.
Защищенное наследство
Защищенное наследование — наименее распространенный метод наследования. Практически никогда не используется, за исключением очень особых случаев. При защищенном наследовании открытые и защищенные члены становятся защищенными, а частные члены остаются недоступными.
Поскольку эта форма наследования встречается очень редко, мы пропустим этот пример и сведем его в таблицу:
Спецификатор доступа в базовом классе | Спецификатор доступа при защищенном наследовании |
---|---|
Общественный | Защищено |
Защищено | Защищено |
Частный | Недоступно |
Частное наследство
При частном наследовании все члены базового класса наследуются как частные.Это означает, что частные члены остаются частными, а защищенные и публичные члены становятся частными.
Обратите внимание, что это не влияет на способ доступа производного класса к членам, унаследованным от его родителя! Это влияет только на код, пытающийся получить доступ к этим членам через производный класс.
1 2 3 4 5 6 7 8 9 10 11 12 13 140002 14000 18 19 20 21 22 23 24 25 26 27 28 29 30 000 000 34 35 36 37 38 39 40 41 | class Base { public: int m_public; защищено: int m_protected; частный: int m_private; }; class Pri: private Base // примечание: частное наследование { // Частное наследование означает: // Публичные унаследованные элементы становятся частными (поэтому m_public рассматривается как частные) // Защищенные унаследованные члены стать закрытым (поэтому m_protected рассматривается как закрытый) // Закрытые унаследованные члены остаются недоступными (поэтому m_private недоступен) public: Pri () { m_public = 1; // хорошо: m_public теперь приватный в Pri m_protected = 2; // хорошо: m_protected теперь приватный в Pri m_private = 3; // не нормально: производные классы не могут получить доступ к закрытым членам в базовом классе } }; int main () { // Внешний доступ использует спецификаторы доступа класса, к которому осуществляется доступ. // В этом случае спецификаторы доступа base. Базовая база; base.m_public = 1; // хорошо: m_public общедоступен в Base base.m_protected = 2; // не нормально: m_protected защищен в Base base.m_private = 3; // не нормально: m_private является частным в базе 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; } |
Обобщить в виде таблицы:
Спецификатор доступа в базовом классе | Спецификатор доступа при частном наследовании |
---|---|
Общественный | Частный |
Защищено | Частный |
Частный | Недоступно |
Частное наследование может быть полезно, когда производный класс не имеет очевидной связи с базовым классом, но использует базовый класс для внутренней реализации.В таком случае мы, вероятно, не хотим, чтобы открытый интерфейс базового класса предоставлялся через объекты производного класса (как это было бы, если бы мы унаследовали публично).
На практике частное наследование используется редко.
Последний пример
class Base { public: int m_public; защищено: int m_protected; частный: int m_private; }; |
Base может получить доступ к своим участникам без ограничений.Публика может получить доступ только к m_public. Производные классы могут обращаться к m_public и m_protected.
class D2: private Base // примечание: частное наследование { // означает частное наследование: // публично унаследованные члены становятся частными // защищенные унаследованные члены становятся частными // частные унаследованные члены остаются недоступный общедоступный: int m_public2; защищено: int m_protected2; частный: int m_private2; }; |
D2 может получить доступ к своим собственным участникам без ограничений.D2 может получить доступ к участникам базы m_public и m_protected, но не m_private. Поскольку D2 унаследовал Base конфиденциально, m_public и m_protected теперь считаются закрытыми при доступе через D2. Это означает, что публика не может получить доступ к этим переменным при использовании объекта D2, а также никакие классы, производные от D2.
класс D3: общедоступные D2 { // Средства общедоступного наследования: // Унаследованные публичные члены остаются общедоступными // Защищенные унаследованные члены остаются защищенными // Частные унаследованные члены остаются недоступными общедоступные: int m_public3; защищено: int m_protected3; частный: int m_private3; }; |
D3 может получить доступ к своим собственным участникам без ограничений.D3 может получить доступ к участникам m_public2 и m_protected2 D2, но не m_private2. Поскольку D3 унаследовал D2 публично, m_public2 и m_protected2 сохраняют свои спецификаторы доступа при доступе через D3. D3 не имеет доступа к m_private Base, который уже был частным в Base. Также у него нет доступа к базовым m_protected или m_public, которые стали закрытыми, когда D2 унаследовал их.
Сводка
Способ взаимодействия спецификаторов доступа, типов наследования и производных классов вызывает большую путаницу.Чтобы попытаться прояснить ситуацию как можно больше:
Во-первых, класс (и его друзья) всегда могут получить доступ к своим собственным ненаследуемым членам. Спецификаторы доступа влияют только на то, могут ли посторонние и производные классы получить доступ к этим членам.
Во-вторых, когда производные классы наследуют члены, эти члены могут изменять спецификаторы доступа в производном классе. Это не влияет на собственные (ненаследуемые) члены производных классов (которые имеют свои собственные спецификаторы доступа). Он влияет только на то, могут ли посторонние и классы, производные от производного класса, получить доступ к этим унаследованным членам.
Вот таблица всех комбинаций спецификатора доступа и типов наследования:
Спецификатор доступа в базовом классе | Спецификатор доступа при публичном наследовании | Спецификатор доступа при частном наследовании | Спецификатор доступа при защищенном наследовании |
---|---|---|---|
Общественный | Общественный | Частный | Защищено |
Защищено | Защищено | Частный | Защищено |
Частный | Недоступно | Недоступно | Недоступно |
В заключение, хотя в приведенных выше примерах мы показали только примеры с использованием переменных-членов, эти правила доступа верны для всех элементов (например,грамм. функции-члены и типы, объявленные внутри класса).
.