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

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

Среди других функций-членов конструктор выделяется тем, что его имя совпадает с именем класса. Для объявления конструктора по умолчанию мы пишем2:

class Account {

public:

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

Account();

// ...

private:

char *_name;

unsigned int _acct_nmbr;

double _balance;

};

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

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

void Account::Account() { ... }

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

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

Откуда мы знаем, сколько и каких конструкторов определить? Как минимум, необходимо присвоить начальное значение каждому члену, который в этом нуждается. Например, номер счета либо задается явно, либо генерируется автоматически таким образом, чтобы гарантировать его уникальность. Предположим, что он будет создаваться автоматически. Тогда мы должны разрешить инициализировать оставшиеся два члена _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 );

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

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

class Account {

public:

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

Account();

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

Account( const char*, double=0.0 );

const char* name() { return name; }

// ...

private:

// ...

};

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

int main()

{

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

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

Account acct( "Ethan Stern" );

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

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

// ...

}

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

Компилятор перестраивает нашу программу, вставляя вызовы конструкторов. Вот как, по всей вероятности, будет модифицировано определение acct внутри main():

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

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

int main()

{

Account acct;

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

// ...

}

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

Обработка оператора 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 закончился неудачей:

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

}

// ...

}

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

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

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;

Определять объект класса, не указывая списка фактических аргументов, можно в том случае, если в нем либо объявлен конструктор по умолчанию, либо вообще нет объявлений конструкторов. Если в классе объявлен хотя бы один конструктор, то не разрешается определять объект класса, не вызывая ни одного из них. В частности, если в классе определен конструктор, принимающий один или более параметров, но не определен конструктор по умолчанию, то в каждом определении объекта такого класса должны присутствовать необходимые аргументы. Можно возразить, что не имеет смысла определять конструктор по умолчанию для класса 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 придется включить проверку целостности объекта перед его использованием.

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

// конструктор по умолчанию класса 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 (см. раздел 13.3.5), поэтому приведенные записи неверны:

class Account {

public:

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

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

// ...

};

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

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

// в каком-то заголовочном файле

extern void print( const Account &acct );

// ...

int main()

{

// преобразует строку "oops" в объект класса Account

// с помощью конструктора Account::Account( "oops", 0.0 )

print( "oops" );

// ...

}

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

Непреднамеренные неявные преобразования классов, например трансформация "oops" в объект класса Account, оказались источником трудно обнаруживаемых ошибок. Поэтому в стандарт C++ было добавлено ключевое слово explicit, говорящее компилятору, что такие преобразования не нужны:

class Account {

public:

explicit Account( const char*, double=0.0 );

};

Данный модификатор применим только к конструктору. (Операторы преобразования и слово explicit обсуждаются в разделе 15.9.2.)

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

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

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

Account::Account() { ... }

iStack::iStack( int size = 0 ) { ... }

Complex::Complex(double re=0.0, double im=0.0) { ... }

Когда мы пишем:

int main()

{

Account acct;

// ...

}

* то компилятор сначала проверяет, определен ли для класса Account конструктор по умолчанию. Возникает одна из следующих ситуаций: Такой конструктор определен. Тогда он применяется к acct.

* Конструктор определен, но не является открытым. В данном случае определение acct помечается компилятором как ошибка: у функции main() нет прав доступа.

* Конструктор по умолчанию не определен, но есть один или несколько конструкторов, требующих задания аргументов. Определение acct помечается как ошибка: слишком мало аргументов у конструктора.

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

Пункты 1 и 3 должны быть уже достаточно понятны (если это не так, перечитайте данную главу) Посмотрим более внимательно на пункты 2 и 4.

Допустим, что все члены класса Account объявлены открытыми и не объявлено никакого конструктора:

class Account {

public:

char *_name;

unsigned int _acct_nmbr;

double _balance;

};

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

// статический класс хранения

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

Account global_scope_acct;

static Account file_scope_acct;

Account foo()

{

static Account local_static_acct;

// ...

}

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

// локальные и распределенные из хипа объекты не инициализированы

// до момента явной инициализации или присваивания

Account bar()

{

Account local_acct;

Account *heap_acct = new Account;

// ...

}

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

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