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

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

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

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

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.

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

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

665

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

 

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) { ... }

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

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

666

int main()

{

Account acct; // ...

}

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

1.Такой конструктор определен. Тогда он применяется к acct.

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

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

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

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

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

class Account {

 

public:

*_name;

char

unsigned int

_acct_nmbr;

double

_balance;

конструктора:

};

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

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

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

Account global_scope_acct; static Account file_scope_acct;

Account foo()

{

static Account local_static_acct; // ...

классов):

}

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

667

Однако объекты, определенные локально или распределенные динамически, в начальный

 

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

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

Account bar()

{

Account local_acct;

Account *heap_acct = new Account; // ...

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

}

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

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

14.2.2. Ограничение прав на создание объекта

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

class Account {

friend class vector< Account >; public:

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

private:

Account(); // ...

открытым:

};

3 Для тех, кто раньше программировал на C: приведенное выше определение класса Account на C выглядело бы так:

typedef struct {

char *_name; unsigned int _acct_nmbr; double _balance;

} Account;

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

668

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

Конструкторы, не являющиеся открытыми, в реальных программах C++ чаще всего используются для:

предотвращения копирования одного объекта в другой объект того же класса (эта проблема рассматривается в следующем подразделе);

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

14.2.3. Копирующий конструктор

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

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

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

inline Account::

Account( const Account &rhs )

: _balance( rhs._balance )

{

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

// копировать rhs._acct_nmbr нельзя _acct_nmbr = get_unique_acct_nmbr();

реализация:

}

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

Account acct2( acct1 );

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

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

669

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

Упражнение 14.1

Какие из следующих утверждений ложны? Почему?

1.У класса должен быть хотя бы один конструктор.

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

3.Если разумных начальных значений у членов класса нет, то не следует предоставлять конструктор по умолчанию.

4.Если в классе нет конструктора по умолчанию, то компилятор генерирует его

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

Упражнение 14.2

Предложите один или несколько конструкторов для данного множества членов.

class NoName { public:

//здесь должны быть конструкторы

//...

protected:

char *pstring; int ival; double dval;

Объясните свой выбор:

};

Упражнение 14.3

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

Книга

Дата

Служащий

Транспортное средство

Объект

Дерево

Упражнение 14.4

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

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

670

class Account { public:

Account();

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

};

(a)Account acct;

(b)Account acct2 = acct;

(c)Account acct3 = "Rena Stern";

(d)Account acct4( "Anna Engel", 400.00 );

объясните, что происходит в результате следующих определений:

(e) Account acct5 = Account( acct3 );

Упражнение 14.5

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

Account::Account( const Account rhs );

14.3. Деструктор класса

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

оператора new выделяется память для массива символов и присваивается уникальный номер счету. Можно также представить ситуацию, когда нужно получить монопольный доступ к разделяемой памяти или к критической секции потока. Для этого необходима симметричная операция, обеспечивающая автоматическое освобождение памяти или возврат ресурса по завершении времени жизни объекта, – деструктор. Деструктор это специальная определяемая пользователем функция-член, которая автоматически вызывается, когда объект выходит из области видимости или когда к указателю на объект применяется операция delete. Имя этой функции образовано из имени класса с предшествующим символом тильда” (~). Деструктор не возвращает значения и не принимает никаких параметров, а следовательно, не может быть перегружен. Хотя разрешается определять несколько таких функций-членов, лишь одна из них будет применяться ко всем объектам класса. Вот, например, деструктор для нашего класса

Account:

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

671

class Account { public:

Account();

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

~Account(); // ...

private:

char *_name; unsigned int _acct_nmbr; double _balance;

};

inline Account::~Account()

{

delete [] _name; return_acct_number( _acct_nnmbr );

}

inline Account::~Account()

{

//необходимо delete [] _name;

return_acct_number( _acct_nnmbr );

//необязательно

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

Обратите внимание, что в нашем деструкторе не сбрасываются значения членов:

}

Делать это необязательно, поскольку отведенная под члены объекта память все равно

class Point3d { public:

// ...

private:

float x, y, z;

будет освобождена. Рассмотрим следующий класс:

};

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

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