17.5 – Наследование и спецификаторы доступа

Добавлено 31 июля 2021 в 16:21

В предыдущих уроках этой главы вы немного узнали о том, как работает базовое наследование. До сих пор во всех наших примерах мы использовали открытое (публичное) наследование. То есть наш производный класс открыто наследовал базовый класс.

В этом уроке мы более подробно рассмотрим открытое (public) наследование, а также два других вида наследования: закрытое (частное, private) и защищенное (protected). Мы также рассмотрим, как различные виды наследования взаимодействуют со спецификаторами доступа, чтобы разрешить или ограничить доступ к членам.

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

class Base
{
private:
    // доступна только членам и друзьям Base (не производным классам)
    int m_private; 
public:
    // может получить доступ кто угодно
    int m_public; 
};

Это довольно просто, и вы уже должны были к этому привыкнуть.

Спецификатор доступа protected

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

В C++ есть третий спецификатор доступа, о котором мы еще не говорили, потому что он полезен только в контексте наследования. Спецификатор доступа protected позволяет получить доступ к члену классу, к которому принадлежит член, друзьям и производным классам. Однако защищенные члены недоступны извне класса.

class Base
{
public:
    // может получить доступ кто угодно
    int m_public; 
protected:
    // доступна для членов Base, друзей и производных классов
    int m_protected; 
private:
    // доступна только членам и друзьям Base (но не производным классам)
    int m_private; 
};
 
class Derived: public Base
{
public:
    Derived()
    {
        // допустимо: можно получить доступ к открытым членам
        // базового класса из производного класса
        m_public = 1; 
        
        // допустимо: можно получить доступ к защищенным членам
        // базового класса из производного класса
        m_protected = 2; 

        // не допустимо: нет доступа к закрытым членам
        // базового класса из производного класса
        m_private = 3; 
    }
};
 
int main()
{
    Base base;

    // допустимо: можно получить доступ к открытым членам извне класса
    base.m_public = 1; 

    // не допустимо: нет доступа к защищенным членам извне класса
    base.m_protected = 2; 

    // не допустимо: нет доступа к закрытым членам извне  класса
    base.m_private = 3;
 
    return 0;
}

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

Итак, когда мне следует использовать спецификатор доступа protected?

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

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

Делая ваши члены закрытыми, вы лучше инкапсулируете и изолируете производные классы от изменений в базовом классе. Но в этом случае появляются затраты на создание открытого или защищенного интерфейсов для поддержки всех методов или возможностей доступа, которые необходимы сторонним и/или производным классам. Это дополнительная работа, которая, вероятно, не стоит того, если только вы не ожидаете, что кто-то другой будет наследоваться от вашего класса, или у вас есть огромное количество производных классов, и обновление их всех будет стоить дорого.

Различные виды наследования и их влияние на доступ

Во-первых, классы могут наследоваться от других классов тремя разными способами: открытый (public), защищенный (protected) и закрытый (private).

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

// Открытое наследование от Base
class Pub: public Base
{
};
 
// Защищенное наследование от Base
class Pro: protected Base
{
};
 
// Закрытое наследование от Base
class Pri: private Base
{
};
 
class Def: Base // По умолчанию закрытое наследование
{
};

Если вы не выбрали тип наследования, C++ по умолчанию использует закрытое наследование (точно так же, как члены по умолчанию имеют закрытый доступ, если вы не укажете иное).

Это дает нам 9 комбинаций: 3 спецификатора доступа к членам (public, private и protected) и 3 типа наследования (public, private и protected).

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

Это может показаться немного запутанным, но всё не так уж и плохо. Мы посвятим оставшуюся часть урока подробному изучению этого.

При рассмотрении примеров помните о следующих правилах:

  • Класс всегда может получить доступ к своим собственным (ненаследуемым) членам.
  • Сторонний код получает доступ к членам класса на основе спецификаторов доступа класса, к которому он обращается.
  • Производный класс обращается к унаследованным членам на основе спецификатора доступа, унаследованного от родительского класса. Это зависит от спецификатора доступа и типа наследования.

Открытое наследование

Открытое наследование – это, безусловно, наиболее часто используемый тип наследования. Фактически, вы очень редко увидите или будете использовать другие типы наследования, поэтому ваше основное внимание должно быть сосредоточено на понимании этого раздела. К счастью, открытое наследование также легче всего для понимания. Когда вы наследуете базовый класс открыто, унаследованные открытые члены остаются открытыми, а унаследованные защищенные члены остаются защищенными. Унаследованные закрытые члены, которые были недоступны, потому что они были закрытыми в базовом классе, остаются недоступными.

Спецификатор доступа в базовом классеСпецификатор доступа при открытом наследовании
publicpublic
protectedprotected
privateне доступен

Ниже приведен пример, показывающий, как всё работает:

class Base
{
public:
    int m_public;
protected:
    int m_protected;
private:
    int m_private;
};
 
class Pub: public Base // обратите внимание: открытое наследование
{
    //  Открытое наследование означает:
    //  Открытые унаследованные члены остаются открытыми
    //(поэтому m_public считается открытым).
    //  Защищенные унаследованные члены остаются защищенными
    // (поэтому m_protected рассматривается как защищенный)
    //  Закрытые унаследованные члены остаются недоступными
    // (поэтому m_private недоступен)
public:
    Pub()
    {
        m_public = 1;    // ok: m_public был унаследован как public
        m_protected = 2; // ok: m_protected был унаследован как protected
        m_private = 3;   // нет: m_private недоступен из производного класса
    }
};
 
int main()
{
    // Внешний доступ использует спецификаторы доступа класса,
    // к которому осуществляется доступ.
    Base base;
    base.m_public = 1;    // ok: m_public открыт в Base
    base.m_protected = 2; // нет: m_protected защищен в Base
    base.m_private = 3;   // нет: m_private закрыт в Base
 
    Pub pub;
    pub.m_public = 1;    // ok: m_public открыт в Pub
    pub.m_protected = 2; // нет: m_protected защищен в Pub
    pub.m_private = 3;   // нет: m_private недоступен Pub
 
    return 0;
}

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

Если у вас нет особой причины делать иначе, то вы должны использовать открытое наследование.

Лучшая практика


Используйте открытое наследование, если у вас нет особых причин делать иначе.

Защищенное наследование

Защищенное наследование – наименее распространенный метод наследования. Практически никогда не используется, за исключением очень особых случаев. При защищенном наследовании открытые и защищенные члены становятся защищенными, а закрытые члены остаются недоступными.

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

Спецификатор доступа в базовом классеСпецификатор доступа при защищенном наследовании
publicprotected
protectedprotected
privateне доступен

Закрытое наследование

При закрытом наследовании все члены базового класса наследуются как закрытые. Это означает, что закрытые члены остаются закрытыми, а защищенные и открытые члены становятся закрытыми.

Обратите внимание, что это не влияет на способ доступа производного класса к членам, унаследованным от его родителя! Это влияет только на код, пытающийся получить доступ к этим членам через производный класс.

class Base
{
public:
    int m_public;
protected:
    int m_protected;
private:
    int m_private;
};
 
class Pri: private Base // обратите внимание: закрытое наследование
{
    // Закрытое наследование означает:
    //  Открытые унаследованные члены становятся закрытыми
    // (поэтому m_public рассматривается как закрытый).
    //  Защищенные унаследованные члены становятся закрытыми
    // (поэтому m_protected рассматривается как закрытый)
    //  Закрытые унаследованные члены остаются недоступными
    // (поэтому m_private недоступен)
public:
    Pri()
    {
        m_public = 1;    // ok: m_public теперь закрытый в Pri
        m_protected = 2; // ok: m_protected теперь закрытый в Pri
        m_private = 3;   // нет: производные классы не могут получить доступ
                         // к закрытым членам базового класса
    }
};
 
int main()
{
    // Внешний доступ использует спецификаторы доступа класса,
    // к которому осуществляется доступ.

    // В этом случае спецификаторы доступа base.
    Base base;
    base.m_public = 1;    // ok: m_public открытый в Base
    base.m_protected = 2; // нет: m_protected защищенный в Base
    base.m_private = 3;   // нет: m_private закрытый в Base
 
    Pri pri;
    pri.m_public = 1;    // нет: m_public теперь закрытый в Pri
    pri.m_protected = 2; // нет: m_protected теперь закрытый в Pri
    pri.m_private = 3;   // нет: m_private недоступен в Pri
 
    return 0;
}

Подведем итоги в виде таблицы:

Спецификатор доступа в базовом классеСпецификатор доступа при закрытом наследовании
publicprivate
protectedprivate
privateне доступен

Закрытое наследование может быть полезно, когда производный класс не имеет очевидной связи с базовым классом, но использует базовый класс для внутренней реализации. В таком случае мы, вероятно, не хотим, чтобы открытый интерфейс базового класса предоставлялся через объекты производного класса (как это было бы, если бы мы наследовали открыто).

На практике закрытое наследование используется редко.

Последний пример

class Base
{
public:
    int m_public;
protected:
    int m_protected;
private:
    int m_private;
};

Base может получить доступ к своим членам без ограничений. Внешний код может получить доступ только к m_public. Производные классы могут обращаться к m_public и m_protected.

class D2 : private Base // обратите внимание: закрытое наследование
{
    // Закрытое наследование означает:
    // Открытые унаследованные члены становятся закрытыми.
    // Защищенные унаследованные члены становятся закрытыми.
    // Закрытые унаследованные члены остаются недоступными
public:
    int m_public2;
protected:
    int m_protected2;
private:
    int m_private2;
};

D2 может получить доступ к своим членам без ограничений. D2 может получить доступ к членам Base m_public и m_protected, но не к m_private. Поскольку D2 наследуется от Base закрыто, при доступе через D2 члены m_public и m_protected теперь считаются закрытыми. Это означает, что внешний код не может получить доступ к этим переменным при использовании объекта D2, равно как и никакие классы, производные от D2.

class D3 : public D2
{
    // Открытое наследование означает:
    // Открытые унаследованные члены остаются открытыми.
    // Защищенные унаследованные члены остаются защищенными
    // Закрытые унаследованные члены остаются недоступными
public:
    int m_public3;
protected:
    int m_protected3;
private:
    int m_private3;
};

D3 может получить доступ к своим собственным членам без ограничений. D3 может получить доступ к членам m_public2 и m_protected2 класса D2, но не к m_private2. Поскольку D3 наследуется от D2 открыто, при доступе через D3 члены m_public2 и m_protected2 сохраняют свои спецификаторы доступа. D3 не имеет доступа к m_private класса Base, который в Base уже был закрытым. У него также нет доступа к членам m_protected или m_public класса Base, которые стали закрытыми, когда D2 унаследовал их.

Резюме

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

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

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

Вот таблица всех комбинаций спецификаторов доступа и типов наследования:

Спецификатор доступа в базовом классеСпецификатор доступа при открытом наследованииСпецификатор доступа при закрытом наследованииСпецификатор доступа при защищенном наследовании
publicpublicprivateprotected
protectedprotectedprivateprotected
privateне доступенне доступенне доступен

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

Теги

C++ / CppLearnCppprivateprotectedpublicКласс (программирование)НаследованиеОбучениеОбъектно-ориентированное программирование (ООП)ПрограммированиеСпецификатор доступа

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.