智能指针可能的内存泄漏

Possible Memory-leaks with smart pointers

我在 C++ 社区呆了一段时间,听说原始指针 "are evil" 并且应该尽可能避免使用它们。虽然在原始指针上使用智能指针的主要原因之一是 "prevent" 内存泄漏。所以我的问题是:即使使用智能指针,是否仍有可能发生内存泄漏?如果是,那怎么可能?

有释放智能指针内存的函数。在这种情况下,您要求智能指针停止管理内存。之后,就看你不泄露内存了

Even when using smart pointers, is it still possible to have memory leak ?

是的,如果您不小心避免在参考文献中创建循环。

If yes how will that be possible ?

基于引用计数的智能指针(如shared_ptr)会在与对象关联的引用计数降为零时删除指向的对象。但是如果你的引用中有一个循环(A->B->A,或者一些更复杂的循环),那么循环中的引用计数将永远不会下降到零,因为智能指针是 "keeping each other alive".

这是一个简单程序的示例,尽管仅使用 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,也许它不是保证)。大多数其他编译器从右到左计算(包括 gccicc)。

但是,无论任何 特定 编译器做什么,C++ 语言标准(C++17 除外,详情请参阅末尾)并未规定参数的计算顺序,因此编译器完全有可能以任何顺序评估函数参数。

来自 cppreference:

Order of evaluation of the operands of almost all C++ operators (including the order of evaluation of function arguments in a function-call expression and the order of evaluation of the subexpressions within any expression) is unspecified. The compiler can evaluate operands in any order, and may choose another order when the same expression is evaluated again.

因此,上面的 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 这样做(在 trying/researching 大约一个小时后我放弃了)。也许编译器专家可以给我们一个更好的例子(如果你正在阅读这篇文章并且是编译器专家,请这样做!!!)。

这是一个玩具示例,我在其中使用 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:Whosebug 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:Whosebug matt$ g++ -std=c++14 -o test.out test.cpp
Matthews-MacBook-Pro:Whosebug matt$ ./test.out 
CONSTRUCTOR CALLED.
Matthews-MacBook-Pro:Whosebug 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。这些都是从 Scott Meyers 的第三版 "Effective C++" 中窃取的。如果你每天使用 C++,这本书绝对值得一读。 C++ 很难做到正确,而这本书很好地提供了如何正确使用它的良好指南 more。固执地遵循指导方针,您仍然可能会犯错,但了解本书中的策略,您将成为更好的 C++ 开发人员。

P.S.S. C++17 修复了这个问题。详情请看这里: