Почему использование "новых" вызывает утечку памяти?
Сначала я изучил С#, и теперь я начинаю с С++. Насколько я понимаю, оператор new
в С++ не похож на тот, что в С#.
Можете ли вы объяснить причину утечки памяти в этом примере кода?
class A { ... };
struct B { ... };
A *object1 = new A();
B object2 = *(new B());
Ответы
Ответ 1
Что происходит
Когда вы пишете T t;
, вы создаете объект типа T
с автоматическим временем хранения. Он будет автоматически очищаться, когда он выходит за рамки.
Когда вы пишете new T()
, вы создаете объект типа T
с динамической продолжительностью хранения. Он не будет автоматически очищаться.
Вам нужно передать указатель на delete
, чтобы очистить его:
Однако ваш второй пример хуже: вы разыгрываете указатель и делаете копию объекта. Таким образом вы теряете указатель на объект, созданный с помощью new
, поэтому вы никогда не сможете удалить его, даже если хотите!
Что вам следует делать
Вы предпочитаете автоматическую продолжительность хранения. Нужен новый объект, просто напишите:
A a; // a new object of type A
B b; // a new object of type B
Если вам нужна динамическая длительность хранения, сохраните указатель на выделенный объект в объекте с автоматической памятью, который автоматически удалит его.
template <typename T>
class automatic_pointer {
public:
automatic_pointer(T* pointer) : pointer(pointer) {}
// destructor: gets called upon cleanup
// in this case, we want to use delete
~automatic_pointer() { delete pointer; }
// emulate pointers!
// with this we can write *p
T& operator*() const { return *pointer; }
// and with this we can write p->f()
T* operator->() const { return pointer; }
private:
T* pointer;
// for this example, I'll just forbid copies
// a smarter class could deal with this some other way
automatic_pointer(automatic_pointer const&);
automatic_pointer& operator=(automatic_pointer const&);
};
automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically
Это распространенная идиома, которая идет не очень описательным именем RAII (Инициализация ресурсов - Инициализация). Когда вы приобретаете ресурс, который нуждается в очистке, вы привязываете его к объекту с автоматическим хранением, поэтому вам не нужно беспокоиться о его очистке. Это относится к любому ресурсу, будь то память, открытые файлы, сетевые подключения или все, что вам нравится.
Эта вещь automatic_pointer
уже существует в различных формах, я просто предоставил ее, чтобы привести пример. Очень похожий класс существует в стандартной библиотеке под названием std::unique_ptr
.
Также есть старый (pre-С++ 11) с именем auto_ptr
, но теперь он устарел, потому что у него странное поведение копирования.
И затем есть некоторые даже более умные примеры, такие как std::shared_ptr
, который позволяет нескольким указателям на один и тот же объект и только очищает его при уничтожении последнего указателя.
Ответ 2
Пошаговое объяснение:
// creates a new object on the heap:
new B()
// dereferences the object
*(new B())
// calls the copy constructor of B on the object
B object2 = *(new B());
Итак, к концу этого объекта у вас есть объект в куче без указателя на него, поэтому его невозможно удалить.
Другой пример:
A *object1 = new A();
- утечка памяти, только если вы забыли delete
выделенную память:
delete object1;
В С++ есть объекты с автоматическим хранилищем, созданные в стеке, которые автоматически удаляются, и объекты с динамическим хранилищем в куче, которые вы выделяете с помощью new
, и должны освобождаться от delete
. (все это грубо говоря)
Подумайте, что у вас должен быть delete
для каждого объекта, выделенного с помощью new
.
ИЗМЕНИТЬ
Подумайте об этом, object2
не должно быть утечкой памяти.
Следующий код - просто сделать точку, это плохая идея, никогда не нравится такой код:
class B
{
public:
B() {}; //default constructor
B(const B& other) //copy constructor, this will be called
//on the line B object2 = *(new B())
{
delete &other;
}
}
В этом случае, поскольку other
передается по ссылке, это будет точный объект, на который указывает new B()
. Поэтому, получив его адрес &other
и удаление указателя освободит память.
Но я не могу это подчеркнуть, не делайте этого. Это просто здесь, чтобы сделать точку.
Ответ 3
Учитывая два "объекта":
obj a;
obj b;
Они не будут занимать одно и то же место в памяти. Другими словами, &a != &b
Присвоение значения одному другому не изменит их местоположение, но оно изменит их содержимое:
obj a;
obj b = a;
//a == b, but &a != &b
Интуитивно указательные "объекты" работают одинаково:
obj *a;
obj *b = a;
//a == b, but &a != &b
Теперь посмотрим на ваш пример:
A *object1 = new A();
Это присваивает значение new A()
object1
. Значение представляет собой указатель, означающий object1 == new A()
, но &object1 != &(new A())
. (Обратите внимание, что этот пример является недопустимым кодом, это только для объяснения)
Поскольку значение указателя сохраняется, мы можем освободить память, на которую он указывает: delete object1;
Из-за нашего правила это ведет себя так же, как delete (new A());
, у которого нет утечки.
Для второго примера вы копируете объект с указателем. Значение - это содержимое этого объекта, а не фактический указатель. Как и в любом другом случае, &object2 != &*(new A())
.
B object2 = *(new B());
Мы потеряли указатель на выделенную память, и поэтому мы не можем ее освободить. delete &object2;
может показаться, что он будет работать, но поскольку &object2 != &*(new A())
, он не эквивалентен delete (new A())
и поэтому недействителен.
Ответ 4
В С# и Java вы используете new для создания экземпляра любого класса, а затем вам не нужно беспокоиться об уничтожении его позже.
В С++ также есть ключевое слово "новое", которое создает объект, но в отличие от Java или С#, это не единственный способ создать объект.
С++ имеет два механизма для создания объекта:
- автоматический
- динамический
С автоматическим созданием вы создаете объект в области видимости:
- в функции или
- как член класса (или структуры).
В функции вы создадите ее следующим образом:
int func()
{
A a;
B b( 1, 2 );
}
Внутри класса вы обычно создаете его следующим образом:
class A
{
B b;
public:
A();
};
A::A() :
b( 1, 2 )
{
}
В первом случае объекты автоматически уничтожаются при выходе из блока области видимости. Это может быть функция или блок областей внутри функции.
В последнем случае объект b уничтожается вместе с экземпляром A, в котором он является членом.
Объекты выделяются новыми, когда вам нужно управлять временем жизни объекта, а затем для удаления требуется удаление. С помощью технологии, известной как RAII, вы берете на себя удаление объекта в том месте, где вы его создаете, помещая его в автоматический объект и дожидаясь, когда этот деструктор автоматического объекта вступит в силу.
Один из таких объектов - shared_ptr, который будет вызывать логику "deleter", но только тогда, когда будут уничтожены все экземпляры shared_ptr, которые делят объект.
В общем, в то время как ваш код может иметь много вызовов на новый, у вас должны быть ограниченные вызовы для удаления и всегда должны быть уверены, что они вызываются из деструкторов или "удаляют" объекты, которые помещаются в смарт-указатели.
Ваши деструкторы также никогда не должны бросать исключения.
Если вы это сделаете, у вас будет немного утечек памяти.
Ответ 5
B object2 = *(new B());
Эта строка является причиной утечки. Пусть немного отбросит это.
object2 - это переменная типа B, хранящаяся по адресу 1 (да, я собираю здесь произвольные номера). Справа вы попросили новый B или указатель на объект типа B. Программа с радостью дает это вам и назначает ваш новый B на адрес 2, а также создает указатель в адресе 3. Теперь, единственный способ доступа к данным в адресе 2 - через указатель в адресе 3. Затем вы разыменовали указатель, используя *
, чтобы получить данные, на которые указывает указатель (данные в адресе 2). Это эффективно создает копию этих данных и назначает ее объекту2, назначенному по адресу 1. Помните, что это COPY, а не оригинал.
Теперь вот проблема:
Вы никогда не сохраняли этот указатель в любом месте, где сможете его использовать! По завершении этого назначения указатель (память в адресе 3, который вы использовали для доступа к адресу2) выходит за рамки и недоступен! Вы больше не можете вызывать удаление на нем и, следовательно, не можете очистить память в адресе2. То, что у вас осталось, - это копия данных с адреса2 в address1. Две вещи, которые сидят в памяти. Один из них вы можете получить, другой вы не можете (потому что вы потеряли путь к нему). Вот почему это утечка памяти.
Я бы посоветовал приходить с вашего фона С#, чтобы вы много читали о том, как работают указатели на С++. Они являются передовой темой и могут занять некоторое время, чтобы понять, но их использование будет бесценным для вас.
Ответ 6
При создании object2
вы создаете копию созданного вами объекта с новым, но вы также теряете (никогда не назначенный) указатель (так что в дальнейшем нет возможности удалить его). Чтобы этого избежать, вам нужно сделать ссылку object2
ссылкой.
Ответ 7
Эта строка немедленно течет:
B object2 = *(new B());
Здесь вы создаете новый объект B
в куче, а затем создаете копию в стеке. Тот, который был выделен на кучу, больше не может быть доступен и, следовательно, утечка.
Эта строка не сразу протекает:
A *object1 = new A();
Там будет утечка, если вы никогда не delete
d object1
.
Ответ 8
Ну, вы создаете утечку памяти, если вы в какой-то момент не освободите память, которую вы выделили, используя оператор new
, передав указатель на эту память оператору delete
.
В ваших двух случаях выше:
A *object1 = new A();
Здесь вы не используете delete
, чтобы освободить память, поэтому, если и когда указатель object1
выходит за пределы области видимости, у вас будет утечка памяти, потому что вы потеряете указатель и, следовательно, можете 't использовать на нем оператор delete
.
И здесь
B object2 = *(new B());
вы отбрасываете указатель, возвращаемый new B()
, и поэтому никогда не сможете передать этот указатель на delete
для освобождения памяти. Отсюда другая утечка памяти.
Ответ 9
Если это упростит, подумайте о том, что компьютерная память похожа на отель, а программы - это клиенты, которые нанимают комнаты, когда они им нужны.
Как работает этот отель, вы бронируете номер и рассказываете портье, когда вы уезжаете.
Если вы заказываете книги в комнате и уезжаете, не рассказывая портье, портье будет думать, что комната все еще используется, и никто не сможет ее использовать. В этом случае происходит утечка в помещении.
Если ваша программа выделяет память и не удаляет ее (она просто перестает ее использовать), тогда компьютер считает, что память все еще используется и не позволит кому-либо еще ее использовать. Это утечка памяти.
Это не точная аналогия, но это может помочь.