C++ - Страустрап Бьярн
Шрифт:
Интервал:
Закладка:
5.5.5 Вектора объектов класса
Чтобы описать вектор объектов класса, имеющего конструтор, этот класс должен иметь конструктор, который может вызваться без списка параметров. Нельзя использовать даже парметры по умолчанию. Например:
table tblvec[10];
будет ошибкой, так как для table::table() требуется целый параметр. Нет способа задать параметры конструктора в описании вектора. Чтобы можно было описывать вектор таблиц table, можно модифицировать описание table (#5.3.1), напрмер, так:
class table (* // ... void init(int sz); // как старый конструктор public: table(int sz) // как раньше, но без по умолчанию (* init(sz); *) table() // по умолчанию (* init(15); *) *)
Когда вектор уничтожается, деструктор должен вызываться для каждого элемента этого вектора. Для векторов, которые не были размещены с помощью new, это делается неявно. Однако для векторов в свободной памяти это не может быть сделано неявно, поскольку компилятор не может отличить указатель на один обект от указателя на первый элемент вектора объектов. Например:
void f() (* table* t1 = new table; table* t2 = new table[10]; delete t1; // одна таблица delete t2; // неприятность: 10 таблиц *)
В этом случае длину вектора должен задавать программист:
void g(int sz) (* table* t1 = new table; table* t2 = new table[sz]; delete t1; delete[] t2; *)
Но почему же компилятор не может найти число элементов вектора из объема выделенной памяти? Потому, что распределтель свободной памяти не является частью языка и может быть задан программистом.
5.5.6 Небольшие объекты
Когда вы используете много небольших объектов, размещамых в свободной памяти, то вы можете обнаружить, что ваша
программа тратит много времени выделяя и освобождая память под эти объекты. Первое решение – это обеспечить более хорший распределитель памяти общего назначения, второе для раработчика классов состоит в том, чтобы взять под контроль уравление свободной памятью для объектов некоторого класса с помощью подходящих конструкторов и деструкторов.
Рассмотрим класс name, который использовался в примерах table. Его можно было бы определить так:
struct name (* char* string; name* next; double value;
name(char*, double, name*); ~name(); *);
Программист может воспользоваться тем, что размещение и освобождение объектов заранее известного размера можно обрбатывать гораздо эффективнее (и по памяти, и по времени), чем с помощью общей реализации new и delete. Общая идея состоит в том, чтобы предварительно разместить «куски» из объектов name, а затем сцеплять их, чтобы свести выделение и освободение к простым операциям над связанным списком. Переменная nfree является вершиной списка неиспользованных name:
const NALL = 128; name* nfree;
Распределитель, используемый операцией new, хранит рамер объекта вместе с объектом, чтобы обеспечить правильную работу операции delete. С помощью распределителя, специализрованного для типа, можно избежать этих накладных расходов. Например, на моей машине следующий распределитель использует для хранения name 16 байт, тогда как для стандартного распрделителя свободной памяти нужно 20 байт. Вот как это можно сделать:
name::name(char* s, double v, name* n) (* register name* p = nfree; // сначала выделить
if (p) nfree = p-»next; else (* // выделить и сцепить name* q = (name*)new char[ NALL*sizeof(name) ]; for (p=nfree= amp;q[NALL-1]; q«p; p–) p-»next = p-1; (p+1)-»next = 0; *)
this = p; // затем инициализировать string = s; value = v; next = n; *)
Присвоение указателю this информирует компилятор о том, что программист взял себе управление, и что не надо использвать стандартный механизм распределения памяти. Конструктор name::name() обрабатывает только тот случай, когда name рамещается посредством new, но для большей части типов это всегда так. В #5.5.8 объясняется, как написать конструктор для обработки как размещения в свободной памяти, так и других видов размещения.
Заметьте, что просто как
name* q = new name[NALL];
память выделять нельзя, поскольку это приведет к бескнечной рекурсии, когда new вызовет name::name().
Освобождение памяти обычно тривиально:
name::~name() (* next = nfree; nfree = this; this = 0; *)
Присваивание указателю this 0 в деструкторе обеспечивет, что стандартный распределитель памяти не используется.
5.5.7 Предостережение
Когда в конструкторе производится указателю this, значние this до этого присваивания неопределено. Таким образом, ссылка на член до этого присваивания неопределена и скорее всего приведет к катастрофе. Имеющийся компилятор не пытается убедиться в том, что присваивание указателю this происходит на всех траекториях выполнения:
mytype::mytype(int i) (* if (i) this = mytype_alloc(); // присваивание членам *);
откомпилируется, и при i==0 никакой объект размещен не будет.
Конструктор может определить, был ли он вызван операцией new, или нет. Если он вызван new, то указатель this на входе имеет нулевое значение, в противном случае this указывает на пространство, уже выделенное для объекта (например, на стек). Поэтому можно просто написать конструктор, который выделяет память, если (и только если) он был вызван через new. Напрмер:
mytype::mytype(int i) (* if (this == 0) this = mytype_alloc(); // присваивание членам *);
Эквивалентного средства, которое позволяет деструктору решить вопрос, был ли его объект создан с помощью new, не имеется, как нет и средства, позволяющего ему узнать, вызвала ли его delete, или он вызван объектом, выходящим из области видимости. Если для пользователя это существенно, то он может сохранить где-то соответствующую информацию для деструктора. Другой способ – когда пользователь обеспечивает, что объекты этого класса размещаются только соответствующим образом. Если удается справиться с первой проблемой, то второй способ интреса не представляет.
Если тот, кто реализует класс, является одновременно и его единственным пользователем, то имеет смысл упростить класс, исходя из предположений о его использовании. Когда класс разрабатывается для более широкого использования, таких допущений, как правило, лучше избегать.
5.5.8 Объекты переменного размера
Когда пользователь берет управление распределением и овобождением памяти, он может конструировать объекты размеры, которых во время компиляции недетерминирован. В предыдущих примерах вмещающие (или контейнерные – перев.) классы vector, stack, intset и table реализовывались как структуры доступа фиксированного размера, содержащие указатели на реальную пмять. Это подразумевает, что для создания таких объектов в свободной памяти необходимо две операции по выделению памяти, и что любое обращение к хранимой информации будет содержать дополнительную косвенную адресацию. Например:
class char_stack (* int size; char* top; char* s; public: char_stack(int sz) (* top=s=new char[size=sz]; *) ~char_stack() (* delete s; *) // деструктор void push(char c) (* *top++ = c; *) char pop() (* return *–top; *) *);
Если каждый объект класса размещается в свободной памти, это делать не нужно. Вот другой вариант:
class char_stack (* int size; char* top; char s[1]; public: char_stack(int sz); void push(char c) (* *top++ = c; *) char pop() (* return *–top; *) *);
char_stack::char_stack(int sz) (* if (this) error(«стек не в свободной памяти»); if (sz « 1) error(„размер стека « 1“); this = (char_stack*) new char[sizeof(char_stack)+sz-1]; size = sz; top = s; *)
Заметьте, что деструктор больше не нужен, поскольку пмять, которую использует char_stack, может освободить delete без всякого содействия со стороны программиста.
5.6 Упражнения
1. (*1) Модифицируйте настольный калькулятор из Главы 3, чтобы использовать класс table.