Некоторое время я был в сообществе C ++ и слышал, что необработанные указатели «злы» и что их следует избегать как можно чаще. Хотя одна из основных причин использовать интеллектуальные указатели вместо необработанных указателей - «предотвратить» утечки памяти. Итак, мой вопрос: возможна ли утечка памяти даже при использовании интеллектуальных указателей? Если да, то как это будет возможно?
Возможные утечки памяти с интеллектуальными указателями
Ответы (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;
}
Помимо круговых ссылок, еще один способ утечки интеллектуальных указателей - это сделать что-то довольно невинно выглядящее:
processThing(std::shared_ptr<MyThing>(new MyThing()), get_num_samples());
Человек, слабо знакомый с C ++, может предположить, что аргументы функции оцениваются слева направо. Это естественно, но, к сожалению, это неверно (интуиция RIP и принцип наименьшего удивления). Фактически, только clang
гарантирует оценку аргументов функции слева направо (AFAIK, возможно, это не гарантия). Большинство других компиляторов выполняют оценку справа налево (включая gcc
и icc
).
Но независимо от того, что делает какой-либо конкретный компилятор, стандарт языка C ++ (за исключением C ++ 17, подробности см. В конце) не определяет, в каком порядке оцениваются аргументы, поэтому для компилятора это вполне возможно. для оценки аргументов функции в ЛЮБОМ порядке.
Из cppreference:
Порядок оценки операндов почти всех операторов C ++ (включая порядок оценки аргументов функции в выражении вызова функции и порядок оценки подвыражений в любом выражении) не указан. Компилятор может вычислять операнды в любом порядке и может выбрать другой порядок, когда то же выражение вычисляется снова.
Следовательно, вполне возможно, что processThing
аргументы функции выше оцениваются в следующем порядке:
new MyThing()
get_num_samples()
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?
Есть функции, освобождающие память от умного указателя. В этом случае вы просите умный указатель прекратить управление памятью. После этого вам решать, чтобы память не протекала