195
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 delete(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;
};
196
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 delete(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 delete(void* space, size_t bytes);
};
С виртуальным деструктором все работает. Если деструктор не виртуальный, в следующем фрагменте
будет вызван правильный оператор
new
и оператор
delete
базового класса:
Foo* foo = new Bar;
delete foo;
Хотя этот фрагмент работает, подобное переопределение перегруженных операторов обычно считается
дурным тоном. Во всяком случае в кругу знатоков С++ о таких вещах не говорят. Когда производный
класс начинает вмешиваться в управление памятью базового класса, во всей программе начинают
возникать непредвиденные эффекты. Если вам захочется использовать несколько стратегий управления
памятью в одной иерархии классов, лучше сразу включить нужную стратегию в конкретный
производный класс средствами множественного наследования, чем унаследовать ее и потом заявить в
производном классе: «Ха-ха, я пошутил».
Do'stlaringiz bilan baham: |