Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих (Стенли Липпман) 3-е хххх.pdf
Скачиваний:
84
Добавлен:
30.05.2015
Размер:
5.92 Mб
Скачать

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

683

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

// печально: не проверяется, что parray адресует массив

неверная запись delete parray;

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

вместо

delete [] parray;

Наличие пары скобок заставляет компилятор найти размер массива. Затем к каждому элементу по очереди применяется деструктор (всего size раз). Если же скобок нет, уничтожается только один элемент. В любом случае освобождается вся память, занятая массивом.

При обсуждении первоначального варианта языка С++ много спорили о том, должно ли наличие квадратных скобок инициировать поиск или же (как было в исходной

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

спецификации) лучше поручить программисту явно указывать размер массива: delete p[10] parray;

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

14.5. Список инициализации членов

#include <string> class Account { public:

// ...

private:

unsigned int _acct_nmbr; double _balance; string _name;

Модифицируем наш класс Account, объявив член _name типа string:

};

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

684

Придется заодно изменить и конструкторы. Возникает две проблемы: поддержание

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

Исходный конструктор Account с двумя параметрами

Account( const char*, double = 0.0 );

string new_client( "Steve Hall" );

не может инициализировать член типа string. Например:

Account new_acct( new_client, 25000 );

не будет компилироваться, так как не существует неявного преобразования из типа string в тип char*. Инструкция

Account new_acct( new_client.c_str(), 25000 );

правильна, но вызовет у пользователей класса недоумение. Одно из решений добавить новый конструктор вида:

Account( string, double = 0.0 );

Если написать:

Account new_acct( new_client, 25000 );

Account *open_new_account( const char *nm )

{

Account *pact = new Account( nm ); // ...

return pacct;

вызывается именно этот конструктор, тогда как старый код

}

по-прежнему будет приводить к вызову исходного конструктора с двумя параметрами.

Так как в классе string определено преобразование из типа char* в тип string (преобразования классов обсуждаются в этой главе ниже), то можно заменить исходный конструктор на новый, которому в качестве первого параметра передается тип string. В таком случае, когда встречается инструкция:

Account myAcct( "Tinkerbell" );

"Tinkerbell" преобразуется во временный объект типа string. Затем этот объект передается новому конструктору с двумя параметрами.

При проектировании приходится идти на компромисс между увеличением числа конструкторов класса Account и несколько менее эффективной обработкой аргументов

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

685

типа char* из-за необходимости создавать временный объект. Мы предоставили две версии конструктора с двумя параметрами. Тогда модифицированный набор

#include <string>

class Account { public:

Account();

Account( const char*, double=0.0 ); Account( const string&, double=0.0 ); Account( const Account& );

//...

private:

//...

конструкторов Account будет таким:

};

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

1.где вызывается конструктор по умолчанию? Внутри конструктора по умолчанию класса Account;

2.где вызывается копирующий конструктор? Внутри копирующего конструктора класса Account и внутри конструктора с двумя параметрами, принимающего в качестве первого тип string;

3.как передать аргументы конструктору класса, являющегося членом другого класса? Это необходимо делать внутри конструктора Account с двумя параметрами, принимающего в качестве первого тип char*.

Решение заключается в использовании списка инициализации членов (мы упоминали о нем в разделе 14.2). Члены, являющиеся классами, можно явно инициализировать с помощью списка, состоящего из разделенных запятыми пар имя члена/значение”. Наш конструктор с двумя параметрами теперь выглядит так (напомним, что _name это член,

inline Account::

Account( const char* name, double opening_bal ) : _name( name ), _balance( opening_bal )

{

_acct_nmbr = het_unique_acct_nmbr();

являющийся объектом класса string):

}

Список инициализации членов следует за сигнатурой конструктора и отделяется от нее двоеточием. В нем указывается имя члена, а в скобках начальные значения, что аналогично синтаксису вызова функции. Если член является объектом класса, то эти значения становятся аргументами, передаваемыми подходящему конструктору, который затем и используется. В нашем примере значение name передается конструктору string, который применяется к члену _name. Член _balance инициализируется значением opening_bal.

Аналогично выглядит второй конструктор с двумя параметрами:

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

686

inline Account::

Account( const string& name, double opening_bal ) : _name( name ), _balance( opening_bal )

{

_acct_nmbr = het_unique_acct_nmbr();

}

В этом случае вызывается копирующий конструктор string, инициализирующий член _name значением параметра name типа string.

Часто у новичков возникает вопрос: в чем разница между использованием списка инициализации и присваиванием значений членам в теле конструктора? Например, в чем

inline Account::

Account( const char* name, double opening_bal ) : _name( name ), _balance( opening_bal )

{

_acct_nmbr = het_unique_acct_nmbr();

разница между

}

Account( const char* name, double opening_bal )

{

_name = name;

_balance = opening_bal;

_acct_nmbr = het_unique_acct_nmbr();

и

}

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

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

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

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

687

inline Account:: Account()

{

_name = ""; _balance = 0.0; _acct_nmbr = 0;

}

то фаза инициализации будет неявной. Еще до выполнения тела конструктора вызывается конструктор по умолчанию класса string, ассоциированный с членом _name. Это означает, что присваивание _name пустой строки излишне.

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

inline Account::

Account() : _name( string() )

{

_balance = 0.0; _acct_nmbr = 0;

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

}

Мы удалили ненужное присваивание _name из тела конструктора. Явный же вызов конструктора по умолчанию string излишен. Ниже приведена эквивалентная, но более

inline Account:: Account()

{

_balance = 0.0; _acct_nmbr = 0;

компактная версия:

}

Однако мы еще не ответили на вопрос об инициализации двух членов встроенных типов. Например, так ли существенно, где происходит инициализация _balance: в списке инициализации или в теле конструктора? Инициализация и присваивание членам, не являющимся объектами классов, эквивалентны как с точки зрения результата, так и с точки зрения производительности (за двумя исключениями). Мы предпочитаем

// предпочтительный стиль инициализации inline Account::

Account() : _balance( 0.0 ), _acct_nmbr( 0 )

использовать список:

{}

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

688

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

class ConstRef { public:

ConstRef(int ii ); private:

int i;

const int ci; int &ri;

};

ConstRef::

ConstRef( int ii )

{// присваивание

i = ii;

// правильно

ci = ii;

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

ri = i;

// ошибка: ri не инициализирована

компилятор выдаст ошибку:

}

К началу выполнения тела конструктора инициализация всех константных членов и членов-ссылок должна быть завершена. Для этого нужно указать их в списке

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

ConstRef:: ConstRef( int ii )

: ci( ii ), ri ( i )

инициализации. Правильная реализация предыдущего примера такова:

{ i = ii; }

Каждый член должен встречаться в списке инициализации не более одного раза. Порядок инициализации определяется не порядком следования имен в списке, а порядком

class Account { public:

// ...

private:

unsigned int _acct_nmbr; double _balance; string _name;

объявления членов. Если дано следующее объявление членов класса Account:

};

inline Account::

Account() : _name( string() ), _balance( 0.0 ), _acct_nmbr( 0 )

то порядок инициализации для такой реализации конструктора по умолчанию

{}

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

689

будет следующим: _acct_nmbr, _balance, _name. Однако члены, указанные в списке (или в неявно инициализируемом члене-объекте класса), всегда инициализируются раньше, чем производится присваивание членам в теле конструктора. Например, в

inline Account::

Account( const char* name, double bal ) : _name( name ), _balance( bal )

{

_acct_nmbr = get_unique_acct_nmbr();

следующем конструкторе:

}

порядок инициализации такой: _balance, _name, _acct_nmbr.

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

class X { int i; int j;

public:

//видите проблему? X( int val )

: j( val ), i( j ) {}

//...

один член класса используется для инициализации другого:

};

Кажется, что перед использованием для инициализации i член j уже инициализирован значением val, но на самом деле i инициализируется первым, для чего применяется еще неинициализированный член j. Мы рекомендуем помещать инициализацию одного

// предпочтительная идиома

члена другим (если вы считаете это необходимым) в тело конструктора:

X::X( int val ) : i( val ) { j = i; }

Упражнение 14.12

Что неверно в следующих определениях конструкторов? Как бы вы исправили обнаруженные ошибки?