Класи, об'єкти та методи
Класи, об'єкти та методи
Від структур до класів: що змінюється?
У попередній статті ми усвідомили фундаментальну проблему процедурного підходу: дані (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";
}
};
Ключові зміни:
structзамінено наclass— це ключове слово, що вводить клас- З'явилося слово
public:— воно робить поля і методи доступними ззовні (детально розглянемо у наступній статті) - Функції перемістилися всередину класу — тепер вони називаються методами
- Методи не приймають
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». Об'єкт стає активним учасником, а не пасивним контейнером даних.
Синтаксис класу: анатомія оголошення
Загальна форма оголошення класу:
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;
}
};
class Rectangle
{
// Ці поля private за замовчуванням!
double width; // Недоступні ззовні
double height;
public: // Треба явно вказати public
double area() const
{
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;
};
Повний приклад:
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.
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
Створіть клас Timer, що представляє таймер зі секундами:
Поля:
seconds— поточне значення таймера (ціле число)
Методи:
start(int initialSeconds)— встановити початкове значення таймераtick()— зменшити таймер на 1 секунду (але не нижче 0)isFinished()— повернутиtrue, якщо таймер дійшов до нуляdisplay()— вивести поточне значення у форматі "Таймер: X сек."
Приклад використання:
int main()
{
Timer countdown;
countdown.start(5);
while (!countdown.isFinished())
{
countdown.display();
countdown.tick();
}
countdown.display();
std::cout << "Час вийшов!\n";
return 0;
}
Очікуваний вивід:
Таймер: 5 сек.
Таймер: 4 сек.
Таймер: 3 сек.
Таймер: 2 сек.
Таймер: 1 сек.
Таймер: 0 сек.
Час вийшов!
#include <iostream>
class Timer
{
public:
int seconds;
void start(int initialSeconds)
{
seconds = initialSeconds;
}
void tick()
{
if (seconds > 0)
{
seconds--;
}
}
bool isFinished() const
{
return seconds == 0;
}
void display() const
{
std::cout << "Таймер: " << seconds << " сек.\n";
}
};
int main()
{
Timer countdown;
countdown.start(5);
while (!countdown.isFinished())
{
countdown.display();
countdown.tick();
}
countdown.display();
std::cout << "Час вийшов!\n";
return 0;
}
Пояснення:
- Поле
secondsзберігає стан таймера - Метод
start()ініціалізує таймер - Метод
tick()зменшує лічильник (з захистом від від'ємних значень) - Метод
isFinished()перевіряє завершення відліку - Метод
display()виводить стан
Зверніть увагу на const після isFinished() і display() — це вказує, що методи не змінюють стан об'єкта (про const-методи детально у статті 57).
Резюме: перший крок у світ класів
У цій статті ми зробили перший практичний крок від процедурного програмування до об'єктно-орієнтованого. Підведімо підсумки того, що ми дізналися:
Ключові концепції:
Клас (Class)
class ClassName { };.Об'єкт (Object)
ClassName objectName;.Метод (Method)
object.method().Неявний об'єкт
this. Метод працює з полями цього об'єкта без явного запису параметра.Синтаксис і конвенції:
- Імена класів: PascalCase (
BankAccount,CustomerRecord) - Імена методів і полів: camelCase (
deposit,getBalance,accountNumber) structvsclass: різниця лише у доступі за замовчуванням (publicvsprivate)- Порядок секцій: public → private (інтерфейс зверху)
- Крапка з комою після
}— обов'язкова!
Зв'язок із STL:
std::string, std::vector, std::cout — усі вони є класами. Ви вже користувалися ООП, навіть не усвідомлюючи цього.
Що далі:
У цій статті ми створили клас із публічними полями, до яких можна звертатися напряму. Це порушує принцип інкапсуляції — один із чотирьох стовпів ООП. У наступній статті ми вивчимо специфікатори доступу (public, private, protected) і навчимося приховувати поля класу, надаючи контрольований доступ через геттери і сеттери. Це перетворить наші класи на справжні інкапсульовані об'єкти.
Ви зробили перший крок у світ об'єктно-орієнтованого програмування. Попереду — інкапсуляція, конструктори, деструктори і багато іншого. Вперед! 🚀
Введення в ООП: від процедур до об'єктів
Чому процедурний підхід досягає своєї межі складності, як виникла об'єктно-орієнтована парадигма, історія C++, три принципи ООП — інкапсуляція, наслідування і поліморфізм — та зміна мислення від функцій до об'єктів.
Специфікатори доступу та інкапсуляція
Поглиблене вивчення public, private і protected. Інкапсуляція як принцип ООП. Геттери і сеттери для контрольованого доступу. Переваги приховування даних. Struct vs class — єдина різниця.