На цьому уроці ми розглянемо конструктори в мові С++.
Конструктори
Коли всі члени класу (або структури) є відкритими, то ми можемо ініціалізувати клас (або структуру) напряму, використовуючи список ініціалізаторів або uniform-ініціалізацію (в C++11):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Boo { public: int m_a; int m_b; }; int main() { Boo boo1 = { 7, 8 }; // список ініціалізаторів Boo boo2 { 9, 10 }; // uniform-ініціалізація (C++11) return 0; } |
Проте, як тільки ми зробимо будь-які змінні-члени класу закритими, то більше не зможемо ініціалізувати їх напряму. В цьому є сенс: якщо ви не можете напряму звертатися до змінної (бо вона закрита), то ви і не повинні мати можливість напряму її ініціалізувати.
Як тоді ініціалізувати клас з закритими змінними-членами? Використовувати конструктори.
Конструктор — це особливий тип методу класу, який автоматично викликається при створенні об’єкта цього ж класу. Конструктори зазвичай використовуються для ініціалізації змінних-членів класу значеннями, які надані за замовчуванням/користувачем, або для виконання будь-яких кроків налаштування, необхідних для використовуваного класу (наприклад, відкрити певний файл або базу даних).
На відміну від звичайних методів, конструктори мають певні правила щодо своїх імен:
конструктори завжди повинні мати те ж ім’я, що і клас (враховуючи верхній і нижній регістри);
конструктори не мають типу повернення (навіть void).
Зверніть увагу, конструктори призначені тільки для виконання ініціалізації. Не слід намагатися викликати конструктор для повторної ініціалізації існуючого об’єкта. Хоча це може скомпілюватися без помилок, результати можуть вийти несподівані (компілятор створить тимчасовий об’єкт, а потім видалить його).
Конструктори за замовчуванням
Конструктор, який не має параметрів (або містить параметри, всі з яких мають значення за замовчуванням), називається конструктором за замовчуванням. Він викликається, якщо користувачем не вказані значення для ініціалізації. Наприклад:
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 Fraction { private: int m_numerator; int m_denominator; public: Fraction() // конструктор за замовчуванням { m_numerator = 0; m_denominator = 1; } int getNumerator() { return m_numerator; } int getDenominator() { return m_denominator; } double getValue() { return static_cast<double>(m_numerator) / m_denominator; } }; int main() { Fraction drob; // оскільки аргументи відсутні, то викликається конструктор за замовчуванням Fraction() std::cout << drob.getNumerator() << "/" << drob.getDenominator() << '\n'; return 0; } |
Цей клас містить дріб у вигляді окремих значень типу int. Конструктор за замовчуванням називається Fraction (як і клас). Оскільки ми створили об’єкт класу Fraction без аргументів, то конструктор за замовчуванням спрацював відразу ж після виділення пам’яті для об’єкта, і ініціалізував наш об’єкт.
Результат виконання програми:
0/1
Зверніть увагу, наш чисельник (m_numerator
) і знаменник (m_denominator
) були ініціалізовані значеннями, які ми вказали в конструкторі за замовчуванням! Це настільки корисна особливість, що майже кожен клас має свій конструктор за замовчуванням. Без нього значеннями нашого чисельника і знаменника було б сміття до тих пір, поки ми явно не присвоїли б їм нормальні значення.
Конструктори з параметрами
Хоча конструктор за замовчуванням відмінно підходить для забезпечення ініціалізації наших класів значеннями за замовчуванням, часто може бути потрібно, щоб екземпляри нашого класу мали певні значення, які ми надамо пізніше. На щастя, конструктори також можуть бути оголошені з параметрами. Ось приклад конструктора, який має два цілочисельних параметри, які використовуються для ініціалізації чисельника і знаменника:
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 <cassert> class Fraction { private: int m_numerator; int m_denominator; public: Fraction() // конструктор за замовчуванням { m_numerator = 0; m_denominator = 1; } // Конструктор з двома параметрами, один з яких має значення за замовчуванням Fraction(int numerator, int denominator=1) { assert(denominator != 0); m_numerator = numerator; m_denominator = denominator; } int getNumerator() { return m_numerator; } int getDenominator() { return m_denominator; } double getValue() { return static_cast<double>(m_numerator) / m_denominator; } }; |
Зверніть увагу, тепер у нас є два конструктори: конструктор за замовчуванням, який викликатиметься, якщо ми не надамо значення, і конструктор з параметрами, який викликатиметься, якщо ми надамо значення. Ці два конструктора можуть мирно співіснувати в одному класі завдяки перевантаженню функцій. Фактично, ви можете визначити будь-яку кількість конструкторів до тих пір, поки у них будуть унікальні параметри (враховується їх кількість і тип).
Як використовувати конструктор з параметрами? Все просто! Пряма ініціалізація:
1 2 |
int a(7); // пряма ініціалізація Fraction drob(4, 5); // ініціалізуємо напряму, викликається конструктор Fraction(int, int) |
Тут ми ініціалізували наш дріб числами 4
і 5
, результат — 4/5
!
У C++11 ми також можемо використати uniform-ініціалізацію:
1 2 |
int a { 7 }; // uniform-ініціалізація Fraction drob {4, 5}; // uniform-ініціалізація, викликається конструктор Fraction(int, int) |
Ми також можемо вказати тільки один параметр для конструктора з параметрами, а друге значення буде значенням за замовчуванням:
1 |
Fraction seven(7); // викликається конструктор Fraction(int, int), другий параметр використовує значення за замовчуванням |
Значення за замовчуванням для конструкторів працюють точно так же, як і для будь-якої іншої функції, тому у вищенаведеному прикладі, коли ми викликаємо seven(7)
, викликається Fraction(int, int)
, другий параметр якого дорівнює 1
(значення за замовчуванням).
Правило: Використовуйте пряму ініціалізацію або uniform-ініціалізацію з об’єктами ваших класів.
Копіююча ініціалізація
Подібно звичайним змінним, класи також можна ініціалізувати, використовуючи копіюючу ініціалізацію:
1 2 3 |
int a = 7; // копіююча ініціалізація Fraction eight = Fraction(8); // копіююча ініціалізація, викликається Fraction(8, 1) Fraction nine = 9; // копіююча ініціалізація. Компілятор шукатиме шляхи конвертації 9 в Fraction, що призведе до виклику конструктора Fraction(9, 1) |
Однак рекомендується уникати цієї форми ініціалізації класів, так як вона може бути менш ефективною. Хоча uniform-ініціалізація, пряма і копіююча ініціалізації працюють однаково з фундаментальними типами даних, з класами це не зовсім так (хоча кінцевий результат часто збігається). Ми розглянемо цей момент більш детально на наступних уроках.
Правило: Не використовуйте копіюючу ініціалізацію з об’єктами ваших класів.
Зменшення кількості конструкторів
У прикладі з класом Fraction і двома конструкторами (за замовчуванням і з параметрами), конструктор за замовчуванням насправді зайвий. Ми могли б спростити цей клас наступним чином:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <cassert> class Fraction { private: int m_numerator; int m_denominator; public: // Конструктор за замовчуванням Fraction(int numerator=0, int denominator=1) { assert(denominator != 0); m_numerator = numerator; m_denominator = denominator; } int getNumerator() { return m_numerator; } int getDenominator() { return m_denominator; } double getValue() { return static_cast<double>(m_numerator) / m_denominator; } }; |
Хоча цей конструктор як і раніше є конструктором за замовчуванням, він тепер визначений таким чином, що може приймати одне або два значення, надані користувачем:
1 2 3 |
Fraction drob; // виклик Fraction(0, 1) Fraction seven(7); // виклик Fraction(7, 1) Fraction sixTwo(6, 2); // виклик Fraction(6, 2) |
На практиці намагайтеся зменшувати кількість конструкторів вашого класу.
Конструктор за замовчуванням, що неявно генерується
Якщо ваш клас не має конструкторів, то мова C++ автоматично згенерує для вашого класу відкритий конструктор за замовчуванням. Його іноді називають неявним конструктором (або “конструктором, що неявно генерується”). Розглянемо наступний клас:
1 2 3 4 5 6 7 |
class Date { private: int m_day = 12; int m_month = 1; int m_year = 2018; }; |
У цього класу немає конструктора, тому компілятор згенерує наступний конструктор:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Date { private: int m_day = 12; int m_month = 1; int m_year = 2018; public: Date() // конструктор, що неявно генерується { } }; |
Цей конструктор дозволяє створювати об’єкти класу, але не виконує їх ініціалізацію і не присвоює значення членам класу.
Хоча ви не можете побачити неявно згенерований конструктор, але його існування можна довести:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Date { private: int m_day = 12; int m_month = 1; int m_year = 2018; // Не було надано конструктора, тому C++ автоматично створить відкритий конструктор за замовчуванням }; int main() { Date date; // виклик неявного конструктора return 0; } |
Вищенаведений код скомпілюється, оскільки в об’єкті date
спрацює неявний конструктор (який є відкритим). Якщо ваш клас має інші конструктори, то неявно згенерований конструктор не створюватиметься. Наприклад:
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 |
class Date { private: int m_day = 12; int m_month = 1; int m_year = 2018; public: Date(int day, int month, int year) // звичайний конструктор (не за замовчуванням) { m_day = day; m_month = month; m_year = year; } // Неявний конструктор не створиться, так як ми вже визначили свій конструктор }; int main() { Date date; // помилка: Неможливо створити об'єкт, так як конструктора за замовчуванням не існує, і компілятор автоматично не згенерував неявний конструктор Date today(14, 10, 2020); // ініціалізуємо об'єкт today return 0; } |
Рекомендується завжди створювати принаймні один конструктор для класу. Це дозволить вам контролювати процес створення об’єктів вашого класу, і запобіжить виникненню потенційних проблем після додання інших конструкторів.
Правило: Створюйте хоча б один конструктор в класі, навіть якщо це пустий конструктор за замовчуванням.
Класи, які містять інші класи
Одні класи можуть містити інші класи в якості змінних-членів. За замовчуванням, при створенні зовнішнього класу, для змінних-членів викликатимуться конструктори за замовчуванням. Це станеться до того, як тіло конструктора виконається. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <iostream> class A { public: A() { std::cout << "A\n"; } }; class B { private: A m_a; // B містить A, як змінну-член public: B() { std::cout << "B\n"; } }; int main() { B b; return 0; } |
Результат виконання програми:
A
B
При створенні змінної b
викликається конструктор B(). Перш ніж тіло конструктора виконається, m_a
ініціалізується, викликаючи конструктор за замовчуванням класу A. Таким чином, виведеться A
. Потім керування повернеться назад до конструктора B, і тіло конструктора B почне своє виконання.
В цьому є сенс, тому що конструктор B() може захотіти використати змінну m_a
, тому спочатку потрібно ініціалізувати m_a
!
Тест
Завдання №1
a) Напишіть клас Ball, який повинен мати наступні дві закриті змінні-члени зі значеннями за замовчуванням:
m_color
(Red
);
m_radius
(20.0
).
У класі Ball повинні бути наступні конструктори:
для встановлення значення тільки для m_color
;
для встановлення значення тільки для m_radius
;
для встановлення значень і для m_radius
, і для m_color
;
для встановлення значень, коли значення не надані взагалі.
Не використовуйте параметри за замовчуванням для конструкторів. Напишіть ще одну функцію для виводу кольору (m_color
) і радіусу (m_radius
) кулі (об’єкта класу Ball).
Наступний код функції main():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int main() { Ball def; def.print(); Ball black("black"); black.print(); Ball thirty(30.0); thirty.print(); Ball blackThirty("black", 30.0); blackThirty.print(); return 0; } |
Повинен генерувати наступний результат:
color: red, radius: 20
color: black, radius: 20
color: red, radius: 30
color: black, radius: 30
Відповідь №1.а)
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
#include <iostream> #include <string> class Ball { private: std::string m_color; double m_radius; public: // Конструктор за замовчуванням без параметрів Ball() { m_color = "red"; m_radius = 20.0; } // Конструктор з параметром color (для radius надано значення за замовчуванням) Ball(const std::string &color) { m_color = color; m_radius = 20.0; } // Конструктор з параметром radius (для color надано значення за замовчуванням) Ball(double radius) { m_color = "red"; m_radius = radius; } // Конструктор з параметрами color і radius Ball(const std::string &color, double radius) { m_color = color; m_radius = radius; } void print() { std::cout << "color: " << m_color << ", radius: " << m_radius << '\n'; } }; int main() { Ball def; def.print(); Ball black("black"); black.print(); Ball thirty(30.0); thirty.print(); Ball blackThirty("black", 30.0); blackThirty.print(); return 0; } |
b) Тепер оновіть ваш код з попереднього завдання з використанням конструкторів з параметрами за замовчуванням. Постарайтеся використовувати якомога менше конструкторів.
Відповідь №1.b)
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 41 42 43 44 45 46 47 |
#include <iostream> #include <string> class Ball { private: std::string m_color; double m_radius; public: // Конструктор з параметром radius (для color надано значення за замовчуванням) Ball(double radius) { m_color = "red"; m_radius = radius; } // Конструктор з параметрами color і radius. // Обробляються випадки, коли не надано ніяких параметрів; тільки color; color + radius Ball(const std::string &color="red", double radius=20.0) { m_color = color; m_radius = radius; } void print() { std::cout << "color: " << m_color << ", radius: " << m_radius << '\n'; } }; int main() { Ball def; def.print(); Ball black("black"); black.print(); Ball thirty(30.0); thirty.print(); Ball blackThirty("black", 30.0); blackThirty.print(); return 0; } |
Завдання №2
Що відбудеться, якщо не оголосити конструктор за замовчуванням?
Відповідь №2
Якщо не визначити ніяких конструкторів, то компілятор автоматично створить пустий відкритий конструктор за замовчуванням. Якщо визначити хоча б один будь-який конструктор (за замовчуванням або інший), то компілятор не створить пустий конструктор за замовчуванням.