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

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

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

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

A *object1 = new A();
B object2 = *(new B());

9 ответов

Решение

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

Когда ты пишешь 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

обновление с помощью Automatic_pointer

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

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

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

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

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

// 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 и удаление указателя освободит память.

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

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

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()) и так недействительным.

В 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, членом которого он является.

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

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

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

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

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

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++. Они представляют собой сложную тему и могут занять некоторое время, но их использование будет для вас бесценным.

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

Этот отель работает так, что вы бронируете номер и сообщаете портье, когда уходите.

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

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

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

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

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

A *object1 = new A();

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

И здесь

B object2 = *(new B());

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

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

Это линия, которая сразу же просачивается:

B object2 = *(new B());

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

Эта строка не является неплотной:

A *object1 = new A();

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

Другие вопросы по тегам