Розподіл класу на заголовковий файл та файл реалізації
Розподіл класу на заголовковий файл та файл реалізації
Чому клас у одному файлі стає проблемою
До цього моменту ми писали класи цілком у одному файлі — оголошення та реалізація методів разом, всередині блоку class. Це зручно для навчання: весь код перед очима, не потрібно перемикатися між файлами, структура класу читається відразу. Але ця зручність має ціну, яка стає відчутною при зростанні розмірів класу.
Уявіть клас Student, що керує даними студента університету. Спочатку в ньому було кілька полів — прізвище, курс, середній бал — та кілька простих методів: конструктор, геттери, один сеттер. Файл займав тридцять рядків. Потім з'явилися методи для обчислення рейтингу, форматування виводу, валідації даних, порівняння двох студентів, сортування списків. Через місяць клас розрісся до двохсот рядків, через два — до п'ятисот. Тепер, щоб побачити, які методи взагалі існують у класі, доводиться прокручувати екрани коду з реалізацією, шукати фігурні дужки, відокремлювати інтерфейс від деталей.
Проблема не лише в читабельності. Інший розробник, який хоче використати ваш клас Student у своєму файлі, підключить заголовок #include "Student.h" — і отримає весь файл цілком: визначення всіх методів, приватні деталі, коментарі до реалізації. Але йому потрібне лише оголошення — список методів, параметри, типи повернення. Решта — це внутрішня кухня, яка не стосується того, хто користується класом.
Тепер змінимо один приватний метод у класі. IDE автоматично перекомпілює кожен файл, що підключає цей заголовок — адже формально вміст заголовка змінився. Якщо це великий проєкт із десятками файлів, які використовують Student, зміна однієї функції породжує хвилю перекомпіляції. Збірка затягується на хвилини. У великих командних проєктах це перетворюється на системну проблему.
Рішення цих проблем — розподіл класу на два файли: заголовковий файл (.h) з оголошенням класу та файл реалізації (.cpp) з визначеннями методів. Це стандартна практика C++, якої дотримується вся індустрія — від стандартної бібліотеки до корпоративних проєктів.
Від монолітного файлу до пари .h + .cpp
Перш ніж перейти до синтаксису, розглянемо зміни концептуально.
Було: клас Date живе цілком у main.cpp — всі методи визначені відразу всередині тіла класу:
#include <iostream>
class Date
{
private:
int day;
int month;
int year;
public:
Date(int d, int m, int y)
{
setDate(d, m, y);
}
void setDate(int d, int m, int y)
{
day = d;
month = m;
year = y;
}
void print() const
{
std::cout << day << "." << month << "." << year << "\n";
}
};
int main()
{
Date today(16, 9, 2026);
today.print();
return 0;
}
Все працює. Але якщо інший файл захоче використати Date, йому доведеться скопіювати оголошення класу або підключити весь main.cpp — що неможливо, адже main() може бути лише одна. Якщо ви змінюєте реалізацію setDate(), доводиться перекомпілювати весь файл.
Стане: клас розбивається на три файли:
Date.h— заголовковий файл з оголошенням класу: структура, імена методів, параметри, типи повернення. Без реалізації.Date.cpp— файл реалізації з визначеннями методів. Підключає власний заголовокDate.h.main.cpp— основний файл програми, який підключаєDate.hі використовує клас.
Структура проєкту стає такою:
Тепер програміст, який працює з класом, бачить у Date.h лише інтерфейс — що клас уміє робити. Як він це робить — приховано в Date.cpp. Зміна реалізації методу в Date.cpp вимагає перекомпіляції лише цього файлу. main.cpp і будь-які інші файли, що використовують Date, залишаються без змін.
Заголовковий файл: оголошення класу
Заголовковий файл містить структуру класу: список полів і методів з їхніми сигнатурами, але без тіл функцій. Це контракт: «клас Date має такі-то методи, приймають такі-то параметри, повертають такі-то типи».
#ifndef DATE_H
#define DATE_H
class Date
{
private:
int day;
int month;
int year;
public:
Date(int d, int m, int y);
void setDate(int d, int m, int y);
void print() const;
int getDay() const { return day; }
int getMonth() const { return month; }
int getYear() const { return year; }
};
#endif
Що відбувається у цьому файлі
Рядки 1–2, 22: Header guard — захист від подвійного підключення. Ми розглядали це детально в статті 46. Багатофайлові програми. Без цього підключення Date.h у кількох файлах призведе до помилки компіляції «redefinition of class Date».
Рядки 4–21: Визначення класу Date — повна структура класу, але без реалізації більшості методів. Компілятор бачить, які поля існують, які методи доступні, які параметри вони приймають. Цього достатньо, щоб перевірити коректність використання класу в інших файлах.
Рядки 12–14: Прототипи методів — оголошення без реалізації. Конструктор Date(int d, int m, int y); повідомляє компілятору: «існує конструктор із трьома параметрами типу int». Де саме цей конструктор визначений — компілятору під час обробки заголовка знати не потрібно. Лінкер знайде визначення пізніше.
Рядки 16–18: Тривіальні геттери визначені відразу всередині класу. Це дозволено і навіть рекомендовано для простих однорядкових методів. Такі методи автоматично стають inline — компілятор може вставити їхній код безпосередньо в місце виклику замість справжнього виклику функції, що прискорює виконання.
.cpp.Файл реалізації: визначення методів поза класом
Файл реалізації містить визначення методів — їхнє повне тіло з реалізацією. Кожен метод оголошується як звичайна функція, але з префіксом ClassName:: — це говорить компілятору, що функція належить класу ClassName.
#include "Date.h"
#include <iostream>
Date::Date(int d, int m, int y)
{
setDate(d, m, y);
}
void Date::setDate(int d, int m, int y)
{
day = d;
month = m;
year = y;
}
void Date::print() const
{
std::cout << day << "." << month << "." << year << "\n";
}
Що відбувається у цьому файлі
Рядок 1: #include "Date.h" — файл реалізації завжди підключає власний заголовок. Це не просто зручність — це захисний механізм. Якщо сигнатура методу в .h і .cpp розійдуться (наприклад, у .h написано void setDate(int, int, int), а в .cpp — void setDate(int, int)), компілятор видасть помилку відразу під час компіляції Date.cpp. Без підключення заголовка ця помилка залишилася б непоміченою до етапу лінкування — де її складніше діагностувати.
Рядок 2: #include <iostream> підключається тут, а не в Date.h. Чому? Тому що iostream потрібен лише для реалізації методу print(). Заголовок Date.h не містить жодного виклику std::cout — йому iostream не потрібен. Правило: підключайте заголовки там, де вони безпосередньо використовуються. Це зменшує залежності та прискорює компіляцію.
Рядок 4: Date::Date(int d, int m, int y) — визначення конструктора. Префікс Date:: перед другим Date каже: «це не вільна функція Date, а конструктор класу Date». Без :: компілятор сприйняв би це як спробу визначити звичайну функцію з ім'ям Date, що повертає тип Date — неможливо і безглуздо.
Рядок 9: void Date::setDate(...) — визначення звичайного методу. Знову префікс Date::. Всередині цієї функції можна звертатися до приватних полів day, month, year — адже функція є членом класу, хоч і визначена поза його тілом.
Рядок 16: void Date::print() const — метод із суфіксом const. Суфікс const має бути і в оголошенні, і в визначенні. Якщо в .h метод оголошений як const, а в .cpp визначений без const — помилка компіляції.
.h і .cpp має збігатися повністю: ім'я, параметри, типи, const. Будь-яка розбіжність — помилка компіляції або лінкування.Оператор дозволу області видимості ::
Оператор :: (scope resolution operator) — це інструмент, що дозволяє уточнити, у якій області видимості знаходиться ідентифікатор. Ми вже зустрічали його раніше: std::cout, std::string — це звернення до ідентифікаторів cout та string у просторі імен std.
Для методів класів :: працює аналогічно: ClassName::methodName означає «метод methodName класу ClassName».
Синтаксис визначення методу поза класом
Загальний шаблон:
ТипПовернення ClassName::methodName(параметри) const_якщо_потрібно
{
// тіло методу
}
Конкретні приклади:
// Конструктор
Date::Date(int d, int m, int y)
{
setDate(d, m, y);
}
// Звичайний метод
void Date::setDate(int d, int m, int y)
{
day = d;
month = m;
year = y;
}
// Константний метод
int Date::getDay() const
{
return day;
}
// Метод, що повертає складний тип
std::string Date::toString() const
{
return std::to_string(day) + "." + std::to_string(month) + "." + std::to_string(year);
}
Важливі правила:
- Префікс
ClassName::обов'язковий — без нього компілятор сприйме це як звичайну вільну функцію. - Суфікс
constмає бути у визначенні, якщо був в оголошенні. - Параметри за замовчуванням вказуються лише в оголошенні (у
.h), не в визначенні (у.cpp).
.h, і в .cpp. Це помилка компіляції. Правило: параметри за замовчуванням лише в заголовку — там, де їх бачить той, хто викликає метод.Основний файл: використання класу
Тепер файл main.cpp стає максимально чистим — він підключає заголовок Date.h і працює з класом, не знаючи нічого про його реалізацію.
#include <iostream>
#include "Date.h"
int main()
{
Date birthday(5, 12, 2005);
Date today(16, 9, 2026);
std::cout << "День народження: ";
birthday.print();
std::cout << "Сьогодні: ";
today.print();
std::cout << "Рік народження: " << birthday.getYear() << "\n";
return 0;
}
Що відбувається під час компіляції
Компіляція відбувається у два етапи:
Етап 1: компіляція кожного .cpp окремо
g++ -c Date.cpp -o Date.o # Date.cpp → Date.o
g++ -c main.cpp -o main.o # main.cpp → main.o
Компілятор обробляє Date.cpp:
- Бачить
#include "Date.h"→ вставляє оголошення класу - Компілює визначення методів
Date::Date(),Date::setDate(),Date::print() - Генерує об'єктний файл
Date.oіз машинним кодом цих методів
Компілятор обробляє main.cpp:
- Бачить
#include "Date.h"→ вставляє оголошення класу - Бачить виклики
Date birthday(5, 12, 2005);таbirthday.print(); - Перевіряє, чи існують ці методи в оголошенні — так, існують
- Генерує об'єктний файл
main.oіз викликами цих методів (адреси методів ще невідомі)
Етап 2: лінкування
g++ Date.o main.o -o program
Лінкер об'єднує два об'єктних файли:
- У
main.oє викликDate::print()без адреси - У
Date.oє визначенняDate::print()з адресою - Лінкер «зшиває» виклик із визначенням — тепер
main.oзнає, куди передати управління
Результат — виконуваний файл program.
main.cpp — компілятору потрібні лише оголошення, щоб перевірити коректність викликів. Визначення потрібні лінкеру, і вони знаходяться в Date.o.Правило одного визначення (ODR) для класів
У статті 46. Багатофайлові програми ми детально розглядали правило ODR: кожна функція може мати лише одне визначення в усій програмі, але оголошень — скільки завгодно.
Для класів ODR працює інакше:
✅ Визначення класу — скільки завгодно
Визначення класу (структура у фігурних дужках) може бути продубльоване у кількох одиницях трансляції. Це не порушує ODR. Компілятор сприймає клас як тип, а типи звільнені від обмеження «одне визначення».
Наслідок: можна безпечно підключати Date.h у скільки завгодно файлів. Кожен .cpp отримає власну копію визначення класу — це нормально.
⚠️ Визначення методів — лише один раз
Визначення методів класу (тіло функції) слідують звичайному ODR для функцій — одне визначення на всю програму.
Наслідок: визначення методів мають бути у .cpp, а не в .h. Якщо визначити метод у .h, кожен файл, що підключить цей заголовок, отримає копію визначення — порушення ODR, помилка лінкера «multiple definition».
Виняток: inline-методи
Методи, визначені всередині тіла класу, автоматично стають inline. Inline-функції звільнені від ODR — їх визначення може бути продубльоване. Тому геттери та інші тривіальні методи можна залишати в заголовку:
class Date
{
public:
int getDay() const { return day; } // inline — безпечно в заголовку
};
Методи, визначені поза тілом класу, не є inline автоматично (якщо не додано ключове слово inline явно). Їх визначення має бути в .cpp:
void Date::print() const // не inline — лише в .cpp
{
std::cout << day << "." << month << "." << year << "\n";
}
inline відсутнє. Але правило залишається: визначення в заголовку лише для тривіальних методів.Структура проєкту з класами: еталонний патерн
Стандартна організація проєкту з класами виглядає так:
project/
├── Date.h # Заголовковий файл класу Date
├── Date.cpp # Реалізація методів класу Date
├── Time.h # Заголовковий файл класу Time
├── Time.cpp # Реалізація методів класу Time
└── main.cpp # Основний файл програми
Кожен клас має пару файлів із однаковим ім'ям: ClassName.h + ClassName.cpp. Це не технічна вимога — компілятору байдуже, як названі файли. Це конвенція, що полегшує навігацію у проєкті: побачивши Student.h, ви одразу знаєте, що реалізація в Student.cpp.
Компіляція багатофайлового проєкту
Простий підхід — передати всі файли одразу:
g++ Date.cpp Time.cpp main.cpp -o program
Компілятор скомпілює кожен .cpp окремо і автоматично виконає лінкування.
Роздільна компіляція — для великих проєктів:
g++ -c Date.cpp -o Date.o
g++ -c Time.cpp -o Time.o
g++ -c main.cpp -o main.o
g++ Date.o Time.o main.o -o program
Переваги роздільної компіляції:
- При зміні
Time.cppперекомпілюється лише цей файл,Date.oіmain.oзалишаються без змін - У великих проєктах це економить години часу збірки
Типові помилки та їх діагностика
Помилка 1: «was not declared in this scope»
error: 'Date' was not declared in this scope
Причина: файл не підключає Date.h. Компілятор не знає, що таке Date.
Рішення: додайте #include "Date.h" на початок файлу.
Помилка 2: «undefined reference to Date::print()»
undefined reference to `Date::print()`
Причина: помилка лінкера, а не компілятора. Файл Date.cpp не доданий до збірки. Компілятор бачить оголошення методу в .h, але лінкер не знаходить його визначення.
Рішення: додайте Date.cpp до команди компіляції або до конфігурації IDE.
Помилка 3: «multiple definition of Date::setDate()»
multiple definition of `Date::setDate()`
Причина: метод визначений у заголовковому файлі, який підключено у кількох .cpp. Кожне підключення породжує нове визначення — порушення ODR.
Рішення: перенесіть визначення методу з .h у .cpp. У заголовку залиште лише оголошення.
Помилка 4: параметри за замовчуванням у .cpp
void setDate(int d = 1, int m = 1, int y = 2000);
void Date::setDate(int d = 1, int m = 1, int y = 2000) // ❌ помилка!
{
day = d;
month = m;
year = y;
}
error: default argument given for parameter 1
Причина: параметри за замовчуванням вказуються лише в оголошенні, не в визначенні.
Рішення:
void Date::setDate(int d, int m, int y) // ✅ без параметрів за замовчуванням
{
day = d;
month = m;
year = y;
}
Практика: клас IntArray із розподілом на файли
Розглянемо клас, що керує динамічним масивом цілих чисел — приклад RAII-класу із попередньої статті 54. Деструктори та ідіома RAII. Тепер розділимо його на файли.
Вихідний код (монолітний):
#include <iostream>
class IntArray
{
private:
int* data;
int size;
public:
IntArray(int s)
{
size = s;
data = new int[size];
for (int i = 0; i < size; i++) {
data[i] = 0;
}
}
~IntArray()
{
delete[] data;
}
void set(int index, int value)
{
if (index >= 0 && index < size) {
data[index] = value;
}
}
int get(int index) const
{
if (index >= 0 && index < size) {
return data[index];
}
return 0;
}
int getSize() const
{
return size;
}
void print() const
{
for (int i = 0; i < size; i++) {
std::cout << data[i] << " ";
}
std::cout << "\n";
}
};
int main()
{
IntArray arr(5);
arr.set(0, 10);
arr.set(1, 20);
arr.set(2, 30);
arr.print();
return 0;
}
Завдання: розділіть клас IntArray на три файли: IntArray.h, IntArray.cpp, main.cpp.
Крок 1: створіть IntArray.h з оголошенням класу. Тривіальний геттер getSize() залиште в заголовку.
Крок 2: створіть IntArray.cpp з визначеннями конструктора, деструктора та методів set(), get(), print().
Крок 3: перепишіть main.cpp — залиште лише підключення IntArray.h і функцію main().
Крок 4: скомпілюйте: g++ IntArray.cpp main.cpp -o program.
IntArray.h:
#ifndef INTARRAY_H
#define INTARRAY_H
class IntArray
{
private:
int* data;
int size;
public:
IntArray(int s);
~IntArray();
void set(int index, int value);
int get(int index) const;
void print() const;
int getSize() const { return size; }
};
#endif
IntArray.cpp:
#include "IntArray.h"
#include <iostream>
IntArray::IntArray(int s)
{
size = s;
data = new int[size];
for (int i = 0; i < size; i++) {
data[i] = 0;
}
}
IntArray::~IntArray()
{
delete[] data;
}
void IntArray::set(int index, int value)
{
if (index >= 0 && index < size) {
data[index] = value;
}
}
int IntArray::get(int index) const
{
if (index >= 0 && index < size) {
return data[index];
}
return 0;
}
void IntArray::print() const
{
for (int i = 0; i < size; i++) {
std::cout << data[i] << " ";
}
std::cout << "\n";
}
main.cpp:
#include <iostream>
#include "IntArray.h"
int main()
{
IntArray arr(5);
arr.set(0, 10);
arr.set(1, 20);
arr.set(2, 30);
arr.print();
return 0;
}
Компіляція та запуск:
Inline-методи: коли визначати в заголовку
Методи, визначені всередині тіла класу, автоматично стають inline. Але що, якщо ви хочете визначити метод поза класом у заголовку, але зробити його inline? Для цього існує ключове слово inline.
Явне ключове слово inline
#ifndef DATE_H
#define DATE_H
#include <iostream>
class Date
{
private:
int day;
int month;
int year;
public:
Date(int d, int m, int y);
void print() const;
};
// Визначення методу поза класом, але в заголовку — треба inline
inline void Date::print() const
{
std::cout << day << "." << month << "." << year << "\n";
}
#endif
Ключове слово inline перед визначенням каже компілятору: «це inline-функція, її визначення може бути продубльоване». Без inline це було б порушенням ODR.
Коли використовувати inline у заголовку
✅ Використовуйте inline
- Тривіальні методи: геттери, сеттери, прості обчислення
- Шаблонні методи (про них у наступних статтях)
- Методи, що викликаються дуже часто у критичних ділянках коду
❌ Не використовуйте inline
- Складні методи з багатьма рядками коду
- Методи, що викликають інші функції
- Методи з циклами або розгалуженнями
inline сьогодні — це радше дозвіл на множинні визначення, ніж директива оптимізації.Параметри за замовчуванням: лише в заголовку
Параметри за замовчуванням вказуються лише в оголошенні методу, а не в визначенні. Причина проста: той, хто викликає метод, бачить лише заголовковий файл. Саме там компілятор дізнається про параметри за замовчуванням.
Правильно
class Date
{
public:
void setDate(int d = 1, int m = 1, int y = 2000);
};
void Date::setDate(int d, int m, int y) // Без параметрів за замовчуванням!
{
day = d;
month = m;
year = y;
}
Неправильно
class Date
{
public:
void setDate(int d, int m, int y); // Без параметрів за замовчуванням
};
void Date::setDate(int d = 1, int m = 1, int y = 2000) // ❌ Помилка!
{
day = d;
month = m;
year = y;
}
Компілятор видасть помилку: «default argument given for parameter». Параметри за замовчуванням мають бути там, де компілятор обробляє виклик методу, а не визначення.
Бібліотеки та попередньо скомпільовані файли
Відокремлення оголошення від реалізації — це не лише зручність організації коду. Це фундамент для створення бібліотек — готових модулів, які можна підключати до різних проєктів.
Стандартна бібліотека C++
Ви вже користувалися бібліотекою — стандартною бібліотекою C++ (STL). Коли ви пишете #include <iostream> або #include <vector>, ви підключаєте заголовкові файли, що містять оголошення класів std::cout, std::vector, std::string. Але де реалізація цих класів?
Реалізація знаходиться у попередньо скомпільованій бібліотеці — файлі .lib (Windows) або .a / .so (Linux/macOS), який автоматично підключається лінкером. Ви ніде не бачите вихідного коду методів std::cout.operator<<() або std::vector::push_back() — вони вже скомпільовані у машинний код і зберігаються окремо.
Як це працює
- Компіляція вашого коду: компілятор бачить оголошення з
<iostream>, перевіряє коректність викликів, генеруєmain.o. - Лінкування: лінкер знаходить визначення методів у системній бібліотеці (наприклад,
libstdc++.a) і зшиває виклики з реалізацією. - Результат: виконуваний файл, що містить ваш код і код стандартної бібліотеки.
Створення власної бібліотеки
Ви можете створити власну бібліотеку — набір класів і функцій, які використовуються у кількох проєктах.
Крок 1: скомпілюйте .cpp-файли у об'єктні файли:
g++ -c Date.cpp -o Date.o
g++ -c Time.cpp -o Time.o
Крок 2: об'єднайте об'єктні файли у статичну бібліотеку:
ar rcs libdatetime.a Date.o Time.o
Крок 3: використовуйте бібліотеку в іншому проєкті:
g++ main.cpp -L. -ldatetime -o program
Прапор -L. каже лінкеру шукати бібліотеки у поточній директорії, -ldatetime — підключити libdatetime.a.
.h і .cpp — це перший крок до побудови модульного коду.Переваги розподілу: підсумок
📖 Читабельність
⚡ Швидкість компіляції
.cpp вимагає перекомпіляції лише цього файлу. Файли, що використовують клас, залишаються без змін.🔒 Приховування деталей
.cpp. Користувач класу бачить лише інтерфейс. Це основа інкапсуляції на рівні модулів.♻️ Повторне використання
.h і .cpp, легко переносити між проєктами. Підключив заголовок, додав .cpp до збірки — готово.Рекомендації щодо організації класів
На завершення — практичні поради від індустрії, які допоможуть уникнути типових пасток.
Правило 1: один клас — одна пара файлів
Клас Student → Student.h + Student.cpp.
Клас Course → Course.h + Course.cpp.
Не поміщайте кілька несповнених класів в один файл utils.h + utils.cpp. Це порушує принцип одиничної відповідальності.
Правило 2: .cpp завжди підключає власний .h
Файл Date.cpp завжди починається з #include "Date.h". Це захисний механізм: якщо сигнатури розійдуться, помилка виявиться одразу.
Правило 3: тривіальні методи — в заголовку, нетривіальні — у .cpp
Тривіальні (у заголовку):
- Геттери:
int getDay() const { return day; } - Порожні деструктори:
~Date() {} - Прості сеттери без логіки
Нетривіальні (у .cpp):
- Методи з циклами, умовами, обчисленнями
- Методи, що викликають інші функції
- Конструктори з ініціалізацією
Правило 4: мінімізуйте залежності в заголовках
Не підключайте заголовки, які не потрібні безпосередньо в .h. Якщо <iostream> використовується лише в .cpp, підключайте його там.
Погано:
#include <iostream> // Не потрібен у заголовку!
#include <string> // Не потрібен у заголовку!
class Date { ... };
Добре:
class Date { ... }; // Нічого зайвого
#include "Date.h"
#include <iostream> // Підключаємо тут
#include у заголовку сповільнює компіляцію всіх файлів, що підключають цей заголовок. У великих проєктах це критично.Висновки
Розподіл класу на заголовковий файл та файл реалізації — це не просто технічний прийом. Це фундаментальна практика організації коду в C++, що забезпечує:
- Інкапсуляцію на рівні модулів: інтерфейс відокремлений від реалізації
- Масштабованість: клас легко використовувати у багатьох файлах і проєктах
- Швидкість збірки: зміни реалізації не вимагають перекомпіляції всього проєкту
- Читабельність: заголовок — це документація класу
Основні правила, які слід запам'ятати:
- Заголовок
.h: оголошення класу, прототипи методів, тривіальні inline-методи - Реалізація
.cpp: визначення методів черезClassName::methodName - Header guards: обов'язковий захист від подвійного підключення
- Параметри за замовчуванням: лише в заголовку, не в
.cpp .cppпідключає власний.h: завжди, без винятків
На наступному уроці ми розглянемо, як класи взаємодіють із ключовим словом const — константні об'єкти та константні методи. Це ще один рівень контролю над поведінкою класу, який забезпечує безпеку та передбачуваність коду.
Прихований покажчик this
Як методи класу знають, до якого обєкта вони належать? Прихований покажчик this, його роль у викликах методів. Явне використання this для розвязання конфліктів імен. Method chaining через return *this.
Класи та const
Константні об'єкти класів, константні методи та їхній зв'язок із this, правило «всі геттери — const», передача об'єктів по константному посиланню, перевантаження за const, практичні патерни використання const у класах.