На этом уроке мы рассмотрим, что такое спецификаторы доступа в языке С++, какие они бывают и как их использовать.
Спецификаторы доступа
Рассмотрим следующую программу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct DateStruct // члены структуры являются открытыми по умолчанию { int day; // открыто по умолчанию, доступ имеет любой объект int month; // открыто по умолчанию, доступ имеет любой объект int year; // открыто по умолчанию, доступ имеет любой объект }; int main() { DateStruct date; date.day = 12; date.month = 11; date.year = 2018; return 0; } |
Здесь мы объявляем структуру DateStruct, а затем напрямую обращаемся к её членам для их инициализации. Это работает, так как все члены структуры являются открытыми по умолчанию. Открытые члены (или «public-члены») — это члены структуры или класса, к которым можно получить доступ извне этой же структуры или класса. В программе, приведенной выше, функция main() находится вне структуры, но она может напрямую обращаться к членам day
, month
и year
, так как они являются открытыми.
С другой стороны, рассмотрим следующий почти идентичный класс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class DateClass // члены класса являются закрытыми по умолчанию { int m_day; // закрыто по умолчанию, доступ имеют только другие члены класса int m_month; // закрыто по умолчанию, доступ имеют только другие члены класса int m_year; // закрыто по умолчанию, доступ имеют только другие члены класса }; int main() { DateClass date; date.m_day = 12; // ошибка date.m_month = 11; // ошибка date.m_year = 2018; // ошибка return 0; } |
Вам бы не удалось скомпилировать эту программу, так как все члены класса являются закрытыми по умолчанию. Закрытые члены (или «private-члены») — это члены класса, доступ к которым имеют только другие члены этого же класса. Поскольку функция main() не является членом DateClass, то она и не имеет доступа к закрытым членам объекта date
.
Хотя члены класса являются закрытыми по умолчанию, мы можем сделать их открытыми, используя ключевое слово public:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class DateClass { public: // обратите внимание на ключевое слово public и двоеточие int m_day; // открыто, доступ имеет любой объект int m_month; // открыто, доступ имеет любой объект int m_year; // открыто, доступ имеет любой объект }; int main() { DateClass date; date.m_day = 12; // ок, так как m_day имеет спецификатор доступа public date.m_month = 11; // ок, так как m_month имеет спецификатор доступа public date.m_year = 2018; // ок, так как m_year имеет спецификатор доступа public return 0; } |
Поскольку теперь члены класса DateClass являются открытыми, то к ним можно получить доступ напрямую из функции main().
Ключевое слово public вместе с двоеточием называется спецификатором доступа. Спецификатор доступа определяет, кто имеет доступ к членам этого спецификатора. Каждый из членов «приобретает» уровень доступа в соответствии со спецификатором доступа (или, если он не указан, в соответствии со спецификатором доступа по умолчанию).
В языке C++ есть 3 уровня доступа:
спецификатор public делает члены открытыми;
спецификатор private делает члены закрытыми;
спецификатор protected открывает доступ к членам только для дружественных и дочерних классов (детально об этом на соответствующем уроке).
Использование спецификаторов доступа
Классы могут использовать (и активно используют) сразу несколько спецификаторов доступа для установки уровней доступа для каждого из своих членов. Обычно переменные-члены являются закрытыми, а методы — открытыми. Почему именно так? Об этом мы поговорим на следующем уроке.
Правило: Устанавливайте спецификатор доступа private переменным-членам класса и спецификатор доступа public — методам класса (если у вас нет веских оснований делать иначе).
Рассмотрим пример класса, который использует спецификаторы доступа private и public:
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 |
#include <iostream> class DateClass // члены класса являются закрытыми по умолчанию { int m_day; // закрыто по умолчанию, доступ имеют только другие члены класса int m_month; // закрыто по умолчанию, доступ имеют только другие члены класса int m_year; // закрыто по умолчанию, доступ имеют только другие члены класса public: void setDate(int day, int month, int year) // открыто, доступ имеет любой объект { // Метод setDate() имеет доступ к закрытым членам класса, так как сам является членом класса m_day = day; m_month = month; m_year = year; } void print() // открыто, доступ имеет любой объект { std::cout << m_day << "/" << m_month << "/" << m_year; } }; int main() { DateClass date; date.setDate(12, 11, 2018); // ок, так как setDate() имеет спецификатор доступа public date.print(); // ок, так как print() имеет спецификатор доступа public return 0; } |
Результат выполнения программы:
12/11/2018
Обратите внимание, хоть мы и не можем получить доступ к переменным-членам объекта date
напрямую из main() (так как они являются private по умолчанию), мы можем получить доступ к ним через открытые методы setDate() и print()!
Открытые члены классов составляют открытый (или «public») интерфейс. Поскольку доступ к открытым членам класса может осуществляться извне класса, то открытый интерфейс и определяет, как программы, использующие класс, будут взаимодействовать с этим же классом.
Некоторые программисты предпочитают сначала перечислить private-члены, а затем уже public-члены. Они руководствуются следующей логикой: public-члены обычно используют private-члены (те же переменные-члены в методах класса), поэтому имеет смысл сначала определять private-члены, а затем уже public-члены. Другие же программисты считают, что сначала нужно указывать public-члены. Здесь уже иная логика: поскольку private-члены закрыты и получить к ним доступ напрямую нельзя, то и выносить их на первое место тоже не нужно. Работать будет и так, и так. Какой способ использовать — выбирайте сами, что вам удобнее.
Рассмотрим следующую программу:
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 38 39 40 41 42 |
#include <iostream> class DateClass // члены класса являются закрытыми по умолчанию { int m_day; // закрыто по умолчанию, доступ имеют только другие члены класса int m_month; // закрыто по умолчанию, доступ имеют только другие члены класса int m_year; // закрыто по умолчанию, доступ имеют только другие члены класса public: void setDate(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } void print() { std::cout << m_day << "/" << m_month << "/" << m_year; } // Обратите внимание на этот дополнительный метод void copyFrom(const DateClass &b) { // Мы имеем прямой доступ к закрытым членам объекта b m_day = b.m_day; m_month = b.m_month; m_year = b.m_year; } }; int main() { DateClass date; date.setDate(12, 11, 2018); // ок, так как setDate() имеет спецификатор доступа public DateClass copy; copy.copyFrom(date); // ок, так как copyFrom() имеет спецификатор доступа public copy.print(); return 0; } |
Один нюанс в языке C++, который часто игнорируют/забывают/неправильно понимают, заключается в том, что контроль доступа работает на основе класса, а не на основе объекта. Это означает, что, когда метод имеет доступ к закрытым членам класса, он может обращаться к закрытым членам любого объекта этого класса.
В примере, приведенном выше, метод copyFrom() является членом класса DateClass, что открывает ему доступ к private-членам класса DateClass. Это означает, что copyFrom() может не только напрямую обращаться к закрытым членам неявного объекта с которым работает (копия объекта), но и имеет прямой доступ к закрытым членам объекта b
класса DateClass!
Это полезно, когда нужно скопировать элементы из одного объекта класса в другой объект того же класса. Детально об этом мы поговорим на следующих уроках.
Структуры vs. Классы
Теперь, когда мы узнали о спецификаторах доступа, мы можем поговорить о фактических различиях между классом и структурой в языке C++. Класс по умолчанию устанавливает всем своим членам спецификатор доступа private. Структура же по умолчанию устанавливает всем своим членам спецификатор доступа public.
Есть еще одно незначительное отличие: структуры наследуют от других конструкций языка С++ открыто, в то время как классы наследуют закрыто.
Тест
Задание №1
a) Что такое открытый член?
Ответ №1.а)
Открытый член — это член класса, доступ к которому имеют объекты как внутри, так и извне класса.
b) Что такое закрытый член?
Ответ №1.b)
Закрытый член — это член класса, доступ к которому имеют только другие члены этого же класса.
c) Что такое спецификатор доступа?
Ответ №1.c)
Спецификатор доступа определяет, кто имеет доступ к членам этого же спецификатора.
d) Сколько есть спецификаторов доступа в языке C++? Назовите их.
Ответ №1.d)
В языке С++ есть 3 спецификатора доступа:
public;
private;
protected.
Задание №2
a) Напишите простой класс с именем Numbers. Этот класс должен иметь:
три закрытые переменные-члены типа double: m_a
, m_b
и m_c
;
открытый метод с именем setValues(), который позволит устанавливать значения для m_a
, m_b
и m_c
;
открытый метод с именем print(), который будет выводить объект класса Numbers в следующем формате: <m_a, m_b, m_c>
.
Следующий код функции main():
1 2 3 4 5 6 7 8 9 |
int main() { Numbers point; point.setValues(3.0, 4.0, 5.0); point.print(); return 0; } |
Должен выдавать следующий результат:
<3, 4, 5>
Ответ №2.а)
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 |
#include <iostream> class Numbers { private: double m_a, m_b, m_c; public: void setValues(double a, double b, double c) { m_a = a; m_b = b; m_c = c; } void print() { std::cout << "<" << m_a << ", " << m_b << ", " << m_c << ">"; } }; int main() { Numbers point; point.setValues(3.0, 4.0, 5.0); point.print(); return 0; } |
b) Добавьте функцию isEqual() в класс Numbers, чтобы следующий код работал корректно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
int main() { Numbers point1; point1.setValues(3.0, 4.0, 5.0); Numbers point2; point2.setValues(3.0, 4.0, 5.0); if (point1.isEqual(point2)) std::cout << "point1 and point2 are equal\n"; else std::cout << "point1 and point2 are not equal\n"; Numbers point3; point3.setValues(7.0, 8.0, 9.0); if (point1.isEqual(point3)) std::cout << "point1 and point3 are equal\n"; else std::cout << "point1 and point3 are not equal\n"; return 0; } |
Ответ №2.b)
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
#include <iostream> class Numbers { private: double m_a, m_b, m_c; public: void setValues(double a, double b, double c) { m_a = a; m_b = b; m_c = c; } void print() { std::cout << "<" << m_a << ", " << m_b << ", " << m_c << ">"; } // Здесь мы можем использовать тот факт, что контроль доступа осуществляется на основе класса для того, // чтобы получить доступ напрямую к закрытым членам объекта d класса Numbers bool isEqual(const Numbers &d) { return (m_a == d.m_a && m_b == d.m_b && m_c == d.m_c); } }; int main() { Numbers point1; point1.setValues(3.0, 4.0, 5.0); Numbers point2; point2.setValues(3.0, 4.0, 5.0); if (point1.isEqual(point2)) std::cout << "point1 and point2 are equal\n"; else std::cout << "point1 and point2 are not equal\n"; Numbers point3; point3.setValues(7.0, 8.0, 9.0); if (point1.isEqual(point3)) std::cout << "point1 and point3 are equal\n"; else std::cout << "point1 and point3 are not equal\n"; return 0; } |
Задание №3
Теперь попробуем что-то посложнее. Напишите класс, который реализует функционал стека. Класс Stack должен иметь:
закрытый целочисленный фиксированный массив длиной 10 элементов;
закрытое целочисленное значение для отслеживания длины стека;
открытый метод с именем reset(), который будет инициализировать значением 0
длину и все значения элементов;
открытый метод с именем push(), который будет добавлять значение в стек. Метод push() должен возвращать значение false
, если массив уже заполнен, в противном случае — true
;
открытый метод с именем pop() для возврата значений из стека. Если в стеке нет значений, то должен выводиться стейтмент assert;
открытый метод с именем print(), который будет выводить все значения стека.
Следующий код функции main():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
int main() { Stack stack; stack.reset(); stack.print(); stack.push(3); stack.push(7); stack.push(5); stack.print(); stack.pop(); stack.print(); stack.pop(); stack.pop(); stack.print(); return 0; } |
Должен выдавать следующий результат:
( )
( 3 7 5 )
( 3 7 )
( )
Ответ №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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
#include <iostream> #include <cassert> class Stack { private: int m_array[10]; // это будут данные нашего стека int m_next; // это будет индексом следующего свободного элемента стека public: void reset() { m_next = 0; for (int i = 0; i < 10; ++i) m_array[i] = 0; } bool push(int value) { // Если стек уже заполнен, то возвращаем false if (m_next == 10) return false; m_array[m_next++] = value; // присваиваем следующему свободному элементу значение value, а затем увеличиваем m_next return true; } int pop() { // Если элементов в стеке нет, то выводим стейтмент assert assert (m_next > 0); // m_next указывает на следующий свободный элемент, поэтому последний элемент со значением - это m_next-1. // Мы хотим сделать следующее: // int val = m_array[m_next-1]; // получаем последний элемент со значением // --m_next; // m_next теперь на единицу меньше, так как мы только что вытянули верхний элемент стека // return val; // возвращаем элемент // Весь вышеприведенный код можно заменить следующей (одной) строкой кода return m_array[--m_next]; } void print() { std::cout << "( "; for (int i = 0; i < m_next; ++i) std::cout << m_array[i] << ' '; std::cout << ")\n"; } }; int main() { Stack stack; stack.reset(); stack.print(); stack.push(3); stack.push(7); stack.push(5); stack.print(); stack.pop(); stack.print(); stack.pop(); stack.pop(); stack.print(); return 0; } |
Если переменные члены имеют спецификатор по умолчанию private, а методы public, то почему в
Visual Studio выдает ошибку на такой код :
стоит раскомментировать строку //public: и все в порядке.
Члены класса по умолчанию являются private.
Устанавливать методы public — это рекомендация (правило), потому что взаимодействие с классом происходит через методы.
Задание 3 . Зачем функция push() должна возвращать логические значения , если с этими значения ничего не делают?
Зачем функция pop() возвращает значение элемента массива, если с этим значением ничего не делают?
Зачем прописывать спецификаторы privat для членов переменных и public для методов , если члены переменные закрыты , а методы открыты по умолчанию?
А насколько правильно объявлять члены класса (не методы, именно члены!) protected, а не private в реальных задачах? В реальных, имеется ввиду, в реальных проектах, а не учебных.
Но ведь этот метод в таком виде не обнуляет значение m_array[m_next], а только возвращает его
И это правильно! Так работают все массивы и контейнеры в STL.
Затирание данных, это лишняя трата ресурсов процессора, потому как указатель на данную область вернется ТОЛЬКО в том случае когда будут добавляться данные. А добавления данных в любом случае ПЕРЕЗАПИСЫВАЕТ содержимое ячейки. Так что любое удаление данных в контейнерах это просто смещение указателя, никто не затирает данные.
Хорошо, а зачем тогда резетить массив, когда можно обнулить только указатель?
Написал функции push() и pop() по принципу LIFO. Новые значения заталкиваются в стек через нулевой элемент массива и через него же выталкиваются обратно.
Два вопроса по третьему заданию:
1. Почему в методе reset нельзя использовать цикл foreach? (у меня выдает такую ошибку — Вызвано исключение: нарушение доступа для записи. this было 0x34F43096.)
2. Зачем методу push быть логическим? Чем плох такой вариант:
Присоединяюсь к вашему первому вопросу.
P.S. Может ему не нравятся изначальные мусорные значения?
Верно Дмитрий, это и есть ответ на ваш первый вопрос. В целях эксперимента, изначально очистил циклом for, массив до нулевых значений. Только после этих манипуляций, цикл foreach заработал:
Ну а второй вопрос, то думаю это просто для разнообразия такой способ реализации. Видите, тут "push сделай чтобы вернул 1 или 0, а pop сделай с assertom". Думаю это чисто в целях нашего развития
Благодарю, за обратную связь!
Вам спасибо за интересный вопрос) Вы бы не спросили, я бы и сам ответа не знал)) Всего вам доброго:)
Вы неправильно понимаете сути работы циклов foreach.
Дело в том, что в вашем случае, на каждой итерации цикла foreach переменной i присваивается не индекс элемента массива, а значение этого элемента. (Таким образом переменная i и есть элемент, и его тип должен совпадать с типом массива)
Таким образом декларация: m_stack[i] превращается в m_stack[m_stack[<Номер_итерации>]], а это явно не то, что вам было нужно.
На самом деле foreach здесь очень даже справляется с задачей:
По вопросу 2.
Логический тип возврата нужен наверное, чтобы можно было сделать проверку, отработало ли как надо.
Что-то типа:
Правда задание 2.а писал без private.
И так и так работает. Хотя если бы сначала функции public — тогда понятно, без private доступ отовсюду.
Задание 2b.
Обязательно ли указывать переменную d как ссылку?
Задание 3.
Сказано, что: "Класс Stack должен иметь:
Открытый целочисленный фиксированный массив длиной 10.
Открытое целочисленное значение для отслеживания длины стека."
При этом в ответе написано:
Здесь нет ошибки, или я чего-то не понял?
Задание 2b.
Обязательно ли указывать переменную d как ссылку?
Необходимо. Лучше еще раз почитайте Урок №104. Указатели на функции.
Я сам протупил с этим, тут всего лишь делается указатель на функцию, которая прочитывает наш point 2 & point 3, делает сравнение.
Не мой день сегодня, с 3м тоже что-то натупил. 1е сделал быстро и четко.
Разве указатель не на объект d?
Я считаю, что в задании 3, строка 15 вместо:
нужно использовать, вот это:
На полученный результат это не повлияет, но с точки зрения возникновения ошибок, 2-ой вариант безопаснее, т.к. при цикле ошибок может быть больше, чем при простой инициализации.
Надеюсь, Юрий, вы заметите этот комментарий.
Пробовал почти так ,как вы говорите,
Но это не работает почему то. Значения массива так и остаются мусором а не нулями.
Просто вы просите обьявить массив внутри функции.. Мы ведь его уже обьявляли выше…
Если в классе сначала идут все private члены, а потом public, обязательно ли явно указывать спецификатор privatе? В классе ведь всем членам по умолчанию устанавливается спецификатор доступа private.
Конечно не обязательно указывать явно, это и подразумевается под "по умолчанию"
В некоторых дальнейших уроках фигурирует код с принудительным указанием private, например итоговый тест этой главы второе задание. Это имеет какую то причину, или так совпали звёзды? ))
так совпали звезды
Я тут недавно пытался написать программу в одном компиляторе и он заставлял меня в классах везде прописывать const int blabla вместо int blabla , почему ?
Мой вариант 3 задания
В задании 2 переменные имеют тип double и сравниваются между собой. Корректно ли это? Например такие не сравниваются (наобум набрал)
Что-то я забыла про значение для отслеживания длины стека и написала код без него.
Корректно получилось?
Момент в том, что у вас такая реализация, что в стек невозможно «положить» число 0. Как раз таки из-за того что вы не использовали доп.переменую обозначающую длину.
Если я захочу заполнить последний элемент массива нулем, я не могу этого сделать. Вернее могу, но «код» будет думать что там пустое место, а не заложенное пользователем значение 0.
Короче у вас 0 используется как служебный символ. Из-за этого пользователь не может оперировать этим числом в своих целях , только в целях служебных
И ещё возник вопрос, правильно ли у меня работает assert hello-site.ru/share/Stek — код программы. priscree.ru/img/ddd58dea7702d7.jpg — скриншот проблемы. Приходится закрывать всплывающее окно, не похоже на нормальную работу программы…
У вас код идентичен с кодом ответа (только другие имена переменных). Всё правильно. Появление ошибки — это нормальное срабатывание стейтмента assert. Assert у вас срабатывает, так как вы вызываете метод pop, прежде чем заполняете стек данными:
Это бессмысленно. Вы пытаетесь вытянуть элемент из пустого стека. Соответственно, срабатывает assert, который и прописан для такого случая.
Есть небольшая недоработка в ответе задания 3, в разделе private класса Stack, нужно инициализировать индекс свободного элемента стека в 0. (На случай, если кто-то сразу захочет вызвать метод pop)
Так ведь используется стейтмент assert, который выведет сообщение об ошибке, если кто-то попытается вызвать метод pop сразу. Зачем инициализировать нулём?
Добрый день! МЕня вот какой момент заинтересовал:
Почему С++ вначале присваивает значение value для нулевого элемента, а потом уже только увеличивает значение m_next++ на единицу? Пытаюсь связать с 38 уроком, но не получается понять порядок действий с++ с этим аргументом.
Мне кажется тут просто нужно вникнуть в разницу между i++ и ++i
Для этих целей в программе предусмотрена функция reset(), которая присваивает длине значение = 0. Ваше утверждение верно, если мы не будем использовать этот метод