Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
237
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

672

ресурсов, выделенных либо в конструкторе, либо во время жизни объекта, например освобождение замка или памяти, выделенной оператором new.

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

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

{

//начало критического участка программы

#ifdef PROFILE Timer t;

#endif

//критический участок

//t уничтожается автоматически

//отображается затраченное время ...

которые мы хотим профилировать, таким образом:

}

Чтобы убедиться в том, что мы понимаем поведение деструктора (да и конструктора

(1)#include "Account.h"

(2)Account global( "James Joyce" );

(3)int main()

(4){

(5)Account local( "Anna Livia Plurabelle", 10000 );

(6)Account &loc_ref = global;

(7)Account *pact = 0;

(8)

(9){

(10)Account local_too( "Stephen Hero" );

(11)pact = new Account( "Stephen Dedalus" );

(12)}

(13)

(14)delete pact;

тоже), разберем следующий пример:

(15)}

Сколько здесь вызывается конструкторов? Четыре: один для глобального объекта global в строке (2); по одному для каждого из локальных объектов local и local_too в строках

(5) и (10) соответственно, и один для объекта, распределенного в хипе, в строке (11). Ни объявление ссылки loc_ref на объект в строке (6), ни объявление указателя pact в строке (7) не приводят к вызову конструктора. Ссылка это псевдоним для уже сконструированного объекта, в данном случае для global. Указатель также лишь адресует объект, созданный ранее (в данном случае распределенный в хипе, строка (11)), или не адресует никакого объекта (строка (7)).

Аналогично вызываются четыре деструктора: для глобального объекта global, объявленного в строке (2), для двух локальных объектов и для объекта в хипе при вызове delete в строке (14). Однако в программе нет инструкции, с которой можно связать

С++ для начинающих

673

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

Конструкторы и деструкторы глобальных объектов вызываются на стадиях инициализации и завершения выполнения программы. Хотя такие объекты нормально ведут себя при использовании в том файле, где они определены, но их применение в ситуации, когда производятся ссылки через границы файлов, становится в C++ серьезной проблемой.4

Деструктор не вызывается, когда из области видимости выходит ссылка или указатель на объект (сам объект при этом остается).

С++ с помощью внутренних механизмов препятствует применению оператора delete к указателю, не адресующему никакого объекта, так что соответствующие проверки кода

// необязательно: неявно выполняется компилятором

необязательны:

if (pact != 0 ) delete pact;

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

#include <memory> #include "Account.h"

Account global( "James Joyce" ); int main()

{

Account local( "Anna Livia Plurabelle", 10000 ); Account &loc_ref = global;

auto_ptr<Account> pact( new Account( "Stephen Dedalus" ));

{

Account local_too( "Stephen Hero" );

}

// объект auto_ptr уничтожается здесь

адресации другого объекта только присваиванием его другому auto_ptr):

}

4 См. статью Джерри Шварца в [LIPPMAN96b], где приводится дискуссия по этому поводу и описывается решение, остающееся пока наиболее распространенным.

С++ для начинающих

674

14.3.1. Явный вызов деструктора

Иногда вызывать деструктор для некоторого объекта приходится явно. Особенно часто такая необходимость возникает в связи с оператором new (см. раздел 8.4). Рассмотрим пример. Когда мы пишем:

char *arena = new char[ sizeof Image ];

то из хипа выделяется память, размер которой равен размеру объекта типа Image, она не инициализирована и заполнена случайными битами. Если же написать:

Image *ptr = new (arena) Image( "Quasimodo" );

то никакой новой памяти не выделяется. Вместо этого переменной ptr присваивается адрес, ассоциированный с переменной arena. Теперь память, на которую указывает ptr, интерпретируется как занимаемая объектом класса Image, и конструктор применяется к уже существующей области. Таким образом, оператор размещения new() позволяет сконструировать объект в ранее выделенной области памяти.

Закончив работать с изображением Quasimodo, мы можем произвести какие-то операции с изображением Esmerelda, размещенным по тому же адресу arena в памяти:

Image *ptr = new (arena) Image( "Esmerelda" );

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

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

Image, но если мы применим оператор delete: delete ptr;

то, помимо вызова деструктора, еще и возвратим в хип память, чего делать не следовало бы. Вместо этого можно явно вызвать деструктор класса Image:

ptr->~Image();

сохранив отведенную под изображение память для последующего вызова оператора размещения new.

Отметим, что, хотя ptr и arena адресуют одну и ту же область памяти в хипе,

// деструктор не вызывается

применение оператора delete к arena delete arena;

не приводит к вызову деструктора класса Image, так как arena имеет тип char*, а компилятор вызывает деструктор только тогда, когда операндом в delete является указатель на объект класса, имеющего деструктор.

С++ для начинающих

675

14.3.2. Опасность увеличения размера программы

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

Account acct( "Tina Lee" ); int swt;

// ...

switch( swt ) { case 0:

return; case 1:

// что-то сделать return;

case 2:

// сделать что-то другое return;

// и так далее

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

}

компилятор подставит деструктор перед каждой инструкцией return. Деструктор класса Account невелик, и затраты времени и памяти на его подстановку тоже малы. В противном случае придется либо объявить деструктор невстроенным, либо реорганизовать программу. В примере выше инструкцию return в каждой метке case можно заменить инструкцией break с тем, чтобы у функции была единственная точка

// переписано для обеспечения единственной точки выхода switch( swt ) {

case 0: break;

case 1:

// что-то сделать break;

case 2:

// сделать что-то другое break;

//и так далее

}

//единственная точка выхода

выхода: return;

Упражнение 14.6

Напишите подходящий деструктор для приведенного набора членов класса, среди которых pstring адресует динамически выделенный массив символов:

С++ для начинающих

676

class NoName { public:

~NoName(); // ...

private:

char *pstring; int ival; double dval;

};

Упражнение 14.7

Необходим ли деструктор для класса, который вы выбрали в упражнении 14.3? Если нет, объясните почему. В противном случае предложите реализацию.

Упражнение 14.8

void mumble( const char *name, fouble balance, char acct_type )

{

Account acct;

if ( ! name ) return;

if ( balance <= 99 ) return;

switch( acct_type ) { case 'z': return; case 'a':

case 'b': return;

}

// ...

Сколько раз вызываются деструкторы в следующем фрагменте:

}

14.4. Массивы и векторы объектов

Массив объектов класса определяется точно так же, как массив элементов встроенного типа. Например:

Account table[ 16 ];

определяет массив из 16 объектов Account. Каждый элемент по очереди инициализируется конструктором по умолчанию. Можно и явно передать конструкторам аргументы внутри заключенного в фигурные скобки списка инициализации массива. Строка:

Account pooh_pals[] = { "Piglet", "Eeyore", "Tigger" };

определяет массив из трех элементов, инициализируемых конструкторами:

С++ для начинающих

 

677

 

Account( "Piglet", 0.0

);

// первый элемент (Пятачок)

 

 

Account( "Eeyore", 0.0

);

// второй элемент (Иа-Иа)

 

Account( "Tigger", 0.0

);

// третий элемент (Тигра)

 

 

 

 

 

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

Account pooh_pals[] = {

Account( "Piglet", 1000.0 ),

Account( "Eeyore", 1000.0 ),

Account( "Tigger", 1000.0 )

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

};

Чтобы включить в список инициализации массива конструктор по умолчанию, мы

Account pooh_pals[] =

{

 

// Бука

Account( "Woozle",

10.0 ),

),

Account( "Heffalump", 10.0

// Слонопотам

Account();

 

 

 

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

};

Account pooh_pals[3] = {

Account( "Woozle", 10.0 ),

Account( "Heffalump", 10.0 )

Эквивалентный массив из трех элементов можно объявить и так:

};

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

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

pooh_pals[0];

обращается к Piglet, а

pooh_pals[1];

к Eeyore и т.д. Для доступа к членам объекта, находящегося в некотором элементе массива, мы сочетаем операторы взятия индекса и доступа к членам:

pooh_pals[1]._name != pooh_pals[2]._name;

С++ для начинающих

678

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

Объявление

Account *pact = new Account[ 10 ];

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

Чтобы уничтожить массив, адресованный указателем pact, необходимо применить

// увы! это не совсем правильно

оператор delete. Однако написать delete pact;

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

//правильно:

//показывает, что pact адресует массив

между оператором delete и адресом удаляемого объекта: delete [] pact;

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

14.4.1. Инициализация массива, распределенного из хипа A

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

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

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

С++ для начинающих

679

#include <utility> #include <vector > #include <new> #include <cstddef> #include "Accounts.h"

typedef pair<char*, double> value_pair;

/* init_heap_array()

*объявлена как статическая функция-член

*обеспечивает выделение памяти из хипа и инициализацию

*массива объектов

*init_values: пары начальных значений элементов массива

*elem_count: число элементов в массиве

*если 0, то размером массива считается размер вектора

*init_values

*/

Account*

Account:: init_heap_array(

vector<value_pair> &init_values, vector<value_pair>::size_type elem_count = 0 )

{

vector<value_pair>::size_type vec_size = init_value.size();

if ( vec_size == 0 && elem_count == 0 ) return 0;

//размер массива равен либо elem_count,

//либо, если elem_count == 0, размеру вектора ...

size_t elems = elem_count

?elem_count : vec_size();

//получить блок памяти для размещения массива char *p = new char[sizeof(Account)*elems];

//по отдельности инициализировать каждый элемент массива int offset = sizeof( Account );

for ( int ix = 0; ix < elems; ++ix )

{

//смещение ix-ого элемента

//если пара начальных значений задана,

//передать ее конструктору;

//в противном случае вызвать конструктор по умолчанию

if ( ix < vec_size )

new( p+offset*ix ) Account( init_values[ix].first, init_values[ix].second );

else new( p+offset*ix ) Account;

}

//отлично: элементы распределены и инициализированы;

//вернуть указатель на первый элемент

return (Account*)p;

}

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

С++ для начинающих

680

char *p = new char[sizeof(Account)*elems];

Далее программа в цикле обходит этот блок, присваивая на каждой итерации переменной p адрес следующего элемента и вызывая либо конструктор с двумя параметрами, если

for ( int ix = 0; ix < elems; ++ix )

{

if ( ix < vec_size )

new( p+offset*ix ) Account( init_values[ix].first, init_values[ix].second );

else new( p+offset*ix ) Account;

задана пара начальных значений, либо конструктор по умолчанию:

}

В разделе 14.3 говорилось, что оператор размещения new позволяет применить конструктор класса к уже выделенной области памяти. В данном случае мы используем new для поочередного применения конструктора класса Account к каждому из выделенных элементов массива. Поскольку при создании инициализированного массива мы подменили стандартный механизм выделения памяти, то должны сами позаботиться о ее освобождении. Оператор delete работать не будет:

delete [] ps;

Почему? Потому что ps (мы предполагаем, что эта переменная была инициализирована вызовом init_heap_array()) указывает на блок памяти, полученный не с помощью стандартного оператора new, поэтому число элементов в массиве компилятору

void Account::

dealloc_heap_array( Account *ps, size_t elems )

{

for ( int ix = 0; ix < elems; ++ix ) ps[ix].Account::~Account();

delete [] reinterpret_cast<char*>(ps);

неизвестно. Так что всю работу придется сделать самим:

}

Если в функции инициализации мы пользовались арифметическими операциями над указателями для доступа к элементам:

new( p+offset*ix ) Account;

то здесь мы обращаемся к ним, задавая индекс в массиве ps:

ps[ix].Account::~Account();

Хотя и ps, и p адресуют одну и ту же область памяти, ps объявлен как указатель на объект класса Account, а p как указатель на char. Индексирование p дало бы ix-й байт, а не ix-й объект класса Account. Поскольку с p ассоциирован не тот тип, что нужно,

С++ для начинающих

681

арифметические операции над указателями приходится программировать самостоятельно.

Мы объявляем обе функции статическими членами класса:

class Account { public:

// ...

static Account* init_heap_array( vector<value_pair> &init_values, vector<value_pair>::size_type elem_count = 0 ); static void dealloc_heap_array( Account*, size_t );

// ...

typedef pair<char*, double> value_pair; };

14.4.2. Вектор объектов

Когда определяется вектор из пяти объектов класса, например:

vector< Point > vec( 5 );

то инициализация элементов производится в следующем порядке5:

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

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

3.Временный объект уничтожается.

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

Point pa[ 5 ];

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

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

5 Сигнатура ассоциированного конструктора имеет следующий смысл.

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

explicit vector( size_type n, const T& value=T(), const Allocator&=Allocator());