На цьому уроці ми розглянемо, що таке специфікатори доступу в мові С++, які вони бувають і як їх використовувати.
Специфікатори доступу
Розглянемо наступну програму:
|
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; } |
