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

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

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

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

682

vector< Point > cvs;

// пустой

int cv_cnt = calc_control_vertices();

//зарезервировать память для хранения cv_cnt объектов класса Point

//cvs все еще пуст ...

cvs.reserve( cv_cnt );

//открыть файл и подготовиться к чтению из него ifstream infile( "spriteModel" ); istream_iterator<Point> cvfile( infile ),eos;

//вот теперь можно вставлять элементы

copy( cvfile, eos, inserter( cvs, cvs.begin() ));

(Алгоритм copy(), итератор вставки inserter и потоковый итератор чтения istream_iterator рассматривались в главе 12.) Поведение объектов list (список) и deque (двусторонняя очередь) аналогично поведению объектов vector (векторов).

Вставка объекта в любой из этих контейнеров осуществляется с помощью копирующего конструктора.

Упражнение 14.9

Какие из приведенных инструкций неверны? Исправьте их.

(b)Account iA[1024] = {

"Nhi", "Le", "Jon", "Mike", "Greg", "Brent", "Hank"

(a) Account *parray[10] = new Account[10];

"Roy", "Elena" };

(d) string as[] = *ps;

(c) string *ps=string[5]("Tina","Tim","Chyuan","Mira","Mike");

Упражнение 14.10

Что лучше применить в каждой из следующих ситуаций: статический массив (такой, как Account pA[10]), динамический массив или вектор? Объясните свой выбор.

Внутри функции Lut() нужен набор из 256 элементов для хранения объектов класса Color. Значения являются константами.

Необходимо хранить набор из неизвестного числа объектов класса Account. Данные счетов читаются из файла.

Функция gen_words(elem_size) должна сгенерировать и передать обработчику текста набор из elem_size строк.

Упражнение 14.11

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

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

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

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

690

(a)Word::Word( char *ps, int count = 1 )

:_ps( new char[strlen(ps)+1] ),

_count( count )

{

if ( ps )

strcpy( _ps, ps ); else {

_ps = 0; _count = 0;

}

(b) class CL1 { public:

CL1() { c.real(0.0); c.imag(0.0); s = "not set"; } // ...

private: complex<double> c; string s;

(c) class CL2 { public:

CL2( map<string,location> *pmap, string key )

:_text( key ), _loc( (*pmap)[key] ) {}

//...

private: location _loc;

string _text;

Account oldAcct( "Anna Livia Plurabelle" );

}

}

};

14.6. Почленная инициализация A

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

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

691

Account newAcct( oldAcct );

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

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

class Account {

 

public:

 

// ...

 

private:

*_name;

char

unsigned int

_acct_nmbr;

double

_balance;

определение нашего класса Account:

};

inline Account::

Account( const Account &rhs )

{

_name = rhs._name; _acct_nmbr = rhs._acct_nmbr; _balance = rhs._balance;

то можно представить, что копирующий конструктор по умолчанию определен так:

}

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

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

Account newAcct( oldAcct );

extern bool cash_on_hand( Account acct ); if ( cash_on_hand( oldAcct ))

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

//...

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