На уроці №40 ми дізналися, що фундаментальні типи даних (int, double, char тощо) можна зробити константними, використовуючи ключове слово const, і що всі константні змінні повинні бути ініціалізовані під час оголошення. У випадку з константними фундаментальними типами даних ініціалізація може бути копіюючою, прямою або uniform-:
|
1 2 3 |
const int value1 = 6; // копіююча ініціалізація const int value2(8); // пряма ініціалізація const int value3 { 11 }; // uniform-ініціалізація (C++11) |
Константні об’єкти класів
Об’єкти класів можна зробити константними (використовуючи ключове слово const). Ініціалізація виконується через конструктори класів:
|
1 2 3 |
const Date date1; // ініціалізація через конструктор за замовчуванням const Date date2(12, 11, 2018); // ініціалізація через конструктор з параметрами const Date date3 { 12, 11, 2018 }; // ініціалізація через конструктор з параметрами в C++11 |
Як тільки константний об’єкт класу ініціалізується через конструктор, то будь-яка спроба модифікувати змінні-члени об’єкта заборонена, оскільки це порушує принципи константності об’єкта. Забороняється як зміна змінних-членів напряму (якщо вони є public), так і виклик методів (сеттерів), за допомогою яких можна вказати значення змінним-членам. Розглянемо наступний клас:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Anything { public: int m_value; Anything(): m_value(0) { } void setValue(int value) { m_value = value; } int getValue() { return m_value ; } }; int main() { const Anything anything; // викликаємо конструктор за замовчуванням anything.m_value = 7; // помилка компіляції: порушення const anything.setValue(7); // помилка компіляції: порушення const return 0; } |
Рядки №16-17 спровокують помилки компіляції, так як вони порушують принципи константності об’єкта, намагаючись напряму змінити змінну-член, викликаючи для цього сеттер.
Константні методи класів
Тепер розглянемо наступний рядок коду:
|
1 |
std::cout << anything.getValue(); |
Дивно, але це також викличе помилку компіляції, хоча метод getValue() не робить нічого для модифікації змінної-члена! Виявляється, константні об’єкти класу можуть явно викликати тільки константні методи класу, а getValue() не вказано, як константний метод. Константний метод — це метод, який гарантує, що не змінюватиме об’єкт або не викликатиме неконстантні методи класу (оскільки вони можуть змінити об’єкт).
Щоб зробити getValue() константним, потрібно просто додати ключове слово const до прототипу функції після списку параметрів, але перед тілом функції:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
class Anything { public: int m_value; Anything() { m_value= 0; } void resetValue() { m_value = 0; } void setValue(int value) { m_value = value; } int getValue() const { return m_value; } // ключове слово const знаходиться після списку параметрів, але перед тілом функції }; |
Тепер getValue() є константним методом. Це означає, що ми можемо викликати його через будь-який константний об’єкт.
Для методів, визначених поза тілом класу, ключове слово const має використовуватися як в прототипі функції (в тілі класу), так і у визначенні функції:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Anything { public: int m_value; Anything() { m_value= 0; } void resetValue() { m_value = 0; } void setValue(int value) { m_value = value; } int getValue() const; // зверніть увагу на ключове слово const тут }; int Anything::getValue() const // і тут { return m_value; } |
Крім того, будь-який константний метод, який намагається змінити змінну-член або викликати неконстантний метод класу, також призведе до помилки компіляції, наприклад:
|
1 2 3 4 5 6 7 |
class Anything { public: int m_value ; void resetValue() const { m_value = 0; } // помилка компіляції, константні методи не можуть змінювати змінні-члени класу }; |
У цьому прикладі метод resetValue() був встановлений константним, але він намагається змінити значення m_value. Це викличе помилку компіляції.
Зверніть увагу, конструктори не можуть бути константними. Це пов’язано з тим, що вони повинні мати можливість ініціалізувати змінні-члени класу, а константний конструктор цього не може зробити. Тому в мові С++ константні конструктори заборонені.
Варто відзначити, що константний об’єкт класу може викликати конструктор, який ініціалізуватиме всі або деякі змінні-члени, або ж не ініціалізуватиме їх взагалі!
Правило: Робіть всі ваші методи, які не змінюють дані об’єкта класу, константними.
Константні посилання і класи
Ще одним способом створення константних об’єктів є передача об’єктів в функцію по константному посиланню.
На уроці №104 ми розглянули переваги передачі аргументів по константному посиланню, ніж по значенню. Якщо коротко, то передача аргументів по значенню створює копію значення (що є повільним процесом). Більшість часу нам не потрібна копія, а посилання вже вказує на вихідний аргумент і є більш ефективним, так як уникає створення і використання непотрібної копії. Ми зазвичай робимо посилання константним для гарантії того, що функція не змінить значення аргументу і зможе працювати з r-values (наприклад, з літералами).
Чи можете ви визначити, що не так з наступним кодом?
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <iostream> class Date { private: int m_day; int m_month; int m_year; public: Date(int day, int month, int year) { setDate(day, month, year); } void setDate(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } int getDay() { return m_day; } int getMonth() { return m_month; } int getYear() { return m_year; } }; // Примітка: Ми передаємо об'єкт date по константному посиланню, щоб уникнути створення копії об'єкта date void printDate(const Date &date) { std::cout << date.getDay() << "." << date.getMonth() << "." << date.getYear() << '\n'; } int main() { Date date(12, 11, 2018); printDate(date); return 0; } |
Відповідь полягає в тому, що всередині функції printDate(), об’єкт date розглядається як константний. І через цей константний date ми викликаємо методи getDay(), getMonth() і getYear(), які є неконстантними. Оскільки ми не можемо викликати неконстантні методи через константні об’єкти, то тут ми отримаємо помилку компіляції.
Рішення просте — зробити getDay(), getMonth() і getYear() константними:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class Date { private: int m_day; int m_month; int m_year; public: Date(int day, int month, int year) { setDate(day, month, year); } // Метод setDate() не може бути const, так як змінює значення змінних-членів void setDate(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } // Всі наступні геттери можуть бути const int getDay() const { return m_day; } int getMonth() const { return m_month; } int getYear() const { return m_year; } }; |
Тепер в функції printDate() константний date зможе викликати getDay(), getMonth() і getYear().
Перевантаження константних і неконстантних функцій
Хоча це робиться не дуже часто, але функцію можна перевантажити таким чином, щоб мати константну і неконстантну версії однієї і тієї ж функції:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <string> class Anything { private: std::string m_value; public: Anything(const std::string &value="") { m_value= value; } const std::string& getValue() const { return m_value; } // getValue() для константних об'єктів std::string& getValue() { return m_value; } // getValue() для неконстантних об'єктів }; |
Константна версія функції викликатиметься для константних об’єктів, а неконстантна версія викликатиметься для неконстантних об’єктів:
|
1 2 3 4 5 6 7 8 9 10 |
int main() { Anything anything; anything.getValue() = "Hello!"; // викликається неконстантний getValue() const Anything anything2; anything2.getValue(); // викликається константний getValue() return 0; } |
Перевантаження методу і його поділ на константну і неконстантну версії зазвичай виконується, коли значення, що повертається, має відрізнятися по константності (коли потрібно константа, і коли — ні). У прикладі, наведеному вище, неконстантна версія getValue() працюватиме тільки з неконстантними об’єктами, але ця версія більш гнучка, так як ми можемо використовувати її як для читання, так і для запису m_value (що ми, власне, і робимо, присвоюючи рядок Hello!).
Але коли ми не змінюємо дані об’єкта класу, то тоді викликається константна версія getValue().
Висновки
Будь-який метод, який не змінює дані об’єкта класу, повинен бути const!

(64 оцінок, середня: 4,94 з 5)