14.4. Массивы и векторы объектов
14.4. Массивы и векторы объектов
Массив объектов класса определяется точно так же, как массив элементов встроенного типа. Например:
Account table[ 16 ];
определяет массив из 16 объектов Account. Каждый элемент по очереди инициализируется конструктором по умолчанию. Можно и явно передать конструкторам аргументы внутри заключенного в фигурные скобки списка инициализации массива. Строка:
Account pooh_pals[] = { "Piglet", "Eeyore", "Tigger" };
определяет массив из трех элементов, инициализируемых конструкторами:
Account( "Piglet", 0.0 ); // первый элемент (Пятачок)
Account( "Eeyore", 0.0 ); // второй элемент (Иа-Иа)
Account( "Tigger", 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 )
};
Таким образом, члены списка инициализации последовательно используются для заполнения очередного элемента массива. Те элементы, для которых явные аргументы не заданы, инициализируются конструктором по умолчанию. Если его нет, то в списке должны быть заданы аргументы конструктора для каждого элемента массива.
Доступ к отдельным элементам массива объектов производится с помощью оператора взятия индекса, как и для массива элементов любого из встроенных типов. Например:
pooh_pals[0];
обращается к Piglet, а
pooh_pals[1];
к Eeyore и т.д. Для доступа к членам объекта, находящегося в некотором элементе массива, мы сочетаем операторы взятия индекса и доступа к членам:
pooh_pals[1]._name != pooh_pals[2]._name;
Не существует способа явно указать начальные значения элементов массива, память для которого выделена из хипа. Если класс поддерживает создание динамических массивов с помощью оператора new, он должен либо иметь конструктор по умолчанию, либо не иметь никаких конструкторов. На практике почти у всех классов есть такой конструктор.
Объявление
Account *pact = new Account[ 10 ];
создает в памяти, выделенной из хипа, массив из десяти объектов класса Account, причем каждый инициализируется конструктором по умолчанию.
Чтобы уничтожить массив, адресованный указателем pact, необходимо применить оператор delete. Однако написать
// увы! это не совсем правильно
delete pact;
недостаточно, так как pact при этом не идентифицируется как массив объектов. В результате деструктор класса Account применяется лишь к первому элементу массива. Чтобы применить его к каждому элементу, мы должны включить пустую пару скобок между оператором delete и адресом удаляемого объекта:
// правильно:
// показывает, что pact адресует массив
delete [] pact;
Пустая пара скобок говорит о том, что pact адресует именно массив. Компилятор определяет, сколько в нем элементов, и применяет деструктор к каждому из них.
14.4.1. Инициализация массива, распределенного из хипа A
По умолчанию инициализация массива объектов, распределенного из хипа, проходит в два этапа: выделение памяти для массива, к каждому элементу которого применяется конструктор по умолчанию, если он определен, и последующее присваивание значения каждому элементу.
Чтобы свести инициализацию к одному шагу, программист должен вмешаться и поддержать следующую семантику: задать начальные значения для всех или некоторых элементов массива и гарантировать применение конструктора по умолчанию для тех элементов, начальные значения которых не заданы. Ниже приведено одно из возможных программных решений, где используется оператор размещения new:
#include utility
#include vector
#include new
#include cstddef
#include "Accounts.h"
typedef pairchar*, double
value_pair;
/* init_heap_array()
* объявлена как статическая функция-член
* обеспечивает выделение памяти из хипа и инициализацию
* массива объектов
* init_values: пары начальных значений элементов массива
* elem_count: число элементов в массиве
* если 0, то размером массива считается размер вектора
* init_values
*/
Account*
Account::
init_heap_array(
vectorvalue_pair &init_values,
vectorvalue_pair ::size_type elem_count = 0 )
{
vectorvalue_pair ::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;
}
Необходимо заранее выделить блок памяти, достаточный для хранения запрошенного массива, как массив байт, чтобы избежать применения к каждому элементу конструктора по умолчанию. Это делается в такой инструкции:
char *p = new char[sizeof(Account)*elems];
Далее программа в цикле обходит этот блок, присваивая на каждой итерации переменной p адрес следующего элемента и вызывая либо конструктор с двумя параметрами, если задана пара начальных значений, либо конструктор по умолчанию:
for ( int ix = 0; ix
В разделе 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_castchar*(ps);
}
Если в функции инициализации мы пользовались арифметическими операциями над указателями для доступа к элементам:
new( p+offset*ix ) Account;
то здесь мы обращаемся к ним, задавая индекс в массиве ps:
ps[ix].Account::~Account();
Хотя и ps, и p адресуют одну и ту же область памяти, ps объявлен как указатель на объект класса Account, а p - как указатель на char. Индексирование p дало бы ix-й байт, а не ix-й объект класса Account. Поскольку с p ассоциирован не тот тип, что нужно, арифметические операции над указателями приходится программировать самостоятельно.
Мы объявляем обе функции статическими членами класса:
typedef pairchar*, double value_pair;
class Account {
public:
// ...
static Account* init_heap_array(
vector value_pair &init_values,
vector value_pair ::size_type elem_count = 0 );
static void dealloc_heap_array( Account*, size_t );
// ...
};