Кто удаляет память, выделенную во время новой операции, которая имеет исключение в конструкторе?

Я действительно не могу поверить, что не смог найти четкого ответа на этот вопрос ...

Как освободить память, выделенную после того, как конструктор класса C ++ выдает исключение, если он инициализируется с помощью оператора new. Например.:

class Blah
{
public:
  Blah()
  {
    throw "oops";
  }
};

void main()
{
  Blah* b = NULL;
  try
  {
    b = new Blah();
  }
  catch (...)
  {
    // What now?
  }
}

Когда я это попробовал, b в блоке catch было NULL (что имеет смысл).

При отладке я заметил, что элемент управления входит в процедуру выделения памяти ДО того, как он попадает в конструктор.

Это на веб-сайте MSDN , похоже, подтверждает это:

Когда new используется для выделения памяти для объекта класса C ++, конструктор объекта вызывается после выделения памяти.

Итак, имея в виду, что локальная переменная b никогда не назначается (т.е. имеет значение NULL в блоке catch), как удалить выделенную память?

Также было бы неплохо получить по этому поводу кроссплатформенный ответ. то есть, что говорится в спецификации C ++?

УТОЧНЕНИЕ: я не говорю о случае, когда класс выделил память в c'tor, а затем выбрасывает. Я понимаю, что в таких случаях d'tor не будет вызван. Я говорю о памяти, используемой для выделения объекта THE (Blah в моем случае).


person John    schedule 04.11.2009    source источник
comment
в любом случае не стоит выполнять в конструкторе тяжелую работу. Оставьте это какому-нибудь методу инициализации.   -  person jldupont    schedule 04.11.2009
comment
Я согласен с jldupont, но тем не менее это интересный вопрос.   -  person Ben S    schedule 04.11.2009
comment
Ты в безопасности. Если конструктор выбрасывает, то память объектов уже освобождена (просто будьте осторожны с членами, так как деструктор не вызывается).   -  person Martin York    schedule 04.11.2009
comment
Напротив. Объект должен быть полностью инициализирован в конструкторе, если вы по какой-то причине не можете использовать исключения. Для получения более подробной информации прочтите приложение E к Stroustrups The C ++ Programming Language: www2.research.att .com / ~ bs / 3rd_safe.pdf   -  person Nemanja Trifunovic    schedule 04.11.2009
comment
Напротив, относится к сообщению jldupont, а не к сообщению Мартина :)   -  person Nemanja Trifunovic    schedule 04.11.2009
comment
@jldupont: Я бы хотел, чтобы мы проголосовали против вашего комментария. Конструкторы предназначены для того, чтобы оставлять объекты в готовом к использованию состоянии, поэтому нам не нужно помнить о вызове каких-либо функций инициализации. Ваш совет, ИМО, очень плохой.   -  person sbi    schedule 04.11.2009
comment
Думаю, этот тяжелый совет исходит из руководящих принципов Google по C ++. Я сам на это не подписываюсь.   -  person rpg    schedule 05.11.2009
comment
Но тогда рекомендации Google по C ++ предпочитают коды ошибок коду исключения, поэтому для них это имеет смысл, поскольку у них нет способа сообщить об ошибке из конструктора.   -  person Matthieu M.    schedule 05.11.2009
comment
Я думаю, вам не гарантировано, что b останется нулевым. Я бы использовал для этого класс RAII, чтобы сбросить его до старого значения, если новое выражение выдает, если вы хотите на это положиться.   -  person Johannes Schaub - litb    schedule 05.11.2009
comment
Я с @sbi; а в современном C ++ эта идея называется RAII: Resource Acquisition is Initialization. Когда у вас есть объект, который должен динамически выделять память, память должна быть выделена в конструкторе и освобождена в деструкторе. Лично меня устраивает частный метод инициализации, если в противном случае конструктор был бы чертовски длинным, и если конструктор - единственное место, где используется этот метод инициализации.   -  person Kaiser Keister    schedule 09.04.2020


Ответы (8)


Вы должны обратиться к аналогичным вопросам здесь и здесь . Обычно, если конструктор генерирует исключение, вы можете быть уверены, что память самого объекта снова будет освобождена. Хотя, если во время конструктора была запрошена другая память, вы можете освободить ее перед выходом из конструктора с исключением.

На ваш вопрос, ВОЗ удаляет память, ответ - это код нового оператора (который генерируется компилятором). Если он распознает исключение, покидающее конструктор, он должен вызвать все деструкторы членов классов (поскольку они уже были успешно созданы до вызова кода конструктора) и освободить их память (может быть выполнено рекурсивно вместе с вызовом деструктора, наиболее вероятно вызывая для них правильное delete), а также освобождает память, выделенную для самого этого класса. Затем он должен повторно передать перехваченное исключение из конструктора вызывающему объекту new. Конечно, может быть еще много работы, но я не могу вытащить из головы все детали, потому что они зависят от реализации каждого компилятора.

person Kosi2801    schedule 04.11.2009
comment
Как именно освобождается память? Меня особенно интересует случай, когда, например, кто-то может использовать собственный оператор new. - person Kylotan; 04.11.2009
comment
Ваши собственные реализации операторов new и delete должны знать только, как выделять и освобождать память. Дополнительную работу выполняет компилятор. Когда вы говорите new Blah(), компилятор генерирует код для (1) вызова нового оператора (который просто выделяет память), (2) вызова c'tor и (3) вызова оператора удаления, если что-то пойдет не так. - person Adrian McCarthy; 04.11.2009
comment
@ Адриан: Чтобы быть педантичным ;-) Вызывает (3), если (1) завершает и (2) выбрасывает. - person Martin York; 04.11.2009
comment
Фух, здорово, я думал, что схожу с ума :-). Спасибо за ответ. - person John; 05.11.2009
comment
PS: Прошу прощения за то, что задаю уже рассмотренный вопрос - сначала я бегло просмотрел, но не смог увидеть этот конкретный вопрос. - person John; 05.11.2009

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

Затем, если объект был выделен с помощью new, соответствующая функция освобождения (operator delete) вызывается с теми же дополнительными аргументами, которые были переданы operator new. Например, new (std::nothrow) SomethingThatThrows() выделит память с помощью operator new (size_of_ob, nothrow), попытается построить SomethingThatThrows, уничтожит все элементы, которые были успешно созданы, а затем вызовет operator delete (ptr_to_obj, nothrow) при распространении исключения - утечки памяти не будет.

Что вы должны быть осторожны, так это последовательное выделение нескольких объектов - если один из последних срабатывает, предыдущие не будут автоматически освобождены. Лучший способ решить эту проблему - использовать интеллектуальные указатели, поскольку их деструкторы в качестве локальных объектов будут вызываться во время раскрутки стека, а их деструкторы будут правильно освобождать память.

person coppro    schedule 04.11.2009
comment
Вы имели в виду Если объект не может завершить строительство ...? - person Walter; 21.02.2018

Если Конструктор выбрасывает выделенную для объекта память, она автоматически возвращается в систему.

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

Примечание:
Как отметило большинство других людей, участникам может потребоваться некоторая очистка.

Члены, которые были полностью инициализированы, будут иметь свои деструкторы, но если у вас есть какие-либо члены указателя RAW, которыми вы владеете (т.е. удалите в деструкторе), вам придется выполнить некоторую очистку, прежде чем выполнять бросок (еще одна причина не использовать принадлежащие Указатели RAW в вашем классе).

#include <iostream>

class Base
{
    public:
        Base()  {std::cout << "Create  Base\n";}
        ~Base() {std::cout << "Destroy Base\n";}
};

class Deriv: public Base
{
    public:
        Deriv(int x)    {std::cout << "Create  Deriv\n";if (x > 0) throw int(x);}
        ~Deriv()        {std::cout << "Destroy Deriv\n";}
};

int main()
{
    try
    {
        {
            Deriv       d0(0);  // All constructors/Destructors called.
        }
        {
            Deriv       d1(1);  // Base constructor and destructor called.
                                // Derived constructor called (not destructor)
        }
    }
    catch(...)
    {
        throw;
        // Also note here.
        // If an exception escapes main it is implementation defined
        // whether the stack is unwound. By catching in main() you force
        // the stack to unwind to this point. If you can't handle re-throw
        // so the system exception handling can provide the appropriate
        // error handling (such as user messages).
    }
}
person Martin York    schedule 04.11.2009
comment
... Note the destructor of the class that threw will not be called ... - очень важный момент. - person iammilind; 21.02.2012

Из стандарта C ++ 2003 5.3.4 / 17 - Новое:

Если какая-либо часть описанной выше инициализации объекта завершается выдачей исключения и может быть найдена подходящая функция освобождения, вызывается функция освобождения для освобождения памяти, в которой создавался объект, после чего исключение продолжает распространяться в контексте. нового выражения. Если не удается найти однозначно совпадающую функцию освобождения памяти, распространение исключения не приводит к освобождению памяти объекта. [Примечание: это уместно, когда вызываемая функция распределения не выделяет память; в противном случае это может привести к утечке памяти. ]

Таким образом, утечка может быть, а может и не быть - это зависит от того, может ли быть найден подходящий освободитель (что обычно бывает, если оператор new / delete не был переопределен). В случае, когда есть подходящий освободитель, компилятор несет ответственность для подключения в вызове к нему, если конструктор бросает.

Обратите внимание, что это более или менее не связано с тем, что происходит с ресурсами, полученными в конструкторе, что обсуждалось в моей первой попытке ответа, и этот вопрос обсуждается во многих часто задаваемых вопросах, статьях и публикациях.

person Michael Burr    schedule 04.11.2009

Короче говоря, если вы не выполнили какие-либо выделения других сущностей в своем объекте (как в вашем примере), тогда выделенная память будет автоматически удалена. Однако любые новые операторы (или что-либо еще, что напрямую управляет памятью) должны обрабатываться в операторе catch в конструкторе. В противном случае объект удаляется без удаления его последующих выделений, и у вас, мой друг, есть утечка.

person cmaynard    schedule 04.11.2009

Цитируется из FAQ по C ++ (parashift.com):

[17.4] Как мне обрабатывать ресурсы, если мои конструкторы могут вызывать исключения?

Каждый член данных внутри вашего объекта должен убирать свой беспорядок.

Если конструктор вызывает исключение, деструктор объекта не запускается. Если ваш объект уже сделал что-то, что необходимо отменить (например, выделение некоторой памяти, открытие файла или блокировку семафора), этот «материал, который необходимо отменить» должен запомнить член данных внутри объекта.

Например, вместо того, чтобы выделять память в необработанном Fred* элементе данных, поместите выделенную память в объект-член «умного указателя», и деструктор этого умного указателя будет delete объект Fred, когда умный указатель умрет. Шаблон std::auto_ptr является примером такого, как «умный указатель». Вы также можете написать собственный умный указатель для подсчета ссылок . Вы также можете использовать интеллектуальные указатели, чтобы «указывать» на диск. записи или объекты на других машинах.

Кстати, если вы думаете, что ваш Fred класс будет выделен в интеллектуальный указатель, будьте вежливы со своими пользователями и создайте typedef в своем Fred классе:

 #include <memory>

 class Fred {
 public:
   typedef std::auto_ptr<Fred> Ptr;
   ...
 };

Этот typedef упрощает синтаксис всего кода, который использует ваши объекты: ваши пользователи могут говорить Fred::Ptr вместо std::auto_ptr<Fred>:

 #include "Fred.h"

 void f(std::auto_ptr<Fred> p);  // explicit but verbose
 void f(Fred::Ptr           p);  // simpler

 void g()
 {
   std::auto_ptr<Fred> p1( new Fred() );  // explicit but verbose
   Fred::Ptr           p2( new Fred() );  // simpler
   ...
 }
person JRL    schedule 04.11.2009
comment
Речь идет о содержимом объекта, тогда как я думаю, что исходный плакат явно спрашивает о памяти, выделенной для размещения объекта. - person Kylotan; 04.11.2009
comment
Не отвечает на вопрос. - person Martin York; 04.11.2009
comment
Это касается родственного вопроса, а не заданного. Это говорит вам, как написать Blah, чтобы Blah мог очистить все, что он выделяет, прежде чем генерировать исключение. Вопрос в том, кто очищает память, выделенную для самого Blah. - person Adrian McCarthy; 04.11.2009

Описанная проблема стара, как дорога в Рим, если пользоваться голландской поговоркой. Я решил проблему, и выделение памяти для объекта, который может вызвать исключение, выглядит следующим образом:

try
{
    std::string *l_string =
        (_heap_cleanup_tpl<std::string>(&l_string),
        new std::string(0xf0000000, ' '));
    delete l_string;
}
catch(std::exception &)
{
}

Перед фактическим вызовом new-оператора создается безымянный (временный) объект, который получает адрес выделенной памяти через определяемый пользователем новый оператор (см. Остальную часть этого ответа). В случае нормального выполнения программы временный объект передает результат нового оператора (вновь созданный и полностью сконструированный объект, в нашем случае очень-очень длинная строка) переменной l_string. В случае исключения значение не передается, но деструктор временного объекта удаляет память (конечно, без вызова деструктора основного объекта).

Это немного нечеткий способ решения проблемы, но он работает. Проблемы могут возникнуть, потому что это решение требует, чтобы определяемый пользователем новый оператор и определяемый пользователем оператор удаления работали вместе с ним. Определяемые пользователем операторы new / delete должны будут вызывать реализацию операторов new / delete в стандартной библиотеке C ++, но я оставил это для краткости и вместо этого полагался на malloc() и free().

Это не окончательный ответ, но я думаю, что над этим стоит подумать.

PS: В приведенном ниже коде была «недокументированная» функция, поэтому я сделал улучшение.

Код временного объекта следующий:

class _heap_cleanup_helper
{
    public:
    _heap_cleanup_helper(void **p_heap_block) :
        m_heap_block(p_heap_block),
        m_previous(m_last),
        m_guard_block(NULL)
    {
        *m_heap_block = NULL;
        m_last = this;
    }
    ~_heap_cleanup_helper()
    {
        if (*m_heap_block == NULL) operator delete(m_guard_block);
        m_last = m_previous;
    }
    void **m_heap_block, *m_guard_block;
    _heap_cleanup_helper *m_previous;
    static _heap_cleanup_helper *m_last;
};

_heap_cleanup_helper *_heap_cleanup_helper::m_last;

template <typename p_alloc_type>
class _heap_cleanup_tpl : public _heap_cleanup_helper
{
    public:
    _heap_cleanup_tpl(p_alloc_type **p_heap_block) :
        _heap_cleanup_helper((void **)p_heap_block)
    {
    }
};

Пользовательский новый оператор выглядит следующим образом:

void *operator new (size_t p_cbytes)
{
    void *l_retval = malloc(p_cbytes);

    if (
        l_retval != NULL &&
        *_heap_cleanup_helper::m_last->m_heap_block == NULL &&
        _heap_cleanup_helper::m_last->m_guard_block == NULL
    )
    {
        _heap_cleanup_helper::m_last->m_guard_block = l_retval;
    }
    if (p_cbytes != 0 && l_retval == NULL) throw std::bad_alloc();

    return l_retval;
}

void operator delete(void *p_buffer)
{
    if (p_buffer != NULL) free(p_buffer);
}
person Bert-Jan    schedule 10.07.2011
comment
В лучшем случае так делает одна конкретная реализация. - person Dennis Zickefoose; 10.07.2011
comment
Можно, конечно, предположить, стоит ли беспокоиться о потере небольшого фрагмента памяти, такого как sizeof (std :: string). Позвольте вашему пользователю добавить несколько дополнительных гигабайт памяти в свой компьютер! - person Bert-Jan; 10.07.2011
comment
Кажется, вы предполагаете, что весь этот шаблонный код необходим, чтобы где-то избежать утечек памяти. Что, как ясно показывают другие ответы на этот вопрос двухлетней давности, не является необходимым. Или вы детализируете код, который компилятор может вставить для защиты от утечек памяти. В этом случае это слишком подробно ... очень простой блок try / catch также может проиллюстрировать эту мысль. В любом случае это ужасный ответ. - person Dennis Zickefoose; 10.07.2011
comment
Мой ответ - единственный ответ, который направлен на предотвращение утечки памяти, а не на избежание исключения, которое должно быть выброшено через границу конструктора. Вы можете найти ответ ужасным, но это единственный ответ, который решает проблему, как указано. Проблема решается простым добавлением одного простого оператора для каждого «нового распределения». Два последних кодовых блока нужно ввести только один раз. На самом деле, если вы не примете мер предосторожности, C ++ позволит утечкам памяти проникнуть в вашу программу таким образом. - person Bert-Jan; 10.07.2011
comment
Из принятого ответа: в основном, если конструктор выдает исключение, вы можете быть уверены, что память самого объекта снова будет освобождена. Из следующего наивысшего количества голосов: Затем, если объект был выделен с помощью new, вызывается соответствующая функция освобождения (оператор delete) с теми же дополнительными аргументами, которые были переданы оператору new. Из следующего наивысшего количества голосов: Если Конструктор выбрасывает память, выделенную для объекта, она автоматически возвращается в систему. Из gcc: ideone.com/IRxHX Какую утечку памяти вы пытаетесь избежать? - person Dennis Zickefoose; 10.07.2011
comment
-1. Если ОП не путали раньше, то теперь он уверен! РЖУ НЕ МОГУ! - person Zach Saw; 13.07.2011

Я думаю, что конструктору было бы странно вызывать исключение. Не могли бы вы получить возвращаемое значение и протестировать его на своем главном сервере?

class Blah
{
   public:

   Blah()
       {
           if Error
           {
              this.Error = "oops";
           }
        }
};

void main()
{
Blah* b = NULL;

b = new Blah();

if (b.Error == "oops")
{
   delete (b);
   b = NULL;
}
person Danielle    schedule 04.11.2009
comment
Исключением является подходящий инструмент в этом случае. Установка значения флага может использоваться только в том случае, если вы не можете использовать исключения. - person JRL; 04.11.2009
comment
Честно говоря, я думаю, что это было бы еще хуже, поскольку пользователю непонятно, что, когда конструктор завершился успешно, объект все еще находится в несогласованном состоянии. Предоставление другим убирать ваш беспорядок - плохой стиль. Я бы предпочел исключительный способ, если вы не можете извлечь из конструктора вещи, вызывающие ошибки. - person Kosi2801; 04.11.2009
comment
Возвращаемое значение, которое вы должны проверить, - это именно то, чего мы не хотим! Это то, что у нас было в простом старом C в течение тридцати лет, и сколько миллиардов игнорируемых возвращаемых значений у нас там сейчас ??? - person Thomas Padron-McCarthy; 04.11.2009
comment
Стандартный способ сделать это - использовать исключения, и в этом нет ничего странного. Подобные возвращаемые значения исторически не имели успеха. За исключениями, либо у нас есть пригодный для использования объект, либо его нет, поэтому мы можем использовать его без теста, который, вероятно, забудем. - person David Thornley; 04.11.2009