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

Обновлено: 19.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 , обрабатывают всё управление своей памятью и имеют перегруженные конструкторы копирования и операторы присваивания, которые выполняют правильное глубокое копирование. Поэтому вместо того, чтобы самостоятельно управлять памятью, вы можете просто инициализировать их и выполнять им присваивание, как обычным переменным базовых типов! Это делает эти классы более простыми в использовании, менее подверженными ошибкам, и вам не нужно тратить время на написание собственных перегруженных функций!

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

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

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

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

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

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

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

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

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

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

4. Пример

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

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

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

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

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

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

  • Конструкторы могут быть объявлены как inline , , explicitfriend или constexpr .
  • Конструктор может инициализировать объект, объявленный как const , volatile или const volatile . Объект становится const после завершения конструктора.
  • Чтобы определить конструктор в файле реализации, присвойте ему полное имя, как и любая другая функция-член: Box::Box() .

Списки инициализаторов элементов

При необходимости конструктор может иметь список инициализаторов элементов, который инициализирует члены класса перед запуском тела конструктора. (Список инициализаторов элементов не совпадает со списком инициализаторов типа std::initializer_list .)

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

Идентификатор должен ссылаться на член класса; он инициализирован со значением аргумента. Аргумент может быть одним из параметров конструктора, вызова функции или . std::initializer_list

const члены и члены ссылочного типа должны быть инициализированы в списке инициализаторов элементов.

Чтобы обеспечить полную инициализацию базовых классов перед запуском производного конструктора, вызовите все параметризованные конструкторы базового класса в списке инициализаторов.

Конструкторы по умолчанию

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

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

Если используется неявный конструктор по умолчанию, обязательно инициализировать элементы в определении класса, как показано в предыдущем примере. Без этих инициализаторов члены будут неинициализированы, а вызов Volume() создаст значение мусора. Как правило, рекомендуется инициализировать элементы таким образом, даже если не используется неявный конструктор по умолчанию.

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

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

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

Это утверждение является примером проблемы "Большинство vexing Parse". Можно интерпретировать myclass md(); как объявление функции или как вызов конструктора по умолчанию. Поскольку средства синтаксического анализа C++ предпочитают объявления по сравнению с другими вещами, выражение рассматривается как объявление функции. Дополнительные сведения см. в разделе "Большинство синтаксического анализа".

Если объявлены какие-либо конструкторы, отличные от по умолчанию, компилятор не предоставляет конструктор по умолчанию:

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

Однако для инициализации массива объектов Box можно использовать набор списков инициализаторов:

Дополнительные сведения см. в разделе "Инициализаторы".

Конструкторы копии

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

Конструктор копирования может иметь одну из следующих сигнатур:

При определении конструктора копирования необходимо также определить оператор присваивания копирования (=). Дополнительные сведения см. в разделе "Назначение " и " Копирование конструкторов" и операторов присваивания копирования.

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

При попытке копирования объекта возникает ошибка C2280: попытка ссылаться на удаленную функцию.

Конструкторы перемещения

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

Компилятор выбирает конструктор перемещения, когда объект инициализируется другим объектом того же типа, если другой объект будет уничтожен и больше не нуждается в его ресурсах. В следующем примере показано одно дело, когда конструктор перемещения выбирается с помощью разрешения перегрузки. В конструкторе, который вызывает get_Box() , возвращаемое значение является xvalue (значение eXpiring). Поэтому он не назначается какой-либо переменной и поэтому выходит за пределы области действия. Чтобы обеспечить мотивацию для этого примера, давайте предоставим Box большой вектор строк, представляющих его содержимое. Вместо копирования вектора и его строк конструктор перемещения "крадет" его из значения "box", чтобы вектор теперь принадлежит новому объекту. Вызов std::move необходим, так как оба vector класса string реализуют собственные конструкторы перемещения.

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

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

Дополнительные сведения о написании конструктора нетривиального перемещения см. в разделе "Конструкторы перемещения" и "Операторы присваивания перемещения" (C++).

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

Конструкторы копирования по умолчанию , конструкторы по умолчанию, конструкторы перемещения, операторы присваивания копирования, операторы присваивания перемещения и деструкторы. Вы можете явно удалить все специальные функции-члены.

Конструкторы constexpr

Конструктор может быть объявлен как constexpr , если

  • он либо объявлен как стандартный, либо удовлетворяет всем условиям для функций constexpr в целом;
  • класс не имеет виртуальных базовых классов;
  • каждый из параметров является литеральным типом;
  • тело не является блоком try-block функции;
  • инициализированы все нестатические члены данных и подобъекты базового класса;
  • Значение , если класс является (a) объединением, имеющим члены варианта, или (б) имеет анонимные объединения, инициализируется только один из членов профсоюза;
  • каждый нестатический член данных типа класса, а все подобъекты базового класса имеют конструктор constexpr.

Конструкторы списков инициализаторов

Затем создайте объекты Box следующим образом:

Явные конструкторы

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

Можно инициализировать Box следующим образом:

Или передать целое значение функции, принимающей объект Box:

В некоторых случаях подобные преобразования могут быть полезны, однако чаще всего они могут привести к незаметным, но серьезным ошибкам в вашем коде. Как правило, необходимо использовать ключевое explicit слово в конструкторе (и определяемых пользователем операторах), чтобы предотвратить такое неявное преобразование типов:

Когда конструктор является явным, эта строка вызывает ошибку компилятора: ShippingOrder so(42, 10.8); . Дополнительные сведения см. в разделе о преобразованиях определяемых пользователем типов.

Порядок строительства

Конструктор выполняет свою работу в следующем порядке.

Вызывает конструкторы базовых классов и членов в порядке объявления.

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

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

Выполняет весь код в теле функции.

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

Выходные данные будут выглядеть следующим образом.

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

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

Если конструктор создает исключение, то удаление выполняется в порядке, обратном созданию.

Отменяется код в теле функции конструктора.

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

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

Производные конструкторы и расширенная инициализация агрегатов

Если конструктор базового класса не является открытым, но доступен для производного класса, нельзя использовать пустые фигурные скобки для инициализации объекта производного типа в /std:c++17 режиме, а затем в Visual Studio 2017 и более поздних версий.

В следующем примере показана соответствующая реакция на событие в C++14:

В C++17 Derived теперь считается агрегатным типом. Это означает, что инициализация Base через закрытый конструктор по умолчанию происходит непосредственно как часть расширенного правила агрегатной инициализации. Ранее частный Base конструктор был вызван через Derived конструктор, и он был успешно выполнен из-за friend объявления.

В следующем примере показано поведение C++17 в Visual Studio 2017 и более поздних версий в /std:c++17 режиме:

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

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

Должны выводиться следующие выходные данные:

Делегирующие конструкторы

Делегирующий конструктор вызывает другой конструктор в том же классе для выполнения некоторых действий по инициализации. Эта функция полезна, если у вас есть несколько конструкторов, которые все должны выполнять аналогичную работу. Основную логику можно написать в одном конструкторе и вызвать из других. В следующем тривиальном примере Box(int) делегирует свою работу Box(int,int,int):

Объект, созданный конструкторами, полностью инициализируется сразу после выполнения любого конструктора. Дополнительные сведения см. в разделе "Делегирование конструкторов".

Наследование конструкторов (C++11)

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

Visual Studio 2017 и более поздних версий: оператор using в /std:c++17 режиме и более поздних версиях преобразует все конструкторы из базового класса, за исключением тех, которые имеют идентичную сигнатуру конструкторам в производном классе. Как правило, рекомендуется использовать наследуемые конструкторы, когда производный класс не объявляет новые члены данных или конструкторы.

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

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

Конструкторы и составные классы

Классы, содержащие члены типа класса, называются составными классами. При создании члена типа класса составного класса конструктор вызывается перед собственным конструктором класса. Если у содержащегося класса нет конструктора по умолчанию, необходимо использовать список инициализации в конструкторе составного класса. В предыдущем примере StorageBox при присвоении типу переменной-члена m_label нового класса Label необходимо вызвать конструктор базового класса и инициализировать переменную m_label в конструкторе StorageBox :

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

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

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