что такое спецификаторы доступа
Спецификаторы доступа
В С++ члены класса классифицируются в соответствии с правами доступа на следующие три категории: публичные (public), частные (private) и защищенные (protected). Любая функция программы имеет доступ к публичным членам. Доступ к частному члену имеют только функции-члены класса или функции-друзья класса. Защищенные члены аналогичны частным членам. Разница между ними появляется только при наследовании классов.
Когда один класс наследует другой, все публичные члены базового класса становятся публичными членами производного класса. В противоположность этому частные члены базового класса не доступны внутри производного класса. Например, рассмотрим следующий фрагмент:
class X <
int i;
int j;
public:
void get_ij();
void put_ij();
>;
class Y: public X <
int k;
public:
int get_k();
void make_k();
>;
Класс Y наследует и имеет доступ к публичным функциям get_ij() и put_ij() класса X, но не имеет доступа к i и j, поскольку они являются частными членами X. Во всех случаях частные члены остаются частными, т. е. доступными только в том классе, в котором они объявлены. Таким образом, частные члены не могут участвовать в наследовании.
В связи с тем, что частные члены не могут быть наследованы, возникает интересный вопрос: что если необходимо оставить член частным и вместе с тем позволить использовать его производным классам? Для таких целей имеется другое ключевое слово — protected (защищенный). Защищенный член подобен частному, за исключением механизма наследования. При наследовании защищенного члена производный класс также имеет к нему доступ. Таким образом, указав спецификатор доступа protected, можно позволить использовать член внутри иерархии классов и запретить доступ к нему извне этой иерархии. Например:
class X <
protected:
int i;
int j;
public:
void get_ij();
void put_ij();
>;
class Y: public X <
int k;
public:
int get_k();
void make_k();
>;
Здесь класс Y имеет доступ к i и j, и в то же время они остаются недоступными для остальной части программы. Когда элемент объявляется защищенным, доступ к нему ограничивается, но вместе с тем можно наследовать права доступа. В отличие от этого в случае частных членов доступ не наследуется.
Другой важной особенностью ключевых слов private, protected и public служит то, что они могут появляться в объявлении в любом порядке и в любом количестве. Например, следующий код является вполне законным:
class my_class <
protected:
int i;
int j;
public:
void f1();
void f2();
protected:
int a;
public:
int b;
>;
Однако считается хорошим стилем, чтобы каждый из спецификаторов доступа встречался в объявлении класса не более одного раза.
12.3 – Спецификаторы доступа public и private
Открытые и закрытые члены
Рассмотрим следующую структуру:
С другой стороны, рассмотрим следующий почти идентичный класс:
Спецификаторы доступа
Хотя члены класса по умолчанию являются закрытыми, с помощью ключевого слова public мы можем сделать их открытыми:
Ключевое слово public вместе с последующим двоеточием называется спецификатором доступа. Спецификаторы доступа определяют, у кого есть доступ к членам, которые следуют за спецификатором. Каждый из членов «получает» уровень доступа предыдущего спецификатора доступа (или, если он не указан, спецификатора доступа по умолчанию).
Смешивание спецификаторов доступа
Класс может (и почти всегда так делает) использовать несколько спецификаторов доступа для установки уровней доступа каждому из своих членов. Ограничений на количество спецификаторов доступа, которые вы можете использовать в классе, нет.
Как правило, переменные-члены обычно делаются закрытыми, а функции-члены обычно становятся открытыми. В следующем уроке мы подробнее рассмотрим, почему.
Правило
Делайте переменные-члены закрытыми, а функции-члены открытыми, если у вас нет веской причины поступать иначе.
Эта программа печатает:
Группа открытых членов класса часто называется открытым интерфейсом. Поскольку только открытые члены могут быть доступны извне класса, открытый интерфейс определяет, как программы, использующие класс, будут взаимодействовать с этим классом. Обратите внимание, что main() ограничена установкой даты и печатью даты. Класс защищает переменные-члены от прямого доступа или редактирования.
Некоторые программисты предпочитают сначала перечислять закрытые члены, потому что открытые члены обычно используют закрытые, поэтому имеет смысл сначала определить закрытые. Однако хороший контраргумент состоит в том, что пользователям класса не важны закрытые члены, поэтому открытые должны быть на первом месте. Хорош любой из этих подходов.
Контроль доступа работает на основе класса
Рассмотрим следующую программу:
Один нюанс C++, который часто упускают или неправильно понимают, заключается в том, что контроль доступа работает на основе класса, а не на основе объекта. Это означает, что когда функция имеет доступ к закрытым членам класса, она может получить доступ к закрытым членам любого объекта этого типа класса, который она может видеть.
Это может быть особенно полезно, когда нам нужно скопировать члены из одного объекта класса в другой объект того же класса. Мы также увидим, что эта тема снова всплывет, когда в следующей главе мы будем говорить о перегрузке operator для печати членов класса.
Снова о структурах и классах
Теперь, когда мы поговорили о спецификаторах доступа, мы можем поговорить о реальных различиях между классом и структурой в C++. Класс по умолчанию делает своих членов закрытыми. Структура по умолчанию делает свои члены открытыми.
(Хорошо, если быть педантичным, есть еще одно незначительное отличие – структуры наследуются от других классов открытым (public) образом, а классы наследуются закрытым (private) образом. Мы рассмотрим, что это означает, в следующей главе, но этот конкретный момент практически не имеет значения, так как в любом случае вы никогда не должны полагаться на значения по умолчанию).
Небольшой тест
Вопрос 1
a) Что такое открытый член?
Открытый член – это член класса, к которому может получить доступ кто угодно.
b) Что такое закрытый член?
c) Что такое спецификатор доступа?
Спецификатор доступа определяет, кто имеет доступ к членам, которые следуют за спецификатором.
d) Сколько существует спецификаторов доступа, и какие они?
Вопрос 2
Убедитесь, что следующая программа выполняется правильно:
Она должна напечатать:
Вопрос 3
А теперь давайте попробуем кое-что посложнее. Давайте с нуля напишем класс, реализующий простой стек. Если вам нужно напомнить, что такое стек, просмотрите урок «11.8 – Стек и куча».
Класс должен называться Stack и содержать:
Убедитесь, что следующая программа выполняется правильно:
Эта программа должна напечатать:
Совет
Модификаторы доступа (Руководство по программированию в C#)
Все типы и члены типов имеют уровень доступности. Он определяет возможность их использования из другого кода в вашей или в других сборках. Следующие модификаторы доступа позволяют указать доступность типа или члена при объявлении:
Сводная таблица
Расположение вызывающего объекта | public | protected internal | protected | internal | private protected | private |
---|---|---|---|---|---|---|
В классе | ✔️️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
Производный класс (та же сборка) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
Непроизводный класс (та же сборка) | ✔️ | ✔️ | ❌ | ✔️ | ❌ | ❌ |
Производный класс (другая сборка) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ |
Непроизводный класс (другая сборка) | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ |
В следующих примерах показано, как изменить модификаторы доступа для типа или члена типа:
Не все модификаторы доступа подходят для всех типов или членов во всех контекстах. В некоторых случаях доступность члена типа ограничивается доступностью содержащего его типа.
Доступность классов, записей и структур
Доступность членов классов, записей и структур
Как правило, уровень доступности члена не может быть выше уровня доступности типа, в который он входит. При этом член public внутреннего класса может быть доступен за пределами сборки, если он реализует методы интерфейса или переопределяет виртуальные методы, определенные в открытом базовом классе.
Методы завершения не могут иметь модификаторы доступа.
Другие типы
Спецификация языка C#
Дополнительные сведения см. в спецификации языка C#. Спецификация языка является предписывающим источником информации о синтаксисе и использовании языка C#.
17.5 – Наследование и спецификаторы доступа
В предыдущих уроках этой главы вы немного узнали о том, как работает базовое наследование. До сих пор во всех наших примерах мы использовали открытое (публичное) наследование. То есть наш производный класс открыто наследовал базовый класс.
В этом уроке мы более подробно рассмотрим открытое ( public ) наследование, а также два других вида наследования: закрытое (частное, private ) и защищенное ( protected ). Мы также рассмотрим, как различные виды наследования взаимодействуют со спецификаторами доступа, чтобы разрешить или ограничить доступ к членам.
Это довольно просто, и вы уже должны были к этому привыкнуть.
Спецификатор доступа protected
При работе с наследованными классами всё становится немного сложнее.
В C++ есть третий спецификатор доступа, о котором мы еще не говорили, потому что он полезен только в контексте наследования. Спецификатор доступа protected позволяет получить доступ к члену классу, к которому принадлежит член, друзьям и производным классам. Однако защищенные члены недоступны извне класса.
В приведенном выше примере вы можете видеть, что защищенный член базового класса m_protected напрямую доступен производному классу, но не внешней функции.
К защищенному члену в базовом классе производные классы могут обращаться напрямую. Это означает, что если вы позже измените что-либо в этом защищенном члене (тип, значение и т.д.), вам, вероятно, придется изменить как базовый класс, так и все производные классы.
Следовательно, использование спецификатора доступа protected наиболее полезно, когда вы (или ваша команда) собираетесь наследоваться от ваших собственных классов, и количество производных классов не слишком велико. Таким образом, если вы вносите изменения в реализацию базового класса и в результате необходимы обновления производных классов, вы можете вносить эти обновления самостоятельно (и это не будет длиться вечно, поскольку количество производных классов ограничено).
Делая ваши члены закрытыми, вы лучше инкапсулируете и изолируете производные классы от изменений в базовом классе. Но в этом случае появляются затраты на создание открытого или защищенного интерфейсов для поддержки всех методов или возможностей доступа, которые необходимы сторонним и/или производным классам. Это дополнительная работа, которая, вероятно, не стоит того, если только вы не ожидаете, что кто-то другой будет наследоваться от вашего класса, или у вас есть огромное количество производных классов, и обновление их всех будет стоить дорого.
Различные виды наследования и их влияние на доступ
Во-первых, классы могут наследоваться от других классов тремя разными способами: открытый ( public ), защищенный ( protected ) и закрытый ( private ).
Для этого при выборе класса для наследования просто укажите, какой тип доступа вам нужен:
Если вы не выбрали тип наследования, C++ по умолчанию использует закрытое наследование (точно так же, как члены по умолчанию имеют закрытый доступ, если вы не укажете иное).
Так в чем разница между ними? Вкратце, когда члены наследуются, спецификатор доступа для унаследованного члена может быть изменен (только в производном классе) в зависимости от типа используемого наследования. Другими словами, члены, которые были открытыми или защищенными в базовом классе, могут изменять спецификаторы доступа в производном классе.
Это может показаться немного запутанным, но всё не так уж и плохо. Мы посвятим оставшуюся часть урока подробному изучению этого.
При рассмотрении примеров помните о следующих правилах:
Открытое наследование
Открытое наследование – это, безусловно, наиболее часто используемый тип наследования. Фактически, вы очень редко увидите или будете использовать другие типы наследования, поэтому ваше основное внимание должно быть сосредоточено на понимании этого раздела. К счастью, открытое наследование также легче всего для понимания. Когда вы наследуете базовый класс открыто, унаследованные открытые члены остаются открытыми, а унаследованные защищенные члены остаются защищенными. Унаследованные закрытые члены, которые были недоступны, потому что они были закрытыми в базовом классе, остаются недоступными.
Спецификатор доступа в базовом классе | Спецификатор доступа при открытом наследовании |
---|---|
public | public |
protected | protected |
private | не доступен |
Ниже приведен пример, показывающий, как всё работает:
Это то же самое, что и в приведенном выше примере, где мы ввели спецификатор защищенного доступа, за исключением того, что мы также создали экземпляр производного класса, просто чтобы показать, что с открытым наследованием в базовом и производном классах всё работает одинаково.
Если у вас нет особой причины делать иначе, то вы должны использовать открытое наследование.
Лучшая практика
Используйте открытое наследование, если у вас нет особых причин делать иначе.
Защищенное наследование
Защищенное наследование – наименее распространенный метод наследования. Практически никогда не используется, за исключением очень особых случаев. При защищенном наследовании открытые и защищенные члены становятся защищенными, а закрытые члены остаются недоступными.
Поскольку эта форма наследования встречается очень редко, мы пропустим этот пример и подведем итоги в виде таблицы:
Спецификатор доступа в базовом классе | Спецификатор доступа при защищенном наследовании |
---|---|
public | protected |
protected | protected |
private | не доступен |
Закрытое наследование
При закрытом наследовании все члены базового класса наследуются как закрытые. Это означает, что закрытые члены остаются закрытыми, а защищенные и открытые члены становятся закрытыми.
Обратите внимание, что это не влияет на способ доступа производного класса к членам, унаследованным от его родителя! Это влияет только на код, пытающийся получить доступ к этим членам через производный класс.
Подведем итоги в виде таблицы:
Спецификатор доступа в базовом классе | Спецификатор доступа при закрытом наследовании |
---|---|
public | private |
protected | private |
private | не доступен |
Закрытое наследование может быть полезно, когда производный класс не имеет очевидной связи с базовым классом, но использует базовый класс для внутренней реализации. В таком случае мы, вероятно, не хотим, чтобы открытый интерфейс базового класса предоставлялся через объекты производного класса (как это было бы, если бы мы наследовали открыто).
На практике закрытое наследование используется редко.
Последний пример
Резюме
Способ взаимодействия спецификаторов доступа, типов наследования и производных классов вызывает большую путаницу. Чтобы попытаться прояснить ситуацию как можно больше:
Во-первых, класс (и его друзья) всегда могут получить доступ к своим собственным ненаследуемым членам. Спецификаторы доступа влияют только на то, могут ли посторонние и производные классы получить доступ к этим членам.
Во-вторых, когда производные классы наследуют члены, эти члены могут изменять спецификаторы доступа в производном классе. Это не влияет на собственные (ненаследуемые) члены производных классов (которые имеют свои собственные спецификаторы доступа). Это влияет только на то, могут ли посторонний код и классы, производные от производного класса, получить доступ к этим унаследованным членам.
Вот таблица всех комбинаций спецификаторов доступа и типов наследования:
Спецификатор доступа в базовом классе | Спецификатор доступа при открытом наследовании | Спецификатор доступа при закрытом наследовании | Спецификатор доступа при защищенном наследовании |
---|---|---|---|
public | public | private | protected |
protected | protected | private | protected |
private | не доступен | не доступен | не доступен |
В заключение, хотя в приведенных выше примерах мы показали только примеры с использованием переменных-членов, эти правила доступа верны для всех членов (например, функций-членов и типов, объявленных внутри класса).
Урок №114. Спецификаторы доступа public и private
Обновл. 13 Сен 2021 |
На этом уроке мы рассмотрим, что такое спецификаторы доступа в языке С++, какие они бывают и как их использовать.
Спецификаторы доступа
Рассмотрим следующую программу:
С другой стороны, рассмотрим следующий почти идентичный класс:
Хотя члены класса являются закрытыми по умолчанию, мы можем сделать их открытыми, используя ключевое слово public:
Поскольку теперь члены класса DateClass являются открытыми, то к ним можно получить доступ напрямую из функции main().
Ключевое слово public вместе с двоеточием называется спецификатором доступа. Спецификатор доступа определяет, кто имеет доступ к членам этого спецификатора. Каждый из членов «приобретает» уровень доступа в соответствии со спецификатором доступа (или, если он не указан, в соответствии со спецификатором доступа по умолчанию).
В языке C++ есть 3 уровня доступа:
спецификатор public делает члены открытыми;
спецификатор private делает члены закрытыми;
спецификатор protected открывает доступ к членам только для дружественных и дочерних классов (детально об этом на соответствующем уроке).
Использование спецификаторов доступа
Классы могут использовать (и активно используют) сразу несколько спецификаторов доступа для установки уровней доступа для каждого из своих членов. Обычно переменные-члены являются закрытыми, а методы — открытыми. Почему именно так? Об этом мы поговорим на следующем уроке.
Правило: Устанавливайте спецификатор доступа private переменным-членам класса и спецификатор доступа public — методам класса (если у вас нет веских оснований делать иначе).
Рассмотрим пример класса, который использует спецификаторы доступа private и public:
Результат выполнения программы:
Обратите внимание, хоть мы и не можем получить доступ к переменным-членам объекта date напрямую из main() (так как они являются private по умолчанию), мы можем получить доступ к ним через открытые методы setDate() и print()!
Открытые члены классов составляют открытый (или «public») интерфейс. Поскольку доступ к открытым членам класса может осуществляться извне класса, то открытый интерфейс и определяет, как программы, использующие класс, будут взаимодействовать с этим же классом.
Некоторые программисты предпочитают сначала перечислить private-члены, а затем уже public-члены. Они руководствуются следующей логикой: public-члены обычно используют private-члены (те же переменные-члены в методах класса), поэтому имеет смысл сначала определять private-члены, а затем уже public-члены. Другие же программисты считают, что сначала нужно указывать public-члены. Здесь уже иная логика: поскольку private-члены закрыты и получить к ним доступ напрямую нельзя, то и выносить их на первое место тоже не нужно. Работать будет и так, и так. Какой способ использовать — выбирайте сами, что вам удобнее.