Оператор присваивания (=
) используется для копирования значений из одного объекта в другой (уже существующий) объект.
Присваивание 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; } |
Добрый день! Можно пояснить для чего необходимо это действие.
Если m_data существует и в нем что-то хранится, то данная область памяти очищается, т.к. в будущем произойдет новое выделение памяти под m_data с размером, как у SomeString &str, так как мы совершаем операцию присваивания
Доброго времени суток!
Столкнулся с такой проблемой.
Есть объект, в котором переопределен оператор присваивания, а также деструктор, который освобождает динамически выделенную память. Соответственно при помощи оператора присваивания мы просто перевешиваем указатели на новый объект (который собственно присваивает себе область памяти старого объекта). Как в таком случае реализовать деструктор, чтобы одна и та же память не освобождалась многократно?
Весьма хорошая подача материала, Вы большой молодец!
Оператор присваивания можно реализовать чуть проще:
Реализовываем дружественную/обычную ф-цию swap:
Далее реализовать оператор присваивания через swap:
В этом случае используется не традиционная реализация оператора:
1) аргумент принимается по значению
2) Данные текущего класса меняются с местами другого класса
3) Возвращается ссылка на текущий класс
4) Временный объект other удаляется по выходу из тела функции
А есть где то описание того что если определить какой то из Конструкторов по умолчанию, копирование, и копирующего присваивания — то все остальные не генерируются если не попросить этого явно? толи я пропустил тему где этот момент описан..
Если вы определите собственный конструктор то автоматически генерируемый конструктор генерироваться не будет. Но только он. Это же касается отдельно оператора присваивания и конструктора копирования
Почитайте "Правило трёх/пяти" с++ на вики
https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D0%BE_%D1%82%D1%80%D1%91%D1%85_(C%2B%2B)
Спасибо за огромный труд, с нетерпением жду следующих уроков!!!!!
Спасибо, что читаете 🙂