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

Обновлено: 26.04.2024

Поскольку мы собираемся много говорить об инициализации в следующих нескольких уроках, давайте сначала вспомним типы инициализации, которые поддерживает 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, некоторые случаи исключения копирования (включая приведенный выше пример) стали обязательными.

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

Начиная с C++11, в языке поддерживаются два типа присваивания: назначение копирования и перемещение. В этой статье "присваивание" означает "присваивание копированием", если явно не указано другое. Сведения о назначении перемещения см. в разделе "Конструкторы перемещения" и "Операторы присваивания перемещения" (C++).

Как при операции назначения, так и при операции инициализации выполняется копирование объектов.

Назначение: когда одному объекту присваивается значение другого объекта, первый объект копируется во второй объект. Таким образом, этот код копирует значение b в a :

Инициализация: инициализация происходит при объявлении нового объекта, при передаче аргументов функции по значению или при возвращении значения из функции.

Можно определить семантику копии объектов типа класса. Рассмотрим для примера такой код:

Приведенный выше код может означать копирование содержимого ФАЙЛА 1. DAT в FILE2. DAT или это может означать "игнорировать FILE2". DAT и сделайте b второй дескриптор в FILE1.DAT". Необходимо присоединить соответствующую семантику копирования к каждому классу следующим образом:

Используйте оператор operator= присваивания, который возвращает ссылку на тип класса и принимает один параметр, передаваемый по const ссылке, например ClassName& operator=(const ClassName& x); .

Используйте конструктор копирования.

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

Конструктор копирования принимает аргумент типа ClassName& , где ClassName — имя класса. Пример:

По возможности сделайте тип аргумента const ClassName& конструктора копирования. Это предотвращает случайное изменение скопированного объекта конструктором копирования. Он также позволяет копировать из const объектов.

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

Конструкторы копирования, созданные компилятором, такие как пользовательские конструкторы копирования, имеют один аргумент типа "ссылка на имя класса". Исключением является то, что все базовые классы и классы-члены имеют конструкторы копирования, объявленные как принимающие один аргумент const типа class-name&. В таком случае аргумент конструктора копирования, созданного компилятором, также const является .

Если тип аргумента конструктору копирования не const является, инициализация путем копирования const объекта приводит к ошибке. Обратный аргумент не имеет значения true: если аргумент имеет значение const , можно инициализировать, скопировав объект, который не const является.

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

Если виртуальные базовые классы инициализированы конструкторами копирования( созданными компилятором или определяемыми пользователем), они инициализируются только один раз: в момент создания.

Последствия аналогичны конструктору копирования. Если тип аргумента не const является, назначение из const объекта приводит к ошибке. Обратный аргумент не имеет значения: если const значение присвоено значению, которое не const так, назначение завершается успешно.

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

Начиная с C++11, в языке поддерживаются два типа присваивания: назначение копирования и перемещение. В этой статье "присваивание" означает "присваивание копированием", если явно не указано другое. Сведения о назначении перемещения см. в разделе "Конструкторы перемещения" и "Операторы присваивания перемещения" (C++).

Как при операции назначения, так и при операции инициализации выполняется копирование объектов.

Назначение: когда одному объекту присваивается значение другого объекта, первый объект копируется во второй объект. Таким образом, этот код копирует значение b в a :

Инициализация: инициализация происходит при объявлении нового объекта, при передаче аргументов функции по значению или при возвращении значения из функции.

Можно определить семантику копии объектов типа класса. Рассмотрим для примера такой код:

Приведенный выше код может означать копирование содержимого ФАЙЛА 1. DAT в FILE2. DAT или это может означать "игнорировать FILE2". DAT и сделайте b второй дескриптор в FILE1.DAT". Необходимо присоединить соответствующую семантику копирования к каждому классу следующим образом:

Используйте оператор operator= присваивания, который возвращает ссылку на тип класса и принимает один параметр, передаваемый по const ссылке, например ClassName& operator=(const ClassName& x); .

Используйте конструктор копирования.

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

Конструктор копирования принимает аргумент типа ClassName& , где ClassName — имя класса. Пример:

По возможности сделайте тип аргумента const ClassName& конструктора копирования. Это предотвращает случайное изменение скопированного объекта конструктором копирования. Он также позволяет копировать из const объектов.

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

Конструкторы копирования, созданные компилятором, такие как пользовательские конструкторы копирования, имеют один аргумент типа "ссылка на имя класса". Исключением является то, что все базовые классы и классы-члены имеют конструкторы копирования, объявленные как принимающие один аргумент const типа class-name&. В таком случае аргумент конструктора копирования, созданного компилятором, также const является .

Если тип аргумента конструктору копирования не const является, инициализация путем копирования const объекта приводит к ошибке. Обратный аргумент не имеет значения true: если аргумент имеет значение const , можно инициализировать, скопировав объект, который не const является.

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

Если виртуальные базовые классы инициализированы конструкторами копирования( созданными компилятором или определяемыми пользователем), они инициализируются только один раз: в момент создания.

Последствия аналогичны конструктору копирования. Если тип аргумента не const является, назначение из const объекта приводит к ошибке. Обратный аргумент не имеет значения: если const значение присвоено значению, которое не const так, назначение завершается успешно.

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

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

Конструктор копирования необходим для того, чтобы мы могли создавать “реальные” (а не побитовые) копии для объектов класса. Такая копия объекта может понадобиться в следующих случаях:

  • при передаче объекта класса в функцию, как параметра по значению (а не по ссылке);
  • при возвращении из функции объекта класса, как результата её работы;
  • при инициализации одного объекта класса другим объектом этого класса.

При передаче объекта в функцию как параметра по значению, эта функция начнет работать с его побитовой копией, а не с полями самого объекта. Допустим определены конструктор и деструктор класса. Первый память выделяет, а второй её освобождает. Во время работы функции, указатель побитовой копии объекта указывает на адрес памяти, где расположен оригинальный объект.

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

Использование конструктора копирования – прекрасный способ обойти эти ошибки и проблемы. Он создаст “реальную” копию объекта, которая будет иметь личную область динамической памяти.

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

Ниже разберём несложный, но очень показательный пример. В нём будут рассмотрены все 3 случая в которых желательно применять конструктор копирования. Будет создан класс, содержащий конструктор без параметров, конструктор копирования и деструктор.

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

Конструктор без параметров будет вызываться во время создания новых объектов класса. Конструктор копирования – во время создания копий объекта. Деструктор срабатывает при удалении и реального объекта и его копии. В теле функций все описано подробно и не требует дополнительных комментариев.

Запустив программу увидим в консоли следующее:

конструктор копирования в с++, конструктор копии c++, программирование на с++ с нуля

Посмотрим что программа выдала в консоль. Блок 1 – во время создания нового объекта, сработал конструктор без параметров. В блоке 2 мы разместили функцию showFunc() . Во время передачи в неё “объекта-параметра” по значению, сработал конструктор копирования и создалась “реальная” копия объекта класса OneClass .

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

В блоке 3 размещена функция returnObjectFunc() . Так как в её теле прописано создание нового объекта класса OneClass – сначала сработал конструктор без параметров. Далее выполняется код функции и во время возврата объекта в главную функцию main , сработал конструктор копирования. В конце, как и должно быть, деструктор отработал дважды: для объекта и для его реальной копии.

В четвертом блоке, во время объявления и инициализации нового объекта object2 , сработал конструктор копирования. При завершении работы программы деструктор сработал для копии объекта из четвертого блока и для объекта object1 из первого блока.

Если же мы закомментируем /*конструктор копирования*/ в классе и снова запустим программу – увидим, что конструктор без параметров сработает 2 раза, а деструктор – пять раз отработает.

конструктор копирования в с++, конструктор копии c++, программирование на с++ с нуля

В этой ситуации, если бы деструктор освобождал память — в программе возникла бы ошибка.

Очень рекомендую прочесть тему Конструктор копирования в книге Стивена Прата “Язык программирования С++. Лекции и упражнения. 6-е издание.” Она раскрыта намного глубже и включает все основные нюансы использования конструктора копирования. Подробно рассмотрена операция присваивания = .

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

🛠 Побитовое и почленное копирование в C++

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

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

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

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

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

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

Чтобы продемонстрировать, как работает неглубокое копирование, давайте взглянем на простой класс прямоугольника:

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

Результат main

Функция main Результат main

Внесенные в rect1 изменения не отражаются на rect2 . Чтобы увидеть проблемы неглубокого копирования, изменим класс Rectangle так, чтобы он содержал указатели:

Измененный класс Rectangle

Выполнение той же функции main выдает другой результат:

Новый результат main

Новый результат main

При изменении rect1 изменилось и содержимое rect2 . Состояние переменных можно выразить с помощью следующей диаграммы:

<span></p>
<p><span><span>Диаграмма, иллюстрирующая поверхностное копирование</span></span></span>
Диаграмма, иллюстрирующая поверхностное копирование

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

Глубокое копирование ещё называют почленным. Чтобы реализовать его для нашего класса, нужно более подробно изучить конструктор копирования и оператор присваивания.

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

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

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

Напомним, что автоматически созданный конструктор копирования выполняет неглубокое копирование. Допустим, класс называется ClassName и имеет поля m 1 , m 2 , m 3 , …, mN . Тогда определение созданного компилятором конструктора выглядит следующим образом:

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

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

Стоит отметить, что после удаления конструктора копирования мы не можем больше передавать объекты по значению. Если объект возвращается из функции, конструктор копирования также может быть вызван, хотя компилятор может использовать оптимизацию возвращаемого значения (RVO), чтобы избежать ненужного копирования.

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

Оператор присваивания – это метод, который используется для выполнения присваивания. Как и в случае с конструктором копирования, C++ предоставляет оператор присваивания по умолчанию.

Пример объявления оператора присваивания:

Оператор присваивания и конструктор копирования реализованы аналогично, хотя есть некоторые заметные различия. Во-первых, мы видим, что оператор присваивания возвращает ссылку на экземпляр, потому что в C ++ разрешены объединенные в цепочку присваивания:

В приведённом выше примере оператор присваивания вызывается для rectangle2 с rectangle3 в качестве аргумента. Затем оператор для rectangle1 вызывается со ссылкой, возвращенной из предыдущего вызова в качестве аргумента. Также необходимо учитывать возможность самоприсваивания:

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

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

  • оператор присваивания;
  • конструктор копирования;
  • деструктор.

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

Реализация глубокого копирования

Ознакомившись с конструктором копирования и оператором присваивания, мы готовы реализовать глубокое копирование для класса Rectangle .

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

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

Новый результат main Диаграмма, иллюстрирующая глубокое копирование" />
Диаграмма, иллюстрирующая глубокое копирование

Выводы

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

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