►"Мелкие " и "глубокие " копии...217

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

_________________

217 стр. Глава 18. Копирующий конструктор

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

    /* ShallowCopy — мелкое копирование */

    /*              неприменимо при захвате */

    /*                ресурсов */

    //

    #include <cstdio>

    #include <cstdlib>

    #include <iostream>

    #include <string.h>

    using namespace std ;

    class Person

    {

      public :

        Person( char *pN )

        {

            cout << "Конструирование " " << pN << " " " << endl ;

            pName = new char[ strlen( pN ) + 1 ] ;

            if ( pName != 0 )

            {

                strcpy( pName , pN ) ;

            }

        }

        ~Person( )

        {

            cout << "Деструкция " " << pName << " " " << endl ;

            strcpy( pName , "Уже освобождённая память" ) ;

            /* delete pName ; */

        }

      protected :

        char *pName ;

    } ;

    void fn( )

    {

        /* Создание нового объекта */

         Person p1( "Достаточно длинное имя" ) ;

        /* Копирование p1 в р2 */

    Person p2(p1);

    }

    int main( int argcs , char* pArgs[ ] )

    {

        setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

        cout << "Вызов fn( )" << endl ;

        fn( ) ;

        cout << "Возврат из fn( )" << endl ;

        /* Пауза для того, чтобы посмотреть на результат работы программы */

        system( "PAUSE" ) ; return 0 ;

    }

_________________

218 стр. Часть 3. Введение в классы

Эта программа порождает следующий вывод:

    Вызов fn( )

    Конструирование "Достаточно длинное имя"

    Деструкция "Достаточно длинное имя"

    Деструкция "Уже освобождённая память"

    Возврат из fn( )

    Press any key to continue...

В этом примере конструктор для Person выделяет память из кучи для хранения в ней имени произвольной длины, что невозможно при использовании массивов. Деструктор возвращает эту память в кучу. Основная программа вызывает функцию fn( ), которая создаёт объект p1, описывающий человека, после чего создаётся копия этого объекта — р2. Оба объекта автоматически уничтожаются при выходе из функции fn( ).

После запуска этой программы вы получите сообщение только от одного конструктора. Это неудивительно, поскольку копия р2 создаётся с помощью предоставляемого С++ конструктора копирования по умолчанию, а он не выводит никаких сообщений. Однако, после того как p1 и р2 выходят из области видимости, вы не получите двух сообщений о ликвидации объектов, как можно было ожидать. Первый конструктор выводит ожидаемое сообщение о деструкции объекта, но второй деструктор сообщает, что память уже была освобождена.

«Если бы мы действительно освобождали память в программе, то программа после попытки освободить уже освобождённую память оказалась бы в нестабильном состоянии и могла аварийно завершиться.»

[Атас!]

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

Когда объекты ликвидируются, деструктор для р2 первым получает доступ к этому блоку памяти. Этот деструктор стирает имя и освобождает блок памяти. К тому времени как деструктор p1 получает доступ к этому блоку, память уже очищена, а имя стёрто. Теперь понятно, откуда взялось сообщение об ошибке. Суть проблемы проиллюстрирована на рис. 18.1. Объект p1 копируется в новый объект р2, но не копируются используемые им ресурсы. Таким образом, р1 и р2 указывают на один и тот же ресурс ( в данном случае это блок памяти ). Такое явление называется "мелким" ( shallow ) копированием, поскольку при этом копируются только члены класса как таковые.

«Решение этой проблемы визуально показано на рис. 18.2. В данном случае нужен такой копирующий конструктор, который будет выделять ресурсы для нового объекта. Давайте добавим такой конструктор к классу и посмотрим, как он работает ( здесь приведён только фрагмент программы, полностью находящейся на прилагаемом компакт-диске ).»

[Диск]

    class Person

    {

      public :

        Person( char *pN )

        {

            cout << "Конструирование " << pN << endl ;

            pName = new char[ strlen( pN ) + 1 ] ;

            if ( pName != 0 )

            {

                strcpy( pName , pN ) ;

            }

        }

_________________

219 стр. Глава 18. Копирующий конструктор

        /* Копирующий конструктор выделяет новый блок памяти из кучи */

        Person( Person& p )

        {

            cout << "Копирование " << p.pName

                   << " в собственный блок" << endl ;

            pName = new char[ strlen( p.pName ) + 1 ] ;

            if ( pName != 0 )

            {

                strcpy( pName , p.pName ) ;

            }

        }

        ~Person( )

        {

            cout << "Деструкция " << pName << endl ;

            strcpy( pName , "Уже освобождённая память" ) ;

            /* delete pName ; */

        }

      protected :

        char *pName ;

    } ;

Рис. 18.1. Мелкое копирование объекта p1 в р2

Здесь копирующий конструктор выделяет новый блок памяти для имени, а затем копирует содержимое блока памяти исходного объекта в этот новый блок ( рис. 18.2 ). Такое копирование называется "глубоким" ( deep ), поскольку копирует не только элементы, но и занятые ими ресурсы ( конечно, аналогия, как говорится, притянута за уши, но ничего не поделаешь — не я придумал эти термины ).

Запуск программы с новым копирующим конструктором приведёт к выводу на экран следующих строк:

    Вызов fn( )

    Конструирование Достаточно_длинное_имя

    Копирование Достаточно_длинное_имя в собственный блок

    Деструкция Достаточно_длинное_имя

    Деструкция Достаточно_длинное_имя

    Возврат из fn( )

    Press any key to continue...

_________________

220 стр. Часть 3. Введение в классы

Как видите, теперь указатели на строки в р1 и р2 указывают на разные данные.

Рис. 18.2. Глубокое копирование объекта p1 в р2