C++

Класи, об'єкти та методи

Перехід від struct до class у C++. Що таке клас, об'єкт та метод. Різниця між struct і class. Неявний об'єкт у методах. Зв'язок із STL: std::string, std::vector як класи. Конвенції іменування класів.

Класи, об'єкти та методи

Від структур до класів: що змінюється?

У попередній статті ми усвідомили фундаментальну проблему процедурного підходу: дані (struct) і функції існують окремо, що призводить до експоненційного зростання складності за формулою N×M. Ми зрозуміли філософію об'єктно-орієнтованого програмування і чотири його принципи. Настав час перейти від теорії до практики.

Почнемо з чогось знайомого — структури BankAccount з попередньої статті:

struct BankAccount
{
    std::string ownerName;
    long accountNumber;
    double balance;
};

void deposit(BankAccount& account, double amount)
{
    account.balance += amount;
}

void withdraw(BankAccount& account, double amount)
{
    if (account.balance >= amount)
    {
        account.balance -= amount;
    }
}

void printInfo(const BankAccount& account)
{
    std::cout << "Рахунок #" << account.accountNumber
              << ", власник: " << account.ownerName
              << ", баланс: " << account.balance << " грн\n";
}

Це типовий процедурний підхід: структура зберігає дані, функції оперують цими даними ззовні. Зв'язок між BankAccount і функціями deposit, withdraw, printInfo існує лише в голові програміста. Компілятор не знає, що ці функції належать цьому типу даних.

Тепер перепишемо це як клас:

class BankAccount
{
public:
    std::string ownerName;
    long accountNumber;
    double balance;

    void deposit(double amount)
    {
        balance += amount;
    }

    void withdraw(double amount)
    {
        if (balance >= amount)
        {
            balance -= amount;
        }
    }

    void printInfo() const
    {
        std::cout << "Рахунок #" << accountNumber
                  << ", власник: " << ownerName
                  << ", баланс: " << balance << " грн\n";
    }
};

Ключові зміни:

  1. struct замінено на class — це ключове слово, що вводить клас
  2. З'явилося слово public: — воно робить поля і методи доступними ззовні (детально розглянемо у наступній статті)
  3. Функції перемістилися всередину класу — тепер вони називаються методами
  4. Методи не приймають BankAccount& як параметр — вони автоматично працюють з «поточним» об'єктом

Використання теж змінюється:

// Процедурний стиль
BankAccount myAccount;
deposit(myAccount, 1000.0);
withdraw(myAccount, 500.0);
printInfo(myAccount);

// Об'єктно-орієнтований стиль
BankAccount myAccount;
myAccount.deposit(1000.0);
myAccount.withdraw(500.0);
myAccount.printInfo();

Синтаксис myAccount.deposit(1000.0) читається природно: «мій рахунок поповнюється на 1000». Об'єкт стає активним учасником, а не пасивним контейнером даних.

Це і є суть переходу до ООП: дані і функції, що з ними працюють, об'єднуються в єдину одиницю — клас. Функції стають методами класу, а змінні структури — полями класу (data members).

Синтаксис класу: анатомія оголошення

Загальна форма оголошення класу:

class ClassName
{
public:
    // Поля (змінні-члени)
    тип1 fieldName1;
    тип2 fieldName2;

    // Методи (функції-члени)
    повертаємийТип methodName1(параметри)
    {
        // тіло методу
    }

    повертаємийТип methodName2(параметри);  // може бути оголошено тут, визначено пізніше
};  // Крапка з комою обов'язкова!

Ключові елементи синтаксису

1. Ключове слово class

Так само, як struct вводить структуру, class вводить клас. Це сигнал компілятору: «Далі йде визначення нового типу даних з інкапсульованими даними і поведінкою».

2. Ім'я класу (ClassName)

За конвенцією C++ (і згідно з нашим code style), імена класів пишуться у PascalCase — кожне слово з великої літери, без пробілів і підкреслень:

// ✅ Правильно
class BankAccount { };
class CustomerRecord { };
class PaymentProcessor { };

// ❌ Неправильно
class bankAccount { };      // camelCase — для змінних та функцій
class customer_record { };  // snake_case — стиль Python/C
class PAYMENT { };          // UPPER_CASE — для констант

3. Тіло класу { }

Всередині фігурних дужок розміщуються:

  • Поля (data members) — змінні, що зберігають стан об'єкта
  • Методи (member functions) — функції, що визначають поведінку об'єкта

4. Крапка з комою після }

class Date
{
    // ...
};  // ← Крапка з комою обов'язкова!

Як і у випадку зі struct, оголошення класу є декларацією типу, а декларації в C++ завершуються крапкою з комою. Її відсутність призведе до помилки компіляції, причому компілятор може поскаржитися на наступний рядок після класу, що ускладнює діагностику.

5. Специфікатор доступу public:

class BankAccount
{
public:  // ← Все, що йде далі, доступне зовні класу
    std::string ownerName;
    void deposit(double amount);
};

public: робить члени класу доступними для зовнішнього коду. Без нього поля і методи за замовчуванням є приватними (private) — недоступними ззовні. Детально про public і private ми поговоримо у наступній статті. Поки що просто запам'ятайте: у класах ставте public: перед полями і методами, які хочете зробити доступними.


Створення об'єкта: від типу до екземпляра

Оголошення класу не виділяє пам'яті. Це лише опис, «креслення» типу даних. Щоб використовувати клас, потрібно створити об'єкт — конкретний екземпляр цього типу.

Аналогія: Клас — це архітектурний план будинку. Об'єкт — це реально збудований будинок за цим планом. За одним планом можна збудувати десятки будинків, кожен займе власну ділянку землі (пам'ять).

Синтаксис створення об'єкта

ClassName objectName;  // Об'єкт зі значеннями за замовчуванням (можуть бути невизначені)

ClassName objectName { value1, value2, ... };  // Uniform initialization (C++11)

ClassName objectName(value1, value2);  // Ініціалізація через конструктор (про них далі)

Приклади:

class Date
{
public:
    int day;
    int month;
    int year;
};

int main()
{
    Date today;                    // Об'єкт створений, поля мають сміттєві значення
    Date birthday { 15, 8, 1995 }; // Uniform initialization: day=15, month=8, year=1995
    Date newYear { 1, 1, 2025 };   // day=1, month=1, year=2025
}
Якщо ви створюєте об'єкт без ініціалізації (Date today;), поля можуть містити невизначені значення (garbage). Завжди ініціалізуйте об'єкти явно: Date today { 16, 9, 2026 };. Пізніше ми вивчимо конструктори, які гарантують коректну ініціалізацію.

Термінологія: об'єкт vs екземпляр

У C++ терміни об'єкт (object) і екземпляр (instance) часто використовуються як синоніми:

  • Об'єкт — загальний термін для будь-якої змінної, що займає пам'ять
  • Екземпляр класу — конкретний об'єкт певного типу класу

Технічно, int x = 5; теж створює об'єкт (об'єкт типу int). Але коли говорять «об'єкт» у контексті ООП, зазвичай мають на увазі екземпляр класу.


Доступ до полів і методів: оператор крапка

Так само, як і у структур, доступ до членів класу здійснюється через оператор вибору членів — крапку (.):

class CreditCard
{
public:
    std::string cardNumber;
    double balance;

    void charge(double amount)
    {
        balance += amount;
    }

    void printBalance() const
    {
        std::cout << "Баланс: " << balance << " грн\n";
    }
};

int main()
{
    CreditCard myCard;
    myCard.cardNumber = "1234-5678-9012-3456";  // Доступ до поля
    myCard.balance = 0.0;                        // Доступ до поля
    
    myCard.charge(500.0);      // Виклик методу
    myCard.printBalance();     // Виклик методу
}

Синтаксис ідентичний для структур і класів: objectName.memberName. Але є фундаментальна семантична різниця:

  • Для структур: крапка надає доступ до даних
  • Для класів: крапка надає доступ до інтерфейсу (публічним членам — даним чи методам)
Хороша практика: у добре спроєктованому класі поля приховані (private), а зовнішній код взаємодіє лише через методи. Це інкапсуляція — один із чотирьох принципів ООП, що ми розглянули у попередній статті. У наступній статті ми реалізуємо цей принцип через специфікатори доступу.

Методи класу: функції з неявним об'єктом

Метод (member function, method) — це функція, визначена всередині класу. Вона має особливу властивість: неявний доступ до полів об'єкта, для якого вона викликана.

Визначення методів всередині класу

Найпростіший спосіб — визначити метод безпосередньо в тілі класу:

class Rectangle
{
public:
    double width;
    double height;

    // Метод, визначений всередині класу
    double area() const
    {
        return width * height;
    }

    double perimeter() const
    {
        return 2 * (width + height);
    }

    void scale(double factor)
    {
        width *= factor;
        height *= factor;
    }
};

Зверніть увагу: методи area() і perimeter() не приймають параметрів, але мають доступ до width і height. Звідки?

Неявний об'єкт (implicit object)

Коли ви викликаєте метод через оператор крапка:

Rectangle rect { 5.0, 3.0 };
double a = rect.area();  // width=5.0, height=3.0

Компілятор неявно передає методу area() вказівник на об'єкт rect. Усередині методу width і height автоматично означають rect.width і rect.height.

Якби ми викликали метод для іншого об'єкта:

Rectangle screen { 1920.0, 1080.0 };
double b = screen.area();  // width=1920.0, height=1080.0

Той самий метод area() тепер працює з screen.width і screen.height.

Принцип роботи: Кожен метод отримує прихований параметр — покажчик на об'єкт, для якого він викликаний. Цей покажчик називається this (про нього детально у статті 55). Компілятор автоматично додає this-> перед кожним зверненням до поля:

// Ви пишете:
double area() const
{
    return width * height;
}

// Компілятор інтерпретує:
double area(const Rectangle* this) const
{
    return this->width * this->height;
}

Це і є неявний об'єкт — об'єкт, що передається методу автоматично, без явного запису у списку параметрів.

Порівняння з процедурним підходом:
// Процедурний: явна передача об'єкта
double area(const Rectangle& rect)
{
    return rect.width * rect.height;
}
// Виклик: area(myRect)

// ООП: неявна передача через this
double area() const
{
    return width * height;
}
// Виклик: myRect.area()
У другому випадку синтаксис природніший: «прямокутник обчислює свою площу», а не «функція обчислює площу прямокутника».

Методи з параметрами: комбінація явного і неявного

Метод може приймати параметри, як і звичайна функція. Але він завжди має неявний доступ до полів об'єкта:

class Counter
{
public:
    int value;

    void increment()
    {
        value++;  // Неявний доступ до поля
    }

    void add(int amount)  // Явний параметр
    {
        value += amount;  // Неявний доступ + явний параметр
    }

    void copyFrom(const Counter& other)  // Явний параметр — інший об'єкт
    {
        value = other.value;  // this->value = other.value
    }
};

int main()
{
    Counter c1 { 10 };
    Counter c2 { 50 };

    c1.increment();        // c1.value стає 11
    c1.add(5);             // c1.value стає 16
    c1.copyFrom(c2);       // c1.value стає 50 (копіюємо з c2)
}

Метод copyFrom демонструє цікавий момент: він працює з двома об'єктами одночасно:

  • Неявний об'єкт this (для c1.copyFrom(c2) це c1)
  • Явний параметр other (це c2)

Вираз value = other.value; насправді означає: c1.value = c2.value.


Різниця між struct і class: лише доступ за замовчуванням

Чи є фундаментальна різниця між struct і class у C++? Відповідь: майже ні. Єдина відмінність — доступ за замовчуванням:

  • struct: члени за замовчуванням public (доступні ззовні)
  • class: члени за замовчуванням private (недоступні ззовні)
struct Rectangle
{
    // Ці поля public, навіть без ключового слова
    double width;
    double height;

    double area() const  // Цей метод теж public
    {
        return width * height;
    }
};

Обидва класи функціонально ідентичні після компіляції — різниця лише у тому, потрібно чи ні явно писати public:.

Коли використовувати struct, а коли class?

Згідно з нашим code style (і загальноприйнятою практикою C++):

Використовуйте struct

Для простих структур даних без поведінки або з мінімальною поведінкою:

struct Point
{
    double x;
    double y;
};

struct Color
{
    unsigned char red;
    unsigned char green;
    unsigned char blue;
};

Такі структури служать лише для групування даних. Всі поля публічні, логіка мінімальна або відсутня.

Використовуйте class

Для об'єктів з інкапсульованим станом і поведінкою:

class BankAccount
{
private:
    double balance;  // Приховано

public:
    void deposit(double amount);
    bool withdraw(double amount);
    double getBalance() const;
};

Такі класи приховують внутрішню структуру і надають контрольований доступ через методи. Це справжня інкапсуляція.

Правило: Якщо ваш тип даних має приховані поля (private) і публічні методи для роботи з ними — використовуйте class. Якщо це просто контейнер даних без приховування — struct допустимий. Але у 90% випадків у ООП-коді використовується class.

Конвенції іменування: стиль коду для класів

Згідно з нашим code style (див. статтю 2), дотримуємося наступних правил:

Імена класів — PascalCase

class BankAccount { };       // ✅
class PaymentProcessor { };  // ✅
class HttpClient { };        // ✅

class bankAccount { };       // ❌ camelCase
class payment_processor { }; // ❌ snake_case

Імена методів — camelCase

class Customer
{
public:
    void updateEmail(const std::string& newEmail);  // ✅
    bool isActive() const;                          // ✅
    double calculateDiscount();                     // ✅

    void UpdateEmail();        // ❌ PascalCase
    bool is_active();          // ❌ snake_case
};

Імена полів — camelCase (БЕЗ префікса m_)

У деяких code style (наприклад, Google C++ Style Guide) поля класу позначають префіксом m_ (від members). У нашому курсі ми НЕ використовуємо цей префікс, згідно з Java-подібним стилем:

class Rectangle
{
public:
    double width;   // ✅ camelCase, без m_
    double height;  // ✅

    // double m_width;  ❌ Не використовуємо префікс m_
};
Префікс m_ корисний у великих класах для розрізнення полів і локальних змінних. Але у навчальному курсі ми дотримуємося більш чистого Java-стилю без префіксів. У реальних проєктах слідуйте code style вашої команди.

Порядок розміщення членів класу

Існує дві популярні конвенції порядку секцій у класі:

Варіант 1: Private → Public (приховане зверху)

class BankAccount
{
private:
    std::string ownerName;
    double balance;

public:
    void deposit(double amount);
    double getBalance() const;
};

Варіант 2: Public → Private (інтерфейс зверху)

class BankAccount
{
public:
    void deposit(double amount);
    double getBalance() const;

private:
    std::string ownerName;
    double balance;
};
Рекомендація курсу: Використовуйте Public → Private. Читач класу спочатку бачить інтерфейс (що клас уміє), а вже потім — деталі реалізації (як це зберігається всередині). Це відповідає принципу «думай від інтерфейсу, а не від реалізації».

Повний приклад:

class Customer
{
public:
    // Конструктори (про них у наступній статті)
    
    // Публічні методи (інтерфейс)
    void updateEmail(const std::string& newEmail);
    std::string getName() const;
    bool isActive() const;

private:
    // Приховані поля (реалізація)
    std::string name;
    std::string email;
    bool active;
};

Методи поза класом: оголошення vs визначення

До цього моменту ми визначали методи безпосередньо в тілі класу. Але для великих класів це робить код важкочитабельним. Методи можна оголосити в класі, а визначити окремо:

class Rectangle
{
public:
    double width;
    double height;

    // Оголошення методів (прототипи)
    double area() const;
    double perimeter() const;
    void scale(double factor);
};

// Визначення методів поза класом
double Rectangle::area() const
{
    return width * height;
}

double Rectangle::perimeter() const
{
    return 2 * (width + height);
}

void Rectangle::scale(double factor)
{
    width *= factor;
    height *= factor;
}

Оператор :: (scope resolution operator)

Rectangle::area читається як «метод area класу Rectangle». Оператор :: вказує, що функція area належить області видимості класу Rectangle, а не є вільною функцією.

Без :: компілятор сприйме area() як звичайну функцію, не пов'язану з класом.

Детально розподіл класу на заголовковий файл (.h) і файл реалізації (.cpp) ми розглянемо у статті 56 «Розподіл класу на заголовковий файл та файл реалізації». Поки що визначайте методи всередині класу для простоти.

Реальні класи навколо: STL як колекція ООП

Ви вже користувалися об'єктно-орієнтованим програмуванням, навіть не усвідомлюючи цього! Стандартна бібліотека C++ (STL — Standard Template Library) повністю побудована на класах.

std::string — клас для роботи з рядками

#include <string>

int main()
{
    std::string message = "Hello";  // Створення об'єкта класу string
    
    // Виклик методів класу string
    message.append(", world!");  // Метод append()
    int len = message.length();  // Метод length()
    char first = message.at(0);  // Метод at()
    
    std::cout << message << "\n";  // Hello, world!
}

std::string — це клас. Коли ви пишете std::string message, ви створюєте об'єкт цього класу. Коли викликаєте message.length(), ви викликаєте метод класу string.

std::vector — клас динамічного масиву

#include <vector>

int main()
{
    std::vector<int> numbers;  // Об'єкт класу vector
    
    numbers.push_back(10);     // Метод push_back()
    numbers.push_back(20);
    numbers.push_back(30);
    
    int size = numbers.size(); // Метод size()
    int first = numbers.at(0); // Метод at()
    
    std::cout << "Розмір: " << size << "\n";  // 3
}

std::cout — об'єкт класу потоку виводу

Навіть std::cout, яким ви користуєтеся з першого дня курсу, є об'єктом класу std::ostream:

std::cout << "Hello";  // Виклик перевантаженого оператора <<

std::cout.put('A');           // Метод put()
std::cout.flush();            // Метод flush()
std::cout.width(10);          // Метод width()

<< — це перевантажений оператор (про перевантаження операторів у статтях 63-65), який насправді викликає метод класу ostream.

Висновок: Від самого початку курсу ви працювали з класами і об'єктами STL. std::string, std::vector, std::array, std::cout, std::cin — усі вони є класами. Тепер ви розумієте, що відбувається «під капотом», коли ви пишете myString.length() чи myVector.push_back(value).

Практичний приклад: клас Product

Створімо клас для представлення товару в інтернет-магазині:

#include <iostream>
#include <string>

class Product
{
public:
    std::string name;
    double price;
    int stock;

    void display() const
    {
        std::cout << "Товар: " << name 
                  << ", ціна: " << price << " грн"
                  << ", на складі: " << stock << " шт.\n";
    }

    bool purchase(int quantity)
    {
        if (stock >= quantity)
        {
            stock -= quantity;
            std::cout << "Придбано " << quantity << " од. товару '" 
                      << name << "'\n";
            return true;
        }
        else
        {
            std::cout << "Недостатньо товару на складі\n";
            return false;
        }
    }

    void restock(int quantity)
    {
        stock += quantity;
        std::cout << "Поповнено склад: +" << quantity << " од.\n";
    }

    double totalValue() const
    {
        return price * stock;
    }
};

int main()
{
    Product laptop;
    laptop.name = "Ноутбук Lenovo";
    laptop.price = 25000.0;
    laptop.stock = 15;

    laptop.display();
    laptop.purchase(3);
    laptop.display();
    laptop.restock(10);
    laptop.display();

    std::cout << "Загальна вартість на складі: " 
              << laptop.totalValue() << " грн\n";

    return 0;
}

Вивід програми:

Товар: Ноутбук Lenovo, ціна: 25000 грн, на складі: 15 шт.
Придбано 3 од. товару 'Ноутбук Lenovo'
Товар: Ноутбук Lenovo, ціна: 25000 грн, на складі: 12 шт.
Поповнено склад: +10 од.
Товар: Ноутбук Lenovo, ціна: 25000 грн, на складі: 22 шт.
Загальна вартість на складі: 550000 грн

Цей приклад демонструє всі ключові концепції:

  • Поля зберігають стан товару (name, price, stock)
  • Методи виконують операції над цим станом (display, purchase, restock, totalValue)
  • Об'єкт laptop інкапсулює дані і поведінку як єдине ціле

Порядок визначення методів: не має значення

На відміну від вільних функцій, де порядок визначення важливий (функція має бути оголошена перед викликом), порядок методів усередині класу не має значення:

class Example
{
public:
    void methodA()
    {
        methodB();  // ✅ Викликає methodB, хоча вона визначена нижче
    }

    void methodB()
    {
        std::cout << "Method B called\n";
    }
};

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

Рекомендація: розміщуйте методи у логічному порядку (наприклад, публічні методи зверху, допоміжні — внизу), а не у порядку викликів. Це полегшує читання класу.

Практичне завдання: клас Timer


Резюме: перший крок у світ класів

У цій статті ми зробили перший практичний крок від процедурного програмування до об'єктно-орієнтованого. Підведімо підсумки того, що ми дізналися:

Ключові концепції:

Клас (Class)

Шаблон, що описує структуру (поля) і поведінку (методи) об'єктів певного типу. Оголошується через class ClassName { };.

Об'єкт (Object)

Конкретний екземпляр класу, що займає пам'ять. Створюється як звичайна змінна: ClassName objectName;.

Метод (Method)

Функція, визначена всередині класу. Має неявний доступ до полів об'єкта, для якого викликана. Викликається через крапку: object.method().

Неявний об'єкт

Об'єкт, що автоматично передається методу через прихований покажчик this. Метод працює з полями цього об'єкта без явного запису параметра.

Синтаксис і конвенції:

  • Імена класів: PascalCase (BankAccount, CustomerRecord)
  • Імена методів і полів: camelCase (deposit, getBalance, accountNumber)
  • struct vs class: різниця лише у доступі за замовчуванням (public vs private)
  • Порядок секцій: public → private (інтерфейс зверху)
  • Крапка з комою після } — обов'язкова!

Зв'язок із STL:

std::string, std::vector, std::cout — усі вони є класами. Ви вже користувалися ООП, навіть не усвідомлюючи цього.

Що далі:

У цій статті ми створили клас із публічними полями, до яких можна звертатися напряму. Це порушує принцип інкапсуляції — один із чотирьох стовпів ООП. У наступній статті ми вивчимо специфікатори доступу (public, private, protected) і навчимося приховувати поля класу, надаючи контрольований доступ через геттери і сеттери. Це перетворить наші класи на справжні інкапсульовані об'єкти.

Перед переходом до наступної статті практикуйтеся: перепишіть кілька своїх старих програм зі структурами, перетворивши їх на класи. Додайте методи замість вільних функцій. Відчуйте, як код стає більш природним і самодокументованим, коли дані і поведінка об'єднані.

Ви зробили перший крок у світ об'єктно-орієнтованого програмування. Попереду — інкапсуляція, конструктори, деструктори і багато іншого. Вперед! 🚀

Copyright © 2026