Деструкторы / FAQ C++

Добавлено 7 октября 2020 в 21:06

Что там с деструкторами?

Деструктор выполняет над объектом последние ритуалы.

Деструкторы используются для высвобождения любых ресурсов, выделенных объектом. Например, класс Lock может заблокировать семафор, и деструктор освободит этот семафор. Самый распространенный пример – когда конструктор использует new, а деструктор использует delete.

Деструкторы – это функция-член «подготовки к смерти». Часто их называют сокращенно «dtor».


В каком порядке разрушаются локальные объекты?

В порядке, обратном созданию: первым создан – последним разрушен.

В следующем примере сначала будет выполнен деструктор b, а затем деструктор a:

void userCode()
{
  Fred a;
  Fred b;
  // ...
}

В каком порядке разрушаются объекты в массиве?

В порядке, обратном созданию: первым создан – последним разрушен.

В следующем примере порядок деструкторов будет a[9], a[8],…, a[1], a[0]:

void userCode()
{
  Fred a[10];
  // ...
}

В каком порядке разрушаются подобъекты объекта?

В порядке, обратном созданию: первым создан – последним разрушен.

Выполняется тело деструктора объекта, за которым следуют деструкторы членов данных объекта (в обратном порядке их появления в определении класса), за которыми следуют деструкторы базовых классов объекта (в обратном порядке их появления в определении класса).

В следующем примере порядок вызовов деструктора, когда d выходит за пределы области видимости, будет ~local1(), ~local0(), ~member1(), ~member0(), ~base1(), ~base0():

struct base0 { ~base0(); };
struct base1 { ~base1(); };
struct member0 { ~member0(); };
struct member1 { ~member1(); };
struct local0 { ~local0(); };
struct local1 { ~local1(); };

struct derived: base0, base1
{
  member0 m0_;
  member1 m1_;

  ~derived()
  {
    local0 l0;
    local1 l1;
  }
}

void userCode()
{
  derived d;
}

Могу ли я перегрузить деструктор для своего класса?

Нет.

У вас может быть только один деструктор для класса Fred. Он всегда называется Fred::~Fred(). Он никогда не принимает никаких параметров и ничего не возвращает.

В любом случае вы не можете передавать параметры деструктору, поскольку вы никогда явно его не вызываете (ну, почти никогда).


Должен ли я явно вызывать деструктор локальной переменной?

Нет!

Деструктор будет вызван снова при закрытии } блока, в котором был создан локальный объект. Это гарантия языка; это происходит автоматически; нет никакого способа предотвратить это. Но вы можете получить действительно плохие результаты, вызвав деструктор для того же объекта второй раз! Взрыв! Вы мертвы!


Что, если я хочу, чтобы локальный объект «умер» до закрытия } области, в которой он был создан? Могу ли я вызвать деструктор локального объекта, если действительно хочу это сделать?

Нет! [Для контекста прочтите ответ на предыдущий вопрос.]

Предположим, что (желательным) побочным эффектом разрушения локального объекта File является закрытие файла. Теперь предположим, что у вас есть объект f класса File, и вы хотите, чтобы файл f был закрыт до конца области (т.е. до }) объекта f:

void someCode()
{
  File f;
  // ... код, который должен выполниться, пока f еще открыт...
  ← Здесь нам нужны побочные эффекты выполнения деструктора f!
  // ...код, который должен выполниться после закрытия f...
}

У этой проблемы есть простое решение. Но в то же время помните: не вызывайте деструктор явно!


Ладно, ладно, уже; я не буду явно называть деструктор локального объекта; но как мне справиться с ситуацией из предыдущего вопроса?

[Для контекста прочтите ответ на предыдущий вопрос].

Просто оберните время жизни локального объекта в искусственный блок {...}:

void someCode()
{
  {
    File f;
    // ...код, который должен выполниться, пока f еще открыт...
  }
  ↑ // Здесь автоматически вызывается деструктор f!
  // ...код, который должен выполниться после закрытия f...
}

Что делать, если я не могу обернуть локальный объект в искусственный блок?

В большинстве случаев вы можете ограничить время жизни локального объекта, заключив его в искусственный блок ({...}). Но если по какой-то причине вы не можете этого сделать, добавьте функцию-член, которая имеет тот же эффект, что и деструктор. Но не вызывайте сам деструктор!

Например, в случае класса File вы можете добавить метод close(). Обычно деструктор просто вызывает этот метод close(). Обратите внимание, что метод close() должен будет пометить объект File, чтобы последующий вызов не закрывал уже закрытый File. Например, он может установить член данных fileHandle_ на какое-то бессмысленное значение, например -1, и может вначале проверить, равен ли fileHandle_ уже -1:

class File {
public:
  void close();
  ~File();
  // ...
private:
  int fileHandle_;   // fileHandle_ >= 0 если /только если файл открыт
};

File::~File()
{
  close();
}

void File::close()
{
  if (fileHandle_ >= 0) {
    // ...код, который вызывает ОС, чтобы закрыть файл...
    fileHandle_ = -1;
  }
}

Обратите внимание, что другим методам File также может потребоваться проверить, имеет ли fileHandle_ значение -1 (т.е. проверить, закрыт ли файл).

Также обратите внимание, что любые конструкторы, которые на самом деле не открывают файл, должны установить для fileHandle_ значение -1.


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

Возможно, нет.

Если вы не использовали размещение new, вам следует просто удалить объект, используя delete, а не явно вызывать деструктор. Например, предположим, что вы разместили объект с помощью типового выражения new:

Fred* p = new Fred();

Тогда деструктор Fred::~Fred() будет автоматически вызван, когда вы удалите его через:

delete p;  // автоматически вызывает p->~Fred()

Вы не должны явно вызывать деструктор, поскольку это не освобождает память, выделенную для самого объекта Fred. Помните: delete p выполняет две функции: вызывает деструктор и освобождает память.


Что такое «размещение new» и зачем его использовать?

Есть много применений размещения new. Самый простой способ – поместить объект в определенном месте в памяти. Это делается путем предоставления места в качестве параметра указателя части new выражения new:

#include <new>        // чтобы использовать "размещение new", необходимо включить этот файл
#include "Fred.h"     // объявление класса Fred

void someCode()
{
  char memory[sizeof(Fred)];     // строка #1
  void* place = memory;          // строка #2

  Fred* f = new(place) Fred();   // строка #3 (смотрите "ОПАСНОСТЬ" ниже)
  // указатели f и place будут равны

  // ...
}

Строка #1 создает массив размером sizeof(Fred) байтов памяти, который достаточно велик для размещения объекта Fred. Строка #2 создает указатель place, который указывает на первый байт этой памяти (опытные программисты на C заметят, что этот шаг не нужен; он нужен только для того, чтобы сделать код более очевидным). Строка #3 просто вызывает конструктор Fred::Fred(). Указатель this в конструкторе Fred будет равен указателю place. Таким образом, возвращаемый указатель f будет равен place.

СОВЕТ. Без необходимости не используйте синтаксис «размещения new». Используйте его только тогда, когда вам действительно важно, чтобы объект был помещен в определенное место в памяти. Например, если ваше оборудование имеет устройство таймера ввода-вывода с отображением в памяти, и вы хотите разместить объект Clock в этом месте памяти.

ОПАСНОСТЬ. Вы несете исключительную ответственность за то, чтобы указатель, который вы передаете оператору «размещение new», указывал на область памяти, которая достаточно велика и правильно выровнена для типа объекта, который вы создаете. Ни компилятор, ни система времени выполнения не пытаются проверить, правильно ли вы это сделали. Если ваш класс Fred нужно выровнять по 4-байтовой границе, но вы указали местоположение, которое не выровнено должным образом, у вас может возникнуть серьезная катастрофа (если вы не знаете, что означает «выравнивание», пожалуйста, не используйте синтаксис размещения new). Вы предупреждены.

Вы также несете полную ответственность за уничтожение размещенного объекта. Это делается явным вызовом деструктора:

void someCode()
{
  char memory[sizeof(Fred)];
  void* p = memory;
  Fred* f = new(p) Fred();
  // ...
  f->~Fred();   // явно вызывает деструктор для размещенного объекта
}

Это примерно единственный раз, когда вы явно вызываете деструктор.

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


Существует ли размещение delete?

Нет, но если оно вам необходимо, вы можете написать свое собственное.

Рассмотрим размещение new, используемое для размещения объектов в наборе арен:

class Arena {
public:
  void* allocate(size_t);
  void deallocate(void*);
  // ...
};

void* operator new(size_t sz, Arena& a)
{
  return a.allocate(sz);
}

Arena a1(some arguments);
Arena a2(some arguments);

Учитывая это, мы можем написать

X* p1 = new(a1) X;
Y* p2 = new(a1) Y;
Z* p3 = new(a2) Z;
// ...

Но как потом правильно удалить эти объекты? Причина отсутствия встроенного «размещения delete», соответствующего размещению new, заключается в том, что нет общего способа гарантировать, что оно будет использоваться правильно. Ничто в системе типов C++ не позволяет нам сделать вывод, что p1 указывает на объект, размещенный в Arena a1. Указатель на любой X, размещенный где угодно, может быть назначен указателю p1.

Однако иногда программист знает, способ есть:

template<class T> void destroy(T* p, Arena& a)
{
  if (p) {
    p->~T();        // явно вызывает деструктор
    a.deallocate(p);
  }
}

Теперь мы можем написать:

destroy(p1,a1);
destroy(p2,a2);
destroy(p3,a3);

Если Arena отслеживает, какие объекты она содержит, вы даже можете написать destroy(), чтобы защитить себя от ошибок.

Также возможно определить пары операторов operator new() и operator delete() для иерархии классов TC++PL(SE) 15.6. Смотрите также D&E 10.4 и TC++PL(SE) 19.4.5.


Когда я пишу деструктор, нужно ли мне явно вызывать деструкторы для моих объектов-членов?

Нет. Вам никогда не нужно явно вызывать деструктор (за исключением размещения new).

Деструктор класса (вне зависимости от того, определили вы его явно или нет) автоматически вызывает деструкторы для объектов-членов. Они уничтожаются в порядке, обратном тому, в котором были указаны в объявлении класса.

class Member {
public:
  ~Member();
  // ...
};

class Fred {
public:
  ~Fred();
  // ...
private:
  Member x_;
  Member y_;
  Member z_;
};

Fred::~Fred()
{
  // Компилятор автоматически вызывает z_.~Member()
  // Компилятор автоматически вызывает y_.~Member()
  // Компилятор автоматически вызывает x_.~Member()
}

Когда я пишу деструктор производного класса, нужно ли мне явно вызывать деструктор для моего базового класса?

Нет. Вам никогда не нужно явно вызывать деструктор (за исключением размещения new).

Деструктор производного класса (вне зависимости от того, определили вы его явно или нет) автоматически вызывает деструкторы для подобъектов базового класса. Базовые классы уничтожаются после объектов-членов. В случае множественного наследования прямые базовые классы уничтожаются в порядке, обратном их появлению в списке наследования.

class Member {
public:
  ~Member();
  // ...
};

class Base {
public:
  virtual ~Base();     // виртуальный деструктор
  // ...
};

class Derived : public Base {
public:
  ~Derived();
  // ...
private:
  Member x_;
};

Derived::~Derived()
{
  // Компилятор автоматически вызывает x_.~Member()
  // Компилятор автоматически вызывает Base::~Base()
}

Примечание. Зависимости порядка с виртуальным наследованием сложнее. Если вы полагаетесь на зависимости порядка в иерархии виртуального наследования, вам понадобится гораздо больше информации, чем в этом FAQ.


Должен ли мой деструктор генерировать исключение при обнаружении проблемы?

Осторожно!!! Подробности смотрите в ответе на этот вопрос (ссылка скоро появится).


Есть ли способ заставить new выделять память из определенной области памяти?

Да. Хорошей новостью является то, что эти «пулы памяти» полезны в ряде ситуаций. Плохая новость в том, что мне придется протащить вас через болото того, как это работает, прежде чем мы обсудим все применения. Но если вы не знаете о пулах памяти, возможно, стоит потратить время на изучение ответа на этот вопрос – вы можете узнать кое-что полезное!

Прежде всего, напомним, что распределитель памяти должен просто возвращать неинициализированные биты памяти; он не должен создавать «объекты». В частности, распределитель памяти не должен устанавливать виртуальный указатель или любую другую часть объекта, так как это задача конструктора, который запускается после распределителя памяти. Начав с простой функции выделения памяти, allocate(), вы должны использовать размещение new для создания объекта в этой памяти. Другими словами, следующий код эквивалентен new Foo():

void* raw = allocate(sizeof(Foo));  // строка 1
Foo* p = new(raw) Foo();            // строка 2

Предполагая, что вы использовали размещение new и выжили после двух приведенных выше строк кода, следующим шагом будет превращение вашего распределителя памяти в объект. Такие объекты называют «пулом памяти» или «ареной памяти». Это позволяет вашим пользователям иметь более одного «пула» или «арены», из которых будет выделяться память. Каждый из этих объектов пула памяти будет выделять большой кусок памяти, используя определенный системный вызов (например, разделяемую память, постоянную память, стековую память и т.д.; смотрите ниже), и при необходимости распределяет его небольшими порциями. Ваш класс пула памяти может выглядеть примерно так:

class Pool {
public:
  void* alloc(size_t nbytes);
  void dealloc(void* p);
private:
  // ...члены данных, используемые в вашем объекте пула...
};

void* Pool::alloc(size_t nbytes)
{
  // ...здесь идет ваш алгоритм...
}

void Pool::dealloc(void* p)
{
  // ...здесь идет ваш алгоритм...
}

Теперь у одного из ваших пользователей может быть Pool с именем pool, из которого он может выделять объекты примерно так:

Pool pool;
// ...
void* raw = pool.alloc(sizeof(Foo));
Foo* p = new(raw) Foo();

Или просто:

Foo* p = new(pool.alloc(sizeof(Foo))) Foo();

Превратить Pool в класс полезно потому, что он позволяет пользователям создавать N разных пулов памяти вместо того, чтобы иметь один массивный пул, используемый совместно всеми пользователями. Это позволяет пользователям делать много забавных вещей. Например, если у них есть кусок системы, который как сумасшедший выделяет память, а затем уходит; они могут выделить всю свою память из Pool, а затем даже не беспокоиться об удалении небольших кусков: можно просто освободить весь пул сразу. Или они могут создать область «совместно используемой (shared) памяти» (где операционная система специально предоставляет память, которая совместно используется несколькими процессами) и сделать так, чтобы пул выделял фрагменты общей памяти, а не локальную память процесса. Другой аспект: многие системы поддерживают нестандартную функцию, часто называемую alloca(), которая выделяет блок памяти из стека, а не из кучи. Естественно, этот блок памяти автоматически удаляется при возврате функции, устраняя необходимость в явном удалении. Кто-то может использовать alloca(), чтобы предоставить пулу большой кусок памяти, тогда все маленькие кусочки, выделенные из этого объекта Pool, будут действовать как локальные: они автоматически исчезают, когда происходит возврат из функции. Конечно, в некоторых из этих случаев деструкторы не вызываются, и если деструкторы делают что-то нетривиальное, вы не сможете использовать эти методы, но в случаях, когда деструктор просто освобождает память, такие методы могут быть полезны.

Предположим, что вы пережили 6 или 8 строк кода, необходимых для обертывания вашей функции выделения памяти как метода класса Pool, следующим шагом будет изменение синтаксиса для выделения памяти для объектов. Цель состоит в том, чтобы перейти от довольно неуклюжего синтаксиса new(pool.alloc(sizeof(Foo))) Foo() к более простому синтаксису new(pool) Foo(). Чтобы это произошло, вам нужно добавить следующие две строки кода чуть ниже определения вашего класса Pool:

inline void* operator new(size_t nbytes, Pool& pool)
{
  return pool.alloc(nbytes);
}

Теперь, когда компилятор видит new(pool) Foo(), он вызывает вышеуказанный оператор new и передает sizeof(Foo) и pool в качестве параметров, и единственная функция, которая в конечном итоге использует метод pool.alloc(nbytes), – это ваш собственный оператор new.

Теперь к вопросу о том, как разрушить/высвободить объекты Foo. Напомним, что метод грубой силы, который иногда используется при размещении new, заключается в явном вызове деструктора, а затем явном освобождении памяти:

void sample(Pool& pool)
{
  Foo* p = new(pool) Foo();
  // ...
  p->~Foo();        // явно вызывает деструктор
  pool.dealloc(p);  // явно освобождает память
}

Здесь есть несколько проблем, и все они решаемы:

  1. Если Foo::Foo() вызовет исключение, произойдет утечка памяти.
  2. Синтаксис уничтожения/освобождения отличается от того, к чему привыкло большинство программистов, поэтому они, вероятно, всё испортят.
  3. Пользователи должны каким-то образом запоминать, какой пул с каким объектом связан. Поскольку код, который выделяет память, часто находится в другой функции, чем код, который освобождает выделенную памяти, программистам придется передавать два указателя (Foo* и Pool*), что быстро становится ужасным (например, что, если бы у них был массив объектов Foo, все из которых потенциально пришли из разных объектов Pool).

Мы исправим эти проблемы в этом же порядке.

Проблема №1: устранение утечки памяти. Когда вы используете «обычный» оператор new, например, Foo* p = new Foo(), компилятор генерирует специальный код для обработки случая, когда конструктор генерирует исключение. Реальный код, сгенерированный компилятором, функционально похож на этот:

// Это то, что функционально происходит при Foo* p = new Foo()

Foo* p;

// не отлавливает исключения, выкинутые самим распределителем памяти
void* raw = operator new(sizeof(Foo));

// отлавливает любые исключения, выкинутые конструктором
try {
  p = new(raw) Foo();  // вызывает конструктор с raw в качестве this
}
catch (...) {
  // упс, конструктор выкинул исключение
  operator delete(raw);
  throw;  // повторно выкидывает исключение конструктора
}

Дело в том, что компилятор освобождает память, если конструктор генерирует исключение. Но в случае синтаксиса «new с параметром» (обычно называемого «размещением new») компилятор не знает, что делать, если возникает исключение, поэтому по умолчанию он ничего не делает:

// Это то, что функционально происходит при Foo* p = new(pool) Foo():

void* raw = operator new(sizeof(Foo), pool);
// функция выше просто возвращает "pool.alloc(sizeof(Foo))"

Foo* p = new(raw) Foo();
// если строка выше "выкидывает" исключение, pool.dealloc(raw) НЕ вызывается

Итак, цель состоит в том, чтобы заставить компилятор делать что-то похожее на то, что он делает с глобальным оператором new. К счастью, это просто: когда компилятор видит new(pool) Foo(), он ищет соответствующий оператор delete. Если он его находит, он выполняет эквивалент обертывания вызова конструктора в блок try, как показано выше. Поэтому мы просто предоставим оператор delete со следующей сигнатурой (будьте осторожны, чтобы сделать всё правильно; если второй параметр имеет тип, отличный от второго параметра operator new(size_t, Pool&), компилятор не пожалуется; он просто обойдет блок try, когда ваши пользователи скажут new(pool) Foo()):

void operator delete(void* p, Pool& pool)
{
  pool.dealloc(p);
}

После этого компилятор автоматически обернет вызовы конструкторов ваших выражений new в блок try:

// Это то, что функционально происходит при Foo* p = new(pool) Foo()

Foo* p;

// не отлавливает исключения, выкинутые самим распределителем памяти
void* raw = operator new(sizeof(Foo), pool);
// функция выше просто возвращает "pool.alloc(sizeof(Foo))"

// отлавливает любые исключения, выкинутые конструктором
try {
  p = new(raw) Foo();  // вызывает конструктор с raw в качестве this
}
catch (...) {
  // упс, конструктор выкинул исключение
  operator delete(raw, pool);  // вот волшебная строчка!!
  throw;  // повторно выкидывает исключение конструктора
}

Другими словами, однострочная функция operator delete(void* p, Pool& pool) заставляет компилятор автоматически закрывать утечку памяти. Конечно, эта функция может быть, но не обязательно, встроенной.

Проблемы №2 («уродливо, поэтому подвержено ошибкам») и №3 («пользователи должны вручную связывать указатели пула с объектом, который выделил для них память, что подвержено ошибкам») решаются одновременно с помощью дополнительных 10-20 строк кода в одном место. Другими словами, мы добавляем 10-20 строк кода в одном месте (заголовочный файл Pool) и упрощаем сколь угодно большое количество других мест (каждый фрагмент кода, который использует ваш класс Pool).

Идея состоит в том, чтобы неявно связывать Pool* с каждым выделением. Pool*, связанный с глобальным распределителем, будет иметь значение NULL, но, по крайней мере, концептуально можно сказать, что каждое выделение памяти имеет связанный Pool*. Затем вы заменяете глобальный оператор delete, чтобы он находил связанный Pool*, и, если тот не равен NULL, вызывал функцию освобождения объекта Pool. Например, если (!) обычный деаллокатор использует free(), замена глобального оператора delete будет выглядеть примерно так:

void operator delete(void* p)
{
  if (p != NULL) {
    Pool* pool = /* как-то получить связанный 'Pool*' */;
    if (pool == NULL)
      free(p);
    else
      pool->dealloc(p);
  }
}

Если вы не уверены, был ли обычный деаллокатор функцией free(), самый простой способ – также заменить глобальный оператор new чем-то, что использует malloc(). Замена глобального оператора new будет выглядеть примерно так (обратите внимание: это определение игнорирует некоторые детали, такие как цикл new_handler и выкидывание исключения throw std::bad_alloc(), которое происходит, если у нас заканчивается память):

void* operator new(size_t nbytes)
{
  if (nbytes == 0)
    nbytes = 1;  // поэтому все выделения памяти получают отдельный адрес
  void* raw = malloc(nbytes);
  // ...как-то связать NULL 'Pool*' с 'raw'...
  return raw;
}

Единственная оставшаяся проблема – связать Pool* с выделением памяти. Один из подходов, используемых, по крайней мере, в одном коммерческом продукте, заключается в использовании std::map<void*, Pool*>. Другими словами, создайте таблицу поиска, ключи которой – это указатели выделения памяти, а значения – связанные Pool*. По причинам, которые я опишу чуть позже, важно, чтобы вы вставляли пару ключ/значение в карту только в операторе new(size_t, Pool&). В частности, вы не должны вставлять пару ключ/значение из глобального оператора new (например, вы не должны указывать poolMap[p] = NULL в глобальном операторе new). Причина: это создало бы неприятную проблему с курицей и яйцом – поскольку std::map, вероятно, использует глобальный оператор new, он заканчивает тем, что вставляет новую запись каждый раз, когда вставляет новую запись, что приводит к бесконечной рекурсии – бах, вы мертвы.

Несмотря на то, что этот метод требует поиска в std::map для каждого освобождения памяти, кажется, что он имеет приемлемую производительность, по крайней мере, во многих случаях.

Другой подход, который быстрее, но может использовать больше памяти и немного сложнее, – это добавить Pool* непосредственно перед всеми выделениями. Например, если nbytes было равно 24, то есть вызывающий запрашивал выделение 24 байтов, мы бы выделили 28 (или 32, если вы думаете, что машине требуется 8-байтовое выравнивание для таких вещей, как double и/или long long), записали бы значение указателя Pool* в первые 4 байта и вернули бы указатель на 4 (или 8) байта справа от начала того, что выделили. Затем ваш глобальный оператор delete возвращается на 4 (или 8) байта, находит Pool*, и, если тот равен NULL, использует free(), в противном случае вызывает pool->dealloc(). Параметр, переданный в free() и pool->dealloc(), будет указателем на 4 (или 8) байта слева от исходного параметра p. Если (!) вы выберете 4-байтовое выравнивание, ваш код будет выглядеть примерно так (хотя, как и раньше, следующий код оператора new исключает обычные обработчики нехватки памяти):

void* operator new(size_t nbytes)
{
  if (nbytes == 0)
    nbytes = 1;                    // поэтому все выделения памяти получают отдельные адреса
  void* ans = malloc(nbytes + 4);  // увеличить выделяемую память на 4 байта
  *(Pool**)ans = NULL;             // использует NULL в глобальном new
  return (char*)ans + 4;           // не позволяет пользователям увидеть Pool*
}

void* operator new(size_t nbytes, Pool& pool)
{
  if (nbytes == 0)
    nbytes = 1;                    // поэтому все выделения памяти получают отдельные адреса
  void* ans = pool.alloc(nbytes + 4); // увеличить выделяемую память на 4 байта
  *(Pool**)ans = &pool;            // поместить сюда Pool* 
  return (char*)ans + 4;           // не позволяет пользователям увидеть Pool*
}

void operator delete(void* p)
{
  if (p != NULL) {
    p = (char*)p - 4;              // возвращается к Pool*
    Pool* pool = *(Pool**)p;
    if (pool == NULL)
      free(p);                     // примечание: 4 байта слева от исходного p
    else
      pool->dealloc(p);            // примечание: 4 байта слева от исходного p
  }
}

Естественно, последние несколько абзацев этого FAQ применимы только в том случае, если вам разрешено изменять глобальные operator new и operator delete. Если вам не разрешено изменять эти глобальные функции, действуют первые три четверти ответа на этот вопрос.

Теги

C++ / CPPFAQnew / deleteВысокоуровневые языки программированияДеструктор / Destructor / dtor (программирование)ПрограммированиеУправление памятьюЯзыки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.