Как сделать копию класса c

Обновлено: 03.07.2024

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

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

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

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

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

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

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

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

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

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

4. Пример

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

Инициализация нового объекта другим объектом того же типа

К примеру, при инициализации некоторого объекта А типа MyClassобъектом В того же типа происходит создание побитовой копии объекта Bс последующей ее присваиванием объекту А.

Пускай у нас есть класс ClassName, в котором реализован конструктор и деструктор. А в функции mainсоздадим объект типа ClassName, который присвоим новому объекту того же типа:

Результатом работы программы станет:

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

Передача аргументов в функцию

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

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

Возврат объекта из функции

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

Результатом работы программы станет:

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

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

Конструктор копирования получает в качестве параметра ссылку на неизменяемый объект. Реализуем этот конструктор в нашем классеClassName, к примеру, для случая с инициализацией объекта.

Заключение

На какой объект будет указывать переменная p2 , на тот же, что и p1 (слева на рисунке) или на его копию (справа на рисунке)?

С вопросом присваивания связан вопрос передачи параметров в метод: при передаче параметров неявно выполняется присваивание переменной-параметру значения передаваемой переменной. Передача параметров выполняется по значению, как и присваивание.

Изучите следующий код, демонстрирующий эти особенности:

Обратим внимание, что при изменении объекта node в рекурсивном методе Add изменяется не копия, созданная в методе, а исходный объект, существующий в одном экземпляре. С другой стороны, когда мы в методе Add изменяем переменную skip , она не меняется в вызвавшем методе, так как это разные переменные. Схема работы кода представлена на следующем рисунке:

Мы рассмотрели копирование и передачу параметров в метод по значению. Этот способ является способом по умолчанию, так как он в большинстве случаев оптимален: значимые типы, как правило, небольшие и их эффективнее передать по значению в стеке; ссылочные типы могут занимать существенный объем, и копировать их каждый раз в стек было бы слишком расточительно. В разных языках программирования способы копирования и передачи параметров в методы могут различаться, но в общем случае выделяют три способа: по значению (by value), по ссылке (by reference) и по указателю (by pointer).

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

Здесь мы использовали ключевое слово ref , обозначающее, что параметр метода передается по ссылке. Как мы показали выше, при передаче параметра по ссылке переменная параметр обозначает ту же область памяти, что и передаваемая переменная 29 . Соответственно, изменение этой переменной в методе приводит к изменению значения переданной переменной. Если бы мы опустили ключевое слово ref , то значимая переменная x была бы передана по значению, то есть переменная-параметр обозначала бы другую область памяти, в которую при входе в метод было бы скопировано значение и его изменение не отразилось бы на значении переменной x в вызывающем коде.

Приведенные примеры демонстрируют передачу по ссылке параметра значимого типа. При передаче по ссылке параметра ссылочного типа мы можем изменить значение самого указателя в вызывающем методе из вызываемого (при передаче параметра ссылочного типа по значению – не можем). Покажем это на следующем примере:

То есть передавая параметр в метод по значению, мы можем быть уверены, что значение переменной-параметра в вызывающем методе не изменится, так как вызываемый метод будет менять (если будет) копию переданного значения. Но если переменная-параметр ссылочного типа, то ее значение – указатель, соответственно, вызываемый метод может изменить поля объекта, на который указывает этот указатель (в примере p1.X изменено внутри метода SomeMethod ). Если вы не вполне ясно понимаете этот механизм, нарисуйте схему памяти для рассматриваемого примера.

В заключение отметим, что передачей параметров в метод по ссылке не следует злоупотреблять. Если метод возвращает один параметр, то всегда лучше использовать механизм возврата значений из метода, а не передачу параметра по ссылке. Если в вашем методе появляется несколько параметров, передаваемых по ссылке, следует подумать, можно ли реализовать метод по-другому. Например, типичное решение – создать класс, включающий все возвращаемые значения как поля, и возвращать его, как возвращаемый методом объект 30 . Также из названия и описания метода всегда должно быть понятно, будет ли метод менять значения полей передаваемой ссылочной переменной.

§ 20. Копирование объектов. Все рассмотренные в предыдущем параграфе механизмы присваивания для ссылочных переменных не предполагают копирование самого объекта. Мы или копируем указатель, или создаем ссылку.

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

Самый очевидный способ – скопировать состояние, то есть значения всех полей объекта:

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

Вопрос: приведет ли изменение центра второго круга в последней строке примера к изменению центра первого? Очевидно, что да, так как операция клонирования копирует значения всех полей, а значение поля Center – указатель на объект точки.

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

Равенство объектов, как и копирование, не является понятием уровня языка и не может быть реализовано универсально, без понимания семантики объекта.

Приведем еще один пример: программа отслеживает положение автобусов, есть класс Bus , метод Redout (получить последние показания физических датчиков местоположения) и свойства X , Y (текущее положение по результатам последнего считывания показаний). У нас есть два объекта, которые привязаны к одному и тому же автобусу: Bus b1; Bus b2 . Но для объекта b1 мы давно не взывали метод Readout , а для объекта b2 – только что. Равны ли эти объекты? С точки зрения значений полей (состояние) – нет. Но с точки зрения смысла задачи – да, так как они обозначают один и тот же физический автобус.

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

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

§ 23. Перегрузка операторов. Зададимся вопросом: возможно ли изменить поведение операторов присваивания или сравнения? Возможно ли в принципе изменить поведение какого-либо оператора?

Многие языки программирования позволяют переопределять встроенное поведение операторов применительно к пользовательским типам данных, то есть переопределять соответствующие методы. Такое переопределение называется перегрузкой операторов.

Перегрузка оператора (operator overloading) – определение собственной реализации оператора для объектов некоторого класса.

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

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

Во-первых, использование перегруженного оператора синтаксически скрыто. Программист, читающий в коде строку bool isEqual = p1 == p2 , будет уверен, что выполняется стандартное сравнение по значению указателя, так как, в отличие от перегруженных методов, мы не можем определить, какая реализация оператора используется, глядя только на строку, где она используется.

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

Вопросы и задания

Охарактеризуйте различия а) присваивания, б) копирования, в) передачи параметров в метод и г) возврата результат выполнения метода (return) следующими способами: 1) по значению; 2) по ссылке; 3) по указателю.

Приведите примеры, когда 1) объекты равны семантически, но имеют различающиеся значения полей; 2) объекты не равны семантически, но имеют идентичные значения полей.

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

В примере из § 21 объясните зачем нужна строка if (p == null) return false , ведь выражение (Point)null == (Point)null должно возвращать true ?

** Познакомьтесь с механизмом замыканий (closures).

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

31. Копирование значений полей объекта также называется shallow copy – поверхностным копированием.↩︎

32. Полное копирование объекта с учетом семантики его полей также называется deep copy – глубоким копированием.

33. Строго говоря, необходимо определять два метода: Equals (сравнение), GetHashCode (вычисление хэш-кода объекта), однако эта тема выходим за рамки книги.

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

Имеем объект класа. Нада получить копию этого объекта а не ссылку на него . Тоесть при изменении первого второй должен оставатся прежним. Посоветовали использовать интерфейс ICloneable. Ничего не вышло. Помогите чем можете (желательно кодом чем по проще) . Очень-очень надо .
Заранее благодарен .

ты можешь сам написать конструктор, который принимает как параметр объект, копию которого ты хочешь сделать. например у тебя есть класс Worker:

class Worker <
string firstName;
string lastName;
int age;

public Worker(string fname, string lname, int age)
this.firstName = fname;
this.lastName = lname;
this.age = age;
>

public Worker(Worker ob)
this.firstName = ob.firstName;
this.lastName = ob.lastName.;
this.age = ob.age;
>

public static void Main()
Worker ob1 = new Worker("Sara", "Fridman", 25);
Worker ob2 = new Worker(ob1); [COLOR=green]//создали копию ob1[/COLOR]
>

class MyBaseClass public static string CompanyName = "My Company";
public int age;
public string name;
>

class MyDerivedClass: MyBaseClass

static void Main()

// Creates an instance of MyDerivedClass and assign values to its fields.
MyDerivedClass m1 = new MyDerivedClass();
m1.age = 42;
m1.name = "Sam";

// Performs a shallow copy of m1 and assign it to m2.
MyDerivedClass m2 = (MyDerivedClass) m1.MemberwiseClone();
>
>

Смотря в каком контексте тебе нужно использовать класс. Если ты создашь структуру (т.е. неявный наследник от ValueType), то переменной такого класса всегда будет передаваться копия (т.е. передача по значению). Интерфейс ICloneable позволяет управлять процессом копирования. Копирование бывает двух типов - глубокое и поверхностное. При глубоком создаётся абсолютно отдельная копия, при поверхностном создаётся копия класса, но она содержит ссылки на те же объекты что и оригинал. Метод MemberwiseClone осуществляет поверхностное копирование.
Для создания поверхностных копий можно использовать копирующий конструктор, как привела OlgaKr или вот эту функцию:


Для создания глубоких копий нужно использовать интерфейс ICloneable, и реализовывать копирование вручную.
Вот, я написал метод, который производит глубокое копирование и подходит для любых объектов:

Заново открываю тему, так как она вновь актуальна )))
Очень нужно полное копирование объектов с большой иерархией вложений ))).


К сожалению вообще не работает. ((((

Причины, которые мне удалось выявить:
1) "typeof(T).GetFields(" - очевидно что уже на втором шаге "object original = f.GetValue(o); f.SetValue(copy, CreateDeepCopy(original));" типом T будет являться тип object, что естественно нас не устраивает, ибо на самом деле это возможно замаскированный тип с кучей вложений.
2) Array.memberwiseClone() не запускает memberwiseClone() для самих элементов массива (((
3) Не у всех классов метод .memberwiseClone() не является перегруженным ( пример - StreamReader )
4) Специфика типа string, не позволяет скопировать его простым .memberwiseClone()
5) Если одно из вложенных полей == null вылезет ошибка


Вот тут было дело. Но это все очень медленно, код генерировать все же эффективнее.

Очень интересно.
Я уже признаться свой код как раз накатал, причем в похожем ключе.
Вообщем добавив некоторые идеи, получил следующее:
Улучшения:
1) MemberwiseClone собирается как делегат, что работает намного быстрее Invoke()
2) Обработка 1,2,3х мерных массивов более быстрая

public static class Clonner
<
static Func object , object > MemberwiseClone =
Delegate . CreateDelegate
(
typeof ( Func object , object > ) ,
typeof ( object ) . GetMethod ( "MemberwiseClone" , BindingFlags . Instance | BindingFlags . NonPublic )
)
as Func object , object >;

public static object DeepClone ( this object obj )
<
if ( object . ReferenceEquals ( obj , null ) ) return null ; //Если объект ~ null, то и клон его - null
else
<
if ( obj is string ) return string . Copy ( obj as string ) ; //если тип - строка то производим "хитрое" копирование
else if ( obj is Array ) //Если объект - массив, необходимо поочередно глубококлонировать его элементы
<
Array array = ( obj as Array ) . Clone ( ) as Array ; //Клонируем массив
switch ( array . Rank ) //Для ускорения различаем массивы по числу размерностей
<
case 1 : // T[]
<
int bi = array . GetLowerBound ( 0 ) , ei = array . GetUpperBound ( 0 ) + 1 ;
for ( int i = bi ; i ei ; i ++ )
array . SetValue ( DeepClone ( array . GetValue ( i ) ) , i ) ;
break ;
>
case 2 : // T[,]
<
int bi = array . GetLowerBound ( 0 ) , ei = array . GetUpperBound ( 0 ) + 1 ;
int bj = array . GetLowerBound ( 1 ) , ej = array . GetUpperBound ( 1 ) + 1 ;
for ( int i = bi ; i ei ; i ++ )
for ( int j = bj ; j ej ; j ++ )
array . SetValue ( DeepClone ( array . GetValue ( i , j ) ) , i , j ) ;
break ;
>
case 3 : // T[,,]
<
int bi = array . GetLowerBound ( 0 ) , ei = array . GetUpperBound ( 0 ) + 1 ;
int bj = array . GetLowerBound ( 1 ) , ej = array . GetUpperBound ( 1 ) + 1 ;
int bk = array . GetLowerBound ( 2 ) , ek = array . GetUpperBound ( 2 ) + 1 ;
for ( int i = bi ; i ei ; i ++ )
for ( int j = bj ; j ej ; j ++ )
for ( int k = bk ; k ek ; k ++ )
array . SetValue ( DeepClone ( array . GetValue ( i , j , k ) ) , i , j , k ) ;
break ;
>
default : // T[. ] - более трех размерностей
<
//для прохода по всем индексам используем специальный индекс-итератор
foreach ( var indx in new Indexer ( array ) ) array . SetValue ( DeepClone ( array . GetValue ( indx ) ) , indx ) ;
break ;
>
>
return array ;
>
else // объект не null, не строка и не массив
<
Type tyobj = obj . GetType ( ) ; //узнаем тип объекта
if ( tyobj . IsPrimitive ) return obj ; //если тип является примитивом (int, double и т.п.) то нет смысла копаться в его внутренностях
else
<
object clone = MemberwiseClone ( obj ) ;
//глубококлонируем все поля объекта
foreach ( var field in tyobj . GetFields ( BindingFlags . Instance | BindingFlags . Public | BindingFlags . NonPublic ) )
field . SetValue ( clone , DeepClone ( field . GetValue ( clone ) ) ) ;
return clone ;
>
>
>
>
>
internal struct Indexer : IEnumerable int [ ] >
<
public int rank ;
public int [ ] LoverBownds ;
public int [ ] UpperBownds ;

public Indexer ( Array array )
<
this . rank = array . Rank ;
this . LoverBownds = new int [ rank ] ;
this . UpperBownds = new int [ rank ] ;
for ( int i = 0 ; i this . rank ; i ++ )
<
this . LoverBownds = array . GetLowerBound ( i ) ;
this . UpperBownds = array . GetUpperBound ( i ) ;
>
>
IEnumerator IEnumerable . GetEnumerator ( )
<
return this . GetEnumerator ( ) ;
>
public IEnumerator int [ ] > GetEnumerator ( )
<
int [ ] CurrentIndexs = this . LoverBownds . Clone ( ) as int [ ] ;
for ( int i = this . rank - 2 ; ; )
<
for ( ; ++ i this . rank ; )
CurrentIndexs = this . LoverBownds ;
for ( -- i ; CurrentIndexs this . UpperBownds ; CurrentIndexs ++ )
yield return CurrentIndexs ;
for ( ; -- i != - 1 && CurrentIndexs == this . UpperBownds ; ) ;

По умолчанию при инициализации одного объекта другим С++ выполняет побитовое копирова­ние. Это означает, что точная копия инициализирующего объекта создается в целевом объекте. Хотя в большинстве случаев такой способ инициализации объекта является вполне приемлемым, имеются случаи, когда побитовое копирование не может использоваться. Например, такая ситу­ация имеет место, когда объект выделяет память при своем создании. Рассмотрим в качестве при­мера два объекта А и В класса ClassType, выделяющего память при создании объекта. Положим, что объект А уже существует. Это означает, что объект A уже выделил память. Далее предполо­жим, что А использовался для инициализации объекта B, как показано ниже:

Если в данном случае используется побитовое копирование, то В станет точной копией А. Это означает, что В будет использовать тот же самый участок выделенной памяти, что и A, вместо того, чтобы выделить свой собственный. Ясно, что такая ситуация нежелательна. Например, если класс ClassType включает в себя деструктор, освобождающий память, то тогда одна и та же па­мять будет освобождаться дважды при уничтожении объектов A и B!

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

Для решения подобных проблем язык С++ позволяет создать конструктор копирования, кото­рый используется компилятором, когда один объект инициализирует другой. При наличии кон­структора копирования побитовое копирование не выполняется. Общая форма конструктора ко­пирования имеет вид:

имя_класса (const имя_класса &о) // тело конструктора
>

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

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

myclass х = у; // инициализация
func (х); // передача параметра
у = func (); // получение временного объекта

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