Згадаймо всі типи ініціалізації, які підтримує мова C++: пряма ініціалізація, uniform-ініціалізація і копіююча ініціалізація.
Конструктор копіювання
Розглянемо приклади всіх вищенаведених ініціалізацій на практиці, використовуючи наступний клас Drob:
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 <cassert> #include <iostream> class Drob { private: int m_numerator; int m_denominator; public: // Конструктор за замовчуванням Drob(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { assert(denominator != 0); } friend std::ostream& operator<<(std::ostream& out, const Drob &d1); }; std::ostream& operator<<(std::ostream& out, const Drob &d1) { out << d1.m_numerator << "/" << d1.m_denominator; return out; } |
Ми можемо виконати пряму ініціалізацію:
1 2 |
int a(7); // пряма ініціалізація цілочисельної змінної Drob sixSeven(6, 7); // пряма ініціалізація об'єкта класу Drob, викликається конструктор Drob(int, int) |
В C++11 ми можемо виконати uniform-ініціалізацію:
1 2 |
int a { 7 }; // uniform-ініціалізація цілочисельної змінної Drob sixSeven {6, 7}; // uniform-ініціалізація об'єкта класу Drob, викликається конструктор Drob(int, int) |
І, нарешті, ми можемо виконати копіюючу ініціалізацію:
1 2 3 |
int a = 7; // копіююча ініціалізація цілочисельної змінної Drob eight = Drob(8); // копіююча ініціалізація об'єкта класу Drob, викликається Drob(8, 1) Drob nine = 9; // копіююча ініціалізація об'єкта класу Drob. Компілятор шукатиме спосіб конвертації 9 в об'єкт класу Drob, що призведе до виклику конструктора Drob(9, 1) |
З прямою ініціалізацією і 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 26 27 28 29 30 31 32 |
#include <cassert> #include <iostream> class Drob { private: int m_numerator; int m_denominator; public: // Конструктор за замовчуванням Drob(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { assert(denominator != 0); } friend std::ostream& operator<<(std::ostream& out, const Drob &d1); }; std::ostream& operator<<(std::ostream& out, const Drob &d1) { out << d1.m_numerator << "/" << d1.m_denominator; return out; } int main() { Drob sixSeven(6, 7); // пряма ініціалізація об'єкта класу Drob, викликається конструктор Drob(int, int) Drob dCopy(sixSeven); // пряма ініціалізація. Який конструктор викликається тут? std::cout << dCopy << '\n'; } |
Результат виконання програми:
6/7
Розглянемо детально, як працює ця програма.
З об’єктом sixSeven
виконується звичайна пряма ініціалізація, яка призводить до виклику конструктора Drob(int, int)
. Тут немає ніяких сюрпризів. А ось ініціалізація об’єкта dCopy
також є прямою ініціалізацією, але який конструктор викликається тут? Відповідь: конструктор копіювання.
Конструктор копіювання — це особливий тип конструктора, який використовується для створення нового об’єкта через копіювання існуючого. І, як у випадку з конструктором за замовчуванням, якщо ви не надасте конструктор копіювання для своїх класів самостійно, то мова C++ створить public-конструктор копіювання автоматично. Оскільки компілятор мало знає про ваш клас, то за замовчуванням створений конструктор копіювання використовуватиме почленну ініціалізацію. Почленна ініціалізація означає, що кожен член об’єкта-копії ініціалізується безпосередньо з члена об’єкта-оригіналу. Тобто в прикладі, наведеному вище, dCopy.m_numerator
матиме значення sixSeven.m_numerator
(6
), а dCopy.m_denominator
дорівнюватиме sixSeven.m_ denominator
(7
).
Так само, як ми можемо явно визначити конструктор за замовчуванням, так само ми можемо явно визначити і конструктор копіювання. Конструктор копіювання виглядає наступним чином:
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 |
#include <iostream> #include <cassert> class Drob { private: int m_numerator; int m_denominator; public: // Конструктор за замовчуванням Drob(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { assert(denominator != 0); } // Конструктор копіювання Drob(const Drob &drob) : m_numerator(drob.m_numerator), m_denominator(drob.m_denominator) // Примітка: Ми маємо прямий доступ до членів об'єкту drob, оскільки ми зараз знаходимося всередині класу Drob { // Немає необхідності виконувати перевірку denominator тут, так як ця перевірка вже здійснюється в конструкторі класу Drob std::cout << "Copy constructor worked here!\n"; // просто, щоб показати, що це працює } friend std::ostream& operator<<(std::ostream& out, const Drob &d1); }; std::ostream& operator<<(std::ostream& out, const Drob &d1) { out << d1.m_numerator << "/" << d1.m_denominator; return out; } int main() { Drob sixSeven(6, 7); // пряма ініціалізація об'єкта класу Drob, викликається конструктор Drob(int, int) Drob dCopy(sixSeven); // пряма ініціалізація, викликається конструктор копіювання класу Drob std::cout << dCopy << '\n'; } |
Результат виконання програми:
Copy constructor worked here!
6/7
Конструктор копіювання у вищенаведеному прикладі використовує почленну ініціалізацію і функціонально еквівалентний конструктору за замовчуванням, за винятком того, що ми додали стейтмент виводу, в якому вказали текст (спрацював конструктор копіювання).
Запобігання створенню копій об’єктів
Ми можемо запобігти створенню копій об’єктів наших класів, зробивши конструктор копіювання закритим:
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> #include <cassert> class Drob { private: int m_numerator; int m_denominator; // Конструктор копіювання (закритий) Drob(const Drob &drob) : m_numerator(drob.m_numerator), m_denominator(drob.m_denominator) { // Немає необхідності виконувати перевірку denominator тут, так як ця перевірка вже здійснюється в конструкторі класу Drob std::cout << "Copy constructor worked here!\n"; // просто, щоб показати, що це працює } public: // Конструктор за замовчуванням Drob(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { assert(denominator != 0); } friend std::ostream& operator<<(std::ostream& out, const Drob &d1); }; std::ostream& operator<<(std::ostream& out, const Drob &d1) { out << d1.m_numerator << "/" << d1.m_denominator; return out; } int main() { Drob sixSeven(6, 7); // пряма ініціалізація об'єкта класу Drob, викликається конструктор Drob(int, int) Drob dCopy(sixSeven); // конструктор копіювання є закритим, тому цей рядок викличе помилку компіляції std::cout << dCopy << '\n'; } |
Тут ми отримаємо помилку компіляції, так як dCopy
повинен використовувати конструктор копіювання, але він не бачить його, оскільки конструктор копіювання є закритим.
Конструктор копіювання можна проігнорувати
Розглянемо наступний код:
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 <cassert> #include <iostream> class Drob { private: int m_numerator; int m_denominator; public: // Конструктор за замовчуванням Drob(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { assert(denominator != 0); } // Конструктор копіювання Drob(const Drob &drob) : m_numerator(drob.m_numerator), m_denominator(drob.m_denominator) { // Немає необхідності виконувати перевірку denominator тут, так як ця перевірка вже здійснюється в конструкторі класу Drob std::cout << "Copy constructor worked here!\n"; // просто, щоб показати, що це працює } friend std::ostream& operator<<(std::ostream& out, const Drob &d1); }; std::ostream& operator<<(std::ostream& out, const Drob &d1) { out << d1.m_numerator << "/" << d1.m_denominator; return out; } int main() { Drob sixSeven(Drob(6, 7)); std::cout << sixSeven; return 0; } |
Спочатку ініціалізується анонімний об’єкт Drob, який призводить до виклику конструктора Drob(int, int)
. Потім цей анонімний об’єкт використовується для ініціалізації об’єкта sixSeven
класу Drob. Оскільки анонімний об’єкт є об’єктом класу Drob, як і sixSeven
, то тут повинен викликатися конструктор копіювання, вірно?
Запустіть цю програму самостійно. Очікуваний результат:
Copy constructor worked here!
6/7
Фактичний результат:
6/7
Чому наш конструктор копіювання не спрацював?
Справа в тому, що ініціалізація анонімного об’єкта, а потім використання цього об’єкта для прямої ініціалізації вже не анонімного об’єкта виконується в два етапи (перший етап — це створення анонімного об’єкта, другий етап — це виклик конструктора копіювання). Однак, кінцевий результат по суті ідентичний простому виконанню прямої ініціалізації, яка займає всього лише один крок.
З цієї причини в таких випадках компілятору дозволяється відмовитися від виклику конструктора копіювання і просто виконати пряму ініціалізацію. Цей процес називається елізією.
Тому, навіть якщо ви напишете:
1 |
Drob sixSeven(Drob(6, 7)); |
Компілятор може змінити це на:
1 |
Drob sixSeven(6, 7); |
В останньому випадку з прямою ініціалізацією потрібен буде виклик тільки одного конструктора (Drob(int, int)
). Зверніть увагу, у випадках, коли використовується елізія, будь-які стейтменти в тілі конструктора копіювання не виконуються, навіть якщо вони мають побічні ефекти (наприклад, виводять щось на екран)!
Нарешті, якщо ви робите свій конструктор копіювання закритим, то будь-яка ініціалізація, яка використовує цей закритий конструктор копіювання, призведе до помилок компіляції, навіть якщо конструктор копіювання буде проігноровано!