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

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

 
  if ( ix < vec_size )
  new( p+offset*ix ) Account( init_values[ix].first,
  init_values[ix].second );
  else new( p+offset*ix ) Account;
  }
 
  // отлично: элементы распределены и инициализированы;
  // вернуть указатель на первый элемент
  return (Account*)p;
 }
 Необходимо заранее выделить блок памяти, достаточный для хранения запрошенного массива, как массив байт, чтобы избежать применения к каждому элементу конструктора по умолчанию. Это делается в такой инструкции:
 char *p = new char[sizeof(Account)*elems];
 Далее программа в цикле обходит этот блок, присваивая на каждой итерации переменной p адрес следующего элемента и вызывая либо конструктор с двумя параметрами, если задана пара начальных значений, либо конструктор по умолчанию:
 for ( int ix = 0; ix < elems; ++ix )
  {
  if ( ix < vec_size )
  new( p+offset*ix ) Account( init_values[ix].first,
  init_values[ix].second );
  else new( p+offset*ix ) Account;
  }
 В разделе 14.3 говорилось, что оператор размещения new позволяет применить конструктор класса к уже выделенной области памяти. В данном случае мы используем new для поочередного применения конструктора класса Account к каждому из выделенных элементов массива. Поскольку при создании инициализированного массива мы подменили стандартный механизм выделения памяти, то должны сами позаботиться о ее освобождении. Оператор delete работать не будет:
 delete [] ps;
 Почему? Потому что ps (мы предполагаем, что эта переменная была инициализирована вызовом init_heap_array()) указывает на блок памяти, полученный не с помощью стандартного оператора new, поэтому число элементов в массиве компилятору неизвестно. Так что всю работу придется сделать самим:
 void
 Account::
 dealloc_heap_array( Account *ps, size_t elems )
 {
  for ( int ix = 0; ix < elems; ++ix )
  ps[ix].Account::~Account();
 
  delete [] reinterpret_cast(ps);
 }
 Если в функции инициализации мы пользовались арифметическими операциями над указателями для доступа к элементам:
 new( p+offset*ix ) Account;
 то здесь мы обращаемся к ним, задавая индекс в массиве ps:
 ps[ix].Account::~Account();
 Хотя и ps, и p адресуют одну и ту же область памяти, ps объявлен как указатель на объект класса Account, а p – как указатель на char. Индексирование p дало бы ix-й байт, а не ix-й объект класса Account. Поскольку с p ассоциирован не тот тип, что нужно, арифметические операции над указателями приходится программировать самостоятельно.
 Мы объявляем обе функции статическими членами класса:
 typedef pair value_pair;
 class Account {
 public:
  // ...
  static Account* init_heap_array(
  vector &init_values,
  vector::size_type elem_count = 0 );
  static void dealloc_heap_array( Account*, size_t );
  // ...
 };
 14.4.2. Вектор объектов
 Когда определяется вектор из пяти объектов класса, например:
 vector< Point > vec( 5 );
 то инициализация элементов производится в следующем порядке5:
 С помощью конструктора по умолчанию создается временный объект типа класса, хранящегося в векторе. .
 К каждому элементу вектора применяется копирующий конструктор, в результате чего каждый объект инициализируется копией временного объекта.
 Временный объект уничтожается.
 Хотя конечный результат оказывается таким же, как при определении массива из пяти объектов класса:
 Point pa[ 5 ];
 эффективность подобной инициализации вектора ниже, так как, во-первых, на конструирование и уничтожение временного объекта, естественно, нужны ресурсы, а во-вторых, копирующий конструктор обычно оказывается вычислительно более сложным, чем конструктор по умолчанию.
 Общее правило проектирования таково: вектор объектов класса удобнее только для вставки элементов, т.е. в случае, когда изначально определяется пустой вектор. Если мы заранее вычислили, сколько придется вставлять элементов, или имеем на этот счет обоснованное предположение, то надо зарезервировать необходимую память, а затем приступать к вставке. Например:
 vector< Point > cvs; // пустой
 int cv_cnt = calc_control_vertices();
 
 // зарезервировать память для хранения cv_cnt объектов класса Point
 // cvs все еще пуст ...
 cvs.reserve( cv_cnt );
 // открыть файл и подготовиться к чтению из него
 ifstream infile( "spriteModel" );
 istream_iterator

cvfile( infile ),eos;
 
 // вот теперь можно вставлять элементы
 copy( cvfile, eos, inserter( cvs, cvs.begin() ));
 (Алгоритм copy(), итератор вставки inserter и потоковый итератор чтения istream_iterator рассматривались в главе 12.) Поведение объектов list (список) и deque (двусторонняя очередь) аналогично поведению объектов vector (векторов). Вставка объекта в любой из этих контейнеров осуществляется с помощью копирующего конструктора.
 Упражнение 14.9
 Какие из приведенных инструкций неверны? Исправьте их.
 (a) Account *parray[10] = new Account[10];
 (b) Account iA[1024] = {
  "Nhi", "Le", "Jon", "Mike", "Greg", "Brent", "Hank"
  "Roy", "Elena" };
 
 (c) string *ps=string[5]("Tina","Tim","Chyuan","Mira","Mike");
 (d) string as[] = *ps;
 Упражнение 14.10
 Что лучше применить в каждой из следующих ситуаций: статический массив (такой, как Account pA[10]), динамический массив или вектор? Объясните свой выбор.
 Внутри функции Lut() нужен набор из 256 элементов для хранения объектов класса Color. Значения являются константами.
 Необходимо хранить набор из неизвестного числа объектов класса Account. Данные счетов читаются из файла.
 Функция gen_words(elem_size) должна сгенерировать и передать обработчику текста набор из elem_size строк.
 Упражнение 14.11
 Потенциальным источником ошибок при использовании динамических массивов является пропуск пары квадратных скобок, говорящей, что указатель адресует массив, т.е. неверная запись
 // печально: не проверяется, что parray адресует массив
 delete parray;
 вместо
 // правильно: определяется размер массива, адресуемого parray
 delete [] parray;
 Наличие пары скобок заставляет компилятор найти размер массива. Затем к каждому элементу по очереди применяется деструктор (всего size раз). Если же скобок нет, уничтожается только один элемент. В любом случае освобождается вся память, занятая массивом.
 При обсуждении первоначального варианта языка С++ много спорили о том, должно ли наличие квадратных скобок инициировать поиск или же (как было в исходной спецификации) лучше поручить программисту явно указывать размер массива:
 // в первоначальном варианте языка размер массива требовалось задавать явно
 delete p[10] parray;
 Как вы думаете, почему язык был изменен таким образом, что явного задания размера не требуется (а значит, нужно уметь его сохранять и извлекать), но скобки, хотя и пустые, в операторе delete остались (так что компилятор не должен запоминать, адресует указатель единственный объект или массив)? Какой вариант языка предложили бы вы?
 14.5. Список инициализации членов
 Модифицируем наш класс Account, объявив член _name типа string:
 #include
 class Account {
 public:
  // ...
 private:
  unsigned int _acct_nmbr;
  double _balance;
  string _name;
 };
 Придется заодно изменить и конструкторы. Возникает две проблемы: поддержание совместимости с первоначальным интерфейсом и инициализация объекта класса с помощью подходящего набора конструкторов.
 Исходный конструктор Account с двумя параметрами
 Account( const char*, double = 0.0 );
 не может инициализировать член типа string. Например:
 string new_client( "Steve Hall" );
 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 и несколько менее эффективной обработкой аргументов типа char* из-за необходимости создавать временный объект. Мы предоставили две версии конструктора с двумя параметрами. Тогда модифицированный набор конструкторов Account будет таким:
 #include
 
 class Account {
 public:
  Account();
  Account( const char*, double=0.0 );
  Account( const string&, double=0.0 );
  Account( const Account& );
  // ...
 private:
  // ...
 };
 Как правильно инициализировать член, являющийся объектом некоторого класса с собственным набором конструкторов? Этот вопрос можно разделить на три:
 где вызывается конструктор по умолчанию? Внутри конструктора по умолчанию класса Account;
 где вызывается копирующий конструктор? Внутри копирующего конструктора класса Account и внутри конструктора с двумя параметрами, принимающего в качестве первого тип string;
 как передать аргументы конструктору класса, являющегося членом другого класса? Это необходимо делать внутри конструктора Account с двумя параметрами, принимающего в качестве первого тип char*.
 Решение заключается в использовании списка инициализации членов (мы упоминали о нем в разделе 14.2). Члены, являющиеся классами, можно явно инициализировать с помощью списка, состоящего из разделенных запятыми пар “имя члена/значение”. Наш конструктор с двумя параметрами теперь выглядит так (напомним, что _name – это член, являющийся объектом класса string):
 inline Account::
 Account( const char* name, double opening_bal )
  : _name( name ), _balance( opening_bal )
 {
  _acct_nmbr = het_unique_acct_nmbr();
 }
 Список инициализации членов следует за сигнатурой конструктора и отделяется от нее двоеточием. В нем указывается имя члена, а в скобках – начальные значения, что аналогично синтаксису вызова функции. Если член является объектом класса, то эти значения становятся аргументами, передаваемыми подходящему конструктору, который затем и используется. В нашем примере значение name передается конструктору string, который применяется к члену _name. Член _balance инициализируется значением opening_bal.
 Аналогично выглядит второй конструктор с двумя параметрами:
 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 при обсуждении объектно-ориентированного программирования.) Например, если написать:
 inline Account::
 Account()
 {
  _name = "";
  _balance = 0.0;
  _acct_nmbr = 0;
 }
 то фаза инициализации будет неявной. Еще до выполнения тела конструктора вызывается конструктор по умолчанию класса string, ассоциированный с членом _name. Это означает, что присваивание _name пустой строки излишне.
 Для объектов классов различие между инициализацией и присваиванием существенно. Член, являющийся объектом класса, всегда следует инициализировать с помощью списка, а не присваивать ему значение в теле конструктора. Более правильной является следующая реализация конструктора по умолчанию класса Account:
 inline Account::
 Account() : _name( string() )
 {
  _balance = 0.0;
  _acct_nmbr = 0;
 }
 Мы удалили ненужное присваивание _name из тела конструктора. Явный же вызов конструктора по умолчанию string излишен. Ниже приведена эквивалентная, но более компактная версия:
 inline Account::
 Account()
 {
  _balance = 0.0;
  _acct_nmbr = 0;
 }
 Однако мы еще не ответили на вопрос об инициализации двух членов встроенных типов. Например, так ли существенно, где происходит инициализация _balance: в списке инициализации или в теле конструктора? Инициализация и присваивание членам, не являющимся объектами классов, эквивалентны как с точки зрения результата, так и с точки зрения производительности (за двумя исключениями). Мы предпочитаем использовать список:
 // предпочтительный стиль инициализации
 inline Account::
 Account() : _balance( 0.0 ), _acct_nmbr( 0 )
 {}
 Два вышеупомянутых исключения – это константные члены и члены-ссылки независимо от типа. Для них всегда нужно использовать список инициализации, в противном случае компилятор выдаст ошибку:
 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; }
 Каждый член должен встречаться в списке инициализации не более одного раза. Порядок инициализации определяется не порядком следования имен в списке, а порядком объявления членов. Если дано следующее объявление членов класса Account:
 class Account {
 public:
  // ...
 private:
  unsigned int _acct_nmbr;
  double _balance;
  string _name;
 };
 то порядок инициализации для такой реализации конструктора по умолчанию
 inline Account::
 Account() : _name( string() ), _balance( 0.0 ), _acct_nmbr( 0 )
 {}
 будет следующим: _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
 Что неверно в следующих определениях конструкторов? Как бы вы исправили обнаруженные ошибки?
 (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 c;
  string s;
  }
  (c) class CL2 {
  public:
  CL2( map *pmap, string key )
  : _text( key ), _loc( (*pmap)[key] ) {}
  // ...
  private:
  location _loc;
  string _text;
 };
 14.6. Почленная инициализация A
 Инициализация одного объекта класса другим объектом того же класса, как, например:

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

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