C++

Класи та const

Константні об'єкти класів, константні методи та їхній зв'язок із this, правило «всі геттери — const», передача об'єктів по константному посиланню, перевантаження за const, практичні патерни використання 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 є основним інструментом контролю коректності: функція, що приймає const Object&, дає гарантію, що об'єкт не буде змінено. Компілятор перевіряє це автоматично.

Константні об'єкти: синтаксис та обмеження

Об'єкт класу оголошується константним так само, як і звичайна змінна — додаванням const перед типом:

ConstObject.cpp
#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 — це обіцянка компілятору: «цей метод не змінить жодного поля об'єкта і не викличе жодного неконстантного методу».

Приклад: геттери як константні методи

Date.cpp
#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;
}
./program
$ ./program
16.9.2026
День: 16
Execution finished with exit code 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 має бути і в оголошенні, і в визначенні.

У заголовковому файлі

Date.h
#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

У файлі реалізації

Date.cpp
#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. Компілятор вибирає версію залежно від константності об'єкта.

Приклад: оператор індексації

IntArray.cpp
#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 використовується рідко — переважно для операторів [] та * (розіменування) у контейнерах. Для звичайних методів достатньо однієї константної версії.

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

Розглянемо клас для роботи з дробами — приклад правильного використання const у всіх аспектах.

Fraction.h
#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
Fraction.cpp
#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);
}
main.cpp
#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;
}
./program
$ ./program
1/2 = 0.5
1/3 = 0.333333
Сума: 5/6 = 0.833333
1/2 дорівнює 2/4
Execution finished with exit code 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 в оголошенні, але не в визначенні

Date.h
class Date
{
public:
    void print() const;
};
Date.cpp
void Date::print()  // Забули const!
{
    std::cout << day << "." << month << "." << year << "\n";
}

Повідомлення лінкера:

undefined reference to `Date::print() const'

Рішення: додайте const у визначення методу.


Ключове слово mutable

Іноді виникає ситуація, коли метод логічно є константним (не змінює видимий стан об'єкта), але потребує модифікувати певне поле — наприклад, для кешування або підрахунку статистики.

Ключове слово mutable дозволяє змінювати поле всередині const-методу:

Cache.cpp
#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;
}
./program
$ ./program
Обчислення...
Результат: 100
Результат: 100
Execution finished with exit code 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.


Константність та 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 забезпечує:

  1. Захист від помилок: компілятор не дозволить випадково змінити об'єкт, який має бути незмінним
  2. Документування коду: const у сигнатурі методу чи параметра — це явна заява про намір
  3. Оптимізацію: компілятор може оптимізувати код, знаючи, що об'єкт не змінюється
  4. Безпеку у багатопотоковості: константні об'єкти можна безпечно читати з кількох потоків без синхронізації

Основні правила, які слід запам'ятати:

📋 Геттери завжди const

Будь-який метод, що лише повертає значення поля, має бути позначений const.

🔒 Константне посилання

Параметри типу «об'єкт класу», які не змінюються функцією, передавайте як const ClassName&.

✍️ Const в обох місцях

Якщо метод визначається поза класом, const має бути і в оголошенні, і в визначенні.

💡 Константні об'єкти

Якщо об'єкт не має змінюватися після створення, оголосіть його const.

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

Copyright © 2026