Есть ли проблемы с распределением памяти в списках инициализации конструктора?

Я очень часто использовал списки инициализации в своих программах на С++, но не знал, что вы можете выделить в них память.

Итак, вы можете что-то сделать (как надуманный пример) следующим образом:

class Test
{
private:
    int* i;
    int* j;
    int count;
    int* k;

public:
    Test(void) : i(new int), j(new int[10]), count(10), k(new int[count])
    {
    }

    ~Test(void)
    {
        delete i;
        delete [] j;
        delete [] k;
    }
};

Есть ли проблемы в распределении памяти таким образом? Что касается порядка инициализации здесь, безопасно ли инициализировать параметр одним инициализированным в том же списке? то есть, когда я выделяю count, прежде чем использовать его, безопасно ли использовать или есть какой-то специальный порядок инициализации, который я мог бы испортить?

Ответ 1

Это не исключение. Если new для j выдает исключение, деструктор для Test не вызывается, поэтому память для i не освобождается.

Деструктор i вызывается, если вызывается инициализатор для j, это просто, что необработанный указатель не имеет деструктора. Поэтому вы можете сделать его безопасным для исключения, заменив i на подходящий умный указатель. В этом случае unique_ptr<int> для i и unique_ptr<int[]> для j будет делать.

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

Ответ 2

Этот код может утечка памяти при наличии инициализатора, который генерирует исключение.

Обратите внимание, что это можно было бы сделать правильно, если члены Test были умными, а не raw-указателями.

Ответ 3

Вы выделяете память в свой список-инициализатор; это совершенно нормально, но затем вы назначаете указатели на эту память на исходные указатели.

Эти исходные указатели не подразумевают какого-либо владения памятью или удаления указателей, на которые они указывают, и, как результат, код содержит несколько утечек памяти, не следует "Правило пяти" и, как правило, плохо.

Лучше всего написать Test:

class Test
{
private:
    //Assuming you actually want dynamic memory allocation:
    std::unique_ptr<int> i;
    std::unique_ptr<int[]> j;
    int count;
    std::unique_ptr<int[]> k;

public:
    Test(void) : i(new int), j(new int[10]), count(10), k(new int[count])
    {
    }
};

Ответ 4

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

Таким образом, единственным недостатком является то, что если вы хотите сохранить защиту от неудачного выделения - тогда вы скорее захотите инициализировать их до nil, а в конструкторе оберните выделение в блоке try. Это, как правило, не требуется, хотя, если ваше приложение не представляет реальной опасности нехватки памяти и нуждается в восстановлении от этого.

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

Ответ 5

Нет никакой конкретной проблемы с вызовом new из списка инициализаторов.

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

Как полагаться на порядок инициализации, это совершенно безопасно. Члены инициализируются в том порядке, в котором они перечислены в объявлении класса. Таким образом, вы можете использовать значение членов, инициализированных раньше, для инициализации членов "позже".

Просто имейте в виду, что это их порядок объявления внутри класса, а не их порядок в списке инициализации, который определяет их порядок инициализации.:)

Ответ 6

Предположим, что у вас есть:

class Foo
{
public:
    T* p1;
    T* p2;

    Foo()
    : p1(new T),
      p2(new T)
    {
    }
};

Если инициализация p2 завершается с ошибкой (либо из-за того, что new выдает исключение из памяти или из-за отказа конструктора T), то p1 будет просочиться. Чтобы бороться с этим, С++ позволяет использовать try/catch в списках инициализации, но обычно это довольно грубо.

Ответ 7

Ответ Стива Джессопа представляет собой подводные камни.

О заказе, о котором я думаю, относится к вашему вопросу:

12.6.2/4

Инициализация должна выполняться в следующем порядке:

[...]

  • Затем нестатические члены данных должны быть инициализированы в том порядке, в котором они были объявлены в определении класса (опять же независимо от порядка из mem-инициализаторов).

Так как в вашем классе count объявлен до k, вы в порядке.