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

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

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

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

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

Ответы

Ответ 1

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

Когда вы пишете 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 (Инициализация ресурсов - Инициализация). Когда вы приобретаете ресурс, который нуждается в очистке, вы привязываете его к объекту с автоматическим хранением, поэтому вам не нужно беспокоиться о его очистке. Это относится к любому ресурсу, будь то память, открытые файлы, сетевые подключения или все, что вам нравится.

Эта вещь 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

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

Как работает этот отель, вы бронируете номер и рассказываете портье, когда вы уезжаете.

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

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

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