что такое глубокое копирование
Глубокое и поверхностное копирование
Давайте сделаем шаг назад и обратимся к нашим структурам данных. К нашим, в буквальном смысле, кучам данных (и к стекам тоже, конечно).
Что происходит, когда мы присваиваем значение переменной? И что происходит, когда мы передаем эту переменную в качестве параметра?
Как программа узнает, что именно мы хотим ей сказать?
Чтобы понять, как думает компьютер, давайте объявим следующую переменную:
Смотрите, когда вы это делаете, вы добавляете значение переменной к другим вещам, которые программа должна помнить. Добавляете наверх вот такой конструкции (она называется стеком):
В данном примере программа сохранит вашу строку следующим образом:
В таблице слева имеется указатель на местоположение строки в памяти, длина строки и количество памяти (в байтах), которое эта строка занимает. Что бы мы ни делали с этой переменной, все это относится к операциям со стеком.
В стеке указатель просто содержит ссылку на место, где фактически хранятся сами данные. Это место называется кучей.
Но зачем все это? Зачем мы оперируем разными структурами данных?
Ответ очень прост. Все дело в скорости.
Стек, в сравнении с кучей, гораздо быстрей. Потому что ссылку передать намного проще, чем перемещать сами данные. Имеется ввиду, что не нужно нагружать программу лишними задачами, которые отвлекали бы ее от основной.
Надо заметить, что не все типы данных хранятся подобным образом. Статические типы данных, которыми являются значения логических, целочисленных, с плавающей запятой и символьных переменных, хранятся непосредственно в самом стеке. Таким образом, для хранения числа 458.98 куча не используется, так как размер памяти, необходимый для содержания данного числа, уже заранее известен программе.
Размер памяти для хранения численных типов также зависит от того, могут ли эти числа быть отрицательными или же они только положительны. Как и в математике, если явно не указано, что число отрицательное, оно считается положительным.
А вот составные типы данных, состоящие из двух и более переменных статического типа, хранятся так, как показано на рисунке.
К составным типам данных, например, относятся:
Возвращаемся к копированию
Допустим, вы хотите, чтобы две переменные ссылались на одно и то же значение, а затем вы хотите изменить одну из переменных так, чтобы другая осталась неизменной.
Давайте присвоим переменной некое значение, а затем присвоим значение первой переменной во вторую:
Как бы мы ни выводили значения этих переменных, результатом будет строка «random». Но мы задублировали только стек, но не кучу:
Дело в том, что целью каждого языка является оптимальная производительность. Все должно быть максимально компактно и аккуратно, никаких лишних накладных расходов. Просто указываем на одну и ту же область памяти.
А что будет, если мы захотим изменить одну из этих переменных?
Мы сразу же заметим, что изменились обе переменные, и теперь они имеют одинаковые значения:
Чтобы получить две разные переменные с одинаковыми значениями, которые при этом могут изменяться независимо друг от друга, нужно использовать другой подход. И он называется глубокое копирование.
После этого вы можете делать с вашими переменными все, что угодно. Хоть отправляйте их на Луну.
Этот же принцип управляет передачей переменных между функциями и объектами. Происходит передача указателя на исходные данные. Надеемся, что вы все поняли. Теперь вы сможете выбирать способы копирования данных осмысленно.
13.16 – Поверхностное и глубокое копирование
Поверхностное копирование
Поскольку C++ мало что знает о вашем классе, конструктор копирования по умолчанию и операторы присваивания по умолчанию, которые он предоставляет, используют метод копирования, известный как поэлементное копирование (также известная как поверхностное копирование). Это означает, что C++ копирует каждый член класса отдельно (используя оператор присваивания для перегруженного operator= и прямую инициализацию для конструктора копирования). Когда классы просты (например, не содержат динамически выделяемой памяти), это работает очень хорошо.
Например, давайте взглянем на наш класс Fraction :
Конструктор копирования и оператор присваивания, предоставленные компилятором для этого класса по умолчанию, выглядят примерно так:
Обратите внимание: поскольку эти версии по умолчанию отлично подходят для копирования данного класса, в этом случае действительно нет причин писать наши собственные версии данных функций.
Однако при разработке классов, которые обрабатывают динамически выделяемую память, поэлементное (поверхностное) копирование может доставить нам массу неприятностей! Это связано с тем, что поверхностное копирование указателя просто копируют адрес указателя – оно не выделяет память и не копирует содержимое, на которое указывает указатель!
Давайте рассмотрим это на примере:
Показанный выше простой строковый класс выделяет память для хранения передаваемой нами строки. Обратите внимание, что мы не определили конструктор копирования и перегруженный оператор присваивания. Следовательно, C++ предоставит конструктор копирования по умолчанию и оператор присваивания по умолчанию, которые выполняют поверхностное копирование. Конструктор копирования будет выглядеть примерно так:
Теперь рассмотрим следующий фрагмент кода:
Хотя этот код выглядит достаточно безобидным, он содержит коварную проблему, которая приведет к сбою программы! Можете ее заметить? Не волнуйтесь, если не можете, она довольно незаметна.
Давайте разберем этот пример построчно:
Корень этой проблемы – поверхностное копирование, выполняемое конструктором копирования: выполнение поверхностного копирования значений указателя в конструкторе копирования или перегруженном операторе присваивания почти всегда вызывает проблемы.
Глубокое копирование
Одно из решений этой проблемы – выполнить глубокое копирование любых копируемых ненулевых указателей. Глубокое копирование выделяет память для копии, а затем копирует фактическое значение, поэтому копия живет в отдельной памяти от источника. Таким образом, копия и источник различаются и никоим образом не влияют друг на друга. Выполнение глубокого копирования требует, чтобы мы написали наши собственные конструкторы копирования и перегруженные операторы присваивания.
Давайте продолжим и покажем, как это делается для нашего класса MyString :
Как видите, это немного сложнее, чем простое поверхностное копирование! Во-первых, мы даже должны проверить, есть ли в источнике строка (строка 14). Если это так, то мы выделяем достаточно памяти для хранения копии этой строки (строка 17). И в конце нам нужно вручную скопировать строку (строки 20 и 21).
Теперь займемся перегруженным оператором присваивания. Перегруженный оператор присваивания немного сложнее:
Обратите внимание, что наш оператор присваивания очень похож на наш конструктор копирования, но есть три основных отличия:
Когда вызывается перегруженный оператор присваивания, элемент, которому выполняется присваивание, может уже содержать предыдущее значение, которое необходимо очистить перед присваиванием памяти для новых значений. Для нединамически назначаемых переменных (которые имеют фиксированный размер) нам не нужно об этом беспокоиться, потому что новое значение просто перезаписывает старое. Однако для динамически размещаемых переменных нам необходимо явно освободить любую старую память, прежде чем выделять новую. Если мы этого не сделаем, код не завершится со сбоем, но у нас будет утечка памяти, которая будет съедать нашу свободную память каждый раз, когда мы выполняем присваивание!
Лучшее решение
Глубокое и поверхностное копирование в Python
Введение
В этом уроке мы собираемся обсудить поверхностное (shallow) и глубокое (deep) копирование с помощью примеров на Python. Мы рассмотрим определение глубокого и поверхностного копирования, а также их реализации на языке Python, чтобы оценить основные различия между двумя типами копирования.
Во многих программах, которые мы пишем, нам часто приходится копировать объекты по какой либо причине, например, из-за улучшения вычислительной эффективности. Есть два способа сделать это: сделать полную копию или поверхностную копию. Прежде чем мы обсудим различия между ними, давайте сначала рассмотрим, что такое глубокое и поверхностное копирование.
Глубокое копирование в Python
Глубокая копия создает новую и отдельную копию всего объекта или списка со своим уникальным адресом памяти. Это означает, что любые изменения, внесенные вами в новую копию объекта или списка, не будут отражаться в исходной. Этот процесс происходит следующим образом, сначала создается новый список или объект, а затем рекурсивно копируя все элементы из исходного в новый.
Короче говоря, оба объекта становятся полностью независимы друг от друга. Это похоже на концепцию передачи по значению в таких языках, как C ++, Java и C #.
Пример глубокого копирования
Для реализации концепции глубокого копирования в Python мы будем использовать модуль copy.
Допустим, у нас есть список с именем result_A, который содержит оценки ученика A по 3 предметам, и мы хотим создать точно такой же список результатов и для ученика B. Мы сделаем глубокую копию списка result_A и позже сделаем несколько изменений в этой копии, чтобы посмотреть на оценки ученика B.
Пример 1:
В приведенном выше сценарии мы используем метод deepcopy из модуля copy, чтобы скопировать список result_A в result_B. Далее мы печатаем содержимое обоих списков на экране.
Как видите, списки идентичны. Позже в этой статье мы рассмотрим, как они отличается от результата поверхностного копирования.
Поверхностное копирование в Python
Поверхностное копирование также создает отдельный новый объект или список, но вместо копирования дочерних элементов в новый объект, оно просто копирует ссылки на их адреса памяти. Следовательно, если вы сделаете изменение в исходном объекте, оно будет отражено в скопированном объекте, и наоборот. Короче говоря, обе копии зависят друг от друга. Это похоже на концепцию передачи по ссылке в таких языках программирования, как C ++, C # и Java.
Пример поверхностного копирования
Чтобы реализовать это в Python, мы снова будем использовать модуль copy, но на этот раз мы будем использовать функцию copy.
Давайте воспользуемся тем же списком.
Пример 2:
В приведенном выше сценарии мы используем метод copy из модуля copy, чтобы сделать поверхностную копию списка result_A, который мы назвали result_B. Далее содержимое обоих списков было напечатано на консоли.
Опять же, такие же списки, как и ожидалось. Далее мы объясним разницу между результатами, которые мы получаем от функций copy и deepcopy.
Разница между глубоким и поверхностным копированием
Теперь, когда мы обсудили, что такое поверхностное и глубокое копирование, пришло время поговорить о разнице между ними. По сути, есть только два основных различия, и они связаны друг с другом:
Прежде чем мы перейдем к примеру в котором будет рассмотрена разница, я бы хотел, чтобы вы представили аналогию для сценария примера. Допустим, два человека хотят поделиться напитком; у них два пустых стакана и две соломинки. Они могут поделиться напитком двумя способами:
Первый сценарий — сценарий это аналогия поверхностного копирования. Обе переменные/экземпляры используют одну и ту же ячейку памяти для своих операций. Второй сценарий — сценарий глубокого копирования. Обе переменные/экземпляры используют две разные области памяти для своих операций.
Пример сравнения
Чтобы прояснить разницу, давайте использовать эту информацию в наших двух приведенных выше примерах, начиная с примера 1.
Выше мы создали список result_A и сделали его полную копию с именем result_B. Давайте попробуем изменить содержимое в result_B и посмотрим, повлияет ли это на содержимое result_A.
Ожидаемый результат заключается в том, что первоначальный список остается без изменений. И, как вы видите, изменения в глубокой копии не повлияли на исходный список.
Теперь давайте попробуем то же самое с примером 2 — поверхностное копирование.
Здесь ожидаемый результат состоит в том, что как исходный список, так и скопированный список изменяются. И, как вы можете видеть, внесение изменений в поверхностную копию привело к тому, что эти изменения также были отражены в исходном списке.
Заключение
В этом посте мы говорили о том, что такое поверхностное и глубокое копирование и как они реализуются на языке Python, используя модуль «copy». Мы использовали две его функции, а именно: copy и deepcopy. Кроме того, мы обсудили два основных различия между поверхностной и глубокой копией, а также реализовали поверхностное и глубокое копирование в python, чтобы лучше понять эти различия.
Использование функций copy и deepcopy в Python
В этой статье я расскажу, как скопировать объект с помощью мелкой копии и глубокой копии в Python.
Введение
При программировании нам нужно скопировать существующие данные. Когда мы присваиваем переменную другому оператору, то оператор присваивания не копирует объект, а просто создает новую ссылку на тот же объект.
Давайте приступим к рассмотрению конкретных примеров.
Как использовать функцию copy в Python?
Мелкая копия — это функция, используемая для создания копии существующего объекта коллекции. Когда мы пытаемся скопировать объект коллекции с помощью неглубокой копии, эта функция создает новый объект коллекции и сохраняет ссылки на элементы исходного объекта.
Мы можем использовать неглубокое копирование в Python с помощью модуля копирования. Для выполнения операции неглубокого копирования мы используем метод copy модуля copy.
Метод copy принимает исходный объект коллекции в качестве входных данных и создает новый объект коллекции со ссылкой на элементы исходного объекта коллекции. Затем он возвращает ссылку на новый объект коллекции.
В следующем примере я создам копию данного словаря Python с помощью метода copy:
В выходных данных мы видим, что мы создали такой же словарь, как и исходный словарь, приведенный выше.
Как работает неглубокое копирование?
В неглубокой копии, когда создается новый объект, он имеет ссылки на элементы исходного объекта. Если мы попытаемся внести какие-либо изменения во вновь созданный объект, он не будет отражен в исходном объекте, учитывая, что элементы в объектах не должны ссылаться на другой объект, то есть не должно быть никакой вложенности.
Если мы внесем какие-либо изменения в исходный объект, это также не отразится на копии исходного объекта, учитывая, что вложенность не должна быть выполнена.
Точно так же, когда мы добавляем какой-либо элемент к исходному объекту, он не будет иметь никакого влияния на новый объект.
Сценарии, рассмотренные выше, изменяются, когда в объектах присутствует вложенность, то есть когда копируемые объекты содержат другие объекты, изменения, происходящие во вложенных объектах, видны как в исходном, так и в копируемом объекте.
Это можно увидеть в следующем примере:
При копировании объекта с помощью метода copy.copy создается только копия объекта, передаваемая в качестве параметра методу copy. Элементы внутри объекта не копируются, копируются только ссылки на элементы.
Таким образом, когда в исходном объекте в качестве элементов присутствуют только примитивные типы данных, такие как int, double, string. Изменения не видны новому объекту при выполнении в исходном объекте, поскольку эти типы данных неизменяемы и для каждого изменения создается новый объект.
Но в случае вложенных объектов ссылки не изменяются, и когда мы вносим какие-либо изменения в один из объектов, они видны в другом объекте.
Как использовать deepcopy в Python?
Чтобы избежать проблемы, обсуждаемой при выполнении неглубокого копирования, мы будем использовать метод deepcopy. Метод deepcopy рекурсивно создает копию каждого элемента объекта и не копирует ссылки.
Это можно сделать следующим образом:
После использования функции deepcopy изменения, внесенные в исходный объект, не будут отображаться в скопированном объекте, даже если присутствует вложенность.
Здесь мы видим, что в отличие от copy, когда мы копируем объект с помощью deepcopy, изменения, внесенные в исходный объект, не влияют на скопированный объект и наоборот, потому что объект, созданный методом deepcopy, не содержит ссылок на элементы исходного словаря, в то время как в случае метода copy вновь созданные объекты содержат ссылки на элементы исходного объекта.
Заключение
В этой статье вы узнали о неглубоком копировании и глубоком копировании в Python.
Вы видели, что при наличии вложенных объектов для создания копии объектов следует использовать функцию deepcopy. Мы также можем написать программы, используемые в этой статье, с обработкой исключений с помощью python try, чтобы сделать программы более надежными или чтобы систематически обрабатывать ошибки.
Урок №145. Поверхностное и глубокое копирование
Обновл. 13 Сен 2021 |
На этом уроке мы рассмотрим поверхностное и глубокое копирование в языке C++.
Поверхностное копирование
Поскольку язык C++ не может знать наперед всё о вашем классе, то конструктор копирования и оператор присваивания, которые C++ предоставляет по умолчанию, используют почленный метод копирования — поверхностное копирование. Это означает, что C++ выполняет копирование для каждого члена класса индивидуально (используя оператор присваивания по умолчанию вместо перегрузки оператора присваивания и прямую инициализацию вместо конструктора копирования). Когда классы простые (например, в них нет членов с динамически выделенной памятью), то никаких проблем с этим не должно возникать.
Рассмотрим следующий класс Drob:
Конструктор копирования и оператор присваивания по умолчанию, предоставляемые компилятором автоматически, выглядят примерно следующим образом:
Поскольку эти конструктор копирования и оператор присваивания по умолчанию отлично подходят для выполнения копирования с объектами этого класса, то действительно нет никакого смысла писать здесь свои собственные версии конструктора копирования и перегрузки оператора.
Однако при работе с классами, в которых динамически выделяется память, почленное (поверхностное) копирование может вызывать проблемы! Это связано с тем, что при поверхностном копировании указателя копируется только адрес указателя — никаких действий по содержимому адреса указателя не предпринимается. Например:
Вышеприведенный класс — это обычный строковый класс, в котором выделяется память для хранения передаваемой строки. Здесь мы не определяли конструктор копирования или перегрузку оператора присваивания. Следовательно, язык C++ предоставит конструктор копирования и оператор присваивания по умолчанию, которые будут выполнять поверхностное копирование. Конструктор копирования выглядит примерно следующим образом:
Хотя этот код выглядит достаточно безвредным, но он имеет в себе коварную проблему, которая приведет к сбою программы! Можете найти эту проблему? Если нет, то ничего страшного.
Разберем этот код по строкам:
Корнем этой проблемы является поверхностное копирование, выполняемое конструктором копирования по умолчанию. Такое копирование почти всегда приводит к проблемам.
Глубокое копирование
Одним из решений этой проблемы является выполнение глубокого копирования. При глубоком копировании память сначала выделяется для копирования адреса, который содержит исходный указатель, а затем для копирования фактического значения. Таким образом копия находится в отдельной, от исходного значения, памяти и они никак не влияют друг на друга. Для выполнения глубокого копирования нам необходимо написать свой собственный конструктор копирования и перегрузку оператора присваивания.
Рассмотрим это на примере с классом SomeString:
Как вы видите, реализация здесь более углубленная, нежели при поверхностном копировании! Во-первых, мы должны проверить, имеет ли исходный объект ненулевое значение вообще (строка №8). Если имеет, то мы выделяем достаточно памяти для хранения копии этого значения (строка №11). Наконец, копируем значение-строку (строки №14-15).
Теперь рассмотрим перегрузку оператора присваивания:
Заметили, что код перегрузки очень похож на код конструктора копирования? Но здесь есть 3 основных отличия:
Мы добавили проверку на самоприсваивание.
Мы возвращаем текущий объект (с помощью указателя *this), чтобы иметь возможность выполнить цепочку операций присваивания.
Мы явно удаляем любое значение, которое объект уже хранит (чтобы не произошло утечки памяти).
При вызове перегруженного оператора присваивания, объект, которому присваивается другой объект, может содержать предыдущее значение, которое нам необходимо очистить/удалить, прежде чем мы выделим память для нового значения. С не динамически выделенными переменными (которые имеют фиксированный размер) нам не нужно беспокоиться, поскольку новое значение просто перезапишет старое. Однако с динамически выделенными переменными нам нужно явно освободить любую старую память до того, как мы выделим любую новую память. Если мы этого не сделаем, сбоя не будет, но произойдет утечка памяти, которая будет съедать нашу свободную память каждый раз, когда мы будем выполнять операцию присваивания!
Лучшее решение
В Стандартной библиотеке C++ классы, которые работают с динамически выделенной памятью, такие как std::string и std::vector, имеют свое собственное управление памятью и свои конструкторы копирования и перегрузку операторов присваивания, которые выполняют корректное глубокое копирование. Поэтому, вместо написания своих собственных конструкторов копирования и перегрузки оператора присваивания, вы можете выполнять инициализацию или присваивание строк, или векторов, как обычных переменных фундаментальных типов данных! Это гораздо проще, менее подвержено ошибкам, и вам не нужно тратить время на написание лишнего кода!
Заключение
Конструктор копирования и оператор присваивания, предоставляемые по умолчанию языком C++, выполняют поверхностное копирование, что отлично подходит для классов без динамически выделенных членов.
Классы с динамически выделенными членами должны иметь конструктор копирования и перегрузку оператора присваивания, которые выполняют глубокое копирование.
Используйте функциональность классов из Стандартной библиотеки C++, нежели самостоятельно выполняйте/реализовывайте управление памятью.
Поделиться в социальных сетях: