Как наследовать private в c
Перейти к содержимому

Как наследовать private в c

  • автор:

Private наследование

Для чего нужно наследование с описателем доступа private, если все поля базового класса станут недоступными?

Отслеживать
1,342 3 3 золотых знака 14 14 серебряных знаков 27 27 бронзовых знаков
задан 16 янв 2016 в 11:06
161 1 1 серебряный знак 3 3 бронзовых знака
Если вам дан исчерпывающий ответ, отметьте его как верный (галка напротив выбранного ответа).
17 янв 2016 в 8:37

3 ответа 3

Сортировка: Сброс на вариант по умолчанию

Именно для того, чтобы все члены базового класса не были доступны снаружи (были доступны только изнутри класса, или друзьям класса).

Наследование мало чем отличается от аггрегации, и
class A : private B < . >; аналогично
class A < private: B b; >; .

Одна в отличие от private: B b; , при наследовании функции B можно вызывать как члены своего класса, т.е. не b.f() , а просто f() .

Также можно унаследоваться от абстрактного класса, и переопределить его методы, не показывая все эти детали наружу, например:

struct Callback < virtual void done() = 0; >; void run(Callback* b); class Cls : private Callback < public: void some() < run(this); >private: void done() override < . >>; 

Отслеживать
207k 28 28 золотых знаков 294 294 серебряных знака 526 526 бронзовых знаков
ответ дан 16 янв 2016 в 11:11
31k 13 13 золотых знаков 96 96 серебряных знаков 157 157 бронзовых знаков
А зачем такое вообще может пригодится? Ну кроме потому что можно?
16 янв 2016 в 11:12

@Flownee Чтоб приватные члены базового класса остались в производном так же скрытыми от остальных, чтоб не получились публичные члены класса (что всегда подозрительно)

16 янв 2016 в 11:18
в примере Callback не должен был зваться Base ?
16 янв 2016 в 11:22

@Flowneee, это наследование функционала без отношения «is», т.е. после такого наследования класс наследник не может быть преобразован к родителю.

16 янв 2016 в 12:08

@Flowneee, как и почти все в ООП для того, чтобы автору было приятно писать, а остальным (при исправлении багов) было сложнее быстро и точно понять, а почему же вся эта фигня не работает.

16 янв 2016 в 13:18

Приватное наследование — это по существу композиция, выраженная немного по-другому. При композиции у вас вложенный объект доступен по своему имени, но не виден снаружи. При приватном наследовании вложенный объект «влит» в *this .

Вообще, роль наследования — выражать отношение Is-A между классами. То есть, выражать наследование интерфейса, при этом наследование имплементации есть техническая деталь. А приватное наследование именно это и не может, при приватном наследовании наследуется именно имплементация, но не интерфейс.

Поэтому практически всегда вместо приватного наследования стоит предпочесть именно композицию: она выражает ту же идею, но явно.

Отслеживать
ответ дан 16 янв 2016 в 12:15
207k 28 28 золотых знаков 294 294 серебряных знака 526 526 бронзовых знаков

Читал ваш ответ, и в голову пришла мысль — возможно использование приватного наследования будет эффективнее по производительности чем явное использование агрегации (меньше потребления памяти) + меньше инструкций на вызов сокрытых методов

C++. Применение модификаторов доступа private, protected, public при наследовании классов

Применение модификаторов доступа private , protected , public при наследовании классов

Перед изучением данной темы рекомендуется ознакомиться со следующей темой:

Поиск на других ресурсах:

1. Особенности применения модификаторов доступа при наследовании классов

В языке C++ модификаторы доступа private , protected , public могут применяться к базовым классам при их наследовании производными классами. Действие того или иного модификатора влияет на уровень доступности элементов базовых классов и проявляется в двух случаях:

  • когда классы образуют иерархию наследования;
  • когда создаются экземпляры унаследованных классов.

Общая форма задания уровня доступа к базовому класса следующая:

class DerivedClass : [access_modifier] BaseClass < >;
  • BaseClass – базовый класс;
  • DerivedClass – производный класс;
  • access_modifier – один из модификаторов доступа private , protected , public .

Модификатор доступа может отсутствовать, в этом случае по умолчанию принимается private -доступ.

2. Модификатор доступа private для базового класса.
2.1. Ограничение доступа для унаследованных классов

Если класс B является производным от базового класса A и базовый класс A содержит модификатор доступа private , то действуют следующие правила видимости:

  • все private -элементы класса A недоступны в классе B ;
  • все protected -элементы класса A видимы в классе B ;
  • все public -элементы класса A видимы в классе B ;
  • все классы, унаследованные от класса B , не будут иметь доступ к элементам класса A (рисунок 1).

C++. Модификатор доступа private для класса. Ограничение доступа для класса в иерархии классов

Рисунок 1. Модификатор доступа private для класса. Ограничение доступа для класса C в иерархии классов

Как видно из рисунка 1 из класса B можно доступиться к protected и public элементам класса A . Однако, для класса C класс A есть полностью закрытым (ограничение модификатора private ). Если из класса C будут унаследованы другие классы ( D , E и т.д.), то класс A для этих классов также будет закрытым.

2.2. Ограничения доступа для экземпляров (объектов) унаследованных классов

Если классы A , B образуют иерархию наследования и класс A является базовым для класса B , то действуют следующие правила относительно экземпляров этих классов:

  • из экземпляра класса A доступны только public -элементы (члены данных, методы);
  • из экземпляра класса B не есть доступными любые элементы класса A . Здесь срабатывает ограничение модификатора private перед именем класса A (рисунок 2).

C++. Модификатор доступа private для класса. Нет доступа к элементу базового класса из экземпляра производного класса

Рисунок 2. Модификатор доступа private для класса. Нет доступа к элементу базового класса A из экземпляра производного класса B

Как видно из рисунка 2, любые элементы private -базового класса A являются закрытыми для экземпляров унаследованного класса B .

3. Модификатор доступа protected

Модификатор доступа protected ограничивает доступ из экземпляров унаследованных классов и не ограничивает доступ из методов унаследованных классов. Если два класса A , B образуют иерархию наследования, и класс A объявлен базовым с модификатором protected , то действуют следующие правила:

  • все protected — и public -элементы класса A доступны из методов класса B (рисунок 3);
  • все protected — и public -элементы класса A доступны из методов класса, унаследованного от класса B (класса C )
  • нет доступа к любому элементу класса A из экземпляров производных классов (рисунок 4).

C++. Модификатор protected для класса. Элементы базового класса, объявленные как protected или public, есть доступны из методов унаследованного класса

Рисунок 3. Модификатор protected для класса. Элементы базового класса, объявленные как protected или public , есть доступны из методов унаследованного класса

C++. Модификатор доступа protected для класса. Нет доступа к членам базового класса из экземпляров любых унаследованных классов

Рисунок 4. Модификатор доступа protected для класса. Нет доступа к членам базового класса A из экземпляров любых унаследованных классов ( B , C , D и т.д.)

Как видно из рисунка 4, члены класса A являются закрытыми для экземпляров производных классов B и C .

4. Модификатор доступа public

Если в имени базового класса используется модификатор доступа public , то действуют следующие правила:

  • protected и public -члены базового класса доступны из методов унаследованного класса;
  • private -члены базового класса недоступны из методов унаследованного класса;
  • public -члены базового класса доступны из экземпляров унаследованного класса;
  • private и protected члены базового класса недоступны из экземпляров унаследованного класса.

Рисунок 5 демонстрирует вышеуказанные правила.

C++. Модификатор доступа public для унаследованного класса

Рисунок 5. Модификатор доступа public для унаследованного класса

Связанные темы

Наследование. Закрытое/частное (private) и защищенное (protected) наследование / FAQ C++

Закрытое наследование – это синтаксический вариант композиции (она же агрегация и/или связь типа «имеет»).

Например, связь « Car (автомобиль) имеет Engine (двигатель)» можно выразить с помощью простой композиции:

class Engine < public: Engine(int numCylinders); void start(); // запускает этот "двигатель" >; class Car < public: Car() : e_(8) < >// Инициализирует автомобиль (Car) с 8 цилиндрами void start() < e_.start(); >// Запускает автомобиль (Car), запуская его двигатель (Engine) private: Engine e_; // Автомобиль (Car) имеет двигатель (Engine) >;

Связь « Car имеет Engine » также можно выразить с помощью закрытого (частного) наследования:

class Car : private Engine < // Автомобиль (Car) имеет двигатель (Engine) public: Car() : Engine(8) < >// Инициализирует автомобиль (Car) с 8 цилиндрами using Engine::start; // Запускает автомобиль (Car), запуская его двигатель (Engine) >;

Между этими двумя вариантами есть несколько общих черт:

  • в обоих случаях в каждом объекте Car содержится ровно один объект-член Engine ;
  • ни в том, ни в другом случае пользователи (посторонние) не могут преобразовать Car* в Engine* ;
  • в обоих случаях класс Car имеет метод start() , который вызывает метод start() содержащегося в нем объекта Engine .

Также есть несколько отличий:

  • вариант простой композиции нужен, если вы хотите иметь в Car несколько объектов Engine ;
  • вариант с частным наследованием может ввести ненужное множественное наследование;
  • вариант с частным наследованием позволяет членам Car преобразовывать Car* в Engine* ;
  • вариант с частным наследованием позволяет получить доступ к защищенным членам базового класса;
  • вариант с частным наследованием позволяет Car переопределять виртуальные функции Engine ;
  • вариант с частным наследованием немного упрощает (20 символов по сравнению с 28 символами) предоставление Car метода start() , который просто вызывает метод start() класса Engine .

Обратите внимание, что частное наследование обычно используется для получения доступа к защищенным членам базового класса, но обычно это краткосрочное решение (перевод: пластырь).

Что предпочесть: композицию или частное наследование?

Используйте композицию, когда можете, а частное наследование, когда вам нужно.

Обычно вы не хотите иметь доступ к внутренним компонентам слишком многих других классов, а частное наследование дает вам некоторые из этих дополнительных полномочий (и ответственностей). Но частное наследование – это не зло; просто его дороже поддерживать, так как оно увеличивает вероятность того, что кто-то изменит что-то, что нарушит ваш код.

Легальное, долгосрочное использование частного наследования – это когда вы хотите создать класс Fred , который использует код в классе Wilma , а код из класса Wilma должен вызывать функции-члены из вашего нового класса Fred . В этом случае Fred вызывает невиртуальные функции в Wilma , а Wilma вызывает в себе (обычно чисто) виртуальные функции, которые перегружены классом Fred . Сделать это с помощью композиции будет намного сложнее.

class Wilma < protected: void fredCallsWilma() < std::cout virtual void wilmaCallsFred() = 0; // чисто виртуальная функция >; class Fred : private Wilma < public: void barney() < std::cout protected: virtual void wilmaCallsFred() < std::cout >;

Должен ли я приводить указатель из производного класса с частным наследованием к его базовому классу?

Как правило, нет.

Об отношении к базовому классу из функции-члена или друга производного класса с частным наследованием известно, и восходящее преобразование из PrivatelyDer* в Base* (или PrivatelyDer& в Base& ) безопасно; приведение типов не требуется и не рекомендуется.

Однако пользователям PrivatelyDer следует избегать этого небезопасного преобразования, поскольку оно основано на закрытом/частном решении PrivatelyDer и может быть изменено без предварительного уведомления.

Как защищенное ( protected ) наследование связано с частным ( private ) наследованием?

Сходства: оба позволяют переопределять виртуальные функции в частном/защищенном базовом классе, и ни одно из них не утверждает, что производный класс является разновидностью базового класса.

Различия: защищенное наследование позволяет производным классам производных классов знать о наследственной связи. Таким образом, ваши внуки действительно узнают о деталях вашей реализации. Это имеет как преимущества (позволяет производным классам защищенного производного класса использовать связь с защищенным базовым классом), так и затраты (защищенный производный класс не может изменить связи, не нарушив при этом последующие производные классы).

Защищенное наследование использует синтаксис : protected .

class Car : protected Engine < public: // . >;

Каковы правила доступа при частном и защищенном наследованиях?

Возьмем в качестве примера следующие классы:

class B < /*. */ >; class D_priv : private B < /*. */ >; class D_prot : protected B < /*. */ >; class D_publ : public B < /*. */ >; class UserClass < B b; /*. */ >;

Ни один из производных классов не может получить доступ к чему-либо, что является private в B . В D_priv открытая и защищенная части B являются частными. В D_prot открытая и защищенная части B являются защищенными. В D_publ открытые части B являются открытыми, а защищенные части B – защищенными ( D_publ – это разновидность B ). Класс UserClass может получить доступ только к открытым частям B , что «изолирует» UserClass от B .

Чтобы сделать публичный член B публичным в D_priv или D_prot , укажите имя члена с префиксом B:: . Например, чтобы сделать член B::f(int, float) открытым в D_prot , вы должны сказать:

class D_prot : protected B < public: using B::f; // Примечание: не using B::f(int,float) >;

Наследование в C++: beginner, intermediate, advanced

В этой статье наследование описано на трех уровнях: beginner, intermediate и advanced. Expert нет. И ни слова про SOLID. Честно.

Beginner

Что такое наследование?

Наследование является одним из основополагающих принципов ООП. В соответствии с ним, класс может использовать переменные и методы другого класса как свои собственные.

Класс, который наследует данные, называется подклассом (subclass), производным классом (derived class) или дочерним классом (child). Класс, от которого наследуются данные или методы, называется суперклассом (super class), базовым классом (base class) или родительским классом (parent). Термины “родительский” и “дочерний” чрезвычайно полезны для понимания наследования. Как ребенок получает характеристики своих родителей, производный класс получает методы и переменные базового класса.

Наследование полезно, поскольку оно позволяет структурировать и повторно использовать код, что, в свою очередь, может значительно ускорить процесс разработки. Несмотря на это, наследование следует использовать с осторожностью, поскольку большинство изменений в суперклассе затронут все подклассы, что может привести к непредвиденным последствиям.

В этом примере, метод turn_on() и переменная serial_number не были объявлены или определены в подклассе Computer . Однако их можно использовать, поскольку они унаследованы от базового класса.

Важное примечание: приватные переменные и методы не могут быть унаследованы.

#include using namespace std; class Device < public: int serial_number = 12345678; void turn_on() < cout private: int pincode = 87654321; >; class Computer: public Device <>; int main() < Computer Computer_instance; Computer_instance.turn_on(); cout 

Типы наследования

В C ++ есть несколько типов наследования:

  • публичный ( public )- публичные ( public ) и защищенные ( protected ) данные наследуются без изменения уровня доступа к ним;
  • защищенный ( protected ) — все унаследованные данные становятся защищенными;
  • приватный ( private ) — все унаследованные данные становятся приватными.

Для базового класса Device , уровень доступа к данным не изменяется, но поскольку производный класс Computer наследует данные как приватные, данные становятся приватными для класса Computer .

#include using namespace std; class Device < public: int serial_number = 12345678; void turn_on() < cout >; class Computer: private Device < public: void say_hello() < turn_on(); cout >; int main() < Device Device_instance; Computer Computer_instance; cout 

Класс Computer теперь использует метод turn_on() как и любой приватный метод: turn_on() может быть вызван изнутри класса, но попытка вызвать его напрямую из main приведет к ошибке во время компиляции. Для базового класса Device , метод turn_on() остался публичным, и может быть вызван из main .

Конструкторы и деструкторы

В C ++ конструкторы и деструкторы не наследуются. Однако они вызываются, когда дочерний класс инициализирует свой объект. Конструкторы вызываются один за другим иерархически, начиная с базового класса и заканчивая последним производным классом. Деструкторы вызываются в обратном порядке.

Важное примечание: в этой статье не освещены виртуальные десктрукторы. Дополнительный материал на эту тему можно найти к примеру в этой статье на хабре.

#include using namespace std; class Device < public: // constructor Device() < cout // destructor ~Device() < cout >; class Computer: public Device < public: Computer() < cout ~Computer() < cout >; class Laptop: public Computer < public: Laptop() < cout ~Laptop() < cout >; int main()

Конструкторы: Device -> Computer -> Laptop .
Деструкторы: Laptop -> Computer -> Device .

Множественное наследование

Множественное наследование происходит, когда подкласс имеет два или более суперкласса. В этом примере, класс Laptop наследует и Monitor и Computer одновременно.

#include using namespace std; class Computer < public: void turn_on() < cout >; class Monitor < public: void show_image() < cout >; class Laptop: public Computer, public Monitor <>; int main()

Проблематика множественного наследования

Множественное наследование требует тщательного проектирования, так как может привести к непредвиденным последствиям. Большинство таких последствий вызваны неоднозначностью в наследовании. В данном примере Laptop наследует метод turn_on() от обоих родителей и неясно какой метод должен быть вызван.

#include using namespace std; class Computer < private: void turn_on() < cout >; class Monitor < public: void turn_on() < cout >; class Laptop: public Computer, public Monitor <>; int main() < Laptop Laptop_instance; // Laptop_instance.turn_on(); // will cause compile time error return 0; >

Несмотря на то, что приватные данные не наследуются, разрешить неоднозначное наследование изменением уровня доступа к данным на приватный невозможно. При компиляции, сначала происходит поиск метода или переменной, а уже после — проверка уровня доступа к ним.

Intermediate

Проблема ромба

Проблема ромба (Diamond problem)- классическая проблема в языках, которые поддерживают возможность множественного наследования. Эта проблема возникает когда классы B и C наследуют A , а класс D наследует B и C .

К примеру, классы A , B и C определяют метод print_letter() . Если print_letter() будет вызываться классом D , неясно какой метод должен быть вызван — метод класса A , B или C . Разные языки по-разному подходят к решению ромбовидной проблем. В C ++ решение проблемы оставлено на усмотрение программиста.

Ромбовидная проблема — прежде всего проблема дизайна, и она должна быть предусмотрена на этапе проектирования. На этапе разработки ее можно разрешить следующим образом:

  • вызвать метод конкретного суперкласса;
  • обратиться к объекту подкласса как к объекту определенного суперкласса;
  • переопределить проблематичный метод в последнем дочернем классе (в коде — turn_on() в подклассе Laptop ).
#include using namespace std; class Device < public: void turn_on() < cout >; class Computer: public Device <>; class Monitor: public Device <>; class Laptop: public Computer, public Monitor < /* public: void turn_on() < cout // uncommenting this function will resolve diamond problem */ >; int main() < Laptop Laptop_instance; // Laptop_instance.turn_on(); // will produce compile time error // if Laptop.turn_on function is commented out // calling method of specific superclass Laptop_instance.Monitor::turn_on(); // treating Laptop instance as Monitor instance via static cast static_cast( Laptop_instance ).turn_on(); return 0; >

Если метод turn_on() не был переопределен в Laptop, вызов Laptop_instance.turn_on() , приведет к ошибке при компиляции. Объект Laptop может получить доступ к двум определениям метода turn_on() одновременно: Device:Computer:Laptop.turn_on() и Device:Monitor:Laptop.turn_on() .

Проблема ромба: Конструкторы и деструкторы

Поскольку в С++ при инициализации объекта дочернего класса вызываются конструкторы всех родительских классов, возникает и другая проблема: конструктор базового класса Device будет вызван дважды.

#include using namespace std; class Device < public: Device() < cout >; class Computer: public Device < public: Computer() < cout >; class Monitor: public Device < public: Monitor() < cout >; class Laptop: public Computer, public Monitor <>; int main()

Виртуальное наследование

Виртуальное наследование (virtual inheritance) предотвращает появление множественных объектов базового класса в иерархии наследования. Таким образом, конструктор базового класса Device будет вызван только единожды, а обращение к методу turn_on() без его переопределения в дочернем классе не будет вызывать ошибку при компиляции.

#include using namespace std; class Device < public: Device() < cout void turn_on() < cout >; class Computer: virtual public Device < public: Computer() < cout >; class Monitor: virtual public Device < public: Monitor() < cout >; class Laptop: public Computer, public Monitor <>; int main()

Примечание: виртуальное наследование в классах Computer и Monitor не разрешит ромбовидное наследование если дочерний класс Laptop будет наследовать класс Device не виртуально ( class Laptop: public Computer, public Monitor, public Device <>; ).

Абстрактный класс

В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется. Также, в С++ создать объект абстрактного класса невозможно — попытка тоже вызовет ошибку при компиляции.

#include using namespace std; class Device < public: void turn_on() < cout virtual void say_hello() = 0; >; class Laptop: public Device < public: void say_hello() < cout >; int main() < Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.say_hello(); // Device Device_instance; // will cause compile time error return 0; >

Интерфейс

С++, в отличии от некоторых ООП языков, не предоставляет отдельного ключевого слова для обозначения интерфейса (interface). Тем не менее, реализация интерфейса возможна путем создания чистого абстрактного класса (pure abstract class) — класса в котором присутствуют только декларации методов. Такие классы также часто называют абстрактными базовыми классами (Abstract Base Class — ABC).

#include using namespace std; class Device < public: virtual void turn_on() = 0; >; class Laptop: public Device < public: void turn_on() < cout >; int main() < Laptop Laptop_instance; Laptop_instance.turn_on(); // Device Device_instance; // will cause compile time error return 0; >

Advanced

Несмотря на то, что наследование — фундаментальный принцип ООП, его стоит использовать с осторожностью. Важно думать о том, что любой код который будет использоваться скорее всего будет изменен и может быть использован неочевидным для разработчика путем.

Наследование от реализованного или частично реализованного класса

Если наследование происходит не от интерфейса (чистого абстрактного класса в контексте С++), а от класса в котором присутствуют какие-либо реализации, стоит учитывать то, что класс наследник связан с родительским классом наиболее тесной из возможных связью. Большинство изменений в классе родителя могут затронуть наследника что может привести к непредвиденному поведению. Такие изменения в поведении наследника не всегда очевидны — ошибка может возникнуть в уже оттестированом и рабочем коде. Данная ситуация усугубляется наличием сложной иерархии классов. Всегда стоит помнить о том, что код может изменяться не только человеком который его написал, и пути наследования очевидные для автора могут быть не учтены его коллегами.

В противовес этому стоит заметить что наследование от частично реализованных классов имеет неоспоримое преимущество. Библиотеки и фреймворки зачастую работают следующим образом: они предоставляют пользователю абстрактный класс с несколькими виртуальными и множеством реализованных методов. Таким образом, наибольшее количество работы уже проделано — сложная логика уже написана, а пользователю остается только кастомизировать готовое решение под свои нужды.

Интерфейс

Наследование от интерфейса (чистого абстрактного класса) преподносит наследование как возможность структурирования кода и защиту пользователя. Так как интерфейс описывает какую работу будет выполнять класс-реализация, но не описывает как именно, любой пользователь интерфейса огражден от изменений в классе который реализует этот интерфейс.

Интерфейс: Пример использования

Прежде всего стоит заметить, что пример тесно связан с понятием полиморфизма, но будет рассмотрен в контексте наследования от чистого абстрактного класса.

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

Отсутствие однозначности касательно форматирования конфигурационного файла не тормозит процесс разработки основной программы. Два разработчика могут работать параллельно — один над бизнес логикой, а другой над парсером. Поскольку они взаимодействуют через этот интерфейс, каждый из них может работать независимо. Данный подход облегчает покрытие кода юнит тестами, так как необходимые тесты могут быть написаны с использованием мока (mock) для этого интерфейса.

Также, при изменении формата конфигурационного файла, бизнес логика приложения не затрагивается. Единственное чего требует полный переход от одного форматирования к другому — написания новой реализации уже существующего абстрактного класса (класса-парсера). В дальнейшем, возврат к изначальному формату файла требует минимальной работы — подмены одного уже существующего парсера другим.

Заключение

Наследование предоставляет множество преимуществ, но должно быть тщательно спроектировано во избежание проблем, возможность для которых оно открывает. В контексте наследования, С++ предоставляет широкий спектр инструментов который открывает массу возможностей для программиста.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *