Класи та const
Класи та const
Константність у світі об'єктів
На попередніх уроках ми вивчили ключове слово const у контексті простих змінних і функцій: const int x = 5;, const double PI = 3.14159;, параметри функцій const int& value. Константність гарантує незмінність — обіцянку не модифікувати значення після ініціалізації. Це не лише допомагає запобігти помилкам, але й документує намір: «це значення не має змінюватися».
Тепер, коли ми працюємо з класами — користувацькими типами, що об'єднують дані та поведінку, — виникає природне питання: чи можна зробити об'єкт класу константним? Відповідь: так. Більше того, константність об'єктів — це центральний механізм забезпечення коректності програми у C++.
Уявіть клас Date, що представляє дату. Ви створюєте об'єкт Date birthday(5, 12, 2005); і хочете гарантувати, що ця дата ніколи не зміниться протягом виконання програми — це дата народження, вона фіксована. Як це записати? Саме так:
const Date birthday(5, 12, 2005);
Ключове слово const перед типом об'єкта робить його константним об'єктом. Але що це означає технічно? Які операції дозволені з константним об'єктом, а які — заборонені? І як компілятор відрізняє «безпечні» методи від «небезпечних»?
Ця стаття відповість на ці питання і покаже, як правильно проєктувати класи з урахуванням константності — один із ключових аспектів професійного C++.
const Object&, дає гарантію, що об'єкт не буде змінено. Компілятор перевіряє це автоматично.Константні об'єкти: синтаксис та обмеження
Об'єкт класу оголошується константним так само, як і звичайна змінна — додаванням const перед типом:
#include <iostream>
class Date
{
private:
int day;
int month;
int year;
public:
Date(int d, int m, int y)
{
day = d;
month = m;
year = y;
}
void setDate(int d, int m, int y)
{
day = d;
month = m;
year = y;
}
void print()
{
std::cout << day << "." << month << "." << year << "\n";
}
};
int main()
{
const Date today(16, 9, 2026);
// today.setDate(17, 9, 2026); // ❌ Помилка компіляції!
// today.print(); // ❌ Помилка компіляції!
return 0;
}
Що відбувається при оголошенні const Date today
Ініціалізація через конструктор: константний об'єкт може бути ініціалізований через конструктор. Конструктор викликається під час створення об'єкта — до того, як об'єкт стає «константним». Тому конструктор має право встановити значення полів. Це не порушує константності.
Заборона модифікації: після завершення конструктора об'єкт стає незмінним. Будь-яка спроба змінити його поля — напряму або через методи — призводить до помилки компіляції.
У прикладі вище обидва закоментовані рядки викличуть помилки:
today.setDate(...)— спроба викликати метод, що змінює поляtoday.print()— несподівано, але навіть виклик методу, що нічого не змінює, також заборонений!
Чому print() заборонений? Адже він лише виводить дані, не модифікує їх. Це ключове питання, яке веде нас до наступного розділу.
const, компілятор вважає, що він може змінити об'єкт.Константні методи: обіцянка не змінювати об'єкт
Щоб дозволити виклик методу на константному об'єкті, потрібно явно вказати, що цей метод не змінює стан об'єкта. Це робиться додаванням ключового слова const після списку параметрів методу:
returnType methodName(parameters) const
{
// тіло методу
}
Суфікс const — це обіцянка компілятору: «цей метод не змінить жодного поля об'єкта і не викличе жодного неконстантного методу».
Приклад: геттери як константні методи
#include <iostream>
class Date
{
private:
int day;
int month;
int year;
public:
Date(int d, int m, int y) : day(d), month(m), year(y) {}
void setDate(int d, int m, int y)
{
day = d;
month = m;
year = y;
}
int getDay() const { return day; }
int getMonth() const { return month; }
int getYear() const { return year; }
void print() const
{
std::cout << day << "." << month << "." << year << "\n";
}
};
int main()
{
const Date today(16, 9, 2026);
// ✅ Дозволено — методи позначені const
today.print();
std::cout << "День: " << today.getDay() << "\n";
// ❌ Заборонено — метод не const
// today.setDate(17, 9, 2026);
return 0;
}
Що робить const після сигнатури методу
Модифікує тип this: кожен метод класу має неявний параметр this — покажчик на об'єкт, що викликав метод (детально розглядали у статті 55. Покажчик this). У звичайному методі this має тип ClassName* — покажчик на змінний об'єкт. У const-методі this має тип const ClassName* — покажчик на константний об'єкт.
Наслідок: всередині const-методу неможливо змінити жодне поле об'єкта через this->field = ..., адже this вказує на const-об'єкт.
class Example
{
private:
int value;
public:
void modify()
{
value = 42; // ✅ Дозволено — звичайний метод
}
void read() const
{
// value = 42; // ❌ Помилка! this має тип const Example*
int x = value; // ✅ Читання дозволено
}
};
const-метод спробує змінити поле або викликати неконстантний метод, ви отримаєте помилку компіляції, а не помилку виконання.Правило: всі геттери — const, сеттери — ніколи
З попереднього розділу випливає фундаментальне правило проєктування класів:
✅ Геттери завжди const
Геттер — це метод, що повертає значення поля, не змінюючи його. Геттери завжди мають бути позначені const, адже вони не модифікують об'єкт.
class Student
{
private:
std::string name;
int age;
public:
std::string getName() const { return name; }
int getAge() const { return age; }
};
❌ Сеттери ніколи не const
Сеттер — це метод, що змінює значення поля. Сеттери не можуть бути const, адже їхня мета — модифікація об'єкта.
class Student
{
private:
std::string name;
int age;
public:
void setName(const std::string& n) { name = n; }
void setAge(int a) { age = a; }
};
Це правило не вимагає запам'ятовування — воно випливає з семантики: геттер не змінює, сеттер змінює. Суфікс const лише формалізує цей факт у сигнатурі методу.
Визначення const-методів поза класом
Якщо метод визначається поза тілом класу (у файлі .cpp), ключове слово const має бути і в оголошенні, і в визначенні.
У заголовковому файлі
#ifndef DATE_H
#define DATE_H
class Date
{
private:
int day;
int month;
int year;
public:
Date(int d, int m, int y);
int getDay() const; // const в оголошенні
int getMonth() const;
int getYear() const;
void print() const;
void setDate(int d, int m, int y);
};
#endif
У файлі реалізації
#include "Date.h"
#include <iostream>
Date::Date(int d, int m, int y) : day(d), month(m), year(y) {}
int Date::getDay() const // const і тут!
{
return day;
}
int Date::getMonth() const
{
return month;
}
int Date::getYear() const
{
return year;
}
void Date::print() const
{
std::cout << day << "." << month << "." << year << "\n";
}
void Date::setDate(int d, int m, int y)
{
day = d;
month = m;
year = y;
}
const, а в визначенні — ні (або навпаки), це різні методи з точки зору компілятора. Ви отримаєте помилку лінкера «undefined reference», адже оголошена версія не має визначення.Передача об'єктів по константному посиланню
Одне з найпоширеніших застосувань const у контексті класів — передача об'єктів у функції по константному посиланню.
Проблема передачі по значенню
Коли ви передаєте об'єкт у функцію по значенню, створюється копія цього об'єкта:
void printDate(Date date) // Копія!
{
date.print();
}
int main()
{
Date today(16, 9, 2026);
printDate(today); // Копіювання всіх полів today → date
}
Для класу Date із трьома полями типу int копіювання швидке. Але уявіть клас Student із полями std::string name, std::vector<int> grades, std::map<std::string, double> courses. Копіювання такого об'єкта — це виділення пам'яті, копіювання динамічних структур, виклик конструкторів копіювання. Це повільно і марно, якщо функція лише читає дані, не змінюючи їх.
Рішення: константне посилання
Посилання не створює копії — це альтернативне ім'я для існуючого об'єкта. Додавання const гарантує, що функція не зможе змінити об'єкт:
void printDate(const Date& date) // Посилання на константний об'єкт
{
date.print(); // ✅ Метод print() має бути const
}
int main()
{
Date today(16, 9, 2026);
printDate(today); // Без копіювання
}
Переваги:
- Швидкість: немає копіювання, незалежно від розміру об'єкта
- Безпека:
constгарантує, що функція не змінить об'єкт - Гнучкість: можна передавати тимчасові об'єкти (r-values):
printDate(Date(1, 1, 2000));
const ClassName&. Це стандартна практика у всій індустрії C++.Типова помилка: виклик неконстантного методу
void printStudent(const Student& student)
{
std::cout << "Name: " << student.getName() << "\n"; // ❌ Помилка!
}
Якщо getName() не позначений як const, компілятор видасть помилку:
error: passing 'const Student' as 'this' argument discards qualifiers
Повідомлення означає: «ви намагаєтеся викликати неконстантний метод на константному об'єкті — це відкидає константність».
Рішення: зробіть getName() константним:
class Student
{
public:
std::string getName() const { return name; }
};
Конструктори та const
Чому конструктори не можуть бути const
Конструктор ініціалізує об'єкт — встановлює значення полів. Це модифікація об'єкта. Тому конструктор не може бути const:
class Date
{
public:
Date(int d, int m, int y) const // ❌ Помилка компіляції!
{
day = d;
month = m;
year = y;
}
};
Це логічно: якби конструктор був const, він не міг би ініціалізувати поля, а це його основна задача.
Константний об'єкт та конструктор
Константний об'єкт може бути ініціалізований через конструктор:
const Date today(16, 9, 2026); // ✅ Дозволено
Конструктор викликається під час створення об'єкта — до того, як об'єкт стає константним. Після завершення конструктора об'єкт «застигає» у незмінному стані.
Перевантаження за const
C++ дозволяє мати дві версії одного й того ж методу: константну і неконстантну. Це називається перевантаженням за const. Компілятор вибирає версію залежно від константності об'єкта.
Приклад: оператор індексації
#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;
}
// Неконстантна версія — для запису
int& operator[](int index)
{
return data[index];
}
// Константна версія — тільки для читання
const int& operator[](int index) const
{
return data[index];
}
int getSize() const { return size; }
};
int main()
{
IntArray arr(3);
arr[0] = 10; // Виклик неконстантної версії
arr[1] = 20;
arr[2] = 30;
const IntArray& constArr = arr;
std::cout << constArr[0] << "\n"; // Виклик константної версії
// constArr[0] = 100; // ❌ Помилка — повертається const int&
return 0;
}
Як це працює
Неконстантна версія int& operator[](int index):
- Викликається на неконстантному об'єкті
- Повертає неконстантне посилання
int&, що дозволяє змінювати елемент - Використання:
arr[0] = 10;
Константна версія const int& operator[](int index) const:
- Викликається на константному об'єкті або посиланні
- Повертає константне посилання
const int&, що дозволяє лише читання - Використання:
int x = constArr[0];
IntArray arr(3);
arr[0] = 10; // ✅ Зміна дозволена
int x = arr[0]; // ✅ Читання дозволене
const IntArray arr(3);
// arr[0] = 10; // ❌ Помилка — повертається const int&
int x = arr[0]; // ✅ Читання дозволене
const використовується рідко — переважно для операторів [] та * (розіменування) у контейнерах. Для звичайних методів достатньо однієї константної версії.Практичний приклад: клас Fraction
Розглянемо клас для роботи з дробами — приклад правильного використання const у всіх аспектах.
#ifndef FRACTION_H
#define FRACTION_H
#include <iostream>
class Fraction
{
private:
int numerator;
int denominator;
public:
Fraction(int num = 0, int den = 1);
int getNumerator() const { return numerator; }
int getDenominator() const { return denominator; }
void setNumerator(int num) { numerator = num; }
void setDenominator(int den);
void print() const;
double toDouble() const;
Fraction add(const Fraction& other) const;
bool isEqual(const Fraction& other) const;
};
#endif
#include "Fraction.h"
Fraction::Fraction(int num, int den) : numerator(num), denominator(den)
{
if (denominator == 0) {
denominator = 1;
}
}
void Fraction::setDenominator(int den)
{
if (den != 0) {
denominator = den;
}
}
void Fraction::print() const
{
std::cout << numerator << "/" << denominator;
}
double Fraction::toDouble() const
{
return static_cast<double>(numerator) / denominator;
}
Fraction Fraction::add(const Fraction& other) const
{
int newNum = numerator * other.denominator + other.numerator * denominator;
int newDen = denominator * other.denominator;
return Fraction(newNum, newDen);
}
bool Fraction::isEqual(const Fraction& other) const
{
return (numerator * other.denominator) == (other.numerator * denominator);
}
#include "Fraction.h"
void printFraction(const Fraction& f)
{
f.print();
std::cout << " = " << f.toDouble() << "\n";
}
int main()
{
const Fraction half(1, 2);
const Fraction third(1, 3);
printFraction(half);
printFraction(third);
Fraction sum = half.add(third);
std::cout << "Сума: ";
printFraction(sum);
if (half.isEqual(Fraction(2, 4))) {
std::cout << "1/2 дорівнює 2/4\n";
}
return 0;
}
Аналіз використання const
Константні об'єкти:
const Fraction half(1, 2);
const Fraction third(1, 3);
Ці дроби не змінюються протягом програми — їх можна оголосити константними.
Геттери — const:
int getNumerator() const { return numerator; }
int getDenominator() const { return denominator; }
Лише читають дані, не змінюють.
Сеттери — не const:
void setDenominator(int den) { denominator = den; }
Змінюють об'єкт.
Методи, що не змінюють об'єкт — const:
void print() const;
double toDouble() const;
Fraction add(const Fraction& other) const;
bool isEqual(const Fraction& other) const;
Метод add() не змінює поточний об'єкт — він створює і повертає новий дріб. Тому він const.
Параметри — const посилання:
Fraction add(const Fraction& other) const;
bool isEqual(const Fraction& other) const;
Параметр other не змінюється — передається по const&.
Типові помилки та їх діагностика
Помилка 1: виклик неконстантного методу на const-об'єкті
const Date today(16, 9, 2026);
today.print(); // Помилка, якщо print() не const
Повідомлення компілятора:
error: passing 'const Date' as 'this' argument discards qualifiers
Рішення: додайте const до методу print().
Помилка 2: спроба змінити поле всередині const-методу
class Counter
{
private:
int count;
public:
void reset() const
{
count = 0; // ❌ Помилка!
}
};
Повідомлення:
error: assignment of member 'Counter::count' in read-only object
Рішення: або зробіть метод неконстантним, або використайте mutable (розглянемо нижче).
Помилка 3: const в оголошенні, але не в визначенні
class Date
{
public:
void print() const;
};
void Date::print() // Забули const!
{
std::cout << day << "." << month << "." << year << "\n";
}
Повідомлення лінкера:
undefined reference to `Date::print() const'
Рішення: додайте const у визначення методу.
Ключове слово mutable
Іноді виникає ситуація, коли метод логічно є константним (не змінює видимий стан об'єкта), але потребує модифікувати певне поле — наприклад, для кешування або підрахунку статистики.
Ключове слово mutable дозволяє змінювати поле всередині const-методу:
#include <iostream>
class ExpensiveCalculation
{
private:
int baseValue;
mutable bool cached;
mutable int cachedResult;
public:
ExpensiveCalculation(int value) : baseValue(value), cached(false), cachedResult(0) {}
int getResult() const
{
if (!cached) {
std::cout << "Обчислення...\n";
cachedResult = baseValue * baseValue; // ✅ Дозволено — cachedResult є mutable
cached = true; // ✅ Дозволено — cached є mutable
}
return cachedResult;
}
};
int main()
{
const ExpensiveCalculation calc(10);
std::cout << "Результат: " << calc.getResult() << "\n";
std::cout << "Результат: " << calc.getResult() << "\n"; // Без повторного обчислення
return 0;
}
При першому виклику getResult() відбувається обчислення та кешування. При другому — результат береться з кешу. Хоча метод const, він модифікує поля cached і cachedResult, позначені як mutable.
mutable слід використовувати обережно — лише для допоміжних полів, що не впливають на логічний стан об'єкта (кеші, лічильники, м'ютекси для багатопотоковості).Підсумкова таблиця: const у контексті класів
| Конструкція | Що означає | Коли використовувати |
|---|---|---|
const ClassName obj; | Константний об'єкт | Об'єкт не змінюється після створення |
void method() const | Константний метод | Метод не змінює об'єкт |
const ClassName& param | Параметр — const посилання | Функція не змінює об'єкт, уникає копіювання |
const int& operator[](int) const | Константна версія оператора | Версія для читання з const-об'єктів |
mutable int field; | Змінне поле | Поле можна змінювати у const-методах |
Практичні рекомендації
1️⃣ Завжди const для геттерів
const. Це не рекомендація — це правило.2️⃣ const& для параметрів
const ClassName&. Це стандартна практика.3️⃣ Використовуйте const-об'єкти
const. Це документує намір і дозволяє компілятору перевірити коректність.4️⃣ Const у сигнатурі — в обох місцях
const має бути і в оголошенні (.h), і в визначенні (.cpp).Практичне завдання: клас Rectangle
Розглянемо клас прямокутника з правильним використанням const.
Створіть клас Rectangle з приватними полями width і height.
Вимоги:
- Конструктор з двома параметрами (ширина та висота)
- Геттери для
widthіheight— константні методи - Сеттери для
widthіheight— звичайні методи з валідацією (значення > 0) - Метод
getArea()— константний, обчислює площу - Метод
getPerimeter()— константний, обчислює периметр - Метод
scale(double factor)— неконстантний, масштабує прямокутник - Метод
print()— константний, виводить розміри - Функція
printRectangle(const Rectangle& rect)— використовує константне посилання
Тестування:
- Створіть константний об'єкт
const Rectangle room(5.0, 3.0); - Викличте на ньому
print(),getArea(),getPerimeter() - Переконайтеся, що виклик
scale()викликає помилку компіляції - Створіть неконстантний об'єкт та масштабуйте його
Rectangle.h:
#ifndef RECTANGLE_H
#define RECTANGLE_H
class Rectangle
{
private:
double width;
double height;
public:
Rectangle(double w, double h);
double getWidth() const { return width; }
double getHeight() const { return height; }
void setWidth(double w);
void setHeight(double h);
double getArea() const;
double getPerimeter() const;
void scale(double factor);
void print() const;
};
#endif
Rectangle.cpp:
#include "Rectangle.h"
#include <iostream>
Rectangle::Rectangle(double w, double h) : width(w), height(h)
{
if (width <= 0) width = 1.0;
if (height <= 0) height = 1.0;
}
void Rectangle::setWidth(double w)
{
if (w > 0) {
width = w;
}
}
void Rectangle::setHeight(double h)
{
if (h > 0) {
height = h;
}
}
double Rectangle::getArea() const
{
return width * height;
}
double Rectangle::getPerimeter() const
{
return 2 * (width + height);
}
void Rectangle::scale(double factor)
{
if (factor > 0) {
width *= factor;
height *= factor;
}
}
void Rectangle::print() const
{
std::cout << "Прямокутник: " << width << " x " << height << "\n";
}
main.cpp:
#include <iostream>
#include "Rectangle.h"
void printRectangleInfo(const Rectangle& rect)
{
rect.print();
std::cout << " Площа: " << rect.getArea() << "\n";
std::cout << " Периметр: " << rect.getPerimeter() << "\n";
}
int main()
{
// Константний прямокутник
const Rectangle room(5.0, 3.0);
std::cout << "=== Константний об'єкт ===\n";
printRectangleInfo(room);
// room.scale(2.0); // ❌ Помилка компіляції!
// room.setWidth(10.0); // ❌ Помилка компіляції!
std::cout << "\n=== Неконстантний об'єкт ===\n";
Rectangle window(2.0, 1.5);
printRectangleInfo(window);
std::cout << "\nМасштабування у 2 рази...\n";
window.scale(2.0);
printRectangleInfo(window);
return 0;
}
Компіляція та запуск:
Аналіз:
✅ Константні методи: getWidth(), getHeight(), getArea(), getPerimeter(), print() — всі позначені const, можна викликати на const Rectangle room.
✅ Неконстантні методи: setWidth(), setHeight(), scale() — змінюють об'єкт, не можна викликати на const об'єкті.
✅ Передача по const-посиланню: функція printRectangleInfo() приймає const Rectangle& — ефективно та безпечно.
Константність та this
У статті 55. Покажчик this ми розглядали, що кожен метод має неявний параметр this — покажчик на об'єкт. Тепер можемо уточнити:
У звичайному методі:
void ClassName::method()
{
// this має тип: ClassName* const
// (константний покажчик на змінний об'єкт)
}
У константному методі:
void ClassName::method() const
{
// this має тип: const ClassName* const
// (константний покажчик на константний об'єкт)
}
Суфікс const після сигнатури методу модифікує тип this, роблячи його покажчиком на константний об'єкт. Це автоматично забороняє модифікацію полів.
Приклад: явне використання this у const-методі
class Point
{
private:
int x;
int y;
public:
Point(int xVal, int yVal) : x(xVal), y(yVal) {}
void print() const
{
// this має тип const Point* const
std::cout << "(" << this->x << ", " << this->y << ")\n";
// this->x = 10; // ❌ Помилка — this вказує на const об'єкт
}
void move(int dx, int dy)
{
// this має тип Point* const
this->x += dx; // ✅ Дозволено
this->y += dy;
}
};
Чому const важливий у великих проєктах
У навчальних прикладах важливість const не завжди очевидна — програми малі, легко простежити, де об'єкт змінюється, а де — ні. Але у великих проєктах const стає критичним інструментом контролю коректності.
Приклад: багатопотокова програма
class Config
{
private:
std::string serverAddress;
int port;
public:
Config(const std::string& addr, int p) : serverAddress(addr), port(p) {}
std::string getServerAddress() const { return serverAddress; }
int getPort() const { return port; }
};
void workerThread(const Config& config)
{
// Гарантія: жоден потік не зміне config
std::string address = config.getServerAddress();
int port = config.getPort();
// ... робота з конфігурацією
}
int main()
{
const Config config("localhost", 8080);
// Запуск 10 потоків, які читають конфігурацію
std::vector<std::thread> threads;
for (int i = 0; i < 10; i++) {
threads.emplace_back(workerThread, std::ref(config));
}
// ...
}
Константність config гарантує, що жоден потік не зможе його змінити. Компілятор перевірить це статично. Без const довелося б використовувати м'ютекси навіть для читання — що сповільнило б програму.
Приклад: передача у бібліотечну функцію
void saveToDatabase(const User& user)
{
// Викликає тільки const-методи:
std::string name = user.getName();
int age = user.getAge();
// Компілятор гарантує, що user не змінюється
}
Сигнатура const User& — це контракт: «функція не змінює об'єкт». Це дозволяє передавати об'єкт без побоювань побічних ефектів.
const-коректність є частиною code review. Метод, що не змінює об'єкт, але не позначений const, зазвичай не пройде перевірку.Висновки
Константність об'єктів і методів — це не декоративна можливість C++, а фундаментальний механізм контролю коректності програм. Правильне використання const забезпечує:
- Захист від помилок: компілятор не дозволить випадково змінити об'єкт, який має бути незмінним
- Документування коду:
constу сигнатурі методу чи параметра — це явна заява про намір - Оптимізацію: компілятор може оптимізувати код, знаючи, що об'єкт не змінюється
- Безпеку у багатопотоковості: константні об'єкти можна безпечно читати з кількох потоків без синхронізації
Основні правила, які слід запам'ятати:
📋 Геттери завжди const
const.🔒 Константне посилання
const ClassName&.✍️ Const в обох місцях
const має бути і в оголошенні, і в визначенні.💡 Константні об'єкти
const.На наступному уроці ми розглянемо статичні члени класу — поля та методи, що належать класу загалом, а не окремим об'єктам. Це дозволить реалізувати патерни на кшталт лічильників об'єктів, глобальних конфігурацій та фабричних методів.
Розподіл класу на заголовковий файл та файл реалізації
Відокремлення оголошення від реалізації методів, організація класів у пари .h/.cpp, оператор дозволу області видимості ::, практичний патерн багатофайлового проєкту з класами, header guards та ODR для класів.
Статичні члени класу
Статичні поля та методи класу, різниця між даними об'єкта та даними класу, ініціалізація статичних полів поза класом, inline static (C++17), практичні патерни: лічильник об'єктів, генератор ID, застереження щодо чисто статичних класів.