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

时间:2016-07-11 01:35:02

标签: c++ memory-leaks raii

我已经在C ++社区工作了一段时间,听到原始指针"是邪恶的"并且应尽可能避免它们。虽然使用智能指针而不是原始指针的一个主要原因是"阻止"内存泄漏。所以我的问题是:即使使用智能指针,是否仍然可能有内存泄漏?如果是,那怎么可能呢?

3 个答案:

答案 0 :(得分: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;
}

答案 1 :(得分:2)

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

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

一个熟悉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 (...) {
    }
}

与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。这些都是从Scott Meyers撰写的“ Effective C ++”(第三版)中窃取的。如果您每天使用C ++,那么绝对值得一读。 C ++很难做到正确,这本书为如何正确实现 more 给出了很好的指导,做得很好。依从教义上的指导原则,您仍然可能会犯错,但是了解本书中的策略将使您成为一个更好的C ++开发人员。

P.S.S。 C ++ 17解决了此问题。有关详情,请参见此处:What are the evaluation order guarantees introduced by C++17?

答案 2 :(得分:-2)

有些功能可以从智能指针释放内存。在这种情况下,您要求智能指针停止管理内存。在那之后,你不要泄漏内存