Программирование >>  Синтаксис инициирования исключений 

1 ... 58 59 60 [ 61 ] 62 63 64 ... 82


class Foo { public:

void* operator new(size t bytes)

if (bytes != sizeof(Foo) fgFreeList == NULL)

return ::operator new(bytes); FreeNode* node = fgFreeList; FgFreeList = fgFreeList->next; Return node;

Мы избавились лишь от проблем, связанных с выделением памяти. Процесс освобождения необходимо изменить в соответствии с этой стратегией. Альтернативная форма оператора delete имеет второй аргумент - количество освобождаемых байт. На первый взгляд кажется, что из затруднений появился изящный выход:

class Foo { public:

void* operator new(size t bytes); См. Выше

void operator de1ete(void* space, size t bytes)

if (bytes != sizeof(Foo))

::operator delete(space); ((FreeNode*)space)->next = fgFreeList; fgFreeList = (FreeNode*)space;

Теперь в список будут заноситься только настоящие Foo и производные классы, совпадающие по размеру. Неплохо, но есть одна проблема. Как компилятор поведет себя в следующем фрагменте?

Foo* foo = new Bar;

delete foo; Какой размер будет использован компилятором?

Bar больше Foo, поэтому Foo::operator new перепоручает работу глобальному оператору new. Но когда подходит время освобождать память, компилятор все путает. Размер, передаваемый Foo::operator delete, основан на догадке компилятора относительно настоящего типа, а эта догадка может оказаться неверной. В данном случае мы сказали компилятору, что это Foo, а не Bar ; компилятор ухмыляется и продолжает играть по нашим правилам. Чтобы справиться с затруднениями, необходимо знать точную последовательность уничтожения, возникающую в операторах вида delete foo; . Сначала вызываются деструкторы, начиная с производного класса, и далее вверх по цепочке. Затем оператор delete вызывается кодом, окружающим деструктор производного класса. Это означает, что проблема возникает только для невиртуальных деструкторов. Если деструктор является виртуальным, аргумент размера в операторе delete всегда будет правильным - 2438-й довод в пользу применения виртуальных деструкторов, если только у вас не находится действительно веских причин против них.

Рабочий класс списка свободной памяти

Учитывая все сказанное, следующий фрагмент всегда будет правильно работать на компиляторах, использующих v-таблицы.

class Foo { private:

struct FreeNode {

FreeNode* next;



static FreeNode* fdFreeList; public:

virtual ~Foo() {}

void* operator new(size t bytes)

if (bytes != sizeof(Foo) fgFreeList == NULL)

return ::operator new(bytes); FreeNode* node = fgFreeList; FgFreeList = fgFreeList->next; return node;

void operator de1ete(void* space, size t bytes)

if (bytes != sizeof(Foo))

return ::operator delete(space); ((FreeNode*)space)->next = fgFreeList; fgFreeList = (FreeNode*)space;

Указатель v-таблицы гарантирует, что каждый Foo по крайней мере не меньше указателя на следующий элемент списка (FreeNode*), а виртуальный деструктор обеспечивает правильность размера, передаваемого оператору delete.

Повторяю: рассмотренная схема управления памятью не предназначена для практического применения (встретив производный класс, она собирает вещи и отправляется домой). Она лишь демонстрирует некоторые базовые принципы перегрузки операторов new и delete.

Наследование операторов new и delete

Если перегрузить операторы new и delete для некоторого класса, перегруженные версии будут унаследованы производными классами. Ничто не помешает вам снова перегрузить new и/или delete в одном из этих производных классов.

class Bar : public Foo { public:

virtual ~Bar(); Foo::~Foo тоже должен быть виртуальным

void* operator new(size t bytes);

void operator de1ete(void* space, size t bytes);

С виртуальным деструктором все работает. Если деструктор не виртуальный, в следующем фрагменте будет вызван правильный оператор new и оператор delete базового класса:

Foo* foo = new Bar; delete foo;

Хотя этот фрагмент работает, подобное переопределение перегруженных операторов обычно считается дурным тоном. Во всяком случае в кругу знатоков С++ о таких вещах не говорят. Когда производный класс начинает вмешиваться в управление памятью базового класса, во всей программе начинают возникать непредвиденные эффекты. Если вам захочется использовать несколько стратегий управления памятью в одной иерархии классов, лучше сразу включить нужную стратегию в конкретный производный класс средствами множественного наследования, чем унаследовать ее и потом заявить в производном классе: Ха-ха, я пошутил .



Аргументы оператора new

Оператор new можно перегрузить так, чтобы помимо размера он вызывался и с другими дополнительными аргументами. Перегрузка лишает вас стандартной сигнатуры void* operator new(size t) , и, если вам этого не хочется, ее придется включить в программу вручную.

#define kPoolSize 4096 struct Pool {

unsigned char* next; Следующий свободный байт

unsigned char space[kPoo1Size];

Poo1() : next(&space[0]) {}

class Foo { public:

void* operator new(size t bytes)

{ return ::operator new(bytes); } void* operator new(size t bytes, Pool* pool)

void* space = poo1->next; poo1->next += bytes; return space;

void f() {

Pool localPool;

Foo* foo1 = new Foo; Использует оператор new по умолчанию Foo* foo2 = new(&1ocalPoo1) Foo; Использует перегрузку

Здесь клиент, а не класс указывает, где следует разместить объект. Показан лишь фрагмент полной стратегии. Например, как оператор delete узнает, откуда была взята память - из глобального пула, используемого оператором new по умолчанию, или настандартного пула, который используется перегруженным оператором new? Впрочем, основная идея проста: предоставить клиенту класса некоторую степень контроля над размещением экземпляров в памяти. Это означает, что способ выделения памяти может выбираться для конкретных объектов и не обязан совпадать для всех экземпляров класса.

Оператор new можно перегружать с любыми новыми сигнатурами при условии, что все они различаются, а первым аргументом каждой перегруженной версии является size t - количество нужных байт. Перегрузки могут быть как глобальными, так и принадлежать конкретным классам. Когда компилятор встречает аргументы между new и именем класса, он подставляет размер в начало списка аргументов и ищет подходящую сигнатуру.

Конструирование с разделением фаз

Эта идиома предложена Джеймсом Коплином (James Coplien), который назвал ее виртуальным конструктором . Что делает следующий перегруженный оператор new?

class Foo { public:

void* operator new(size t, void* p) { return p; }

На первый взгляд - ничего; пустая трата времени. Но так ли это? Что произойдет в следующем фрагменте?



1 ... 58 59 60 [ 61 ] 62 63 64 ... 82

© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки.
Яндекс.Метрика