Урок 145. Поверхностное и глубокое копирование

   ⁄ 

 Обновлено 20 мая 2018  ⁄ 

 ⁄   3 

⁄   610

В этом уроке мы рассмотрим поверхностное и глубокое копирование в C++.

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

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

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

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

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

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

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

m_data здесь — это всего лишь поверхностная копия указателя source.m_data, что означает, что теперь они оба указывают на один и тот же адрес памяти. Теперь рассмотрим следующий фрагмент кода:

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

Разберем этот код по строкам:

Эта строчка безвредна. Здесь вызывается конструктор класса SomeString, который выделяет память, указывает hello.m_data указывать на эту память, а затем копирует в выделенный адрес памяти значение — строку «Hello, world!».

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

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

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

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



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

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

Рассмотрим это на примере с классом SomeString:

Как вы видите, реализация здесь более углублённая, нежели при поверхностном копировании! Во-первых, мы должны проверить, имеет ли исходный объект ненулевое значение вообще (строка 8). Если имеет, то мы выделяем достаточно памяти для хранения копии этого значения (строка 11). Наконец, копируем значение-строку (строки 14 и 15).

Теперь рассмотрим перегрузку оператора присваивания:

Заметили, что код перегрузки очень похож на код конструктора копирования? Но здесь есть три основных отличия:

  Мы добавили проверку на самоприсваивание.

  Мы возвращаем текущий объект (с помощью *this), чтобы иметь возможность выполнять цепочку операций присваивания.

  Мы явно удаляем любое значение, которое объект уже хранит (чтобы не произошло утечки памяти).

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

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

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

Итого

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

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

  Используйте функциональность классов из стандартной библиотеки C++, нежели самостоятельно выполняйте/реализовывайте управление памятью.

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

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (9 оценок, среднее: 5,00 из 5)
Загрузка...
Подписаться на обновления:

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

  1. korvell:

    Ещё уроки будут?

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

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

ПОДПИСЫВАЙТЕСЬ

НА КАНАЛ RAVESLI В TELEGRAM

@ravesli

ПОДПИСАТЬСЯ БЕСПЛАТНО