|
Программирование >> Синтаксис инициирования исключений
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; } На первый взгляд - ничего; пустая трата времени. Но так ли это? Что произойдет в следующем фрагменте?
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |