Конструкторы копий и операторы присваивания копий (C++)
Начиная с C++ 11, в языке поддерживаются два вида присваивания: копирование назначения и Перемещение. В этой статье «присваивание» означает «присваивание копированием», если явно не указано другое. Сведения о назначении Move см. в разделе конструкторы Move и операторы присваивания перемещения (C++).
Как при операции назначения, так и при операции инициализации выполняется копирование объектов.
Назначение: когда значение одного объекта присваивается другому объекту, первый объект копируется во второй объект. Поэтому
Инициализация: инициализация происходит при объявлении нового объекта, когда аргументы передаются в функции по значению или когда значения возвращаются из функций по значению.
Можно определить семантику копии объектов типа класса. Рассмотрим для примера такой код:
Приведенный выше код может означать «копировать содержимое файла FILE1. DAT в FILE2. DAT» или это может означать «Ignore FILE2. DAT и создайте b второй обработчик для file1. dat. » Необходимо прикрепить соответствующую семантику копирования к каждому классу, как показано ниже.
С помощью конструктора копии.
Если конструктор копии не объявлен, компилятор создает конструктор копии для каждого члена. Если оператор присваивания копированием не объявлен, компилятор создает оператор присваивания копированием для каждого члена. Объявление конструктора копии не подавляет созданный компилятором оператор присваивания копий, и наоборот. При реализации любого из этих способов рекомендуется также реализовать другой способ, чтобы значение кода было четким.
Конструкторы копии, создаваемые компилятором
Если виртуальные базовые классы инициализируются конструкторами копии, создаются компиляторами или определяются пользователем, они инициализируются только один раз, во время создания.
Дополнительные сведения о перегруженных операторах присваивания см. в разделе назначение.
Урок №141. Конструктор копирования
Обновл. 13 Сен 2021 |
Вспомним все типы инициализации, которые поддерживает язык C++: прямая инициализация, uniform-инициализация и копирующая инициализация.
Конструктор копирования
Рассмотрим примеры всех вышеприведенных инициализаций на практике, используя следующий класс Drob:
Мы можем выполнить прямую инициализацию:
В C++11 мы можем выполнить uniform-инициализацию:
И, наконец, мы можем выполнить копирующую инициализацию:
С прямой инициализацией и uniform-инициализацией создаваемый объект непосредственно инициализируется. Однако с копирующей инициализацией дела обстоят несколько сложнее. Мы рассмотрим это детально на следующем уроке. Но перед этим нам еще нужно кое в чём разобраться.
Рассмотрим следующую программу:
Результат выполнения программы:
Рассмотрим детально, как работает эта программа.
Конструктор копирования — это особый тип конструктора, который используется для создания нового объекта через копирование существующего объекта. И, как в случае с конструктором по умолчанию, если вы не предоставите конструктор копирования для своих классов самостоятельно, то язык C++ создаст public-конструктор копирования автоматически. Поскольку компилятор мало знает о вашем классе, то по умолчанию созданный конструктор копирования будет использовать почленную инициализацию. Почленная инициализация означает, что каждый член объекта-копии инициализируется непосредственно из члена объекта-оригинала. Т.е. в примере, приведенном выше, dCopy.m_numerator будет иметь значение sixSeven.m_numerator ( 6 ), а dCopy.m_denominator будет равен sixSeven.m_ denominator ( 7 ).
Так же, как мы можем явно определить конструктор по умолчанию, так же мы можем явно определить и конструктор копирования. Конструктор копирования выглядит следующим образом:
Урок №145. Поверхностное и глубокое копирование
Обновл. 13 Сен 2021 |
На этом уроке мы рассмотрим поверхностное и глубокое копирование в языке C++.
Поверхностное копирование
Поскольку язык C++ не может знать наперед всё о вашем классе, то конструктор копирования и оператор присваивания, которые C++ предоставляет по умолчанию, используют почленный метод копирования — поверхностное копирование. Это означает, что C++ выполняет копирование для каждого члена класса индивидуально (используя оператор присваивания по умолчанию вместо перегрузки оператора присваивания и прямую инициализацию вместо конструктора копирования). Когда классы простые (например, в них нет членов с динамически выделенной памятью), то никаких проблем с этим не должно возникать.
Рассмотрим следующий класс Drob:
Конструктор копирования и оператор присваивания по умолчанию, предоставляемые компилятором автоматически, выглядят примерно следующим образом:
Поскольку эти конструктор копирования и оператор присваивания по умолчанию отлично подходят для выполнения копирования с объектами этого класса, то действительно нет никакого смысла писать здесь свои собственные версии конструктора копирования и перегрузки оператора.
Однако при работе с классами, в которых динамически выделяется память, почленное (поверхностное) копирование может вызывать проблемы! Это связано с тем, что при поверхностном копировании указателя копируется только адрес указателя — никаких действий по содержимому адреса указателя не предпринимается. Например:
Вышеприведенный класс — это обычный строковый класс, в котором выделяется память для хранения передаваемой строки. Здесь мы не определяли конструктор копирования или перегрузку оператора присваивания. Следовательно, язык C++ предоставит конструктор копирования и оператор присваивания по умолчанию, которые будут выполнять поверхностное копирование. Конструктор копирования выглядит примерно следующим образом:
Хотя этот код выглядит достаточно безвредным, но он имеет в себе коварную проблему, которая приведет к сбою программы! Можете найти эту проблему? Если нет, то ничего страшного.
Разберем этот код по строкам:
Корнем этой проблемы является поверхностное копирование, выполняемое конструктором копирования по умолчанию. Такое копирование почти всегда приводит к проблемам.
Глубокое копирование
Одним из решений этой проблемы является выполнение глубокого копирования. При глубоком копировании память сначала выделяется для копирования адреса, который содержит исходный указатель, а затем для копирования фактического значения. Таким образом копия находится в отдельной, от исходного значения, памяти и они никак не влияют друг на друга. Для выполнения глубокого копирования нам необходимо написать свой собственный конструктор копирования и перегрузку оператора присваивания.
Рассмотрим это на примере с классом SomeString:
Как вы видите, реализация здесь более углубленная, нежели при поверхностном копировании! Во-первых, мы должны проверить, имеет ли исходный объект ненулевое значение вообще (строка №8). Если имеет, то мы выделяем достаточно памяти для хранения копии этого значения (строка №11). Наконец, копируем значение-строку (строки №14-15).
Теперь рассмотрим перегрузку оператора присваивания:
Заметили, что код перегрузки очень похож на код конструктора копирования? Но здесь есть 3 основных отличия:
Мы добавили проверку на самоприсваивание.
Мы возвращаем текущий объект (с помощью указателя *this), чтобы иметь возможность выполнить цепочку операций присваивания.
Мы явно удаляем любое значение, которое объект уже хранит (чтобы не произошло утечки памяти).
При вызове перегруженного оператора присваивания, объект, которому присваивается другой объект, может содержать предыдущее значение, которое нам необходимо очистить/удалить, прежде чем мы выделим память для нового значения. С не динамически выделенными переменными (которые имеют фиксированный размер) нам не нужно беспокоиться, поскольку новое значение просто перезапишет старое. Однако с динамически выделенными переменными нам нужно явно освободить любую старую память до того, как мы выделим любую новую память. Если мы этого не сделаем, сбоя не будет, но произойдет утечка памяти, которая будет съедать нашу свободную память каждый раз, когда мы будем выполнять операцию присваивания!
Лучшее решение
В Стандартной библиотеке C++ классы, которые работают с динамически выделенной памятью, такие как std::string и std::vector, имеют свое собственное управление памятью и свои конструкторы копирования и перегрузку операторов присваивания, которые выполняют корректное глубокое копирование. Поэтому, вместо написания своих собственных конструкторов копирования и перегрузки оператора присваивания, вы можете выполнять инициализацию или присваивание строк, или векторов, как обычных переменных фундаментальных типов данных! Это гораздо проще, менее подвержено ошибкам, и вам не нужно тратить время на написание лишнего кода!
Заключение
Конструктор копирования и оператор присваивания, предоставляемые по умолчанию языком C++, выполняют поверхностное копирование, что отлично подходит для классов без динамически выделенных членов.
Классы с динамически выделенными членами должны иметь конструктор копирования и перегрузку оператора присваивания, которые выполняют глубокое копирование.
Используйте функциональность классов из Стандартной библиотеки C++, нежели самостоятельно выполняйте/реализовывайте управление памятью.
Поделиться в социальных сетях:
Что такое конструктор копирования c
1. Конструктор копирования
Конструктор копирования, в отличии от других, в качестве параметра принимает константную ссылку на объект класса.
Данный конструктор вызывается всякий раз, когда создаётся новый объект и для его инициализации берётся значение существующего объекта того же типа. Например, в следующих случаях:
Также конструктор копирования вызывается при передаче объекта в функцию или возврате из неё по значению. Аналогично, с помощью конструктора копирования создаются временные объекты при вычислении арифметических и других операций.
В чём же проблема отсутствия конструктора копирования при выделении в классе динамической памяти? Дело в том, что при отсутствии явного описания, он описывается неявно. Неявный конструктор выполняет поверхностное копирование, т. е. просто дублирует биты из переменных. Таким образом, вместо данных из динамической памяти, копируется адреса на них. В результате, появляется несколько объектов, указывающих на одну область памяти. При изменении этой области через один объект, она также изменится и в другом, что в большинстве случаев является нежелательным поведением. Поэтому в классах, работающих с динамической памятью, необходимо всегда явно объявлять конструктор копирования (см. пример в конце). Как вариант исключения данной проблемы, можно поместить конструктор копирования в приватной области класса, что вовсе запретит выполнять копирование.
2. Перегруженная операция присваивания
Перегруженная операция присваивания используется при присваивании одного объекта другому существующему объекту. Здесь присутствует такая же проблема, что и в конструкторе копирования. К тому же, у объекта, которому присваивается значение, уже может быть выделена динамическая память. Перед присваиванием новых данных, выделенную ранее память необходимо очистить, чтобы не допустить её утечки (см. пример в конце). Также необходимо обработать случай самоприсваивания. В противном случае, данные в динамической памяти просто будут утеряны. Аналогично копированию, присваивание также можно запретить, поместив операцию в приватной области класса.
3. Деструктор
Деструктор вызывается перед удалением объекта и предназначен для освобождения всех используемых ресурсов. Чтобы не допустить утечки памяти, в деструкторе необходимо её очистить.
4. Пример
Стоить отметить, что во всех трёх функциях память должна выделяться и удаляться одинаковым образом. Т. е. нельзя в одном случае использовать delete, а в другом delete[].
BestProg
С++. Конструктор копирования. Примеры использования. Передача объекта класса в функцию. Возврат класса из функции
В данной теме рассмотрена работа конструктора копирования на примере unmanaged ( native ) классов. При рассмотрении данной темы рекомендуется прочитать тему:
Содержание
Поиск на других ресурсах:
1. Какое назначение конструктора копирования ( copy constructor )?
Конструктор копирования – это специальный конструктор, который позволяет получить идентичный к заданному объект. То есть, с помощью конструктора копирования можно получить копию уже существующего объекта. Конструктор копирования еще называется инициализатором копии ( copy initializer ). Конструктор копирования должен получать входным параметром константную ссылку ( & ) на объект такого же класса.
⇑
2. В каких случаях вызывается конструктор копирования?
Конструктор копирования вызывается в случаях, когда нужно получить полную копию объекта. В C++ полная копия объекта нужна в трех случаях.
Случай 2. Когда нужно передать объект в функцию как параметр-значение. В этом случае создается полная копия объекта.
Случай 3. Когда нужно вернуть объект из функции по значению. В этом случае также создается полная копия объекта.
⇑
3. В каких случаях целесообразно использовать конструктор копирования?
Если в классе нету динамического выделения памяти для данных, то конструктор копирования можно не использовать. В этом случае побитового копирования (по умолчанию) достаточно для корректной работы класса. Исключение, если при инициализации объекта другим объектом нужно установить некоторые специальные условия копирования.
⇑
4. Пример объявления конструктора копирования в классе, где нет динамического выделения памяти
Данный пример носит демонстрационный характер. Для класса, где нет динамического выделения памяти использовать конструктор копирования необязательно.
Демонстрация использования конструктора копирования в некотором программном коде (методе)
⇑
5. Пример передачи объекта класса в функцию как параметр-значение
Использование функции в другом программном коде
⇑
6. Пример возврата объекта класса из функции по значению с помощью конструктора копирования
Объявление класса точно такое же как в п. 4.
Демонстрация использования функций
⇑
7. Как осуществляется копирование объектов, когда в классе отсутствует конструктор копирования?
Если в классе не объявлен конструктор копирования, то используется конструктор копирования, который автоматически генерируется компилятором. Этот конструктор копирования реализует побитовое копирование для получения копии объекта.
Побитовое копирование есть приемлемым для классов, в которых нет динамического выделения памяти. Однако, если в классе есть динамическое выделение памяти (класс использует указатели), то побитовое копирование приведет к тому, что указатели обоих объектов будут указывать на один и тот же участок памяти. А это ошибка.