Почему использование «нового» вызывает утечки памяти? - PullRequest
129 голосов
/ 12 января 2012

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

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

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

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

Ответы [ 9 ]

461 голосов
/ 12 января 2012

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

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

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

new without cleanup

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

newing with delete

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

newing with 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

newing with automatic_pointer

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

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

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

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

34 голосов
/ 12 января 2012

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

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

EDIT

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

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

11 голосов
/ 12 января 2012

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

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

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

9 голосов
/ 12 января 2012

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

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

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

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

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

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

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

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

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

8 голосов
/ 08 февраля 2012

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

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

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

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

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

7 голосов
/ 12 января 2012

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

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

A *object1 = new A();

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

А здесь

B object2 = *(new B());

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

7 голосов
/ 12 января 2012

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

B object2 = *(new B());

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

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

A *object1 = new A();

Будет утечка, если вы никогда не delete d object1.

7 голосов
/ 12 января 2012

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

...