JavaRush /Курси /C++ SELF /struct vs class: інкапсуляція та доступ за замовчуванням

struct vs class: інкапсуляція та доступ за замовчуванням

C++ SELF
Рівень 48 , Лекція 0
Відкрита

struct vs class: інкапсуляція та доступ за замовчуванням

1. struct і class: доступ та інкапсуляція

Коли ви лише починаєте програмувати, природно міркувати так: є дані — отже, робимо struct із полями, і цього досить. Для старту це справді добрий варіант. Але що більшою стає програма, то частіше виникає проста, хоч і неприємна проблема: один фрагмент коду змінює поле напряму, другий забуває перевірити діапазон, третій упевнений, що поле «точно не порожнє», — і зрештою програма перетворюється на суцільний детектив.

Саме тут і зʼявляється корисна думка: варто заборонити зовнішньому коду довільно змінювати «нутрощі» обʼєкта і дати доступ лише через зрозумілі методи. Ця ідея й називається інкапсуляцією. Технічно в C++ вона тримається не на магії, а на двох речах: секціях public/private і тому, який доступ стоїть за замовчуванням у struct та class.

Невелике уточнення щодо контексту: у прикладах ми орієнтуємося на сучасний C++23, який формалізують робочі чернетки стандарту.

struct: public за замовчуванням

struct у C++ — це спосіб оголосити власний тип, у якому зручно зберігати кілька повʼязаних значень. У ранніх розділах курсу ми використовували такі структури як «моделі даних»: поля відкриті, їх можна читати й змінювати напряму. Це чудово підходить для простих обʼєктів, де немає складних правил коректності.

Ключовий момент: усередині struct усі члени за замовчуванням мають доступ public. Тобто зовнішній код може звертатися до полів напряму. Це не «погана» поведінка — це просто налаштування за замовчуванням.

Мініприклад із точкою на площині:


#include <iostream>

struct Point {
    int x{};
    int y{};
};

int main() {
    Point p{10, 20};
    p.x = 15;                               // OK: public за замовчуванням
    std::cout << p.x << ' ' << p.y << '\n'; // 15 20
}

Зверніть увагу: після закривальної } стоїть ; — він обовʼязковий. І це не якийсь «особливий» тип: це такий самий користувацький тип, як і class.

class: private за замовчуванням

Слово class звучить так, ніби зараз почнеться «вища математика, ООП, патерни та UML на стіні». Насправді ні: class — це той самий механізм користувацького типу, що й struct. Тут теж можна оголошувати поля й методи.

Різниця, яка важлива для нас зараз, лише одна: у class члени за замовчуванням мають доступ private.

Приклад із простим лічильником:


#include <iostream>

class Counter {
    int value_ = 0;          // private за замовчуванням
public:
    void inc() { ++value_; }
    int value() const { return value_; }
};

int main() {
    Counter c;
    c.inc();
    std::cout << c.value() << '\n'; // 1
    // c.value_ = 42;               // помилка компіляції: private
}

Це виглядає як дрібниця, але фактично саме тут закладено фундамент: тепер стан обʼєкта не можна зламати випадковим присвоюванням із зовнішнього коду. А отже, правила коректності можна тримати «в одному місці» — усередині методів.

Різниця лише в замовчуваннях

Саме тут часто виникає міф: ніби struct — це «проста структура», а class — «справжній обʼєкт». У C++ це неправда. struct і class — майже одне й те саме: методи можна писати і там, і там; private можна робити і там, і там; секції доступу теж можна змішувати в обох випадках.

Різницю варто запамʼятати дуже чітко:

  • у struct за замовчуванням public
  • у class за замовчуванням private

Для ясності — невелика таблиця:

Що порівнюємо
struct
class
Оголошує користувацький тип так так
Можна мати поля та методи так так
Можна використовувати
public:
/
private:
так так
Доступ за замовчуванням
public
private

І ще одна практична дрібниця: крапка з комою після оголошення типу обовʼязкова в обох випадках:

struct A { int x{}; };  // ; потрібна
class  B { int y{}; };  // ; потрібна

Інкапсуляція в struct теж можлива

Коли хтось каже «ми використовуємо struct, отже інкапсуляції немає», він плутає стиль із можливостями мови. Інкапсуляція в C++ реалізується через public/private, а не через саме слово class.

Приклад struct із приватним полем:

#include <iostream>

struct Box {
private:
    int value_ = 0;
public:
    void set(int v) { value_ = v; }
    int value() const { return value_; }
};

int main() {
    Box b;
    b.set(7);
    std::cout << b.value() << '\n'; // 7
}

То навіщо взагалі потрібні два слова? Відповідь — не в техніці, а в комунікації з читачем. Поширена домовленість у командах така: якщо це «просто дані без хитрих правил» — пишемо struct; якщо це «обʼєкт із правилами, яким треба користуватися обережно» — пишемо class. Це не закон мови, а радше соціальний сигнал: «Тут є контракт».

Мінітест: різниця проявляється саме через доступ

Короткий фрагмент, який допомагає закріпити думку: різниця не в «магії», а саме в доступі.

#include <iostream>
#include <string>

struct UserStruct {
    std::string name = "anonymous";
};

class UserClass {
    std::string name_ = "anonymous";
public:
    const std::string& name() const { return name_; }
};

int main() {
    UserStruct a;
    a.name = "Alice";                       // OK
    std::cout << a.name << '\n';            // Alice

    UserClass b;
    // b.name_ = "Bob";                     // помилка: private
    std::cout << b.name() << '\n';          // anonymous
}

Якщо ви запамʼятаєте цю відмінність майже «на автоматі», то значно рідше дивуватиметеся: «чому воно недоступне, я ж нічого такого не роблю».

3. Інкапсуляція на прикладі Account

Публічні поля змушують обʼєкт бути «сам собі тестувальником»

Уявіть, що у вас є модель банківського рахунку. Наївний, але дуже поширений варіант — зробити struct із балансом і змінювати його напряму. Працює? Працює. До першої помилки.

#include <iostream>

struct Account {
    int balance_cents = 0;
};

int main() {
    Account a;
    a.balance_cents += 500;
    a.balance_cents -= 1'000'000;           // ой
    std::cout << a.balance_cents << '\n';   // -999500
}

З погляду компілятора все чудово. З погляду здорового глузду — рахунок «пішов у мінус» на суму, яку ми навіть не перевіряли. Можна сказати: «Ну, програміст має бути уважним». Так. І користувач кавомашини теж має чистити її щодня. Але виробники однаково додають автоматичне промивання.

Інкапсуляція — це і є ваше «автоматичне промивання»: ми закриваємо поле і робимо методи, які не дадуть виконати безглузду операцію.

#include <iostream>

class Account {
    int balance_cents_ = 0;
public:
    void deposit(int cents) {
        if (cents > 0) balance_cents_ += cents;
    }

    bool withdraw(int cents) {
        if (cents <= 0) return false;
        if (cents > balance_cents_) return false;
        balance_cents_ -= cents;
        return true;
    }

    int balance_cents() const { return balance_cents_; }
};

int main() {
    Account a;
    a.deposit(500);
    a.withdraw(1'000'000);                  // не вдалося, але обʼєкт коректний
    std::cout << a.balance_cents() << '\n'; // 500
}

Поки що ми не обговорюємо винятки, складні конструктори чи «красиву архітектуру». Ми лише зробили так, щоб обʼєкт не можна було привести в безглуздий стан одним присвоюванням.

Схема: хто тепер має право змінювати стан обʼєкта

Корисно бодай раз побачити, що відбувається під час переходу від «публічних полів» до «публічних методів». У першому варіанті будь-який код може змінити поле. У другому — лише код усередині класу, тобто його методи, може змінювати приватні поля.

flowchart TD
    Outside["Зовнішній код"] -->|виклик public-методів| Obj["Обʼєкт класу"]
    Obj -->|читає/змінює| Private["private-поля"]
    Outside -.->|напряму змінювати не можна| Private

Сенс цієї схеми простий: правила перевірки живуть усередині методів, а отже, не розпорошуються по всьому проєкту. Якщо правило зміниться, наприклад «не можна знімати менше 10 центів», ви зміните одне місце, а не двадцять.

4. Приклад із проєкту: Task як більш осмислений тип

Щоб не говорити абстрактно, уявімо, що в попередніх розділах курсу ми робили консольний мініпланувальник задач: зберігали задачі в std::vector, виводили список, позначали виконаними. Раніше модель задачі могла мати такий вигляд:

#include <string>

struct Task {
    int id = 0;
    std::string title;
    bool done = false;
};

Проблема такого підходу не в тому, що він «не ООП». Проблема в іншому: зовнішній код може зробити ось так — і компілятор не заперечить:

Task t;
t.id = -5;            // «задача номер мінус пʼять»
t.title = "";         // порожній заголовок
t.done = true;        // виконано… але що саме?

Якщо у вас немає правил, це нормально. Але якщо ви хочете, щоб задача завжди мала коректний id і непорожній заголовок, зручніше покласти ці правила всередині типу.

Зробімо перший крок без конструкторів — вони будуть у наступних лекціях: оголосимо class Task і дамо йому мінімальний набір методів.

#include <string>

class Task {
    int id_ = 0;
    std::string title_ = "untitled";
    bool done_ = false;
public:
    bool set_id(int id) {
        if (id <= 0) return false;
        id_ = id;
        return true;
    }

    bool rename(const std::string& title) {
        if (title.empty()) return false;
        title_ = title;
        return true;
    }
};

Зараз це може виглядати так, ніби «ми просто сховали поля й додали трохи зайвого коду». Але зверніть увагу на ефект: обʼєкт уже не можна випадково привести в некоректний стан. Якщо set_id повернув false, отже, стан не змінився, — а це вже зародок дисципліни.

Додамо методи читання — лише мінімум, щоб обʼєкт можна було виводити:

#include <string>

class Task {
    int id_ = 0;
    std::string title_ = "untitled";
    bool done_ = false;
public:
    int id() const { return id_; }
    const std::string& title() const { return title_; }
    bool done() const { return done_; }

    void mark_done() { done_ = true; }
};

Тут уже видно стиль, який ми далі розвиватимемо: «читати можна безпечно, а змінювати — через контрольовані операції».

І так, суфікс _ у приватних полях — це не вимога мови, а корисна звичка: коли ви бачите done_, мозок автоматично зчитує це як «внутрішнє поле, напряму не чіпати».

5. Як обирати між struct і class

Коли ви лише вчитеся, дуже хочеться отримати залізне правило: «завжди робіть так». У реальній розробці воно частіше звучить інакше: «робіть зрозуміло й безпечно».

Зазвичай struct чудово підходить для простих даних, де немає жорстких обмежень і внутрішніх правил коректності. Наприклад, Point{ x, y } або Rgb{ r, g, b }: там логіки мінімум, а прямий доступ до полів робить код коротшим і читабельнішим.

class частіше обирають тоді, коли в обʼєкта є правила коректності і ви хочете, щоб компілятор допомагав вам не порушувати їх. Щойно ви ловите себе на думці «поле не можна змінювати як завгодно» або «значення має бути лише в певному діапазоні» — це вже кандидат на private і публічні методи.

Водночас важливо памʼятати: перехід на class не робить програму правильною автоматично. Це лише інструмент, який допомагає виразити контракт. Якщо ви закриєте поле, але зробите метод встановлення значення без перевірок, то просто додасте коду, не додавши змісту. Зміст зʼявляється тоді, коли методи справді захищають стан.

6. Типові помилки

Помилка № 1: забути public: у class і дивуватися, що «нічого не працює».
Це найчастіша ситуація для новачків: ви пишете class, додаєте метод, а потім компілятор скаржиться на «private». Причина проста: у class усе private за замовчуванням. Лікується це не заклинаннями, а рядком public: перед тим, що ви хочете відкрити назовні.

Помилка № 2: думати, що struct «не вміє» інкапсуляцію.
Іноді struct сприймають як «бідного родича класу». Але private: і public: працюють однаково і там, і там. Різниця лише в замовчуваннях. Якщо вам зручно оголосити тип словом struct, але хочеться приховати частину деталей, — приховуйте, мова не заперечує.

Помилка № 3: робити class, але залишати всі поля public, бо «так швидше».
Це виглядає як компроміс, але зазвичай це програшний варіант: ви берете «сигнал для читача» — слово class, — але не використовуєте його зміст. У підсумку наступний розробник очікує інкапсуляцію й обережне API, а натомість отримує вільний доступ до полів. Якщо поля мають бути відкриті, чесніше написати struct.

Помилка № 4: плутати «доступ за замовчуванням» і «права методів».
Іноді здається, що якщо поле private, то й методи не зможуть його змінювати. Насправді навпаки: методи цього ж класу мають доступ до приватних полів свого обʼєкта. Саме тому інкапсуляція і працює: зовнішній код не може, а методи — можуть, і роблять це за правилами.

Помилка № 5: забути, що після } в оголошенні типу потрібен ;.
Компілятор у такому разі видає повідомлення, яке новачкові може здатися загадковим. Запамʼятайте шаблон: class X { ... }; і struct Y { ... };. Так, крапка з комою тут — як кришка від пляшки: наче дрібниця, але без неї все розливається.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ