我已经在C ++社区工作了一段时间,听到原始指针"是邪恶的"并且应尽可能避免它们。虽然使用智能指针而不是原始指针的一个主要原因是"阻止"内存泄漏。所以我的问题是:即使使用智能指针,是否仍然可能有内存泄漏?如果是,那怎么可能呢?
答案 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,也许不是保证)。大多数其他编译器从右到左求值(包括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。这些都是从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)
有些功能可以从智能指针释放内存。在这种情况下,您要求智能指针停止管理内存。在那之后,你不要泄漏内存