C++ для начинающих.

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 );

// ...

};

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 Point 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 остались (так что компилятор не должен запоминать, адресует указатель единственный объект или массив)? Какой вариант языка предложили бы вы?