智能指针可能发生内存泄漏

Fal*_*aly 5 c++ memory-leaks raii

我有一段时间在C++社区,听到原始指针"是邪恶的",应尽可能避免它们.虽然在原始指针上使用智能指针的主要原因之一是"防止"内存泄漏.所以我的问题是:即使使用智能指针,是否仍然可能有内存泄漏?如果是,那怎么可能呢?

Jer*_*ner 9

即使使用智能指针,是否仍然可能有内存泄漏?

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

如果是,那怎么可能呢?

基于引用计数的智能指针(例如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;
}
Run Code Online (Sandbox Code Playgroud)


Mat*_*ith 7

除了循环引用之外,另一种泄漏智能指针的方法是做一些看起来很无辜的事情:

processThing(std::shared_ptr<MyThing>(new MyThing()), get_num_samples());
Run Code Online (Sandbox Code Playgroud)

稍微熟悉 C++ 的人可能会假设函数参数是从左到右计算的。这是很自然的想法,但不幸的是它是错误的(RIP 直觉和最小惊喜原则)。事实上,只clang保证从左到右的函数参数评估(AFAIK,也许它不是保证)。大多数其他编译器从右到左计算(包括gccicc)。

但是无论任何特定的编译器做什么,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 (...) {
    }
}
Run Code Online (Sandbox Code Playgroud)

用 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$
Run Code Online (Sandbox Code Playgroud)

请注意,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());
Run Code Online (Sandbox Code Playgroud)

PS 这都是从 Scott Meyers 的“Effective C++”第三版中偷来的。如果您每天都使用 C++,这绝对是一本值得一读的书。C++ 很难做到正确,而这本书很好地提供了关于如何使其正确的指导。你仍然可以教条式地遵循指导方针,但你会成为一个更好的 C++ 开发人员,了解本书中的策略。

PSS C++17 修复了这个问题。有关详细信息,请参见此处:C++17 引入的求值顺序保证是什么?