На цьому уроці ми розглянемо, як ініціалізувати змінні-члени класу за допомогою списку ініціалізації в мові С++, а також особливості і нюанси, які при цьому можуть виникнути.
Списки ініціалізації членів класу
На попередньому уроці ми ініціалізували члени нашого класу в конструкторі через оператор присвоювання:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Values { private: int m_value1; double m_value2; char m_value3; public: Values() { // Це все операції присвоювання, а не ініціалізація m_value1 = 3; m_value2 = 4.5; m_value3 = 'd'; } }; |
Спочатку створюються m_value1
, m_value2
і m_value3
. Потім виконується тіло конструктора, де цим змінним присвоюються значення. Аналогічний код в НЕ об’єктно-орієнтованому С++:
1 2 3 4 5 6 7 |
int m_value1; double m_value2; char m_value3; m_value1 = 3; m_value2 = 4.5; m_value3 = 'd'; |
Хоча в плані синтаксису мови C++ питань ніяких немає — все коректно, але більш ефективно використовувати ініціалізацію, а не присвоювання після оголошення.
Як ми вже знаємо з попередніх уроків, деякі типи даних (наприклад, константи і посилання) повинні бути ініціалізовані відразу. Розглянемо наступний приклад:
1 2 3 4 5 6 7 8 9 10 11 |
class Values { private: const int m_value; public: Values() { m_value = 3; // помилка: константам заборонено присвоювати значення } }; |
Аналогічний код в НЕ об’єктно-орієнтованому C++:
1 2 |
const int m_value; // помилка: константи повинні бути ініціалізовані значеннями m_value = 7; // помилка: константам заборонено присвоювати значення |
Для вирішення цієї проблеми в C++ додали метод ініціалізації змінних-членів класу через список ініціалізації членів, замість присвоювання їм значень після оголошення. Не плутайте цей список з аналогічним списком ініціалізаторів, який використовується для ініціалізації масивів.
З уроку №31 ми вже знаємо, що ініціалізувати змінні можна трьома способами: через копіюючу ініціалізацію, пряму ініціалізацію або uniform-ініціалізацію.
1 2 3 |
int value1 = 3; // копіююча ініціалізація double value2(4.5); // пряма ініціалізація char value3 {'d'} // uniform-ініціалізація |
Використання списку ініціалізації майже ідентично виконанню прямої ініціалізації (або uniform-ініціалізації в C++11).
Щоб було зрозуміліше, розглянемо приклад. Ось код з присвоюванням значень змінним-членам класу в конструкторі:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Values { private: int m_value1; double m_value2; char m_value3; public: Values() { // Це все операції присвоювання, а не ініціалізація m_value1 = 3; m_value2 = 4.5; m_value3 = 'd'; } }; |
Тепер давайте перепишемо цей код, але вже з використанням списку ініціалізації:
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 |
#include <iostream> class Values { private: int m_value1; double m_value2; char m_value3; public: Values() : m_value1(3), m_value2(4.5), m_value3('d') // напряму ініціалізуємо змінні-члени класу { // Немає необхідності у використанні присвоювання } void print() { std::cout << "Values(" << m_value1 << ", " << m_value2 << ", " << m_value3 << ")\n"; } }; int main() { Values value; value.print(); return 0; } |
Результат виконання програми:
Values(3, 4.5, d)
Список ініціалізації членів знаходиться відразу ж після параметрів конструктора. Він починається з двокрапки (:
), а потім значення для кожної змінної вказується в круглих дужках. Більше не потрібно виконувати операції присвоювання в тілі конструктора. Також зверніть увагу, що список ініціалізації членів не закінчується крапкою з комою.
Можна також додати можливість caller-у передавати значення для ініціалізації:
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 |
#include <iostream> class Values { private: int m_value1; double m_value2; char m_value3; public: Values(int value1, double value2, char value3='d') : m_value1(value1), m_value2(value2), m_value3(value3) // напряму ініціалізуємо змінні-члени класу { // Немає необхідності у використанні присвоювання } void print() { std::cout << "Values(" << m_value1 << ", " << m_value2 << ", " << m_value3 << ")\n"; } }; int main() { Values value(3, 4.5); // value1 = 3, value2 = 4.5, value3 = 'd' (значення за замовчуванням) value.print(); return 0; } |
Результат виконання програми:
Values(3, 4.5, d)
Ми можемо використовувати параметри за замовчуванням для надання значень за замовчуванням, якщо користувач їх не надав. Наприклад, клас, який має константну змінну-член:
1 2 3 4 5 6 7 8 9 10 |
class Values { private: const int m_value; public: Values(): m_value(7) // напряму ініціалізуємо константну змінну-член { } }; |
Це працює, оскільки нам дозволено ініціалізувати константні змінні (але не присвоювати їм значення після оголошення!).
Правило: Використовуйте списки ініціалізації членів, замість операцій присвоювання, для ініціалізації змінних-членів вашого класу.
uniform-ініціалізація в C++11
У C++11 замість прямої ініціалізації можна використовувати uniform-ініціалізацію:
1 2 3 4 5 6 7 8 9 10 |
class Values { private: const int m_value; public: Values(): m_value { 7 } // використовуємо uniform-ініціалізацію { } }; |
Рекомендується використовувати цей синтаксис (навіть якщо ви не використовуєте константи або посилання в якості змінних-членів вашого класу), оскільки списки ініціалізації членів необхідні при композиції і успадкуванні (це розглянемо трохи пізніше).
Правило: Використовуйте uniform–ініціалізацію замість прямої ініціалізації в C++11.
Ініціалізація масивів у класі
Розглянемо клас з масивом в якості змінної-члена:
1 2 3 4 5 6 |
class Values { private: const int m_array[7]; }; |
До C++11 ми могли тільки обнулити масив через список ініціалізації:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Values { private: const int m_array[7]; public: Values(): m_array {} // обнуляємо масив { // Якщо ми хочемо, щоб масив мав значення, то ми повинні тут використовувати присвоювання } }; |
Однак в C++11 ви можете повністю ініціалізувати масив, використовуючи uniform-ініціалізацію:
1 2 3 4 5 6 7 8 9 10 11 |
class Values { private: const int m_array[7]; public: Values(): m_array { 3, 4, 5, 6, 7, 8, 9 } // використовуємо uniform-ініціалізацію для ініціалізації масиву { } }; |
Ініціалізація змінних-членів, які є класами
Список ініціалізації членів також може використовуватися для ініціалізації членів, які є класами:
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 |
#include <iostream> class A { public: A(int a) { std::cout << "A " << a << "\n"; } }; class B { private: A m_a; public: B(int b) : m_a(b-1) // викликається конструктор A(int) для ініціалізації члена m_a { std::cout << "B " << b << "\n"; } }; int main() { B b(7); return 0; } |
Результат виконання програми:
A 6
B 7
При створенні змінної b
викликається конструктор B(int)
зі значенням 7
. До того, як тіло конструктора виконається, ініціалізується m_a
, викликаючи конструктор A(int)
зі значенням 6
. Таким чином, виведеться A 6
. Потім керування повернеться назад до конструктора B( ), і тоді вже він виконається і виведеться B 7
.
Використання списків ініціалізації
Якщо список ініціалізації поміщається на тому самому рядку, що і ім’я конструктора, то краще все розмістити в одному рядку:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Values { private: int m_value1; double m_value2; char m_value3; public: Values() : m_value1(3), m_value2(4.5), m_value3('d') // все знаходиться в одному рядку { } }; |
Якщо список ініціалізації членів не поміщається в рядку з ім’ям конструктора, то на наступному рядку (використовуючи перенесення) ініціалізатори повинні бути з відступом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Values { private: int m_value1; double m_value2; char m_value3; public: Values(int value1, double value2, char value3='d') // на цьому рядку вже і так багато чого, : m_value1(value1), m_value2(value2), m_value3(value3) // тому переносимо ініціалізатори на новий рядок (не забуваємо використовувати відступ) { } }; |
Якщо всі ініціалізатори не поміщаються в одному рядку, то ви можете виділити для кожного ініціалізатора окремий рядок:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Values { private: int m_value1; double m_value2; char m_value3; float m_value4; public: Values(int value1, double value2, char value3='d', float value4=17.5) // на цьому рядку вже і так багато чого, : m_value1(value1), // тому виділяємо кожному ініціалізатору окремий рядок, не забуваючи про кому в кінці m_value2(value2), m_value3(value3), m_value4(value4) { } }; |
Порядок виконання в списку ініціалізації
Дивно, але змінні в списку ініціалізації НЕ ініціалізуються в тому порядку, в якому вони вказані. Замість цього вони ініціалізуються в тому порядку, в якому оголошені в класі, тому слід дотримуватися наступних рекомендацій:
Не ініціалізуйте змінні-члени таким чином, щоб вони залежали від інших змінних-членів, які ініціалізуються першими (іншими словами, переконайтеся, що всі ваші змінні-члени правильно ініціалізуються, навіть якщо порядок в списку ініціалізації відрізняється).
Ініціалізуйте змінні в списку ініціалізації в тому порядку, в якому вони оголошені в класі.
Висновки
Списки ініціалізації членів дозволяють ініціалізувати члени, а не присвоювати їм значення. Це єдиний спосіб ініціалізації констант і посилань, які є змінними-членами вашого класу. У багатьох випадках використання списку ініціалізації може бути більш результативним, ніж присвоювання значень змінним-членам в тілі конструктора. Списки ініціалізації працюють як зі змінними фундаментальних типів даних, так і з членами, які самі є класами.
Тест
Напишіть клас з ім’ям RGBA, який містить 4 змінні-члени типу std::uint8_t (підключіть заголовок cstdint для доступу до типу std::uint8_t):
m_red
;
m_green
;
m_blue
;
m_alpha
.
Присвойте 0
в якості значення за замовчуванням для m_red
, m_green
і m_blue
, і 255
для m_alpha
. Створіть конструктор зі списком ініціалізації членів, який дозволить користувачу передавати значення для m_red
,m_green
, m_blue
і m_alpha
. Напишіть функцію print(), яка буде виводити значення змінних-членів.
Підказка: Якщо функція print() працює некоректно, то переконайтеся, що ви конвертували std::uint8_t в int.
Наступний код функції main():
1 2 3 4 5 6 7 |
int main() { RGBA color(0, 135, 135); color.print(); return 0; } |
Повинен видати наступний результат:
r=0 g=135 b=135 a=255
Відповідь
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 |
#include <iostream> #include <cstdint> // для std::uint8 class RGBA { private: std::uint8_t m_red; std::uint8_t m_green; std::uint8_t m_blue; std::uint8_t m_alpha; public: RGBA(std::uint8_t red=0, std::uint8_t green=0, std::uint8_t blue=0, std::uint8_t alpha=255) : m_red(red), m_green(green), m_blue(blue), m_alpha(alpha) { } void print() { std::cout << "r=" << static_cast<int>(m_red) << " g=" << static_cast<int>(m_green) << " b=" << static_cast<int>(m_blue) << " a=" << static_cast<int>(m_alpha) << '\n'; } }; int main() { RGBA color(0, 135, 135); color.print(); return 0; } |