Топ контрибуторов
loading
loading
Знаете ли Вы, что

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

Лента обновлений
ссылка Oct 22 23:24
Комментарий от alexcei88:
В вопросе переменная s даже не выводиться, а выводитьс...
ссылка Oct 22 17:46
Комментарий от AlexFurm:
Это UB, так можно вызывать только статические функции ч...
ссылка Oct 22 17:43
Комментарий от AlexFurm:
Любые битовые операции с signed это UB
ссылка Oct 21 20:30
Комментарий от yoori:
Любой вариант скомпилируется если компилировать не в конеч...
ссылка Oct 21 16:53
Добавлен вопрос в тест QA (Quality Assurance)
Статистика

Тестов: 153, вопросов: 8596. Пройдено: 443368 / 2177510.

Необходимость конструктора копирования в С++

head tail Статья
категория
C++
дата25.11.2013
авторlesha1980
голосов18

В языке С++, в отличие, к примеру, от С# присутствует понятие конструктора копирования. Этот конструктор вызывается при создании копии объекта некоторого класса, что инициализирует новый объект того же типа. Этот же конструктор вызывается и в менее очевидных случаях, когда происходит создание временной копии некоторого объекта.

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

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


      MyClassB; 
      MyClassA=B;

При этом происходит так называемое «поверхностное» копирование. Этот вид копирования в отличие от «глубокого» предполагает копирование значений элементов объекта в соответствующие элементы другого объекта. Однако если одним из этих элементов окажется указатель на выделенную память, тогда согласно такому копированию будет скопировано лишь значение самого указателя в соответствующее поле другого объекта, содержащее аналогичный указатель. В таком случае мы окажемся перед ситуацией, когда два указателя из разных объектов того же типа будут указывать на одну и ту же область памяти. Такой подход создает явные сложности при необходимости удаления объектов. Удаление объекта В повлечет за собой и высвобождение памяти, на которую указывает указатель. Однако на ту же память продолжает указывать указатель объекта А и в случае его удаления деструктор попытается во второй раз высвободить одну и ту же память, что приведет к ошибке.

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


    ClassName{
    public:
          ClassName(){
                  cout << “classname_constructor”;
            }
          ~ClassName(){
                 cout <<”~classname_destructor”;
           }
        }
     int main (){
       ClassName cname;
       ClassName cname1 = cname;
       return 0;
    }

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


   classname_constructor
   ~classname_destructor
   ~classname_destructor

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

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

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


ClassName{
         public:
           ClassName(){
                  cout << “classname_constructor”;
            }
          ~ClassName(){
                 cout <<”~classname_destructor”;
           }
        }
       void function (ClassName cname){
            cout << “function”;
        }
     int main (){
       ClassName cname;
       function(cname);
       return 0;
    }
   Результатом работы программы станет:
    classname_constructor
    function
   ~classname_destructor
   ~classname_destructor

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

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

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

 
      ClassName{
         public:
          ClassName(){
                  cout << “classname_constructor”;
           }
          ~ClassName(){
                cout <<”~classname_destructor”;
           }
        }
       ClassName function (){
           ClassName cname1;
           cout << “function”;
           return cname1;
        }
     int main (){
       ClassName cname;
       function();
       return 0;
}
Результатом работы программы станет:
 
    classname_constructor
    classname_constructor
    function
    ~classname_destructor
    ~classname_destructor
    ~classname_destructor

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

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

Чтобы избежать таких сложностей, как правило, в классе реализовывается явный конструктор копирования. Изначально в любом классе присутствуют неявный конструктор копирования, конструктор по умолчанию и деструктор. Однако ценность неявного конструктора копирования невелика по причине его «поверхностной» работы. Чтобы осуществить «глубокое копирование», такой конструктор определяют явно, обозначая параметры копирования. Синтаксис такого конструктора выглядит следующим образом:

  
     имя_класс (const имя_класса & object ){
     }
Конструктор копирования получает в качестве параметра ссылку на неизменяемый объект. Реализуем этот конструктор в нашем классеClassName, к примеру, для случая с инициализацией объекта.
  
  ClassName{
         public:
          ClassName()
                  cout << “classname_constructor”;
            }
         ClassName(const ClassName &obj){
                 cout << “copy_object”;
        }
          ~ClassName(){
                 cout <<”~classname_destructor”;
           }
        }
     int main (){
       ClassName cname;
       ClassName cname1 = cname;
       return 0;
}
   Результатом работы программы станет:
    classname_constructor
    copy_object
    ~classname_destructor
    ~classname_destructor

В этом случае создается объект cname (сообщение «classname_constructor») и его побитовая копия, о чем свидетельствует copy_object. Затем для каждого из объектов вызываются деструкторы. Таким образом, у нас получается инструмент, с помощью которого мы сможем контролировать динамическое выделение памяти и ее высвобождение деструкторами. Если в классе предполагается выделение динамической памяти, тогда в конструкторе копирования не просто происходит присваивание одноименных полей класса, но и очередное выделение динамической памяти с поэлементным копированием значений, находящихся в выделенной памяти. Таким образом, получается два объекта с элементом, указывающим на различные области выделенной памяти, и деструктор будет правильно уничтожать объекты. Такой вид копирования имеет название «глубокого».

Заключение

Необходимость реализации явного конструктора копирования в С++ вытекает сугубо из практических соображений, позволяющих избежать некоторых неудобств при работе с объектами класса. Это инициализация новых объектов класса (при их создании) уже существующими объектами того же класса, передача объектов в качестве аргументов в функцию и возврат объекта из функции. Хотя конструктор копирования и определен по умолчанию в классе, но его функциональности недостаточно для полной реализации, так как в нем определено лишь «поверхностное» копирование. Многие программы требуют более глубокого подхода в своей разработке с применением «глубокого» копирования, для чего конструктор копирования приходится переопределять явно. Надо также сказать, что данный конструктор не применяется в выражениях вида А=В, где используется подход с перегрузкой операции присваивания. Однако он важен именно при инициализации вновь создаваемого объекта.

Если Вам понравилась статья, проголосуйте за нее

Голосов: 18  loading...
lalka   AlexVovolka   ghostromaniv   Eugene_V   oxmap   bahdannn   ura_arendar   ingwarsmith   vadim092   iAndrew5   yrk93   JustPain   ksune4ka_00x   miha2227   mazahaka_tod   NazarPalko   serothim   PetrenkoSergii