Перегрузка оператора присваивания
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 44 из 104.
Оператор присваивания копированием (=) вызывается, когда вы присваиваете один существующий объект другому. В отличие от конструктора копирования (который создает новый объект), оператор присваивания работает с объектом, который уже существует и может иметь ресурсы, которые необходимо сначала очистить.
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s), data(new int[s]) {}
~Buffer() { delete[] data; }
Buffer& operator=(const Buffer& other) {
if (this != &other) { // 1. Проверка на самоприсваивание
delete[] data; // 2. Очистка существующего ресурса
size = other.size; // 3. Копирование данных
data = new int[size];
for (size_t i = 0; i < size; ++i)
data[i] = other.data[i];
}
return *this; // 4. Возврат *this
}
};Проверка на самоприсваивание (this != &other) критически важна. Без неё b = b; удалит данные перед попыткой их копирования, что приведет к неопределенному поведению.
Оператор возвращает *this по ссылке, чтобы обеспечить возможность цепочки присваиваний вида a = b = c;. Это соответствует поведению встроенных типов.
Для классов, управляющих ресурсами, вы часто будете реализовывать как копирующий, так и перемещающий операторы присваивания:
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data; // Очистка текущего ресурса
data = other.data; // Захват ресурса
size = other.size;
other.data = nullptr; // Оставление источника в валидном состоянии
other.size = 0;
}
return *this;
}Оператор перемещающего присваивания передает владение вместо копирования, делая такие операции, как vec[0] = createBuffer();, гораздо более эффективными.
Задание
ЛегкоДавайте создадим класс DynamicString, который управляет динамически выделенным массивом символов и правильно обрабатывает присваивание между существующими объектами. Это практический сценарий, в котором вам нужно безопасно передавать строковые данные от одного объекта к другому без утечек памяти или висячих указателей.
Вы создадите два файла для организации своего кода:
DynamicString.h: Определите классDynamicString, который хранит строку в стиле C, используя динамическую память. Ваш класс должен управлять:- Приватным указателем
char*для данных строки - Приватной переменной
size_tдля длины - Конструктором, который принимает
const char*и создает глубокую копию - Деструктором, который правильно освобождает выделенную память
- Оператором копирующего присваивания, который выполняет глубокое копирование с защитой от самоприсваивания
- Оператором перемещающего присваивания, который эффективно передает владение
- Методом
c_str(), который возвращает внутреннюю строку (const) - Методом
length(), который возвращает длину строки (const)
Ваш оператор копирующего присваивания должен следовать четырем основным этапам: проверка на самоприсваивание, очистка существующих ресурсов, копирование новых данных и возврат
*this. Оператор перемещающего присваивания должен «украсть» указатель источника и оставить источник в валидном пустом состоянии (nullptr с длиной 0).- Приватным указателем
main.cpp: Считайте три строки из входных данных (каждую с новой строки). Создайте три объектаDynamicString:str1с первой строкой,str2со второй иstr3с третьей.Продемонстрируйте работу ваших операторов присваивания:
- Выведите начальное состояние всех трех строк
- Используйте копирующее присваивание:
str1 = str2; - Выведите str1 и str2 после копирующего присваивания (оба должны иметь одинаковое содержимое)
- Используйте перемещающее присваивание:
str2 = std::move(str3); - Выведите str2 после перемещающего присваивания и длину str3 (она должна быть равна 0 после перемещения)
Формат вывода:
Initial: str1: <value> str2: <value> str3: <value> After copy (str1 = str2): str1: <value> str2: <value> After move (str2 = std::move(str3)): str2: <value> str3 length: 0
Используйте <cstring> для функций strlen и strcpy. Пометьте оператор перемещающего присваивания как noexcept. Включите <utility> в main.cpp для использования std::move. Не забудьте про guard-вставки (header guards) в заголовочном файле.
Шпаргалка
Оператор присваивания копированием (=) присваивает один существующий объект другому. Он должен выполнять очистку существующих ресурсов перед копированием новых данных.
Структура оператора присваивания копированием
ClassName& operator=(const ClassName& other) {
if (this != &other) { // 1. Проверка на самоприсваивание
// 2. Очистка существующих ресурсов
// 3. Копирование данных из other
}
return *this; // 4. Возврат *this
}Пример с динамической памятью
class Buffer {
int* data;
size_t size;
public:
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data; // Очистка существующего ресурса
size = other.size;
data = new int[size];
for (size_t i = 0; i < size; ++i)
data[i] = other.data[i];
}
return *this;
}
};Проверка на самоприсваивание (this != &other) предотвращает удаление данных перед их копированием в таких случаях, как b = b;.
Возврат *this по ссылке позволяет создавать цепочки присваиваний: a = b = c;.
Оператор присваивания перемещением
Оператор присваивания перемещением передает владение вместо копирования, что повышает эффективность:
ClassName& operator=(ClassName&& other) noexcept {
if (this != &other) {
// Очистка текущего ресурса
// "Кража" ресурса у other
// Оставление other в валидном состоянии (nullptr, 0 и т.д.)
}
return *this;
}Пример присваивания перемещением
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data; // "Кража" ресурса
size = other.size;
other.data = nullptr; // Оставление источника в валидном состоянии
other.size = 0;
}
return *this;
}Помечайте оператор присваивания перемещением как noexcept для оптимальной производительности со стандартными контейнерами.
Попробуйте сами
#include <iostream>
#include <string>
#include <utility>
#include "DynamicString.h"
using namespace std;
int main() {
// Считать три строки из входного потока
string input1, input2, input3;
getline(cin, input1);
getline(cin, input2);
getline(cin, input3);
// Создать три объекта DynamicString
DynamicString str1(input1.c_str());
DynamicString str2(input2.c_str());
DynamicString str3(input3.c_str());
// TODO: Вывести начальное состояние всех трех строк
// Формат:
// Initial:
// str1: <value>
// str2: <value>
// str3: <value>
// TODO: Использовать присваивание копированием: str1 = str2;
// TODO: Вывести str1 и str2 после присваивания копированием
// Формат:
// After copy (str1 = str2):
// str1: <value>
// str2: <value>
// TODO: Использовать присваивание перемещением: str2 = std::move(str3);
// TODO: Вывести str2 после перемещения и длину str3
// Формат:
// After move (str2 = std::move(str3)):
// str2: <value>
// str3 length: 0
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер