Чтение онлайн

ЖАНРЫ

Программирование. Принципы и практика использования C++ Исправленное издание
Шрифт:

// а N == 10,

// потому что аргументом является объект b2

}

С формальной точки зрения вызов

fill(buf,'x')
является сокращенной формой записи
fill<char,1024>(buf,'x')
, а
fill(b2,0)
— сокращение вызова
fill<double,10>(b2,0)
, но, к счастью, мы не всегда обязаны быть такими конкретными. Компилятор сам извлекает эту информацию за нас.

19.3.6. Обобщение класса vector

Когда мы создавали обобщенный класс

vector
на основе класса “
vector
элементов типа
double
” и вывели шаблон “
vector
элементов типа
T
”, мы не проверяли определения функций
push_back
,
resize
и
reserve
. Теперь мы обязаны это сделать, поскольку в разделах 19.2.2 и 19.2.3 эти функции были определены на основе предположений, которые были справедливы для типа
double
, но не выполняются для всех типов, которые мы хотели бы использовать как тип элементов вектора.

• Как запрограммировать класс

vector<X>
, если тип
X
не имеет значения по умолчанию?

• Как гарантировать, что элементы вектора будут уничтожены в конце работы с ним?

Должны ли мы вообще решать эти проблемы? Мы могли бы заявить: “Не создавайте векторы для типов, не имеющих значений по умолчанию” или “Не используйте векторы для типов, деструкторы которых могут вызвать проблемы”. Для конструкции, предназначенной для общего использования, такие ограничения довольно обременительны и создают впечатление, что разработчик не понял задачи или не думал о пользователях. Довольно часто такие подозрения оказываются правильными, но разработчики стандартной библиотеки к этой категории не относятся. Для того чтобы повторить стандартный класс vector, мы должны устранить две указанные выше проблемы.

Мы можем работать с типами, не имеющими значений по умолчанию, предоставив пользователю возможность задавать это значение самостоятельно.

template<class T> void vector<T>::resize(int newsize, T def = T);

Иначе говоря, используйте в качестве значения по молчанию объект, созданный конструктором

T
, если пользователь не указал иначе. Рассмотрим пример.

vector<double> v1;

v1.resize(100); // добавляем 100 копий объекта double, т.е. 0.0

v1.resize(200, 0.0); // добавляем 200 копий числа 0.0 — упоминание

// излишне

v1.resize(300, 1.0); // добавляем 300 копий числа 1.0

struct No_default {

No_default(int); // единственный конструктор класса No_default

// ...

};

vector<No_default> v2(10); // ошибка: попытка создать 10

// No_default

vector<No_default> v3;

v3.resize(100, No_default(2)); // добавляем 100 копий объектов

// No_default(2)

v3.resize(200); // ошибка: попытка создать 200

// No_default

Проблему, связанную с деструктором, устранить труднее. По существу, мы оказались в действительно трудной ситуации: в структуре данных часть данных проинициализирована, а часть — нет. До сих пор мы старались избегать неинициализированных данных и ошибок, которые ими порождаются. Теперь, как разработчики класса

vector
, мы столкнулись с проблемой, которой раньше, как пользователи класса
vector
, не имели.

Во-первых, мы должны

найти способ для получения неинициализированной памяти и манипулирования ею. К счастью, стандартная библиотека содержит класс
allocator
, распределяющий неинициализированную память. Слегка упрощенный вариант приведен ниже.

template<class T> class allocator {

public:

// ...

T* allocate(int n); // выделяет память для n объектов типа T

void deallocate(T* p, int n); // освобождает память, занятую n

// объектами типа T, начиная с адреса p

void construct(T* p, const T& v); // создает объект типа T

// со значением v по адресу p

void destroy(T* p); // уничтожает объект T по адресу p

};

Если вам нужна полная информация по этому вопросу, обратитесь к книге The C++ Programming Language или к стандарту языка С++ (см. описание заголовка <memory> ), а также к разделу B.1.1. Тем не менее в нашей программе демонстрируются четыре фундаментальных операции, позволяющих выполнять следующие действия:

• Выделение памяти, достаточной для хранения объекта типа

T
без инициализации.

• Создание объекта типа

T
в неинициализированной памяти.

• Уничтожение объекта типа

T
и возвращение памяти в неинициализированное состояние.

• Освобождение неинициализированной памяти, достаточной для хранения объекта типа

T
без инициализации.

Не удивительно, что класс

allocator
— то, что нужно для реализации функции
vector<T>::reserve
. Начнем с того, что включим в класс
vector
параметр класса
allocator
.

template<class T, class A = allocator<T> > class vector {

A alloc; // используем объект класса allocator для работы

// с памятью, выделяемой для элементов

// ...

};

Кроме распределителя памяти, используемого вместо оператора

new
, остальная часть описания класса
vector
не отличается от прежнего. Как пользователи класса
vector
, мы можем игнорировать распределители памяти, пока сами не захотим, чтобы класс
vector
управлял памятью, выделенной для его элементов, нестандартным образом. Как разработчики класса
vector
и как студенты, пытающиеся понять фундаментальные проблемы и освоить основные технологии программирования, мы должны понимать, как вектор работает с неинициализированной памятью, и предоставить пользователям правильно сконструированные объекты. Единственный код, который следует изменить, — это функции-члены класса
vector
, непосредственно работающие с памятью, например функция
vector<T>::reserve
.

template<class T, class A>

void vector<T,A>::reserve(int newalloc)

{

if (newalloc<=space) return; // размер не уменьшается

T* p = alloc.allocate(newalloc); // выделяем новую память

for (int i=0; i<sz; ++i) alloc.construct(&p[i],elem[i]);

// копируем

for (int i=0; i<sz; ++i) alloc.destroy(&elem[i]); // уничтожаем

Поделиться с друзьями: