Оператор присвоювання (=
) використовується для копіювання значень з одного об’єкту в інший (вже існуючий) об’єкт.
Присвоювання vs. Конструктор копіювання
Конструктор копіювання і оператор присвоювання виконують майже ідентичну роботу: обидва копіюють значення з одного об’єкту в значення іншого об’єкту. Однак конструктор копіювання використовується при ініціалізації нових об’єктів, тоді як оператор присвоювання замінює вміст вже існуючих об’єктів. Все просто:
Якщо новий об’єкт створений перед виконанням операції копіювання, то використовується конструктор копіювання (передача або повернення об’єктів виконуються по значенню).
Якщо новий об’єкт не створювали, а робота ведеться з уже існуючим об’єктом, то використовується оператор присвоювання.
Перевантаження оператора присвоювання
Перевантаження оператора присвоювання (=
) досить-таки просте і виконується через метод класу, але є один нюанс:
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 |
#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 ©) : m_numerator(copy.m_numerator), m_denominator(copy.m_denominator) { // Немає необхідності виконувати перевірку denominator тут, тому що ця перевірка вже виконана в конструкторі за замовчуванням std::cout << "Copy constructor worked here!\n"; // просто, щоб показати, що це працює } // Перевантаження оператора присвоювання Drob& operator= (const Drob &drob) { // Виконуємо копіювання значень m_numerator = drob.m_numerator; m_denominator = drob.m_denominator; // Повертаємо поточний об'єкт, щоб мати можливість з'єднати в ланцюжок виконання декількох операцій присвоювання return *this; } 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 d; d = sixSeven; // викликається перевантажений оператор присвоювання std::cout << d; return 0; } |
Результат виконання програми:
6/7
До цього моменту все ок. Функція перевантаження operator=() повертає прихований вказівник *this, і ми можемо навіть з’єднати виконання декількох операцій присвоювання:
1 2 3 4 5 6 7 8 9 10 |
int main() { Drob d1(6,7); Drob d2(8,3); Drob d3(10,4); d1 = d2 = d3; // ланцюжок операцій присвоювання return 0; } |
Самоприсвоювання
Тут вже стає цікавіше. Самоприсвоювання — це той нюанс, про який згадувалося вище. Мова C++ дозволяє виконувати самоприсвоювання:
1 2 3 4 5 6 7 |
int main() { Drob d1(6,7); d1 = d1; // самоприсвоювання return 0; } |
У прикладі, наведеному вище, самоприсвоювання не призведе до зміни стану чого-небудь і буде лише марною тратою часу і ресурсів. У більшості випадків самоприсвоювання не слід виконувати взагалі.
Крім того, у випадках, коли використовується динамічне виділення пам’яті, самоприсвоювання може бути навіть небезпечним:
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 |
#include <iostream> class SomeString { private: char *m_data; int m_length; public: SomeString(const char *data="", int length=0) : m_length(length) { if (!length) m_data = nullptr; else m_data = new char[length]; for (int i=0; i < length; ++i) m_data[i] = data[i]; } SomeString& operator= (const SomeString &str); friend std::ostream& operator<<(std::ostream& out, const SomeString &s); }; std::ostream& operator<<(std::ostream& out, const SomeString &s) { out << s.m_data; return out; } // Перевантаження оператора присвоювання (поганий варіант перевантаження) SomeString& SomeString::operator= (const SomeString &str) { // Якщо m_data вже має значення, то видаляємо це значення if (m_data) delete[] m_data; m_length = str.m_length; // Копіюємо значення з str в m_data неявного об'єкта m_data = new char[str.m_length]; for (int i=0; i < str.m_length; ++i) m_data[i] = str.m_data[i]; // Повертаємо поточний об'єкт return *this; } int main() { SomeString anton("Anton", 7); SomeString employee; employee = anton; std::cout << employee; return 0; } |
Запустіть програму, і ви побачите, що виведеться Anton
, як і очікувалось.
Тепер замініть функцію main() на наступну:
1 2 3 4 5 6 7 8 |
int main() { SomeString anton("Anton", 7); anton = anton; std::cout << anton; return 0; } |
В результаті ви отримаєте або значення-сміття, або збій.
Розглянемо, що відбувається при виконанні операції присвоювання, коли неявний і переданий в якості аргументу об’єкти є об’єктом anton
. В цьому випадку m_data
дорівнює str.m_data
(тобто Anton
). Перше, що станеться — функція перевантаження перевірить, чи є вже значенням неявного об’єкту рядок Anton
. Якщо є, то відбудеться видалення цього значення, щоб не стався витік пам’яті. Тобто значення m_data
неявного об’єкта видаляється, але справа в тому, що str.m_data
має ту ж саму адресу в пам’яті (значення якої видаляється)! Це означає, що str.m_data
стане висячим вказівником.
Пізніше, коли ми копіюватимемо дані з параметру str
функції перевантаження в наш неявний об’єкт, ми звертатимемося до висячого вказівника str.m_data
. Це призведе до того, що ми або скопіюємо дані-сміття, або спробуємо отримати доступ до пам’яті, яку наша програма більше не має в розпорядженні (відбудеться збій).
Виявлення і обробка самоприсвоювання
На щастя, ми можемо виявити виконання самоприсвоювання. Це робиться за допомогою досить простої перевірки в функції перевантаження operator=():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Перевантаження оператора присвоювання (хороший варіант, його і використовуйте) Drob& Drob::operator= (const Drob &drob) { // Перевірка на самоприсвоювання if (this == &drob) return *this; // Виконуємо копіювання значень m_numerator = drob.m_numerator; m_denominator = drob.m_denominator; // Повертаємо поточний об'єкт return *this; } |
Перевіряючи, чи є наш неявний об’єкт тим же, що і переданий в якості параметру, ми зможемо відразу ж повернути його без виконання будь-якого коду.
Зверніть увагу, немає необхідності виконувати перевірку на самоприсвоювання в конструкторі копіювання. Це пов’язано з тим, що конструктор копіювання викликається тільки при створенні нових об’єктів, а способу присвоїти щойно створений об’єкт самому собі, щоб викликати конструктор копіювання — немає.
Оператор присвоювання за замовчуванням
На відміну від інших операторів, компілятор автоматично надасть відкритий оператор присвоювання за замовчуванням для вашого класу при його використанні, якщо ви не надасте його самостійно. В операторі присвоювання за замовчуванням виконується почленне присвоювання (яке є аналогічним почленній ініціалізації, яка використовується в конструкторах копіювання, що надаються мовою C++ за замовчуванням).
Як і з іншими конструкторами і операторами, ви можете заборонити виконання операції присвоювання з об’єктами ваших класів, зробивши оператор присвоювання закритим або використовуючи ключове слово delete:
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 |
#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 ©) = delete; // Перевантаження оператора присвоювання Drob& operator= (const Drob &drob) = delete; // ні створенню копій об'єктів через операцію присвоювання! 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 d; d = sixSeven; // помилка компіляції, operator= був видалений std::cout << d; return 0; } |