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

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

659

вмешательства программиста, что увеличивает вероятность появления ошибок (забыл

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

Так нужно ли применять явную инициализацию вместо конструкторов? Да. Для

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

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

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

Примечание [O.A.4]: Нумера ция сносок сбита.

14.2. Конструктор класса

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

class Account { public:

//конструктор по умолчанию ...

Account();

//...

private:

char *_name;

unsigned int _acct_nmbr; double _balance;

именем класса. Для объявления конструктора по умолчанию мы пишем2:

};

Единственное синтаксическое ограничение, налагаемое на конструктор, состоит в том, что он не должен иметь тип возвращаемого значения, даже void. Поэтому следующие

// ошибки: у конструктора не может быть типа возвращаемого значения void Account::Account() { ... }

объявления ошибочны:

Account* Account::Account( const char *pc ) { ... }

1 Более подробное обсуждение этой темы с примерами и приблизительными оценками производительности см. в [LIPPMAN96a].

2 В реальной программе мы объявили бы член _name как имеющий тип string. Здесь он объявлен как C-строка, чтобы отложить рассмотрение вопроса об инициализации членов класса до раздела 14.4.

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

660

Количество конструкторов у одного класса может быть любым, лишь бы все они имели разные списки формальных параметров.

Откуда мы знаем, сколько и каких конструкторов определить? Как минимум, необходимо присвоить начальное значение каждому члену, который в этом нуждается. Например, номер счета либо задается явно, либо генерируется автоматически таким образом, чтобы гарантировать его уникальность. Предположим, что он будет создаваться автоматически. Тогда мы должны разрешить инициализировать оставшиеся два члена _name и _balance:

Account( const char *name, double open_balance );

Объект класса Account, инициализируемый конструктором, можно объявить следующим образом:

Account newAcct( "Mikey Matz", 0 );

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

Account( const char *name );

Другой способ включить в конструктор с двумя параметрами значение по умолчанию, равное нулю:

Account( const char *name, double open_balance = 0.0 );

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

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

предоставляет полный интерфейс для указания начальных значений тех членов класса

class Account { public:

//конструктор по умолчанию ...

Account();

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

Account( const char*, double=0.0 );

const char* name() { return name; }

//...

private:

//...

Account, которые могут быть инициализированы пользователем:

};

Ниже приведены два примера правильного определения объекта класса Account, где конструктору передается один или два аргумента:

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

661

int main()

{

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

//с двумя параметрами

Account acct( "Ethan Stern" );

Account *pact = new Account( "Michael Lieberman", 5000 );

if ( strcmp( acct.name(), pact->name() ))

// ...

}

C++ требует, чтобы конструктор применялся к определенному объекту до его первого использования. Это означает, что как для acct, так и для объекта, на который указывает pact, конструктор будет вызван перед проверкой в инструкции if.

Компилятор перестраивает нашу программу, вставляя вызовы конструкторов. Вот как, по

//псевдокод на C++,

//иллюстрирующий внутреннюю вставку конструктора int main()

{

Account acct; acct.Account::Account("Ethan Stern", 0.0);

// ...

всей вероятности, будет модифицировано определение acct внутри main():

}

Конечно, если конструктор определен как встроенный, то он подставляется в точке вызова.

Обработка оператора new несколько сложнее. Конструктор вызывается только тогда, когда он успешно выделил память. Модификация определения pact в несколько

//псевдокод на C++,

//иллюстрирующий внутреннюю вставку конструктора при обработке new int main()

{

// ...

Account *pact; try {

pact = _new( sizeof( Account )); pact->Acct.Account::Account(

"Michael Liebarman", 5000.0);

}

catch( std::bad_alloc ) {

//оператор new закончился неудачей:

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

}

// ...

упрощенном виде выглядит так:

}

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

662

// в общем случае эти формы эквивалентны

Account acct1( "Anna Press" );

Account acct2 = Account( "Anna Press" );

Существует три в общем случае эквивалентных формы задания аргументов конструктора:

Account acct3 = "Anna Press";

Форма acct3 может использоваться только при задании единственного аргумента. Если аргументов два или более, мы рекомендуем пользоваться формой acct1, хотя допустима

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

иacct2.

Account acct1( "Anna Press" );

Новички часто допускают ошибку при объявлении объекта, инициализированного

// увы! работает не так, как ожидалось

конструктором по умолчанию:

Account newAccount();

Эта инструкция компилируется без ошибок. Однако при попытке использовать объект в

// ошибка компиляции ...

таком контексте:

if ( ! newAccount.name() ) ...

компилятор не сможет применить к функции нотацию доступа к членам класса.

//определяет функцию newAccount,

//а не объект класса

Определение

Account newAccount();

интерпретируется компилятором как определение функции без параметров, которая возвращает объект типа Account. Правильное объявление объекта класса,

// правильно: определяется объект класса ...

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

Account newAccount;

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

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

663

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

class Account { public:

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

Account( const char*, double=0.0 );

const char* name() { return name; }

//...

private:

//...

исключен:

};

Теперь при объявлении каждого объекта Account в конструкторе обязательно надо указать как минимум аргумент типа C-строки, но это скорее всего бессмысленно. Почему? Контейнерные классы (например, vector) требуют, чтобы для класса помещаемых в них элементов был либо задан конструктор по умолчанию, либо вообще никаких конструкторов. Аналогичная ситуация имеет место при выделении динамического массива объектов класса. Так, следующая инструкция вызвала бы ошибку

// ошибка: требуется конструктор по умолчанию для класса Account

компиляции для новой версии Account:

Account *pact = new Account[ new_client_cnt ];

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

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

// конструктор по умолчанию для класса Account inline Account::

Account() { _name = 0;

_balance = 0.0; _acct_nmbr = 0;

инициализирован корректными значениями:

}

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

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

664

Существует и альтернативный синтаксис: список инициализации членов, в котором через запятую указываются имена и начальные значения. Например, конструктор по

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

//списка инициализации членов

inline Account:: Account()

:_name(0),

_balance( 0.0 ), _acct_nmbr( 0 )

умолчанию можно переписать следующим образом:

{}

Такой список допустим только в определении, но не в объявлении конструктора. Он помещается между списком параметров и телом конструктора и отделяется двоеточием.

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

inline Account::

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

{

_name = new char[ strlen(name)+1 ]; strcpy( _name, name );

_acct_nmbr = get_unique_acct_nmbr();

списка инициализации членов:

}

get_unique_acct_nmbr() это не являющаяся открытой функция-член, которая возвращает гарантированно не использованный ранее номер счета.

Конструктор нельзя объявлять с ключевыми словами const или volatile (см. раздел

class Account { public:

Account() const; // ошибка Account() volatile; // ошибка // ...

13.3.5), поэтому приведенные записи неверны:

};

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

спецификатором const считается константным с момента завершения работы конструктора до момента запуска деструктора. То же самое относится и к спецификатору volatile.

Рассмотрим следующий фрагмент программы: