Почему использование «нового» вызывает утечку памяти?

131

Сначала я выучил C #, а теперь начинаю с C ++. Насколько я понимаю, оператор newв C ++ не похож на оператор в C #.

Можете ли вы объяснить причину утечки памяти в этом примере кода?

class A { ... };
struct B { ... };

A *object1 = new A();
B object2 = *(new B());
Xeo
источник
Практически повторяется: автоматическая ли сборка мусора в стандартном C ++?
Брент Брэдберн,

Ответы:

464

Что происходит

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

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

новый без очистки

Вам нужно передать на него указатель delete, чтобы очистить его:

создание с удалением

Однако ваш второй пример хуже: вы разыменовываете указатель и делаете копию объекта. Таким образом, вы потеряете указатель на созданный объект new, и вы никогда не сможете его удалить, даже если бы захотели!

новичок с deref

Что ты должен делать

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

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

создание с automatic_pointer

Это распространенная идиома, которая носит не очень информативное название RAII ( Resource Acquisition Is Initialization ). Когда вы приобретаете ресурс, который требует очистки, вы вставляете его в объект с автоматической продолжительностью хранения, поэтому вам не нужно беспокоиться об его очистке. Это применимо к любому ресурсу, будь то память, открытые файлы, сетевые соединения или что угодно.

Эта automatic_pointerштука уже существует в разных формах, я просто привел ее для примера. Очень похожий класс существует в стандартной библиотеке под названием std::unique_ptr.

Также есть старый (до C ++ 11), названный, auto_ptrно теперь он устарел из-за странного поведения копирования.

А есть еще несколько более умных примеров, например std::shared_ptr, которые позволяют использовать несколько указателей на один и тот же объект и очищают его только после уничтожения последнего указателя.

Р. Мартиньо Фернандес
источник
4
@ user1131997: рад, что вы задали еще один вопрос. Как видите, это непросто объяснить в комментариях :)
Р. Мартиньо Фернандес
@ R.MartinhoFernandes: отличный ответ. Всего один вопрос. Почему вы использовали возврат по ссылке в функции operator * ()?
Деструктор
@Destructor поздний ответ: D. Возврат по ссылке позволяет вам изменить указатель, чтобы вы могли делать, например *p += 2, как с обычным указателем. Если бы он не возвращался по ссылке, он бы не имитировал поведение обычного указателя, что и является целью.
Р. Мартиньо Фернандес
Большое спасибо за совет «хранить указатель на выделенный объект в объекте автоматической продолжительности хранения, который автоматически удаляет его». Если бы только был способ потребовать от кодеров выучить этот шаблон, прежде чем они смогут скомпилировать любой C ++!
Энди
35

Пошаговое объяснение:

// 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;

В C ++ есть объекты с автоматическим хранилищем, созданные в стеке, которые автоматически удаляются, и объекты с динамическим хранилищем в куче, которые вы выделяете 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и удаление указателя освободит память.

Но я не могу этого особо подчеркнуть, не делай этого. Это просто для того, чтобы подчеркнуть.

Лучиан Григоре
источник
2
Я думал то же самое: мы можем взломать его, чтобы не протекать, но вы не захотите этого делать. У объекта object1 также нет утечки, поскольку его конструктор может присоединиться к какой-либо структуре данных, которая в какой-то момент удалит его.
CashCow,
2
Всегда так заманчиво писать ответы «это можно сделать, но не надо»! :-) Я знаю это чувство
Кос
11

Даны два «объекта»:

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())и поэтому недействительно.

Pubby
источник
9

В C # и Java вы используете new для создания экземпляра любого класса, и тогда вам не нужно беспокоиться о его уничтожении позже.

В C ++ также есть ключевое слово new, которое создает объект, но, в отличие от Java или C #, это не единственный способ создания объекта.

В C ++ есть два механизма для создания объекта:

  • автоматический
  • динамический

При автоматическом создании вы создаете объект в среде с заданной областью действия: - в функции или - как член класса (или структуры).

В функции вы должны создать это следующим образом:

int func()
{
   A a;
   B b( 1, 2 );
}

Внутри класса вы обычно создаете его следующим образом:

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

В первом случае объекты уничтожаются автоматически при выходе из блока области видимости. Это может быть функция или блок области видимости внутри функции.

В последнем случае объект b уничтожается вместе с экземпляром A, членом которого он является.

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

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

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

Ваши деструкторы также никогда не должны создавать исключения.

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

Дойная корова
источник
4
Есть больше чем automaticи dynamic. Также есть static.
Mooing Duck
9
B object2 = *(new B());

Эта линия является причиной утечки. Давайте немного разберем это ...

object2 - это переменная типа B, хранящаяся, скажем, по адресу 1 (да, я выбираю здесь произвольные числа). Справа вы запросили новый B или указатель на объект типа B. Программа с радостью предоставит его вам и присваивает вашему новому B адресу 2, а также создает указатель на адресе 3. Теперь, единственный способ получить доступ к данным в адресе 2 - через указатель в адресе 3. Затем вы разыменовали указатель, используя* для получения данных, на которые указывает указатель (данные в адресе 2). Это эффективно создает копию этих данных и назначает ее объекту 2, назначенному по адресу 1. Помните, что это КОПИЯ, а не оригинал.

Теперь вот проблема:

На самом деле вы никогда не хранили этот указатель где-либо, где могли бы его использовать! Как только это назначение завершено, указатель (память в адресе 3, который вы использовали для доступа к адресу 2) выходит за рамки и вне вашей досягаемости! Вы больше не можете вызывать для него delete и, следовательно, не можете очистить память по адресу address2. У вас остается копия данных с адреса 2 в адресе 1. Две одинаковые вещи сидят в памяти. К одному вы можете получить доступ, к другому - нет (потому что вы потеряли к нему путь). Вот почему это утечка памяти.

Я бы посоветовал, исходя из вашего опыта работы с C #, много читать о том, как работают указатели в C ++. Это сложная тема, на освоение которой может потребоваться время, но их использование будет для вас неоценимо.

MGZero
источник
8

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

Этот отель работает так: вы бронируете номер и говорите носильщику, когда уезжаете.

Если вы запрограммируете номер и уйдете, не сказав носильщику, портье подумает, что комната все еще используется, и не позволит никому ее использовать. В этом случае есть утечка в помещении.

Если ваша программа выделяет память и не удаляет ее (она просто перестает ее использовать), тогда компьютер считает, что память все еще используется, и не позволит никому ее использовать. Это утечка памяти.

Это не точная аналогия, но она может помочь.

Стефан
источник
5
Мне очень нравится эта аналогия, она не идеальна, но это определенно хороший способ объяснить утечки памяти людям, которые с ней плохо знакомы!
AdamM
1
Я использовал это в интервью для старшего инженера Bloomberg в Лондоне, чтобы объяснить утечку памяти девушке из отдела кадров. Я прошел через это интервью, потому что смог объяснить утечки памяти (и проблемы с потоками) не программисту так, как она понимала.
Стефан
7

При создании object2вы создаете копию объекта, который вы создали с помощью new, но вы также теряете (никогда не назначенный) указатель (поэтому нет возможности удалить его позже). Чтобы этого избежать, вам нужно сделать object2ссылку.

Марио
источник
3
Использовать адрес ссылки для удаления объекта - это невероятно плохая практика. Используйте умный указатель.
Том Уитток
3
Невероятно плохая практика, а? Как вы думаете, что интеллектуальные указатели используют за кулисами?
Blindy
3
Умные указатели @Blindy (по крайней мере, прилично реализованные) используют указатели напрямую.
Лучиан Григоре
2
Честно говоря, вся идея не так уж хороша, не так ли? На самом деле, я даже не уверен, где образец, опробованный в OP, действительно будет полезен.
Марио
7

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

В ваших двух случаях выше:

A *object1 = new A();

Здесь вы не используете deleteдля освобождения памяти, поэтому, если и когда ваш object1указатель выйдет за пределы области видимости, у вас будет утечка памяти, потому что вы потеряете указатель и не сможете использовать для него deleteоператор.

И тут

B object2 = *(new B());

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

razlebe
источник
7

Вот эта строка немедленно просачивается:

B object2 = *(new B());

Здесь вы создаете новый B объект в куче, а затем создаете копию в стеке. Тот, который был выделен в куче, больше недоступен и, следовательно, утечка.

Эта строка не сразу становится дырявой:

A *object1 = new A();

Там будет течь , если вы никогда не deleted , object1хотя.

mattjgalloway
источник
4
Пожалуйста, не используйте кучу / стек при объяснении динамического / автоматического хранения.
Pubby
2
@Pubby, почему бы не использовать? Из-за динамического / автоматического хранения всегда куча, а не стек? И поэтому нет необходимости подробно рассказывать о стеке / куче, я прав?
4
@ user1131997 Куча / стек - это детали реализации. О них важно знать, но они не имеют отношения к этому вопросу.
Pubby
2
Хм, я бы хотел дать на него отдельный ответ, то есть такой же, как у меня, но заменив кучу / стек тем, что вы считаете лучшим. Мне было бы интересно узнать, как вы предпочли бы это объяснить.
mattjgalloway