Возможные утечки памяти с интеллектуальными указателями

Некоторое время я был в сообществе C ++ и слышал, что необработанные указатели «злы» и что их следует избегать как можно чаще. Хотя одна из основных причин использовать интеллектуальные указатели вместо необработанных указателей - «предотвратить» утечки памяти. Итак, мой вопрос: возможна ли утечка памяти даже при использовании интеллектуальных указателей? Если да, то как это будет возможно?


person Falla Coulibaly    schedule 11.07.2016    source источник
comment
Может показаться, что этот вопрос интересен . Возможно, этот тоже. Оба имеют прямое отношение к вашему преобладающему вопросу (и были обнаружены путем поиска при использовании интеллектуальных указателей, возможна ли утечка памяти на этом сайте).   -  person WhozCraig    schedule 11.07.2016


Ответы (3)


Возможна ли утечка памяти даже при использовании интеллектуальных указателей?

Да, если вы не будете осторожны, чтобы избежать цикла в ваших ссылках.

Если да, то как это будет возможно?

Интеллектуальные указатели, основанные на подсчете ссылок (например, shared_ptr), удаляют указанный объект, когда счетчик ссылок, связанный с объектом, упадет до нуля. Но если у вас есть цикл в ваших ссылках (A-> B-> A или какой-то более сложный цикл), то счетчики ссылок в цикле никогда не упадут до нуля, потому что интеллектуальные указатели «поддерживают друг друга в живых».

Вот пример простой программы, в которой происходит утечка памяти, несмотря на то, что для ее указателей используется только shared_ptr. Обратите внимание, что когда вы его запускаете, конструкторы выводят сообщение, а деструкторы - никогда:

#include <stdio.h>
#include <memory>

using namespace std;

class C
{
public:
   C() {printf("Constructor for C:  this=%p\n", this);}
   ~C() {printf("Destructor for C:  this=%p\n", this);}

   void setSharedPointer(shared_ptr<C> p) {pC = p;}

private:
   shared_ptr<C> pC;
};

int main(int argc, char ** argv)
{
   shared_ptr<C> pC(new C);
   shared_ptr<C> pD(new C);

   pC->setSharedPointer(pD);
   pD->setSharedPointer(pC);

   return 0;
}
person Jeremy Friesner    schedule 11.07.2016
comment
Непонятно, что вы подразумеваете под «самыми» умными указателями. В стандарте C ++ их два (и weak ptr, который сам по себе не является интеллектуальным указателем, а является заполнителем для получения интеллектуального указателя), и один из них использует счетчик ссылок, а другой - нет. - person SergeyA; 11.07.2016

Помимо круговых ссылок, еще один способ утечки интеллектуальных указателей - это сделать что-то довольно невинно выглядящее:

processThing(std::shared_ptr<MyThing>(new MyThing()), get_num_samples());

Человек, слабо знакомый с C ++, может предположить, что аргументы функции оцениваются слева направо. Это естественно, но, к сожалению, это неверно (интуиция RIP и принцип наименьшего удивления). Фактически, только clang гарантирует оценку аргументов функции слева направо (AFAIK, возможно, это не гарантия). Большинство других компиляторов выполняют оценку справа налево (включая gcc и icc).

Но независимо от того, что делает какой-либо конкретный компилятор, стандарт языка C ++ (за исключением C ++ 17, подробности см. В конце) не определяет, в каком порядке оцениваются аргументы, поэтому для компилятора это вполне возможно. для оценки аргументов функции в ЛЮБОМ порядке.

Из cppreference:

Порядок оценки операндов почти всех операторов C ++ (включая порядок оценки аргументов функции в выражении вызова функции и порядок оценки подвыражений в любом выражении) не указан. Компилятор может вычислять операнды в любом порядке и может выбрать другой порядок, когда то же выражение вычисляется снова.

Следовательно, вполне возможно, что processThing аргументы функции выше оцениваются в следующем порядке:

  1. new MyThing()
  2. get_num_samples()
  3. std::shared_ptr<MyThing>()

Это может вызвать утечку, потому что get_num_samples() может вызвать исключение, и поэтому std::shared_ptr<MyThing>() может никогда не вызываться. Акцент на май. Это возможно в соответствии со спецификацией языка, но на самом деле я не видел, чтобы какой-либо компилятор выполнял это преобразование (по общему признанию, gcc / icc / clang - единственные компиляторы, которые я использую на момент написания). Мне не удалось заставить gcc или clang сделать это (примерно через час попыток / исследований я отказался от этого). Может быть, эксперт по компиляторам может дать нам лучший пример (пожалуйста, сделайте это, если вы читаете это и являетесь экспертом по компиляторам !!!).

Вот игрушечный пример, в котором я заставляю этот заказ использовать gcc. Я немного обманул, потому что оказалось, что трудно заставить компилятор gcc произвольно переупорядочить оценку аргумента (это все еще выглядит довольно невинно, и утечка утечки подтверждается некоторыми сообщениями на stderr):

#include <iostream>
#include <stdexcept>
#include <memory>

struct MyThing {
    MyThing() { std::cerr << "CONSTRUCTOR CALLED." << std::endl; }
    ~MyThing() { std::cerr << "DESTRUCTOR CALLED." << std::endl; }
};

void processThing(std::shared_ptr<MyThing> thing, int num_samples) {
    // Doesn't matter what happens here                                                                                                                                                                     
}

int get_num_samples() {
    throw std::runtime_error("Can't get the number of samples for some reason...and I've decided to bomb.");
    return 0;
}

int main() {
    try {
        auto thing = new MyThing();
        processThing(std::shared_ptr<MyThing>(thing), get_num_samples());
    }
    catch (...) {
    }
}

Скомпилировано с помощью gcc 4.9, MacOS:

Matthews-MacBook-Pro:stackoverflow matt$ g++ --version
g++-4.9 (Homebrew GCC 4.9.4_1) 4.9.4
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Matthews-MacBook-Pro:stackoverflow matt$ g++ -std=c++14 -o test.out test.cpp
Matthews-MacBook-Pro:stackoverflow matt$ ./test.out 
CONSTRUCTOR CALLED.
Matthews-MacBook-Pro:stackoverflow matt$

Обратите внимание, что DESTRUCTOR CALLED никогда не печатается в stderr.

Вы можете решить эту проблему, убедившись, что у вас есть другой оператор для создания shared_ptr, а затем передать этот результат функции. Это работает, потому что компилятор не имеет (большой) свободы между различными операторами (в отличие от одного и того же оператора). Вот как можно исправить приведенный выше пример игрушки:

// ensures entire shared_ptr allocation statement is executed before get_num_samples()
auto memory_related_arg = std::shared_ptr<MyThing>(new MyThing());
processThing(memory_related_arg, get_num_samples());

P.S. Все это украдено Скоттом Мейерсом из Третьего издания «Эффективного C ++». Определенно книга, которую стоит прочитать, если вы используете C ++ каждый день. C ++ сложно понять правильно, и эта книга дает хорошие рекомендации о том, как сделать это больше правильно. Вы все равно можете ошибиться, следуя догматическим рекомендациям, но вы станете лучшим разработчиком C ++, зная стратегии, описанные в этой книге.

P.S.S. C ++ 17 устраняет эту проблему. Подробнее см. Здесь: Каковы гарантии порядка оценки, представленные C ++ 17?

person Matt Messersmith    schedule 19.07.2018

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

person user3001855    schedule 11.07.2016