Чем отличается конструктор копирования от оператора присваивания
Перейти к содержимому

Чем отличается конструктор копирования от оператора присваивания

  • автор:

Разница между конструктором копирования и оператором присваивания в C ++

Конструктор копирования и оператор присваивания — это два способа инициализации одного объекта с использованием другого объекта. Принципиальное различие между конструктором копирования и оператором присваивания состоит в том, что конструктор копирования выделяет отдельную память обоим объектам, то есть вновь созданному целевому объекту и исходному объекту. Оператор присваивания

Please enable JavaScript

Давайте изучим разницу между конструктором копирования и оператором присваивания.

Сравнительная таблица

Основа для сравнения Копировать конструктор Оператор присваивания
основной Конструктор копирования является перегруженным конструктором. Оператор присваивания является побитовым оператором.
Имея в виду Конструктор копирования инициализирует новый объект уже существующим объектом. Оператор присваивания присваивает значение одного объекта другому объекту, оба из которых уже существуют.
Синтаксис имя_класса (продолжение имя_класса и имя_объекта) <
// тело конструктора
>
имя_класса Ob1, Ob2;
Ob2 = OB1;
Запускает (1) Конструктор копирования вызывается, когда новый объект инициализируется с существующим.
(2) Объект передается функции в качестве нереферентного параметра.
(3) Объект возвращается из функции.
Оператор присваивания вызывается только при назначении существующему объекту нового объекта.
Выделение памяти И целевой объект, и инициализирующий объект находятся в разных местах памяти. И целевой объект, и инициализирующий объект используют одну и ту же выделенную память
По умолчанию Если вы не определяете конструктор копирования в программе, компилятор C ++ неявно предоставляет его. Если вы не перегружаете оператор «=», будет сделана побитовая копия.

Определение Копирующего Конструктора

«Конструктор копирования» — это форма перегруженного конструктора . Конструктор копирования вызывается или вызывается только для целей инициализации. Конструктор копирования инициализирует вновь созданный объект другим существующим объектом. Когда конструктор копирования используется для инициализации вновь созданного целевого объекта, тогда и целевой объект, и исходный объект совместно используют разные области памяти. Изменения, внесенные в исходный объект, не отражаются в целевом объекте. Общая форма конструктора копирования

Если программист не создает конструктор копирования в программе на C ++, то компилятор неявно предоставляет конструктор копирования. Неявный конструктор копирования, предоставляемый компилятором, делает членскую копию исходного объекта. Но иногда для каждого члена копии недостаточно, поскольку объект может содержать переменную-указатель. Копирование переменной указателя означает, что мы копируем адрес, сохраненный в переменной указателя, но мы не хотим копировать адрес, сохраненный в переменной указателя, вместо этого мы хотим скопировать то, на что указывает указатель. Следовательно, существует необходимость в явном «конструкторе копирования» в программе для решения такого рода проблем.

Конструктор копирования вызывается в трех условиях:

  • Конструктор копирования вызывается, когда новый объект инициализируется с существующим.
  • Объект передан функции в качестве нереферентного параметра.
  • Объект возвращается из функции.

Давайте разберемся с конструктором копирования на примере.

В приведенном выше коде я явно объявил конструктор «copy (copy & c)». Этот конструктор копирования вызывается, когда объект B инициализируется с использованием объекта A. Во второй раз он вызывается, когда объект C инициализируется с использованием объекта A. Когда объект D инициализируется с использованием объекта A, конструктор копирования не вызывается, поскольку при инициализации D оно уже существует, а не вновь созданное. Следовательно, здесь вызывается оператор присваивания.

Определение оператора присваивания

Оператор присваивания является оператором присваивания C ++. Оператор «=» используется для вызова оператора присваивания. Он копирует данные в одном объекте идентично другому объекту. Оператор присваивания копирует один объект в другой для каждого члена. Если вы не перегружаете оператор присваивания, он выполняет побитовое копирование. Поэтому вам необходимо перегрузить оператор присваивания.

В приведенном выше коде, когда объект A назначен объекту B, оператор присваивания вызывается, поскольку оба объекта уже существуют. Аналогичным образом, тот же самый случай, когда объект C инициализируется с объектом A.

Когда выполняется побитовое присвоение, оба объекта совместно используют одну и ту же ячейку памяти, и изменения в одном объекте отражаются в другом объекте.

Ключевые различия между конструктором копирования и оператором присваивания

  1. Конструктор копирования является перегруженным конструктором, где в качестве оператора присваивания используется побитовый оператор.
  2. Используя конструктор копирования, вы можете инициализировать новый объект уже существующим объектом. С другой стороны, оператор присваивания копирует один объект в другой объект, оба из которых уже существуют.
  3. Конструктор копирования инициализируется всякий раз, когда новый объект инициализируется уже существующим объектом, когда объект передается в функцию в качестве параметра без ссылки или когда объект возвращается из функции. С другой стороны, оператор присваивания вызывается только тогда, когда объект назначается другому объекту.
  4. Когда объект инициализируется с помощью конструктора копирования, инициализирующий объект и инициализированный объект совместно используют разные места в памяти. С другой стороны, когда объект инициализируется с использованием оператора присваивания, тогда инициализированный и инициализирующий объекты совместно используют одну и ту же область памяти.
  5. Если вы явно не определяете конструктор копирования, тогда его предоставляет компилятор. С другой стороны, если вы не перегружаете оператор присваивания, то выполняется операция побитового копирования.

Заключение:

Конструктор Copy лучше всего подходит для копирования одного объекта в другой, когда объект содержит необработанные указатели.

RAII, move семантика, конструкторы копирования и операторы присваивания, умные указатели, исключения

Предположим, что мы хотим сделать класс Array, у которого будет следующая реализация:

Мы хотим сделать возможным копирование объекта. Сделать это можно двумя путями. Первый путь — положиться на обычный конструктор копирования и ничего не писать. Тогда он просто скопирует все поля класса(shallow copy), и если мы вызовем у одного объекта деструктор, то второй будет указывать на пустую область памяти и может получиться undefined behavior. Второй — реализовать свой конструктор копирования(deep copy):

Конструктор копирования вызывается когда мы пишем код в таком стиле:

В последнем случае нужно реализовать оператор присваивания:

move семантика

Рассмотрим фрагмент кода:

Как указано в комментариях в коде, происходит бессмысленное копирование. Идея move семантики заключается в том, чтобы избежать этого, передав нужному объекту содержимое array, а значения array сделать неопределенными, но валидными(то есть мы не можем полагаться на его поля, но при этом, например, должен корректно отработать деструктор). Попробуем преобразовать код, указанный выше:

Заметим в коде странный синтаксис — &&(rvalue reference). Он отличается от & тем, что мы не можем передать в него что попало, например не временный объект, который не собирается удаляться. Соотвественно такой вариант конструктора вызовется только от объекта, который скоро удалится. Стоит отметить, что компилятор умный, и он сам понимает, что объект скоро удалится. Теперь при создании array2 будет вызываться новый конструктор.

Напишем move-оператор присваивания:

std::move

Допустим мы хотим использовать функцию f, которая принимает Array&& :

Но мы не можем передавать array1 в нашу функцию, потому что array1 может быть использован после f. Чтобы все-таки передать array1 в функцию, используется функция std::move:

То есть, грубо говоря std::move() = static_cast<Array&&>(); после такого использования НЕ СТОИТ полагаться на значения array1:

Стоит отметить, что обхитрить компилятор, создав дополнительную функцию g не получится:

Если есть нетривиальный конструктор копирования, оператор присваивания или деструктор, то move-конструктор не генерируется. Чтобы его все-таки сгененировать, нужно прописать:

RAII и умные указатели

RAII — концепция, согласно которой захват ресурсов объекта происходит в конструкторе, а освобождение происходит в деструкторе. На такой концепции реализованы умные указатели. Умный указатель — объект, который по структуре похож на реализованный класс, но внутри себя хранит не буффер из объектов, а указатель на один объект.

Говоря более простым языком, умный указатель — указатель, у которого можно не писать каждый раз delete, что бывает очень удобно. Рассмотрим типы умных указателей:

std::unique_ptr

Фишка unique_ptr в том, что у него удален констурктор копирования и оператор присваивания, чтобы два указателя не указывали на один и тот же буффер(иначе один из них мог бы удалиться и удалить данные, которые были привязаны ко второму указателю). То есть unique_ptr единлично владеет памятью, на которую он указан.

Но при этом у unique_ptr работает std::move:

Чтобы указывать на один объект из нескольких мест, существует:

std::shared_ptr

Устроен std::shared_ptr следующим образом: помимо данных в указателе хранится переменная use_count, в которой лежит количество использованний данных, которые захвачены std::shared_ptr. Когда std::shared_ptr умирает, он делает –use_count. Когда use_count = 0, освобождается память, которой владел указатель. std::shared_ptr можно копировать только от std::shared_ptr.

Исключения

Допустим у нашего Array есть оператор [] и мы хотим как-то обработать ситуации, когда index выходит за границы. Для таких случаев в C++ есть специальный механизм, для того, чтобы сообщать об подобных ошибках — исключения.

Чтобы ловить исключения, существует конструкция try-catch:

Сначала выполняется блок try. Если все ок и исключения не выбрасываются, он идет дальше, иначе он переходит в блок catch, в котором обрабатывает ошибку и идет дальше. Вместо … можно обрабатывать свои исключения. Например:

Исключения образуют иерархию, то есть различные виды exception наследуются друг от друга. Например std::out_of_range наследуется от std::exception. Если в операторе [] выбрасывать std::out_of_range, то в коде выше это исключение так же будет обрабатываться в блоке catch. Блоков catch может быть несколько, например:

catch блоки перебираются по очереди и выбирается первый подходящий. Если один блок отработал, то другие не обрабатываются. Исключения можно выбрасывать в виде указателей. Если выбрасывать исключения из конструктора, то объект не будет сконструирован и для него не будет зваться деструктор.

Оператор присваивания и конструктор копирования

Хочу разобраться с вызовом конструктора копирования и оператора присваивания в классе.

Правда ли, что здесь вызовется конструктор копирования и только он:

А здесь только оператор присваивания:

Если так, то чем отличается конструктор копирования от оператора присваивания, почему нельзя заменить один другим и зачем в С++ они разделены?

Задача любого конструктора — создать в сырой неинициализированной памяти объект, которого до этого момента еще не существовало. В общем случае, объект, который «видит» конструктор в начале своей работы, содержит непредсказуемый мусор. Задача конструктора — проинициализировать объект, т.е. придать всем его компонентам осмысленные значения.

Оператор присваивания же работает с уже ранее созданным объектом, т.е. с объектом, который уже ранее был полностью сконструирован (каким-то конструктором). Такой объект уже содержит какие-то корректно помещенные в него осмысленные данные. Задача оператора присваивания — заменить эти старые осмысленные данные новыми осмысленными данными, скопированными из объекта-источника (из правой части оператора присваивания).

Это две совершенно разные задачи.

В общем случае, работа оператора присваивания состоит из двух этапов: корректно убрать из объекта старое содержимое, а затем корректно скопировать в него новое содержимое. Работа же конструктора копирования не имеет первого этапа, ибо никакого старого содержимого в этот момент в объекте нет.

То есть упрощенно можно сказать, что конструктор копирования — это «половинка» оператора присваивания.

13.15 – Перегрузка оператора присваивания

Оператор присваивания ( operator= ) используется для копирования значений из одного объекта в другой, уже существующий объект.

Присваивание и конструктор копирования

Назначение конструктора копирования и оператора присваивания почти эквивалентны – оба копируют один объект в другой. Однако конструктор копирования инициализирует новые объекты, тогда как оператор присваивания заменяет содержимое существующих объектов.

Разница между конструктором копирования и оператором присваивания вызывает много путаницы у начинающих программистов, но на самом деле это не так уж и сложно. Обобщим:

  • Если новый объект должен быть создан до того, как может произойти копирование, используется конструктор копирования (примечание: это включает в себя передачу или возврат объектов по значению).
  • Если новый объект не нужно создавать до того, как может произойти копирование, используется оператор присваивания.

Перегрузка оператора присваивания

Перегрузка оператора присваивания ( operator= ) довольно проста, с одной конкретной оговоркой, которую мы рассмотрим. Оператор присваивания должен быть перегружен как функция-член.

Эта программа печатает:

Теперь всё должно быть довольно просто. Наш перегруженный operator= возвращает *this , чтобы мы могли объединить несколько присваиваний в цепочку:

Проблемы из-за самоприсваивания

Здесь всё становится немного интереснее. C++ допускает самоприсваивание:

Этот код вызовет f1.operator=(f1) , и в упрощенной реализации, приведенной выше, все члены будут присвоены сами себе. В этом конкретном примере самоприсваивание приводит к тому, что каждый член присваивается самому себе, что ни на что не влияет, кроме потери времени. В большинстве случаев самоприсваиванию вообще ничего не нужно делать!

Однако в случаях, когда оператору присваивания необходимо динамически присваивать память, самоприсваивание может быть опасным:

Сначала запустите программу как есть. Вы увидите, что программа печатает " Alex ", как и должна.

Теперь запустите следующую программу:

Вероятно, вы получите мусор. Что случилось?

Рассмотрим, что происходит в перегруженном operator= , когда неявный объект и переданный параметр ( str ) являются переменной alex . В этом случае m_data совпадает с str.m_data . Первое, что происходит, это то, что функция проверяет, есть ли уже у неявного объекта строка. Если это так, ее необходимо удалить, чтобы не произошло утечки памяти. В этом случае размещается m_data , поэтому функция удаляет m_data . Но поскольку str совпадает с *this , строка, которую мы хотели скопировать, была удалена, а m_data (и str.m_data ) стали висячими указателями.

Затем мы выделяем новую память для m_data (и str.m_data ). Поэтому, когда мы впоследствии копируем данные из str.m_data в m_data , мы копируем мусор, потому что str.m_data никогда не инициализировалась.

Обнаружение и обработка самоприсваивания

К счастью, мы можем определить, когда происходит самоприсваивание. Вот обновленная реализация нашего перегруженного operator= для класса MyString :

Проверяя, совпадает ли адрес нашего неявного объекта с адресом объекта, переданного в качестве параметра, мы можем заставить наш оператор присваивания немедленно возвращаться без выполнения какой-либо другой работы.

Поскольку это просто сравнение указателей, оно должно быть быстрым и не требует перегрузки operator== .

Когда не обрабатывать самоприсваивание

Обычно проверка на самоприсваивание опускается в конструкторах копирования. Поскольку создаваемый объект для копирования создается заново, единственный случай, когда вновь созданный объект может быть равен копируемому, – это когда вы пытаетесь инициализировать новый определяемый объект самим собой:

В таких случаях ваш компилятор должен предупредить вас, что c – неинициализированная переменная.

Во-вторых, проверка на самоприсваивание может быть опущена в классах, которые могут обрабатывать самоприсваивание естественным образом. Рассмотрим следующий оператор присваивания класса Fraction , который имеет защиту от самоприсваивания:

Если бы защиты от самоприсваивания не было, эта функция всё равно работала бы правильно во время самоприсваивания (потому что все операции, выполняемые функцией, могут правильно обрабатывать самоприсваивание).

Поскольку самоприсваивание – редкое событие, некоторые видные гуру C++ рекомендуют опускать защиту самоприсваивания даже в тех классах, которым это было бы полезно. Мы не рекомендуем этого делать, поскольку считаем, что лучше использовать защитный код, а затем, при необходимости, выборочно оптимизировать его.

Идиома копирования и обмена

Лучший способ справиться с проблемами самоприсваивания – использовать так называемую идиому копирования и обмена. Статья о том, как эта идиома работает, появится чуть позже.

Оператор присваивания по умолчанию

В отличие от других операторов, компилятор предоставит для вашего класса открытый оператор присваивания по умолчанию, если вы не предоставите его сами. Этот оператор присваивания выполняет поэлементное присваивание (что по сути то же самое, что и поэлементная инициализация, которую выполняют конструкторы копирования по умолчанию).

Как и другие конструкторы и операторы, вы можете предотвратить выполнение присваивания, сделав свой оператор присваивания закрытым или используя ключевое слово delete :

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *