Урок №141. Конструктор копирования

  Юрий  | 

  |

  Обновл. 24 Янв 2022  | 

 121669

 ǀ   11 

Вспомним все типы инициализации, которые поддерживает язык C++: прямая инициализация, uniform-инициализация и копирующая инициализация.

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

Рассмотрим примеры всех вышеприведенных инициализаций на практике, используя следующий класс Drob:

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

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

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

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

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

Результат выполнения программы:

6/7

Рассмотрим детально, как работает эта программа.

С объектом sixSeven выполняется обычная прямая инициализация, которая приводит к вызову конструктора Drob(int, int). Здесь нет никаких сюрпризов. А вот инициализация объекта dCopy также является прямой инициализацией, но какой конструктор вызывается здесь? Ответ: конструктор копирования.

Конструктор копирования — это особый тип конструктора, который используется для создания нового объекта через копирование существующего объекта. И, как в случае с конструктором по умолчанию, если вы не предоставите конструктор копирования для своих классов самостоятельно, то язык C++ создаст public-конструктор копирования автоматически. Поскольку компилятор мало знает о вашем классе, то по умолчанию созданный конструктор копирования будет использовать почленную инициализацию. Почленная инициализация означает, что каждый член объекта-копии инициализируется непосредственно из члена объекта-оригинала. Т.е. в примере, приведенном выше, dCopy.m_numerator будет иметь значение sixSeven.m_numerator (6), а dCopy.m_denominator будет равен sixSeven.m_ denominator (7).

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



Результат выполнения программы:

Copy constructor worked here!
6/7

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

Предотвращение создания копий объектов


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

Здесь мы получим ошибку компиляции, так как dCopy должен использовать конструктор копирования, но он не видит его, поскольку конструктор копирования является закрытым.

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

Рассмотрим следующий код:

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

Запустите эту программу самостоятельно. Ожидаемый результат:

Copy constructor worked here!
6/7

Реальный результат:

6/7

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

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

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

Поэтому, даже если вы напишите:

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

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

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


Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (261 оценок, среднее: 4,87 из 5)
Загрузка...

Комментариев: 11

  1. Kayfor:

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

  2. Алексей:

    В статье не хватает примера, когда без констуктора копирования обойтись невозможно, потому что код породит ошибку.

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

    пусть есть функции определения расстояния между точками в виде:

    ну и функция вывода координат точек:

    теперь использую класс:

    и хочу воспользоваться функцией вычисления расстояния, вызывая её так:

    1. Валерий:

      Запишите в список благодарящих и меня.
      Но…. Добавлю немного критики.
      Если открыта возможность комментировать и комментарии проходят модерацию, хотелось бы видеть комментарии хотя бы не содержащие явных ошибок. На приведенный выше код компилятор матерится. Отсутствующие ;
      Запятые вместо меньше.

      Так ac или bc?

      По самому примеру.
      1. Кто гарантирует возможность работы с набором отдельных переменных как с массивом? Где гарантия, что отдельные переменные будут располагаться подряд и не будет по каким-то причинам использовано выравнивание по адресам? Где гарантия, что компилятор не оптимизирует распределение памяти?
      2. Перефразирую начало. Я хочу использовать классы, но использовать классы не хочу.

      Хотя в целом мысль идет в правильном направлении.

      1. Валерий:

        И сам ошибся 🙂

        Так ac или ab?
        в названии переменной — ac а вызывается a и b.

      2. Алексей:

        Возможность обращения как к массиву и строго последовательное размещение в памяти при описании одним оператором, именно в таком виде- double x,y,z; гарантируется стандартом языка.
        Читал про это очень давно, ещё для языка с. Для с++ то же самое вытекает из правила "любая корректная программа на с является корректной на с++". Неоднократно встречал случаи такого использования в реальных программах и применял сам, проблем никогда не было.

        1. Алексей Л.:

          Qt 5.14.2 (MSVC 2017, 32 бита) выдает следующее:

          1
          4199920
          2

          Так что, в реальных программах такие фокусы лучше не использовать.

  3. Андрей:

    может еще описать способ =delete? или если есть уже такая тема, указать ссылку на него( раз способ закрытия конструктора копирования указывается в этой теме)

  4. Crafty Fox:

    Спасибо тебе большое за этот титанический труд по переводу! Уроки действительно очень хорошие и легко читаются. Надеюсь, у тебя хватит сил и терпению перевести их до конца. Думаю, они многим помогут выучить С++.

    1. Фото аватара Юрий:

      Спасибо за отзыв. Надеюсь, что сил и терпения действительно хватит 🙂

      1. Илья:

        Всячески присоединяюсь к предыдущему комментарию! Спасибо, Юрий, огромное! Тяжелый труд — великолепный результат — безграничная благодарность тебе!

        1. Фото аватара Юрий:

          Спасибо и Вам, что читаете 🙂

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *