<< Пред.           стр. 63 (из 121)           След. >>

Список литературы по разделу

 По умолчанию как инициализация, так и присваивание одного объекта класса другому выполняются почленно, т.е. путем последовательного копирования всех членов. Хотя этого обычно достаточно, при некоторых обстоятельствах такая семантика оказывается неадекватной. Тогда проектировщик класса должен предоставить специальный копирующий конструктор и копирующий оператор присваивания. Самое сложное в поддержке этих функций-членов – понять, что они должны быть написаны.
 14.1. Инициализация класса
 Рассмотрим следующее определение класса:
 class Data {
 public:
  int ival;
  char *ptr;
 };
 Чтобы безопасно пользоваться объектом класса, необходимо правильно инициализировать его члены. Однако смысл этого действия для разных классов различен. Например, может ли ival содержать отрицательное значение или нуль? Каковы правильные начальные значения обоих членов класса? Мы не ответим на эти вопросы, не понимая абстракции, представляемой классом. Если с его помощью описываются служащие компании, то ptr, вероятно, указывает на фамилию служащего, а ival – его уникальный номер. Тогда отрицательное или нулевое значения ошибочны. Если же класс представляет текущую температуру в городе, то допустимы любые значения ival. Возможно также, что класс Data представляет строку со счетчиком ссылок: в таком случае ival содержит текущее число ссылок на строку по адресу ptr. При такой абстракции ival инициализируется значением 1; как только значение становится равным 0, объект класса уничтожается.
 Мнемонические имена класса и обоих его членов сделали бы, конечно, его назначение более понятным для читателя программы, но не дали бы никакой дополнительной информации компилятору. Чтобы компилятор понимал наши намерения, мы должны предоставить одну или несколько перегруженных функций инициализации – конструкторов. Подходящий конструктор выбирается в зависимости от множества начальных значений, указанных при определении объекта. Например, любая из приведенных ниже инструкций представляет корректную инициализацию объекта класса Data:
 Data dat01( "Venus and the Graces", 107925 );
 Data dat02( "about" );
 Data dat03( 107925 );
 Data dat04;
 Бывают ситуации (как в случае с dat04), когда нам нужен объект класса, но его начальные значения мы еще не знаем. Возможно, они станут известны позже. Однако начальное значение задать необходимо, хотя бы такое, которое показывает, что разумное начальное значение еще не присвоено. Другими словами, инициализация объекта иногда сводится к тому, чтобы показать, что он еще не инициализирован. Большинство классов предоставляют специальный конструктор по умолчанию, для которого не требуется задавать начальных значений. Как правило, он инициализирует объект таким образом, чтобы позже можно было понять, что реальной инициализации еще не проводилось.
 Обязан ли наш класс Data иметь конструктор? Нет, поскольку все его члены открыты. Унаследованный из языка C механизм поддерживает явную инициализацию, аналогичную используемой при инициализации массивов:
 int main()
 {
  // local1.ival = 0; local1.ptr = 0
  Data local1 = { 0, 0 };
 
  // local2.ival = 1024;
  // local3.ptr = "Anna Livia Plurabelle"
  Data.local2 - { 1024, "Anna Livia Plurabelle" };
 
  // ...
 }
 Значения присваиваются позиционно, на основе порядка, в котором объявляются данные-члены. Следующий пример приводит к ошибке компиляции, так как ival объявлен перед ptr:
 // ошибка: ival = "Anna Livia Plurabelle";
 // ptr = 1024
 Data.local2 - { "Anna Livia Plurabelle", 1024 };
 Явная инициализация имеет два основных недостатка. Во-первых, она может быть применена лишь для объектов классов, все члены которых открыты (т.е. эта инициализация не поддерживает инкапсуляции данных и абстрактных типов – их не было в языке C, откуда она заимствована). А во-вторых, такая форма требует вмешательства программиста, что увеличивает вероятность появления ошибок (забыл включить список инициализации или перепутал порядок следования инициализаторов в нем).
 Так нужно ли применять явную инициализацию вместо конструкторов? Да. Для некоторых приложений более эффективно использовать список для инициализации больших структур постоянными значениями. К примеру, мы можем таким образом построить палитру цветов или включить в текст программы фиксированные координаты вершин и значения в узлах сложной геометрической модели. В подобных случаях инициализация выполняется во время загрузки, что сокращает затраты времени на запуск конструктора, даже если он определен как встроенный. Это особенно удобно при работе с глобальными объектами1.
 Однако в общем случае предпочтительным методом инициализации является конструктор, который гарантированно будет вызван компилятором для каждого объекта до его первого использования. В следующем разделе мы познакомимся с конструкторами детально.
 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 практически невозможно.
 14.2.2. Ограничение прав на создание объекта
 Доступность конструктора определяется тем, в какой секции класса он объявлен. Мы можем ограничить или явно запретить некоторые формы создания объектов, если поместим соответствующий конструктор в неоткрытую секцию. В примере ниже конструктор по умолчанию класса Account объявлен закрытым, а с двумя параметрами – открытым:
 class Account {
  friend class vector< Account >;
 public:
  explicit Account( const char*, double = 0.0 );
  // ...
 private:
  Account();
  // ...
 };
 Обычная программа сможет теперь определять объекты класса 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. Если он объявлен и доступен, то он и вызывается; а если недоступен, то определение acct2 считается ошибкой. В случае, когда копирующий конструктор не объявлен, выполняется почленная инициализация по умолчанию. Если впоследствии объявление копирующего конструктора будет добавлено или удалено, никаких изменений в программы пользователей вносить не придется. Однако перекомпилировать их все же необходимо. (Более подробно почленная инициализация рассматривается в разделе 14.6.)
 Упражнение 14.1
 Какие из следующих утверждений ложны? Почему?
 У класса должен быть хотя бы один конструктор.
 Конструктор по умолчанию – это конструктор с пустым списком параметров.
 Если разумных начальных значений у членов класса нет, то не следует предоставлять конструктор по умолчанию.
 Если в классе нет конструктора по умолчанию, то компилятор генерирует его автоматически и инициализирует каждый член значением по умолчанию для соответствующего типа.
 Упражнение 14.2
 Предложите один или несколько конструкторов для данного множества членов. Объясните свой выбор:
 class NoName {

<< Пред.           стр. 63 (из 121)           След. >>

Список литературы по разделу