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

Обновлено: 27.04.2024

Поскольку C++ мало что знает о вашем классе, конструктор копирования по умолчанию и операторы присваивания по умолчанию, которые он предоставляет, используют метод копирования, известный как поэлементное копирование (также известная как поверхностное копирование). Это означает, что C++ копирует каждый член класса отдельно (используя оператор присваивания для перегруженного operator= и прямую инициализацию для конструктора копирования). Когда классы просты (например, не содержат динамически выделяемой памяти), это работает очень хорошо.

Например, давайте взглянем на наш класс Fraction :

Конструктор копирования и оператор присваивания, предоставленные компилятором для этого класса по умолчанию, выглядят примерно так:

Обратите внимание: поскольку эти версии по умолчанию отлично подходят для копирования данного класса, в этом случае действительно нет причин писать наши собственные версии данных функций.

Однако при разработке классов, которые обрабатывают динамически выделяемую память, поэлементное (поверхностное) копирование может доставить нам массу неприятностей! Это связано с тем, что поверхностное копирование указателя просто копируют адрес указателя – оно не выделяет память и не копирует содержимое, на которое указывает указатель!

Давайте рассмотрим это на примере:

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

Обратите внимание, что m_data – это просто поверхностная копия указателя source.m_data , то есть теперь они оба указывают на одно и то же.

Теперь рассмотрим следующий фрагмент кода:

Хотя этот код выглядит достаточно безобидным, он содержит коварную проблему, которая приведет к сбою программы! Можете ее заметить? Не волнуйтесь, если не можете, она довольно незаметна.

Давайте разберем этот пример построчно:

Эта строка достаточно безобидна. Она вызывает конструктор MyString , который выделяет некоторую память, устанавливает hello.m_data , чтобы указать на нее, а затем копирует в нее строку " Hello, world! ".

Эта строка тоже кажется достаточно безобидной, но на самом деле она является источником нашей проблемы! Когда эта строка вычисляется, C++ будет использовать конструктор копирования по умолчанию (потому что мы не предоставили свой собственный). Этот конструктор копирования будет выполнять поверхностное копирование, инициализируя copy.m_data тем же адресом, что и hello.m_data . В результате copy.m_data и hello.m_data теперь указывают на один и тот же участок памяти!

Когда copy выходит за пределы области видимости, для нее вызывается деструктор MyString . Деструктор удаляет динамически выделенную память, на которую указывают copy.m_data и hello.m_data ! Следовательно, удалив copy , мы также (непреднамеренно) повлияли на hello . Переменная copy затем уничтожается, но hello.m_data остается, указывая на удаленную (недействительную) память!

Теперь вы можете понять, почему эта программа имеет неопределенное поведение. Мы удалили строку, на которую указывал hello , и теперь пытаемся вывести значение в памяти, которая больше не выделена.

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

Глубокое копирование

Одно из решений этой проблемы – выполнить глубокое копирование любых копируемых ненулевых указателей. Глубокое копирование выделяет память для копии, а затем копирует фактическое значение, поэтому копия живет в отдельной памяти от источника. Таким образом, копия и источник различаются и никоим образом не влияют друг на друга. Выполнение глубокого копирования требует, чтобы мы написали наши собственные конструкторы копирования и перегруженные операторы присваивания.

Давайте продолжим и покажем, как это делается для нашего класса MyString :

Как видите, это немного сложнее, чем простое поверхностное копирование! Во-первых, мы даже должны проверить, есть ли в источнике строка (строка 14). Если это так, то мы выделяем достаточно памяти для хранения копии этой строки (строка 17). И в конце нам нужно вручную скопировать строку (строки 20 и 21).

Теперь займемся перегруженным оператором присваивания. Перегруженный оператор присваивания немного сложнее:

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

  • мы добавили проверку на самоприсваивание;
  • мы возвращаем *this , чтобы можно было добавить оператор присваивания в цепочку;
  • нам нужно явно освободить любое значение, которое уже содержится в строке (чтобы не было утечки памяти, когда m_data заново размещается позже).

Когда вызывается перегруженный оператор присваивания, элемент, которому выполняется присваивание, может уже содержать предыдущее значение, которое необходимо очистить перед присваиванием памяти для новых значений. Для нединамически назначаемых переменных (которые имеют фиксированный размер) нам не нужно об этом беспокоиться, потому что новое значение просто перезаписывает старое. Однако для динамически размещаемых переменных нам необходимо явно освободить любую старую память, прежде чем выделять новую. Если мы этого не сделаем, код не завершится со сбоем, но у нас будет утечка памяти, которая будет съедать нашу свободную память каждый раз, когда мы выполняем присваивание!

Лучшее решение

Классы в стандартной библиотеке, которые имеют дело с динамической памятью, например std::string и std::vector , обрабатывают всё управление своей памятью и имеют перегруженные конструкторы копирования и операторы присваивания, которые выполняют правильное глубокое копирование. Поэтому вместо того, чтобы самостоятельно управлять памятью, вы можете просто инициализировать их и выполнять им присваивание, как обычным переменным базовых типов! Это делает эти классы более простыми в использовании, менее подверженными ошибкам, и вам не нужно тратить время на написание собственных перегруженных функций!

have pretty much the same code, the same parameter, and only differ on the return, is it possible to have a common function for them both to use?

". have pretty much the same code. "? Hmm. You must be doing something wrong. Try to minimize the need to use user-defined functions for this and let the compiler do all the dirty work. This often means encapsulating resources in their own member object. You could show us some code. Maybe we have some good design suggestions.

3 Answers 3

You can now choose to sort by Trending, which boosts votes that have happened recently, helping to surface more up-to-date answers.

Trending is based off of the highest score sort and falls back to it if no posts are trending.

Yes. There are two common options. One - which is generally discouraged - is to call the operator= from the copy constructor explicitly:

However, providing a good operator= is a challenge when it comes to dealing with the old state and issues arising from self assignment. Also, all members and bases get default initialized first even if they are to be assigned to from other . This may not even be valid for all members and bases and even where it is valid it is semantically redundant and may be practically expensive.

An increasingly popular solution is to implement operator= using the copy constructor and a swap method.

A swap function is typically simple to write as it just swaps the ownership of the internals and doesn't have to clean up existing state or allocate new resources.

Advantages of the copy and swap idiom is that it is automatically self-assignment safe and - providing that the swap operation is no-throw - is also strongly exception safe.

To be strongly exception safe, a 'hand' written assignment operator typically has to allocate a copy of the new resources before de-allocating the assignee's old resources so that if an exception occurs allocating the new resources, the old state can still be returned to. All this comes for free with copy-and-swap but is typically more complex, and hence error prone, to do from scratch.

The one thing to be careful of is to make sure that the swap method is a true swap, and not the default std::swap which uses the copy constructor and assignment operator itself.

Typically a memberwise swap is used. std::swap works and is 'no-throw' guaranteed with all basic types and pointer types. Most smart pointers can also be swapped with a no-throw guarantee.

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

1. Конструктор копирования

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

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

Также конструктор копирования вызывается при передаче объекта в функцию или возврате из неё по значению. Аналогично, с помощью конструктора копирования создаются временные объекты при вычислении арифметических и других операций.

В чём же проблема отсутствия конструктора копирования при выделении в классе динамической памяти? Дело в том, что при отсутствии явного описания, он описывается неявно. Неявный конструктор выполняет поверхностное копирование, т. е. просто дублирует биты из переменных. Таким образом, вместо данных из динамической памяти, копируется адреса на них. В результате, появляется несколько объектов, указывающих на одну область памяти. При изменении этой области через один объект, она также изменится и в другом, что в большинстве случаев является нежелательным поведением. Поэтому в классах, работающих с динамической памятью, необходимо всегда явно объявлять конструктор копирования (см. пример в конце). Как вариант исключения данной проблемы, можно поместить конструктор копирования в приватной области класса, что вовсе запретит выполнять копирование.

2. Перегруженная операция присваивания

Перегруженная операция присваивания используется при присваивании одного объекта другому существующему объекту. Здесь присутствует такая же проблема, что и в конструкторе копирования. К тому же, у объекта, которому присваивается значение, уже может быть выделена динамическая память. Перед присваиванием новых данных, выделенную ранее память необходимо очистить, чтобы не допустить её утечки (см. пример в конце). Также необходимо обработать случай самоприсваивания. В противном случае, данные в динамической памяти просто будут утеряны. Аналогично копированию, присваивание также можно запретить, поместив операцию в приватной области класса.

3. Деструктор

Деструктор вызывается перед удалением объекта и предназначен для освобождения всех используемых ресурсов. Чтобы не допустить утечки памяти, в деструкторе необходимо её очистить.

4. Пример

Стоить отметить, что во всех трёх функциях память должна выделяться и удаляться одинаковым образом. Т. е. нельзя в одном случае использовать delete, а в другом delete[].

Всем привет, сразу к делу. После прочтения 11 главы Лафоре столкнулся с такой бедой как понятие перегрузка оператора присвоения. Дело в том что Лафоре говорит что именно оператор присвоения является уникальным и не наследуется. Но когда создаёшь указатель на базовый класс там естественно делаешь все методы виртуальными (для полиморфизма), при раз адресации указателя происходит вызов перегруженного оператора присвоения базового класса а не производного который по факту там находиться!! Как же так? Естественно что я перегрузил эти конструкторы и в производных классах. Код ниже подскажите как мне быть?? Как вызвать нужный мне перегруженный оператор?
Базовый класс

Использование инструметрария:
Идей заключается в следующем создается в динамической памяти массив указателей на базовый класс.
И процессе работы программы на уровне пользователя решается какой класс будет создан вот кусок кода

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

Вот в этой строке 31 и 38 почему то вызывается оператор присвоения базового класса а не соответствующего производного

Как мне разрешить эту ситуацию?

Добавлено через 1 час 11 минут
Если кому интересно, то я решил эту проблему вот так:
для класса "тип"

Теперь всё работает как надо т.е. вызывается перегруженный оператор присваения именно того класса на который указывает указатель в данный момент.
НО возник вопрос-почему так? Неужели если работаешь с указателями при полиморфизме необходимо постоянно вручную преобразовывать указатели? И почему казалось бы альтернативный способ не приносит тех же результатов

Это всё очень странно и не понятно, да и самое удивительно то что перед операцией присвоения я проверял с помощью функции typeid какому классу принадлежит созданный объект. Так вот всё нормально
функция показывала что в памяти содержиться тот или иной но нужный в конкретной ситуации объект, но всеравно продолжал вызываться перегруженный метод присвоения базового класса.
Может кто нибудь объяснить? Ни у Лафоре ни у Павловской я этого не нашёл

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

Вот примеры всех их с использованием нашего класса Fraction :

Мы можем выполнить прямую инициализацию:

В C++11 мы можем выполнить унифицированную инициализацию:

И наконец, мы можем выполнить копирующую инициализацию:

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

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

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

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

Давайте подробнее рассмотрим, как она работает.

Инициализация переменной fiveThirds – это просто стандартная прямая инициализация, которая вызывает конструктор Fraction(int, int) . Никаких сюрпризов. А как насчет следующей строки? Инициализация переменной fCopy также явно является прямой инициализацией, и вы знаете, что функции-конструкторы используются для инициализации классов. Итак, какой конструктор вызывает эта строка?

Ответ заключается в том, что эта строка вызывает конструктор копирования Fraction . Конструктор копирования – это особый тип конструктора, используемый для создания нового объекта как копии существующего объекта. И так же, как конструктор по умолчанию, если вы не предоставляете конструктор копирования для своих классов, C++ создаст для вас открытый (public) конструктор копирования. Поскольку компилятор мало что знает о вашем классе, созданный по умолчанию конструктор копирования использует метод инициализации, называемый поэлементной инициализацией. Поэлементная инициализация просто означает, что каждый член копии инициализируется напрямую членом копируемого класса. В приведенном выше примере fCopy.m_numerator будет инициализирован из fiveThirds.m_numerator и т.д.

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

При запуске этой программы вы получите:

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

В отличие от конструкторов по умолчанию, конструктор копирования по умолчанию можно использовать, если он соответствует вашим потребностям.

Предотвращение копирования

Мы можем предотвратить создание копий объектов наших классов, сделав конструктор копирования закрытым:

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

Конструктор копирования может быть опущен

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

Рассмотрим, как работает эта программа. Сначала мы напрямую инициализируем анонимный объект Fraction , используя конструктор Fraction(int, int) . Затем мы используем этот анонимный объект Fraction в качестве инициализатора для Fraction fiveThirds . Поскольку анонимный объект является Fraction , как и fiveThirds , это должно вызывать конструктор копирования, верно?

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

Но на самом деле у вас больше шансов получить такой результат:

Почему не был вызван наш конструктор копирования?

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

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

Итак, хотя вы написали:

Компилятор может изменить это на:

для чего требуется только один вызов конструктора ( Fraction(int, int) ). Обратите внимание, что в случаях, когда используется исключение, любые инструкции в теле конструктора копирования не выполняются, даже если они могут вызывать побочные эффекты (например, печать на экране)!

До C++17 исключение копирования – это оптимизация, которую может сделать компилятор. Начиная с C++17, некоторые случаи исключения копирования (включая приведенный выше пример) стали обязательными.

Наконец, обратите внимание, что если вы сделаете конструктор копирования закрытым, любая инициализация, которая будет использовать конструктор копирования, вызовет ошибку компиляции, даже если конструктор копирования опущен!

Читайте также: