Avatar ·

В чем разница между public, protected и private наследованием в C++?

Приветствую, коллеги! Я начал изучать C++ и столкнулся с путаницей в наследовании. Понимаю, что есть три спецификатора доступа: public, protected и private. Но когда я пытаюсь применить их к наследованию, то теряюсь. Например, у меня есть базовый класс: ```cpp class Base { public: int pub = 1; protected: int prot = 2; private: int priv = 3; }; ``` Я хочу создать класс Derived, который наследует от Base. Пробовал писать `class Derived : public Base`, `class Derived : protected Base` и `class Derived : private Base`. В каждом случае поведение разное, но я не могу понять, в чем именно разница. Почему, например, при private-наследовании я не могу обратиться к pub извне? И что такое "доступность" членов класса-наследника? Мне кажется, что я упускаю какую-то фундаментальную логику. Буду благодарен за объяснение с примерами, желательно с точки зрения того, как это влияет на код при попытке использовать унаследованные члены. Заранее спасибо! <<>>

Ключевая идея: наследование определяет уровень доступа к унаследованным членам

Разница между public, protected и private наследованием в C++ — это не про то, что члены базового класса "исчезают", а про то, как меняется их видимость для внешнего мира и для классов-наследников. Каждый спецификатор наследования "понижает" уровень доступа унаследованных членов до указанного уровня (или оставляет как есть).

Давайте разберем на примере базового класса:

class Base {
public:
    int pub = 1;
protected:
    int prot = 2;
private:
    int priv = 3;
};

Теперь создадим три разных класса наследника и посмотрим, что произойдет с каждым членом.

1. Public наследование: "как есть"

class DerivedPublic : public Base {
    // pub остается public
    // prot остается protected
    // priv остается недоступным (остается private в Base)
};
  • pub — доступен отовсюду: внутри DerivedPublic, из объектов этого класса, из классов-наследников.
  • prot — доступен внутри DerivedPublic и его наследников, но не извне (через объект).
  • priv — недоступен даже внутри DerivedPublic, так как private члены базового класса никогда не наследуются.

Это самый распространенный вид наследования — моделирует отношение "является" (is-a).

2. Protected наследование: "понижаем до protected"

class DerivedProtected : protected Base {
    // pub становится protected
    // prot остается protected
    // priv остается недоступным
};
  • pub — теперь защищен: доступен внутри DerivedProtected и его наследников, но не извне.
  • prot — остается protected (уровень не понижается, так как он уже protected).
  • priv — все еще недоступен.

Используется, когда вы хотите скрыть от внешнего мира тот факт, что класс наследует от Base, но при этом хотите, чтобы наследники могли пользоваться унаследованными членами.

3. Private наследование: "понижаем до private"

class DerivedPrivate : private Base {
    // pub становится private
    // prot становится private
    // priv остается недоступным
};
  • pub и prot — теперь приватные: доступны только внутри самого DerivedPrivate, но не извне и не в классах-наследниках.
  • priv — все еще недоступен.

Это моделирует отношение "реализовано через" (implemented-in-terms-of). Внешний мир не должен знать, что DerivedPrivate наследует от Base — это деталь реализации.

Проверка на практике

int main() {
    DerivedPublic dp;
    dp.pub = 10;  // OK: pub унаследован как public
    // dp.prot = 20;  // Ошибка: prot защищен

    DerivedProtected dprot;
    // dprot.pub = 10;  // Ошибка: pub стал protected, недоступен извне

    DerivedPrivate dpriv;
    // dpriv.pub = 10;  // Ошибка: pub стал private

    return 0;
}

Важное замечание: private члены базового класса

Независимо от типа наследования, private члены базового класса никогда не наследуются. Они существуют в объекте (занимают память), но напрямую обратиться к ним нельзя — только через public/protected методы базового класса.

Итоговая таблица

Тип наследования Что происходит с public членами Base Что происходит с protected членами Base private члены Base
public остаются public остаются protected недоступны
protected становятся protected остаются protected недоступны
private становятся private становятся private недоступны

Запомните правило: спецификатор наследования — это "потолок" доступа. Унаследованный член не может иметь более высокий уровень доступа, чем указанный при наследовании.

<<>>

Дополнение: когда что использовать на практике

Коллега выше отлично описал механику. Я добавлю практические рекомендации, когда какой тип наследования выбирать.

Public наследование (90% случаев)

Используйте всегда, если класс-наследник логически является разновидностью базового класса. Например, class Dog : public Animal. Внешний код должен иметь доступ к интерфейсу Animal через объект Dog. Это основное отношение "is-a".

Protected наследование (редко)

Применяется, когда вы хотите скрыть наследование от внешнего мира, но разрешить его своим наследникам. Например, если вы пишете библиотеку и не хотите, чтобы пользователи знали, что ваш класс наследует от какого-то внутреннего класса, но при этом ваши собственные классы-наследники могут использовать его члены. Встречается в паттерне "Template Method".

Private наследование (иногда)

Используется для композиции на уровне наследования, когда нужно получить доступ к protected членам или переопределить виртуальные функции, но не хотите, чтобы внешний код знал о наследовании. Например:

class Engine {
public:
    void start();
protected:
    int rpm;
};

class Car : private Engine {
public:
    void drive() { start(); } // используем Engine, но не показываем это
};

Внешний код не сможет написать Car c; c.start(); — наследование скрыто. Это альтернатива композиции (члену класса), когда нужно переопределить виртуальные методы.

Золотое правило: если сомневаетесь — используйте public. Private и protected наследование — это инструменты для специфических случаев, которые новичку встречаются редко.