Рассмотрим следующую программу:
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. Public и private используются для членов, чтобы сделать их открытыми, либо закрытыми. Третий спецификатор доступа, protected (защищенный), работает почти так же, как и private. Детальнее о нем и его отличиях от private мы поговорим, когда будем рассматривать наследование.
Использование спецификаторов доступа
Классы могут использовать (и активно используют) сразу несколько спецификаторов доступа для установки уровней доступа каждого из своих членов.
Обычно переменные-члены являются закрытыми, а методы – открытыми. Почему именно так — мы поговорим в следующем уроке.
Правило: Устанавливайте спецификатор доступа 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 по умолчанию), мы можем получить доступ к ним через public методы setDate() и print()!
На группу членов public еще ссылаются как на открытый (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) Что такое открытый член?
Ответ а)
Открытый член — это член класса, доступ к которому может быть осуществлен кем-угодно.
b) Что такое закрытый член?
Ответ b)
Закрытый член — это член класса, доступ к которому разрешен только другим членам класса.
c) Что такое спецификатор доступа?
Ответ c)
Спецификатор доступа определяет, кто имеет доступ к членам, которые следуют за спецификатором.
d) Сколько есть спецификаторов доступа, назовите их?
Ответ d)
3. Public, private и protected.
Задание №2
a) Напишите простой класс с именем Numbers. Этот класс должен иметь:
три private переменные-члены типа double: m_a, m_b и m_c;
public метод с именем setValues(), который позволит устанавливать значения для m_a, m_b и m_c;
public метод с именем 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>
Ответ а)
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; } |
Ответ 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
a) Теперь попробуем что-то сложнее. Напишите класс, который реализует функциональность стека. Что такое стек — смотрите урок 105.
Класс Stack должен иметь:
private фиксированный массив целых чисел длиной 10;
private целочисленное значение для отслеживания длины стека;
public метод с именем reset(), который будет сбрасывать длину и все значения элементов на 0;
public метод с именем push(), который будет добавлять значение в стек. push() должен возвращать значение false, если массив уже заполнен, и true в противном случае;
public метод с именем pop() для вытягивания и возврата значения из стека. Если в стеке нет значений, то должен выводиться стейтмент assert;
public метод с именем 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 )
( )
Ответ a)
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; } |
Мой вариант 3 задания
В задании 2 переменные имеют тип double и сравниваются между собой. Корректно ли это? Например такие не сравниваются (наобум набрал)
Что-то я забыла про значение для отслеживания длины стека и написала код без него.
Корректно получилось?
И ещё возник вопрос, правильно ли у меня работает 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 уроком, но не получается понять порядок действий с++ с этим аргументом.
Для этих целей в программе предусмотрена функция reset(), которая присваивает длине значение = 0. Ваше утверждение верно, если мы не будем использовать этот метод