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

Обновлено: 27.03.2024

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Начиная с 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 так, назначение завершается успешно.

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

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

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

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

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

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

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

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

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

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

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

4. Пример

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

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

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

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