Особенности копирования объектов
Конструктор копирования Конструктор копирования - это специальный вид конструктора, получающий в качестве единственного параметра указатель на объект этого же класса: T::T(const T&) {... /* Тело конструктора */ } где T - имя класса. Этот конструктор вызывается в тех случаях, когда новый объект создается путем копирования существующего:
Если программист не указал ни одного конструктора копирования, компилятор создает его автоматически. Такой конструктор выполняет поэлементное копирование полей. Если класс содержит указатели или ссылки, это, скорее всего, будет неправильным, поскольку и копия, и оригинал будут указывать на одну и ту же область памяти. Запишем конструктор копирования для класса monster. Поскольку в нем есть поле name, содержащее указатель на строку символов, конструктор копирования должен выделять память под новую строку и копировать в нее исходную: monster::monster(const monster &M){if (M.name) { name = new char [strlen(M.name) + 1]; strcpy(name, M.name); } else name = 0;health = M.health; ammo = M.ammo; skin = M.skin;}...monster Vasia (blue); monster Super = Vasia; // Работает конструктор копированияmonster *m = new monster ("Ork");monster Green = *m; // Работает конструктор копирования
Заготовка класса без наследников Для того чтобы избежать проблем с неправильным использованием копирования, рекомендуется создавать класс на основе приведенной ниже заготовки. Нужно поменять имена и наполнить смыслом методы класса. class XClass { OClass obj; public: XClass(); XClass(const XClass &); ~XClass(); private: }; inline XClass::XClass():obj(value) { .......... } inline XClass::XClass(const XClass &r):obj(r.obj) { .......... } inline XClass::~XClass() {}
inline const XClass & XClass:: operator=(const XClass &r) { if(this!=&r) { obj=r.obj; } return *this; } Можно сказать, что заготовка класса с наследниками отличается от этой заготовки тем, что деструктор должен быть виртуальным (virtual). Недостатком предложенного выше подхода является неэкономичный расход памяти и времени при передаче параметров и возвращение значений из функции. Можно определить конструктор копий в закрытой части класса. Можно будет работать через ссылки и указатели. Альтернативным решением является использование счетчика ссылок на содержимое данных классов.
Пример вектора с неповерхностным копированием. Существуют два основных подхода к реализации операции клонирования: 1. Поверхностное копирование означает, что переменные клонированного объекта содержат те же значения, что и переменные исходного объекта, и что все ссылки указывают на одинаковые объекты. Другими словами, при поверхностном копировании копируется только клонируемый объект, но не объекты, на которые он ссылается. И оригинал, и поверхностная копия ссылаются на одни и те же объекты. 2. Глубокое копирование означает, что переменные клонированного объекта содержат те же самые значения, что и переменные исходного объекта, исключая переменные, которые ссылаются на объекты. Теперь они ссылаются на копии тех объектов, на которые ссылается исходный объект. Другими словами, при глубоком копировании копируется клонируемый объект и те объекты, на которые он ссылается. Глубокая копия ссылается на копии тех объектов, на которые ссылается исходный объект. Реализация глубокого копирования может быть очень сложной. Нужно будет принимать решение, делать глубокие или поверхностные копии косвенно копируемых объектов. Кроме того, необходимо очень осторожно обращаться с любыми циклическими ссылками. Поверхностное копирование реализуется проще, так как все классы наследуют Метод clone класса Object, который легко это делает. Однако если класс объекта не реализует интерфейс Cloneable, то метод clone не будет работать. Если все объекты-прототипы, используемые программой, будут клонировать сами себя по методу поверхностного копирования, то, объявив интерфейс
PrototypelF как расширение интерфейса Cloneable, можно будет сэкономить время. Таким образом, все классы, реализующие интерфейс PrototypelF, будут реализовывать также интерфейс Cloneable. Некоторые объекты, например потоки и сокеты, не могут просто копироваться или совместно использоваться. Какая бы стратегия копирования ни применялась, если имеются ссылки на такие объекты, то для использования скопированных объектов придется создавать эквивалентные объекты. Излишнее копирование. Конструктор копии. Операции присваивания. Излишнее копирование При выполнении любой бинарной операции для типа complex реализующей эту операцию функции будут передаваться как параметры копии обоих операндов. Дополнительные расходы, вызванные копированием двух значений типа double, заметны, хотя по всей видимости допустимы. К сожалению представление не всех классов является столь удобно компактным. Чтобы избежать избыточного копирования, можно определять функции с параметрами типа ссылки: class matrix { double m[4][4]; public: matrix(); friend matrix operator+(const matrix&, const matrix&); friend matrix operator*(const matrix&, const matrix&); }; Ссылки позволяют без излишнего копирования использовать выражения с обычными арифметическими операциями и для больших объектов. Указатели для этой цели использовать нельзя, т.к. невозможно переопределить интерпретацию операции, если она применяется к указателю. Это допустимо, но возникает проблема с выделением памяти. Поскольку ссылка на результат операции будет передаваться как ссылка на возвращаемое функцией значение, оно не может быть автоматической переменной этой функции. Поскольку операция может использоваться неоднократно в одном выражении, результат не может быть и локальной статической переменной. Как правило, результат будет записываться в отведенный в свободной памяти объект. Обычно бывает дешевле (по затратам на время выполнения и память данных и команд) копировать результирующее значение, чем размещать его в свободной памяти и затем в конечном счете освобождать выделенную память. К тому же этот способ проще запрограммировать.
Конструктор копирования Как правило, при создании объекта вызывается конструктор, за исключением случая, когда объект создается как копия другого объекта этого же класса, например: date date2 = date1; Однако имеются случаи, в которых создание объекта без вызова конструктора осуществляется неявно: - формальный параметр - объект, передаваемый по значению, создается в стеке в момент вызова функции и инициализируется копией фактического параметра; - результат функции - объект, передаваемый по значению, в момент выполнения оператора return копируется во временный объект, сохраняющий результат функции. Во всех этих случаях транслятор не вызывает конструктора для вновь создаваемого объекта: - dat2 в приведенном определении; - создаваемого в стеке формального параметра; - временного объекта, сохраняющего значение, возвращаемое функцией. Вместо этого в них копируется содержимое объекта-источника: - dat1 в приведенном примере; - фактического параметра; - объекта - результата в операторе return. При наличии в объекте указателей на динамические переменные и массивы или идентификаторов связанных ресурсов, такое копирование требует дублирования этих переменных или ресурсов в объекте-приемнике, как это было сделано выше в операции присваивания. С этой целью вводится конструктор копирования, который автоматически вызывается во всех перечисленных случаях. Он имеет единственный параметр - ссылку на объект-источник: class string { char *Str; int size; public: string(string&); // Конструктор копирования }; string::string(string& right) // Создает копии динамических { // переменных и ресурсов s = new char[right->size]; strcpy(Str,right->Str); } Конструктор копирования обязателен, если в программе используются функции-элементы и переопределенные операции, которые получают формальные параметры и возвращают в качестве результата такой объект не по ссылке, а по значению.
Операции присваивания При отсутствии переопределения операции присваивания производится побайтное копирование объектов. Такая интерпретация операции присваивания некорректна, если объект имеет указатели на динамические переменные или массивы, идентификаторы связанных ресурсов и т.д. При копировании таких объектов необходимо сначала уничтожить связанные динамические переменные и ресурсы левого операнда, а затем заново резервировать, но уже с параметрами, необходимыми для интерпретации операции присваивания. Присваивание – это тоже операция, она является частью выражения. Значение правого операнда присваивается левому операнду. x = 2; // переменной x присвоить значение 2cond = x < 2; // переменной cond присвоить значение true, если x меньше 2, // в противном случае присвоить значение false3 = 5; // ошибка, число 3 неспособно изменять свое значениеПоследний пример иллюстрирует требование к левому операнду операции присваивания. Он должен быть способен хранить и изменять свое значение. Переменные, объявленные в программе, обладают подобным свойством. В следующем фрагменте программы int x = 0;x = 3;x = 4;x = x + 1;вначале объявляется переменная x с начальным значением 0. После этого значение x изменяется на 3, 4 и затем 5. Опять-таки, обратим внимание на последнюю строчку. При вычислении операции присваивания сначала вычисляется правый операнд, а затем левый. Когда вычисляется выражение x + 1, значение переменной x равно 4. Поэтому значение выражения x + 1 равно 5. После вычисления операции присваивания (или, проще говоря, после присваивания) значение переменной x становится равным 5. У операции присваивания тоже есть результат. Он равен значению левого операнда. Таким образом, операция присваивания может участвовать в более сложном выражении: z = (x = y + 3);В приведенном примере переменным x и z присваивается значение y + 3. Очень часто в программе приходится значение переменной увеличивать или уменьшать на единицу. Для того чтобы сделать эти действия наиболее эффективными и удобными для использования, применяются предусмотренные в Си++ специальные знаки операций: ++ (увеличить на единицу) и -- (уменьшить на единицу). Существует две формы этих операций: префиксная и постфиксная. Рассмотрим их на примерах. int x = 0;++x;Значение x увеличивается на единицу и становится равным 1. --x;Значение x уменьшается на единицу и становится равным 0. int y = ++x;Значение x опять увеличивается на единицу. Результат операции ++ – новое значение x, т.е. переменной y присваивается значение 1. int z = x++;Здесь используется постфиксная запись операции увеличения на единицу. Значение переменной x до выполнения операции равно 1. Сама операция та же – значение x увеличивается на единицу и становится равным 2. Однако результат постфиксной операции – это значение аргумента до увеличения. Таким образом, переменной z присваивается значение 1. Аналогично, результатом постфиксной операции уменьшения на единицу является начальное значение операнда, а префиксной – его конечное значение.
Подобными мотивами оптимизации и сокращения записи руководствовались создатели языка Си (а затем и Си++), когда вводили новые знаки операций типа "выполнить операцию и присвоить". Довольно часто одна и та же переменная используется в левой и правой части операции присваивания, например: x = x + 5;y = y * 3;z = z – (x + y);В Си++ эти выражения можно записать короче: x += 5;y *= 3;z -= x + y;Т.е. запись oper= означает, что левый операнд вначале используется как левый операнд операции oper, а затем как левый операнд операции присваивания результата операции oper. Кроме краткости выражения, такая запись облегчает оптимизацию программы компилятором.
Воспользуйтесь поиском по сайту: ©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...
|