Функции копирования памяти в c

Обновлено: 06.07.2024

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

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

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

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

m_numerator ( numerator ) , m_denominator ( denominator ) friend std :: ostream & operator << ( std :: ostream & out , const Drob &d1 ) ; std :: ostream & operator << ( std :: ostream & out , const Drob &d1 )

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

m_numerator ( numerator ) , m_denominator ( denominator ) m_numerator ( d . m_numerator ) , m_denominator ( d . m_denominator ) friend std :: ostream & operator << ( std :: ostream & out , const Drob &d1 ) ; std :: ostream & operator << ( std :: ostream & out , const Drob &d1 ) // Возвращаем текущий объект, чтобы иметь возможность выполнять цепочку операций присваивания

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

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

assert ( source ) ; // проверяем не является ли source нулевой строкой // Определяем длину source + еще один символ для нуль-терминатора (символ завершения строки) // Выделяем достаточно памяти для хранения копируемого значения в соответствии с длиной этого значения // Копируем значение по символам в нашу выделенную память

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

m_length ( source . m_length ) , m_data ( source . m_data ) SomeString copy = hello ; // используется конструктор копирования по умолчанию > // объект copy является локальной переменной, которая уничтожается здесь. Деструктор удаляет значение-строку объекта copy, оставляя, таким образом, hello с висячим указателем std :: cout << hello . getString ( ) << '\n' ; // здесь неопределенные результаты

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

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

SomeString copy = hello ; // используется конструктор копирования по умолчанию

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

std :: cout << hello . getString ( ) << '\n' ; // здесь неопределенные результаты

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

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

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

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

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

// Поскольку m_length не является указателем, то мы можем выполнить поверхностное копирование // m_data является указателем, поэтому нам нужно выполнить глубокое копирование, при условии, что этот указатель не является нулевым

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

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

SomeString & SomeString :: operator = ( const SomeString & source ) // Сначала нам нужно очистить предыдущее значение m_data (члена неявного объекта) // Поскольку m_length не является указателем, то мы можем выполнить поверхностное копирование // m_data является указателем, поэтому нам нужно выполнить глубокое копирование, при условии, что этот указатель не является нулевым

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

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

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

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

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

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

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

Заключение

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

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

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

Программируя на Си многие сталкивались с такими функциями как memcpy() и memmove() , по сути, функции делают одно и тоже, но вторая корректно отрабатывает ситуацию, когда области памяти перекрываются (на что появляются дополнительные накладные расходы).

В мире С++ никто не запрещает пользоваться этими функциями (часто эти функции используют различные механизмы оптимизации и могут статься быстрее своих собратьев из мира C++), но есть и более родное средство, работающее через итераторы: std::copy . Это средство применимо не только к POD типам, а к любым сущностям, поддерживающим итераторы. О деталях реализации в стандарте ничего не сказано, но можно предположить, что разработчики библиотеки не настолько глупы, что бы не использовать, оптимизированные memcpy() / memmove() когда это возможно.

Но по наитию, хочется посмотреть, а что там с пересекающимися областями (overlapping memory blocks)? Ведь задача, на самом деле, не такая уж редкая. К примеру, хотим мы читать MPEG-TS пакеты (размер каждого 188 байт, каждый пакет начинается с 0x47 /sync byte/) из какого-то потока, и есть вероятность, что первое (а может и последующее: например, имеем дело с M2TS контейнером, размер блока которого 192 байт и лишние 4 байта в большинстве случаем мы можем игнорировать /timestamp/) чтение может попасть на середину пакета. В таких случаях обычно делается так: вычитываем блок 188 байт, далее ищем байт синхронизации, если он в нулевой позиции — всё отлично, если нет, то данные от него и до конца, нужно переместить в начало блока, в освободившееся место нужно дочитать недостающую порцию, после чего пакет считается вычитанным и можно отдавать его на обработку.

image

Наглядно процесс копирования данных в начало блока можно показать этой картинкой:

Т.е. видим, что есть перекрытие. Логично было бы применить какой-то аналог memmove() , но в стандартной библиотеке есть только std::move который делает совершенно не то (тут нужно улыбнуться). Но при этом, читая описание для std::copy видим следующую строчку:

The ranges shall not overlap in such a way that result points to an element in the range [first,last).

т.е. на самом деле, если начало области (result) куда копировать, лежит вне области [first,last), то всё должно быть ок. И это реально так.

image

Но посмотрим такую схему копирования с перекрытием:

пока не обращаем внимание на то, что result тут в конце. Смысл картинки в том, что блок памяти нужно сдвинуть от начала на какое-то смещение вперёд, соответственно, если это смещение меньше размера сдвигаемого блока, то адрес назначения у нас будет лежать в пределах [first,last), таким образом условие применимости std::copy не соблюдаются. И если применить его, мы просто затрём данным в перекрывающейся области.

Но тут на помощь нам приходит его собрат, как раз решающий эту проблему: std::copy_backward , всё отличие этой функции в том, что он осуществляет копирование с конца. Т.е. для случая изображённой на второй картинке, он возьмёт (далее очень грубо) элемент из last и ложится в result, далее из last-1 в result-1, далее из last-2 в result-2 и так далее.

Видно, что при такой схеме копирования, когда мы начнём писать в перекрывающуюся область, данные в ней уже будут обработаны. Т.е. для нас всё хорошо. Забавно, что условие применимости при перекрывающийся областях для std::copy_backward слово в слово повторяет данное условие для std::copy .

Функция определена в заголовочном файле string.h (а также в mem.h), описывается в стандартах ANSI C и POSIX.

Компания Microsoft планирует отказаться от использования этой популярной функции, по вине которой годами выявлялись проблемы с безопасностью не только в ОС Windows, но и в огромном количестве числе других приложений, написанных на языке C. Ближе к концу 2009 г. Microsoft добавит memcpy(), CopyMemory() и RtlCopyMemory() в список функций, забаненных в соответствии с методикой разработки безопасных программ Secure Development Lifecycle (SDL). Те разработчики, которые хотят создавать совместимые с SDL приложения, должны будут использовать вместо memcpy() функцию memcpy_s, позволяющую указывать размер буфера. Эта команда уже поддерживается в Microsoft Visual C++, однако родной поддержки в компиляторе GCC пока нет.

Содержание

Определение

  • dst — адрес буфера назначения
  • srс — адрес источника
  • n — количество байт для копирования

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

(\|src-dst\|\ge n)

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

после копирования буфер dst содержит данные отличные от исходных, так как они были разрушены в процессе копирования:

Что получится на самом деле, зависит от реализации функции (пример относится к одной из реализации приведенных ниже).

Для правильного копирования перекрывающихся областей нужно использовать функцию memmove(). Некоторые реализации memcpy() (например в libc FreeBSD и OpenBSD) делают то же что и memmove(), принуждая работать правильно даже неправильно написанную программу, однако при написании переносимой программы на это надеяться нельзя.

Алгоритм работы и реализации

memcpy() копирует содержимое src в буфер dst, например, так:

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

  • не используют индексы, как в примере
  • перемещают за один цикл не один байт, а блок равный машинному слову (2, 4 или 8 байт; 16, 32 или 64 бит), которое копируется процессором за то же время, что и байт. Такой подход наиболее эффективен при копировании данных, выравненых на границу машинного слова
  • иногда используют инструкции процессора для работы с блоками данных (movsX для i386) в этом случае функция пишется с применением языка ассемблера

Пример частично оптимизированной версии:

Данная версия копирует 4 или 8 байт (размер типа long равен 32 битам на 32-битной платформе и 64 на 64-битной) за цикл, но не проверяет выравненности данных.

Пример использования

Ссылки

См. также

  • memmove
  • Стандартная библиотека Си
  • libc

Wikimedia Foundation . 2010 .

Полезное

Смотреть что такое "Memcpy" в других словарях:

Vector (C++) — Стандартная библиотека языка программирования C++ fstream iomanip ios iostream sstream Стандартная библиотека шаблонов algorithm … Википедия


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

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

Поверхностное (неглубокое) копирование – простой и дешевый способ, который можно реализовать просто копируя каждый бит объекта. Такой способ известен и как побитовое копирование.

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

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

Результат main

Функция main Результат main

Внесенные в rect1 изменения не отражаются на rect2 . Чтобы увидеть проблемы неглубокого копирования, изменим класс Rectangle так, чтобы он содержал указатели:

Измененный класс Rectangle

Выполнение той же функции main выдает другой результат:

Новый результат main

Новый результат main

При изменении rect1 изменилось и содержимое rect2 . Состояние переменных можно выразить с помощью следующей диаграммы:

<span><span><span>Диаграмма, иллюстрирующая поверхностное копирование</span></span></span>

Диаграмма, иллюстрирующая поверхностное копирование

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

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

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

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

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

Напомним, что автоматически созданный конструктор копирования выполняет неглубокое копирование. Допустим, класс называется ClassName и имеет поля m 1 , m 2 , m 3 , …, mN . Тогда определение созданного компилятором конструктора выглядит следующим образом:

Конструктор копирования вызывается многократно в разных ситуациях. Самый очевидный случай – когда мы явно создаем новый объект на основе другого экземпляра класса:

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

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

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

Оператор присваивания – это метод, который используется для выполнения присваивания. Как и в случае с конструктором копирования, C++ предоставляет оператор присваивания по умолчанию.

Пример объявления оператора присваивания:

Оператор присваивания и конструктор копирования реализованы аналогично, хотя есть некоторые заметные различия. Во-первых, мы видим, что оператор присваивания возвращает ссылку на экземпляр, потому что в C ++ разрешены объединенные в цепочку присваивания:

В приведённом выше примере оператор присваивания вызывается для rectangle2 с rectangle3 в качестве аргумента. Затем оператор для rectangle1 вызывается со ссылкой, возвращенной из предыдущего вызова в качестве аргумента. Также необходимо учитывать возможность самоприсваивания:

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

При определении этих методов нужно всегда помнить о правиле трех. Оно гласит, что если класс определяет один из следующих методов, он должен явно определить все три метода:

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

Реализация глубокого копирования

Ознакомившись с конструктором копирования и оператором присваивания, мы готовы реализовать глубокое копирование для класса Rectangle .

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

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

Новый результат main Диаграмма, иллюстрирующая глубокое копирование

Выводы

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

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