676
};
Упражнение 14.7
Необходим ли деструктор для класса, который вы выбрали в упражнении 14.3? Если нет, объясните почему. В противном случае предложите реализацию.
Упражнение 14.8
Сколько раз вызываются деструкторы в следующем фрагменте:
}
14.4.
Массивы и векторы объектов
Массив объектов класса определяется точно так же, как массив элементов встроенного типа. Например:
Account table[ 16 ]; определяет массив из 16 объектов Account. Каждый элемент по очереди инициализируется конструктором по умолчанию. Можно и явно передать конструкторам аргументы внутри заключенного в фигурные скобки списка инициализации массива.
Строка:
Account pooh_pals[] = { "Piglet", "Eeyore", "Tigger" }; определяет массив из трех элементов, инициализируемых конструкторами: class NoName { public:
NoName();
// ... private: char *pstring; int ival; double dval; void mumble( const char *name, fouble balance, char acct_type )
{
Account acct; if ( ! name ) return; if ( balance <= 99 ) return; switch( acct_type ) { case 'z': return; case 'a': case 'b': return;
}
// ...
С++ для начинающих
677
Account( "Tigger", 0.0 ); // третий элемент (Тигра)
Один аргумент можно задать явно, как в примере выше. Если же необходимо передать несколько аргументов, то придется воспользоваться явным вызовом конструктора:
};
Чтобы включить в список инициализации массива конструктор по умолчанию, мы употребляем явный вызов с пустым списком параметров:
};
Эквивалентный массив из трех элементов можно объявить и так:
};
Таким образом, члены списка инициализации последовательно используются для заполнения очередного элемента массива. Те элементы, для которых явные аргументы не заданы, инициализируются конструктором по умолчанию. Если его нет, то в списке должны быть заданы аргументы конструктора для каждого элемента массива.
Доступ к отдельным элементам массива объектов производится с помощью оператора взятия индекса, как и для массива элементов любого из встроенных типов. Например: pooh_pals[0]; обращается к Piglet, а pooh_pals[1]; к Eeyore и т.д. Для доступа к членам объекта, находящегося в некотором элементе массива, мы сочетаем операторы взятия индекса и доступа к членам: pooh_pals[1]._name != pooh_pals[2]._name;
Account( "Piglet", 0.0 ); // первый элемент (Пятачок)
Account( "Eeyore", 0.0 ); // второй элемент (Иа-Иа)
Account pooh_pals[] = {
Account( "Piglet", 1000.0 ),
Account( "Eeyore", 1000.0 ),
Account( "Tigger", 1000.0 )
Account pooh_pals[] = {
Account( "Woozle", 10.0 ), //
Бука
Account( "Heffalump", 10.0 ), //
Слонопотам
Account();
Account pooh_pals[3] = {
Account( "Woozle", 10.0 ),
Account( "Heffalump", 10.0 )
С++ для начинающих
678Не существует способа явно указать начальные значения элементов массива, память для которого выделена из хипа. Если класс поддерживает создание динамических массивов с помощью оператора new, он должен либо иметь конструктор по умолчанию, либо не иметь никаких конструкторов. На практике почти у всех классов есть такой конструктор.
Объявление
Account *pact = new Account[ 10 ]; создает в памяти, выделенной из хипа, массив из десяти объектов класса Account, причем каждый инициализируется конструктором по умолчанию.
Чтобы уничтожить массив, адресованный указателем pact, необходимо применить оператор delete. Однако написать delete pact; недостаточно, так как pact при этом не идентифицируется как массив объектов. В результате деструктор класса Account применяется лишь к первому элементу массива.
Чтобы применить его к каждому элементу, мы должны включить пустую пару скобок между оператором delete и адресом удаляемого объекта: delete [] pact;
Пустая
пара скобок говорит о том, что pact адресует именно массив. Компилятор определяет, сколько в нем элементов, и применяет деструктор к каждому из них.
14.4.1. Инициализация массива, распределенного из хипа A По умолчанию инициализация массива объектов, распределенного из хипа, проходит в два этапа: выделение памяти для массива, к каждому элементу которого применяется конструктор по умолчанию, если он определен, и последующее присваивание значения каждому элементу.
Чтобы свести инициализацию к одному шагу, программист должен вмешаться и поддержать следующую семантику: задать начальные значения для всех или некоторых элементов массива и гарантировать применение конструктора по умолчанию для тех элементов, начальные значения которых не заданы. Ниже приведено одно из возможных программных решений, где используется оператор размещения new:
// увы! это не совсем правильно
// правильно:
// показывает, что pact адресует массив
С++ для начинающих
679}
Необходимо заранее выделить блок памяти,
достаточный для хранения запрошенного массива, как массив байт, чтобы избежать применения к каждому элементу конструктора по умолчанию. Это делается в такой инструкции:
#include
#include
#include
#include
#include "Accounts.h" typedef pair value_pair;
/* init_heap_array()
* объявлена как статическая функция-член
* обеспечивает выделение памяти из хипа и инициализацию
* массива объектов
* init_values: пары начальных значений элементов массива
* elem_count: число элементов в массиве
* если 0, то размером массива считается размер вектора
* init_values
*/
Account*
Account:: init_heap_array( vector &init_values, vector::size_type elem_count = 0 )
{ vector::size_type vec_size = init_value.size(); if ( vec_size == 0 && elem_count == 0 ) return 0;
// размер массива равен либо elem_count,
// либо, если elem_count == 0, размеру вектора ... size_t elems = elem_count
? elem_count : vec_size();
// получить блок памяти для размещения массива char *p = new char[sizeof(Account)*elems];
// по отдельности инициализировать каждый элемент массива int offset = sizeof( Account ); for ( int ix = 0; ix < elems; ++ix )
{
// смещение ix-ого элемента
// если пара начальных значений задана,
// передать ее конструктору;
// в противном случае вызвать конструктор по умолчанию 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;
С++ для начинающих
680char *p = new char[sizeof(Account)*elems];
Далее программа в цикле обходит этот блок, присваивая на каждой итерации переменной p
адрес следующего элемента и вызывая либо конструктор с двумя параметрами, если задана пара начальных значений, либо конструктор по умолчанию:
}
В разделе 14.3 говорилось, что оператор размещения new позволяет применить конструктор класса к уже выделенной области памяти. В данном случае мы используем new для поочередного применения конструктора класса Account к каждому из выделенных элементов массива. Поскольку при создании инициализированного массива мы подменили
стандартный механизм выделения памяти, то должны сами позаботиться о ее освобождении. Оператор delete работать не будет: delete [] ps;
Почему? Потому что ps (мы предполагаем, что эта переменная была инициализирована вызовом init_heap_array()) указывает на блок памяти, полученный не с помощью стандартного оператора new, поэтому число элементов в массиве компилятору неизвестно. Так что всю работу придется сделать самим:
}
Если в функции инициализации мы пользовались арифметическими операциями над указателями для доступа к элементам: new( p+offset*ix ) Account; то здесь мы обращаемся к ним, задавая индекс в массиве ps: ps[ix].Account::Account();
Хотя и ps, и p адресуют одну и ту же область памяти, ps объявлен как указатель на объект класса Account, а p – как указатель на char. Индексирование p дало бы ix-й байт, а не ix-й объект класса Account. Поскольку с 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; 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);
С++ для начинающих
681арифметические операции над указателями приходится программировать самостоятельно.
Мы объявляем обе функции статическими членами класса: typedef pair
value_pair;
};
14.4.2.
Вектор объектов
Когда определяется вектор из пяти объектов класса, например: vector< Point > vec( 5 ); то инициализация элементов производится в следующем порядке5:
1. С помощью конструктора по умолчанию создается временный объект типа класса, хранящегося в векторе. .
2. К каждому элементу вектора применяется копирующий конструктор, в результате чего каждый объект инициализируется копией временного объекта.
3. Временный объект уничтожается.
Хотя конечный результат оказывается таким же, как при определении массива из пяти объектов класса:
Point pa[ 5 ]; эффективность подобной инициализации вектора ниже, так как, во-первых, на конструирование и уничтожение временного объекта, естественно, нужны ресурсы, а во- вторых, копирующий конструктор обычно оказывается вычислительно более сложным, чем конструктор по умолчанию.
Общее правило проектирования таково: вектор объектов класса удобнее только для вставки элементов, т.е. в случае, когда изначально определяется пустой вектор. Если мы заранее вычислили, сколько придется вставлять элементов, или имеем на этот счет обоснованное предположение, то надо зарезервировать необходимую память, а затем приступать к вставке. Например:
5
Сигнатура ассоциированного конструктора имеет следующий смысл.
Копирующий конструктор применяет некоторое значение к каждому элементу по очереди. Задавая в качестве второго аргумента объект класса, мы делаем создание временного объекта излишним: explicit vector( size_type n, const T& value=T(), const Allocator&=Allocator());
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 );
// ...
С++ для начинающих
682
copy( cvfile, eos, inserter( cvs, cvs.begin() ));
(Алгоритм copy(), итератор вставки inserter и потоковый итератор чтения istream_iterator рассматривались в главе 12.) Поведение объектов list (список) и deque
(двусторонняя очередь) аналогично поведению объектов vector (векторов).
Вставка объекта в любой из этих контейнеров осуществляется с помощью копирующего конструктора.
Упражнение 14.9
Какие из приведенных инструкций неверны? Исправьте их.
(a) Account *parray[10] = new Account[10];
"Roy", "Elena" };
(c) string *ps=string[5]("Tina","Tim","Chyuan","Mira","Mike");
Упражнение 14.10
Что лучше применить в каждой из следующих ситуаций: статический массив (такой, как
Account pA[10]), динамический массив или вектор? Объясните свой выбор.
Внутри функции Lut() нужен набор из 256 элементов для хранения объектов класса
Color
. Значения являются константами.
Необходимо хранить набор из неизвестного числа объектов класса Account. Данные счетов читаются из файла.
Функция gen_words(elem_size) должна сгенерировать и передать обработчику текста набор из elem_size строк.
Упражнение 14.11 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;
// вот теперь можно вставлять элементы
(b) Account iA[1024] = {
"Nhi", "Le", "Jon", "Mike", "Greg", "Brent", "Hank"
(d) string as[] = *ps;
С++ для начинающих
683Потенциальным источником ошибок при использовании динамических массивов является пропуск пары квадратных скобок, говорящей, что указатель адресует массив, т.е. неверная запись delete parray; вместо delete [] parray;
Наличие пары скобок заставляет компилятор найти размер массива. Затем к каждому элементу по очереди применяется деструктор (всего size раз). Если же скобок нет, уничтожается только один элемент. В любом случае освобождается вся память, занятая массивом.
При обсуждении первоначального варианта языка С++ много спорили о том, должно ли наличие квадратных скобок инициировать поиск или же (как было в исходной спецификации) лучше поручить программисту явно указывать размер массива: delete p[10] parray;
Как вы думаете,
почему язык был изменен таким образом, что явного задания размера не требуется (а значит, нужно уметь его сохранять и извлекать), но скобки, хотя и пустые, в операторе delete остались (так что компилятор не должен запоминать, адресует указатель единственный объект или массив)? Какой вариант языка предложили бы вы?
14.5.
Список инициализации членов
Модифицируем наш класс Account, объявив член _name типа string:
};
// печально: не проверяется, что parray адресует массив
// правильно: определяется размер массива, адресуемого parray
// в первоначальном варианте языка размер массива требовалось задавать явно
#include
class Account { public:
// ... private: unsigned int _acct_nmbr; double _balance; string _name;
С++ для начинающих
684
Придется заодно изменить и конструкторы. Возникает две проблемы: поддержание совместимости с первоначальным интерфейсом и инициализация объекта класса с помощью подходящего набора конструкторов.
Исходный конструктор Account с двумя параметрами
Account( const char*, double = 0.0 ); не может инициализировать член типа string. Например:
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 ); вызывается именно этот конструктор, тогда как старый код
} по-прежнему будет приводить к вызову исходного конструктора с двумя параметрами.
Так как в классе string определено преобразование из типа char* в тип string
(преобразования классов обсуждаются в этой главе ниже), то можно заменить исходный конструктор на новый, которому в качестве первого параметра передается тип string. В таком случае, когда встречается инструкция:
Account myAcct( "Tinkerbell" );
"Tinkerbell"
преобразуется во временный объект типа string. Затем этот объект передается новому конструктору с двумя параметрами.
При проектировании приходится идти на компромисс между увеличением числа конструкторов класса Account и несколько менее эффективной обработкой аргументов string new_client( "Steve Hall" );
Account *open_new_account( const char *nm )
{
Account *pact = new Account( nm );
// ... return pacct;
С++ для начинающих
685типа char* из-за необходимости создавать временный объект. Мы предоставили две версии конструктора с двумя параметрами. Тогда модифицированный набор конструкторов Account будет таким:
};
Как правильно инициализировать член, являющийся объектом некоторого класса с собственным набором конструкторов? Этот вопрос можно разделить на три:
1. где вызывается конструктор по умолчанию? Внутри конструктора по умолчанию класса Account;
2. где вызывается копирующий конструктор? Внутри копирующего конструктора класса
Account и внутри конструктора с двумя параметрами, принимающего в качестве первого тип string;
3. как
передать аргументы конструктору класса, являющегося членом другого класса?
Это необходимо делать внутри конструктора Account с двумя параметрами, принимающего в качестве первого тип char*.
Решение заключается в использовании списка инициализации членов (мы упоминали о нем в разделе 14.2). Члены, являющиеся классами, можно явно инициализировать с помощью списка, состоящего из разделенных запятыми пар “имя члена/значение”. Наш конструктор с двумя параметрами теперь выглядит так (напомним, что _name – это член, являющийся объектом класса string):
}
Список инициализации членов следует за сигнатурой конструктора и отделяется от нее двоеточием. В нем указывается имя члена, а в скобках – начальные значения, что аналогично синтаксису вызова функции. Если член является объектом класса, то эти значения становятся аргументами, передаваемыми подходящему конструктору, который затем и используется. В нашем примере значение name передается конструктору string, который применяется к члену _name. Член _balance инициализируется значением opening_bal
Аналогично выглядит второй конструктор с двумя параметрами:
#include
class Account { public:
Account();
Account( const char*, double=0.0 );
Account( const string&, double=0.0 );
Account( const Account& );
// ... private:
// ... inline Account::
Account( const char* name, double opening_bal )
: _name( name ), _balance( opening_bal )
{
_acct_nmbr = het_unique_acct_nmbr();
С++ для начинающих
686}
В этом случае вызывается копирующий конструктор string, инициализирующий член
_name значением параметра name типа string.
Часто у новичков возникает вопрос: в чем разница между использованием списка инициализации и присваиванием значений членам в теле конструктора? Например, в чем разница между
} и
}
В конце работы обоих конструкторов все три члена будут иметь одинаковые значения.
Разница в том, что только список обеспечивает инициализацию тех членов, которые являются объектами класса. В теле конструктора установка значения члена – это не инициализация, а присваивание. Важно это различие или нет, зависит от природы члена.
С концептуальной точки зрения выполнение конструктора состоит из двух фаз: фаза явной или неявной инициализации и фаза вычислений, включающая все инструкции в теле конструктора. Любая установка значений членов во второй фазе рассматривается как присваивание, а не инициализация. Непонимание этого различия приводит к ошибкам и неэффективным программам.
Первая фаза может быть явной или неявной в зависимости от того, имеется ли список инициализации членов. При неявной инициализации сначала вызываются конструкторы по умолчанию
всех базовых классов в порядке их объявления, а затем конструкторы по умолчанию всех членов, являющихся объектами классов. (Базовые классы мы будем рассматривать в главе
17 при обсуждении объектно-ориентированного программирования.) Например, если написать: inline Account::
Account( const string& name, double opening_bal )
: _name( name ), _balance( opening_bal )
{
_acct_nmbr = het_unique_acct_nmbr(); 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();