Есть ли проблемы с распределением памяти в списках инициализации конструктора?
Я очень часто использовал списки инициализации в своих программах на С++, но не знал, что вы можете выделить в них память.
Итак, вы можете что-то сделать (как надуманный пример) следующим образом:
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
, вы в порядке.