В каких ситуациях удалить указатель
Мой следующий вопрос касается управления памятью. У меня есть, например, переменная int, не распределенная динамически в классе, скажем, invar1. И я передаю адрес памяти этого int в другой конструктор классов. Этот класс делает это:
class ex1{
ex1(int* p_intvar1)
{
ptoint = p_intvar1;
}
int* ptoint;
};
Удалить ptoint? Поскольку у него есть адрес неунимально выделенного int, я думал, что мне не нужно его удалять.
И снова объявляю объект классу с новым оператором:
objtoclass = new ex1();
И передаю это другому классу:
class ex2{
ex2(ex1* p_obj)
{
obj = p_obj;
}
ex1* obj;
};
Должен ли я удалить obj, когда я уже удаляю objtoclass?
Спасибо!
Ответы
Ответ 1
Поскольку у него есть адрес неунимально выделенного int, я думал, что его не нужно удалять.
Правильно.
Должен ли я удалить obj, когда я уже удаляю objtoclass?
Нет.
Вспомните, что вы на самом деле не удаляете указатели; вы используете указатели, чтобы удалить предмет, на который они указывают. Таким образом, если вы написали как delete obj
, так и delete objtoclass
, поскольку оба указателя указывают на один и тот же объект, вы дважды удаляете этот объект.
Я бы предупредил вас, что это очень простая ошибка с вашим классом ex2
, в котором семантика собственности этого объекта с указателем не совсем понятна. Вы можете подумать об использовании реализации интеллектуального указателя для устранения риска.
Ответ 2
просто приложение к другим ответам
Вы можете избавиться от необработанных указателей и забыть об управлении памятью с помощью умных указателей (shared_ptr
, unique_ptr
).
Умный указатель отвечает за освобождение памяти, когда она выходит из области видимости.
Вот пример:
#include <iostream>
#include <memory>
class ex1{
public:
ex1(std::shared_ptr<int> p_intvar1)
{
ptoint = p_intvar1;
std::cout << __func__ << std::endl;
}
~ex1()
{
std::cout << __func__ << std::endl;
}
private:
std::shared_ptr<int> ptoint;
};
int main()
{
std::shared_ptr<int> pi(new int(42));
std::shared_ptr<ex1> objtoclass(new ex1(pi));
/*
* when the main function returns, these smart pointers will go
* go out of scope and delete the dynamically allocated memory
*/
return 0;
}
Вывод:
ex1
~ex1
Ответ 3
Должен ли я удалить obj, когда я уже удаляю objtoclass?
Ну, вы можете не забывать, что удаление одного и того же объекта дважды - это поведение undefined, и его следует избегать. Это может произойти, например, если у вас есть два указателя, например, указывающие на один и тот же объект, и вы удаляете исходный объект с помощью одного указателя - тогда вы также не должны удалять эту память с помощью другого указателя. В вашей ситуации вы также можете добавить два указателя, указывающих на один и тот же объект.
В общем, для создания класса, который управляет внутренней памятью (как и вы, похоже), нетривиально, и вам приходится учитывать такие вещи, как правило из трех, и т.д.
Относительно того, что вы должны удалить динамически выделенную память, вы правы. Вы не должны удалять память, если она не была распределена динамически.
PS. Во избежание таких сложностей, как выше, вы можете использовать интеллектуальные указатели.
Ответ 4
В настоящее время вы не удаляете этот int или не показываете, где он был выделен. Если ни один объект не должен владеть своим параметром, я бы написал
struct ex1 {
ex1(int &i_) : i(i_) {}
int &i; // reference implies no ownership
};
struct ex2 {
ex2(ex1 &e_) : e(e_) {}
ex1 &e; // reference implies no ownership
};
int i = 42;
ex1 a(i);
ex2 b(a);
Если любой аргумент должен принадлежать новому объекту, передайте его как unique_ptr
. Если любой аргумент должен быть общим, используйте shared_ptr
. Я обычно предпочитаю любой из них (справочный или умный указатель) для сырых указателей, потому что они дают больше информации о ваших намерениях.
В общем, чтобы принять эти решения,
Должен ли я удалить ptoint?
- неправильный вопрос. Сначала рассмотрите вещи на несколько более высоком уровне:
- что это int представляет в вашей программе?
- кто, если кто-нибудь, владеет им?
- как долго он должен жить, по сравнению с этими классами, которые его используют?
а затем посмотрите, как ответ естественно выпадет для этих примеров:
-
этот int является управляемым регистром ввода-вывода.
В этом случае он не был создан с помощью new
(он существует вне вашей всей программы), и поэтому вы, конечно же, не должны его удалять. Вероятно, он также должен быть отмечен volatile
, но это не влияет на время жизни.
Возможно, что-то вне вашего класса сопоставлено с адресом, а также его следует отменить, что совсем не похоже на (де) его выделение или, может быть, просто на известный адрес.
-
этот int является глобальным уровнем ведения журнала.
В этом случае он, по-видимому, имеет либо статическое время жизни, и в этом случае никто не владеет им, он не был явно выделен и поэтому не должен быть явно выделен
или он принадлежит объекту logger/singleton/mock/whatever, и этот объект отвечает за его освобождение при необходимости
-
этот int явно указывается вашему объекту на собственный
В этом случае хорошая практика сделать это очевидным, например.
ex1::ex1(std::unique_ptr<int> &&p) : m_p(std::move(p)) {}
Обратите внимание, что если ваш локальный элемент данных a unique_ptr
или аналогичный, также автоматически берется за время жизни без каких-либо усилий с вашей стороны.
-
этот int предоставляется вашему объекту для использования, но другие объекты также могут его использовать, и неясно, в каком порядке они будут завершены.
Используйте shared_ptr<int>
вместо unique_ptr
для описания этого отношения. Опять же, умный указатель будет управлять временем жизни для вас.
В общем случае, если вы можете закодировать информацию о собственности и жизни в типе, вам не нужно помнить, где вручную выделять и освобождать вещи. Это намного яснее и безопаснее.
Если вы не можете кодировать эту информацию в типе, вы можете, по крайней мере, четко указать свои намерения: тот факт, что вы спрашиваете об освобождении без упоминания о жизни или собственности, предполагает, что вы работаете на неправильном уровне абстракции.
Ответ 5
Поскольку у него есть адрес неунимально выделенного int, я подумал, что мне не нужно его удалять.
Это правильно. Просто не удаляйте его.
Вторая часть вашего вопроса касалась динамически распределенной памяти. Здесь вам нужно немного подумать и принять некоторые решения.
Предположим, что ваш класс с именем ex1 получает необработанный указатель в своем конструкторе для памяти, динамически выделенной вне класса.
Вы, как разработчик класса, должны решить, будет ли этот конструктор "владеть" этим указателем или нет. Если это так, то ex1 отвечает за удаление своей памяти, и вы должны сделать это, вероятно, в деструкторе класса:
class ex1 {
public:
/**
* Warning: This constructor takes the ownership of p_intvar1,
* which means you must not delete it somewhere else.
*/
ex1(int* p_intvar1)
{
ptoint = p_intvar1;
}
~ex1()
{
delete ptoint;
}
int* ptoint;
};
Однако это, как правило, плохое дизайнерское решение. Вы должны root, чтобы пользователь этого класса прочитал комментарий к конструктору и не забыл удалить память, выделенную где-то вне класса ex1.
Метод (или конструктор), который получает указатель и принимает его собственность, называется "потоком".
Кто-то будет использовать этот класс, например:
int* myInteger = new int(1);
ex1 obj(myInteger); // sink: obj takes the ownership of myInteger
// never delete myInteger outside ex1
Другой подход - сказать, что ваш класс ex1 не принимает права собственности, и тот, кто выделяет память для этого указателя, несет ответственность за его удаление. Класс ex1 не должен удалять что-либо из его деструктора, и его следует использовать следующим образом:
int* myInteger = new int(1);
ex1 obj(myInteger);
// use obj here
delete myInteger; // remeber to delete myInteger
Опять же, пользователь вашего класса должен прочитать некоторую документацию, чтобы знать, что он несет ответственность за удаление этого материала.
Вам нужно выбирать между этими двумя проектными решениями, если вы не используете современный С++.
В современных С++ (С++ 11 и 14) вы можете сделать вещи явными в коде (т.е. не нужно полагаться только на документацию кода).
Во-первых, в современном С++ вы избегаете использования исходных указателей. Вы должны выбрать между двумя типами "умных указателей": unique_ptr или shared_ptr. Разница между ними заключается в владении.
Как говорят их имена, уникальный указатель принадлежит только одному парню, в то время как общий указатель может принадлежать одному или нескольким (право собственности разделяется).
Уникальный указатель (std:: unique_ptr) не может быть скопирован, только "перемещен" из одного места в другое. Если класс имеет уникальный указатель как атрибут, он явно указывает, что этот класс имеет право владения этим указателем. Если метод получает уникальный указатель как копию, он явно указывает, что он является "приемником" (принимает права собственности на указатель).
Ваш класс ex1 может быть написан следующим образом:
class ex1 {
public:
ex1(std::unique_ptr<int> p_intvar1)
{
ptoint = std::move(p_intvar1);
}
std::unique_ptr<int> ptoint;
};
Пользователь этого класса должен использовать его как:
auto myInteger = std::make_unique<int>(1);
ex1 obj(std::move(myInteger)); // sink
// here, myInteger is nullptr (it was moved to ex1 constructor)
Если вы забыли сделать "std:: move" в приведенном выше коде, компилятор сгенерирует ошибку, сообщающую вам, что unique_ptr не копируется.
Также обратите внимание, что вам не нужно явно удалять память. Умные указатели обрабатывают это для вас.