C++11小结:使用智能指针的坑
shared_ptr
不能访问一个空智能指针所接管对象的数据成员或函数成员
当不能确定一个智能指针释放已经释放接管的内存时,需要对其进行空指针判断。因为决不能访问一个空智能指针对象的成员(数据成员或函数成员),否则可能会造成程序崩溃。
如果程序异常退出码为139,则有可能是因为访问空智能指针成员。
这点对unique_ptr是类似的。但对weak_ptr会有所不同,因为从weak_ptr提升为shared_ptr,通常会先用lock进行提升,然后进行空指针判断,因此较少犯这个错误。
// 错误示范
class A
{
public:
A() { cout << "Create a new A object" << endl; }
void func(){ cout << "invoke A::func()" << endl; }
};
shared_ptr<A> pa(new A);
... // 可能pa已经释放接管的A对象
pa->func(); //调用A对象方法是错误的
正确使用方法,当不能确定shared_ptr是否已释放所指对象时,可以用以下方法判断:
shared_ptr<A> pa(new A);
... // 可能pa已经释放接管的A对象
// 方式1
if (pa) {
pa->func();
}
// 方式2
if (pa.get()) {
pa->func();
}
shared_from_this 不能用在构造函数中
参见与enable_shared_from_this/shared_from_this有关的异常:bad_weak_ptr 第1点
shared_from_this 不能用在构造函数中,因为此时当前对象尚未构造完成,enable_shared_from_this<>的_M_weak_this尚未设置。
// 错误使用范例
class D : public std::enable_shared_from_this<D>
{
public:
D() {
cout << "D::D()" << endl;
// 这里会throw std::exception异常
shared_ptr<D> p = shared_from_this();
}
};
/**
* 导致抛出异常bad_weak_ptr的示例演示
*/
int main()
{
shared_ptr<D> a(new D);
return 0;
}
正确使用方法:
class D : public std::enable_shared_from_this<D>
{
public:
D() {
cout << "D::D()" << endl;
}
void func() {
// OK
shared_ptr<D> p = shared_from_this();
}
};
int main()
{
shared_ptr<D> a(new D);
return 0;
}
使用shared_from_this()的类必须被shared_ptr直接接管
参见与enable_shared_from_this/shared_from_this有关的异常:bad_weak_ptr 第3点
即使是通过其他类将当前类作为成员,从而用shared_ptr间接接管当前类,也是不行的。因为基类enable_shared_from_this成员_M_weak_this,只有在shared_ptr接管当前类对象时,才会被初始化。
错误示范:
// 错误范例:D类使用了shared_from_this,但没有被shared_ptr直接接管,而是通过shared_ptr接管A对象,而间接关联D,这样依然无法在D中使用shared_from_this
class D : public enable_shared_from_this<D>
{
public:
D() {
cout << "D::D()" << endl;
}
void func() {
cout << "D::func()" << endl;
shared_ptr<D> p = shared_from_this(); // 这里会抛出异常bad_weak_ptr
}
};
class A
{
public:
A() {
cout << "A::A()" << endl;
}
void funcA() {
cout << "A::funcA()" << endl;
d.func();
}
private:
D d;
};
int main()
{
shared_ptr<A> a(new A()); // 这里shared_ptr接管的是A对象,并非继承自enable_from_this的D对象,基类_M_weak_this未被初始化
a->funcA();
return 0;
}
正确范例:
// OK范例
class D : public enable_shared_from_this<D>
{
public:
D() {
cout << "D::D()" << endl;
}
void func() {
cout << "D::func()" << endl;
shared_ptr<D> p = shared_from_this();
}
};
int main()
{
shared_ptr<D> p(new D); // 这里shared_ptr直接接管了D对象,会初始化基类_M_weak_this成员
p->func();
return 0;
}
隐式循环引用
像下面这种很明显的循环引用,A持有指向B类对象的shared_ptr<B> b
,而B也持有指向A类对象的shared_ptr<A> a
。这就很容易造成循环引用,解决办法是将其中一个shared_ptr的成员,修改为weak_ptr。
可参见之前这篇文章。
// 循环引用示例
#include <memory>
#include <iostream>
using namespace std;
class A;
class B;
class A
{
public:
A()
{
cout << "Create a new A object" << endl;
}
~A()
{
cout << "Destroy an A object" << endl;
}
void func(shared_ptr<B> pb)
{
b = pb;
}
private:
shared_ptr<B> b;
};
class B
{
public:
B()
{
cout << "Create a new B object" << endl;
}
~B()
{
cout << "Destroy an B object" << endl;
}
void func(shared_ptr<A> pa)
{
a = pa;
}
private:
shared_ptr<A> a;
};
int main()
{
shared_ptr<A> a(new A);
shared_ptr<B> b(new B);
a->func(b);
b->func(a);
return 0;
}
然而,这里要讲不是上面这种很明显的循环引用,而是下面这种不那么明显的,隐式循环引用。主要是由于将A用shared_ptr包裹,为class B设置回调函数引起的,不容易察觉。
// 循环引用示例:通过传递函数或lambda表达式,为成员变量设置回调,导致循环引用
#include <memory>
#include <iostream>
#include <functional>
using namespace std;
class B
{
public:
typedef std::function<void(void)> Callback;
B()
{
cout << "Create a new B object" << endl;
}
~B()
{
cout << "Destroy an B object" << endl;
}
void setCallback(Callback cb)
{
cb_ = std::move(cb);
}
void exeFunc()
{
cb_();
}
private:
Callback cb_;
};
class A : public enable_shared_from_this<A>
{
public:
A()
: b(new B)
{
cout << "Create a new A object" << endl;
}
~A()
{
cout << "Destroy an A object" << endl;
}
void print()
{
cout << "A print()" << endl;
}
void func()
{
shared_ptr<A> me = shared_from_this();
b->setCallback(std::bind(&A::onMessage, me)); // 将包裹this的shared_ptr指针交给了b的Callback cb_成员接管, 导致循环引用
}
void func1()
{
shared_ptr<A> me = shared_from_this();
b->setCallback([me]() { // 将包裹this的shared_ptr指针, 通过lambda表达式交给了b的Callback cb_成员接管, 导致循环引用
me->onMessage();
});
}
private:
void onMessage(void)
{
cout << "A onMessage" << endl;
}
shared_ptr<B> b;
};
int main()
{
shared_ptr<A> a(new A);
// 下面2行代码调用, 都将导致循环引用
a->func();
//a->func1();
return 0;
}
上面代码会造成循环引用。如何解决这个问题?
我们分情况讨论,
1)先谈谈对于lambda表达式捕获shared_ptr<A>
类型的me的情况。可以修改为捕获weak_ptr类型,然后在lambda表达式内部提升为shared_ptr后,再调用A类的函数。
class A : enable_shared_from_this<A>
{
...
void func1()
{
weak_ptr<A> me = shared_from_this();
b->setCallback([me]() { // 将包裹this的shared_ptr指针, 通过lambda表达式交给了b的Callback cb_成员接管, 导致循环引用
shared_ptr<A> guard = me.lock();
if (guard) {
guard->onMessage();
}
});
}
}
2)对于,setCallback + bind,可以将bind 包裹this的shared_ptr,修改为weak_ptr,不过,这就要求调用的函数不能再是A的成员函数,因为不存在onMessage(weak_ptr<A> a)
或a->onMessage()
的函数或调用。
class A;
class B;
...
// 注意这里重新定义了一个函数
void onMessage1(weak_ptr<A> a)
{
}
class A : enable_shared_from_this<A>
{
...
void func()
{
weak_ptr<A> me = shared_from_this();
weak_ptr<A> weakMe = shared_from_this();
b->setCallback(
std::bind(onMessage1, weakMe)); // 注意这里bind的函数不再是原来的A::onMessage
}
}
3)修改B类定义,增添weak_ptr<A> a
成员,通过弱引用指向a。这样就无需通过bind来传递share_ptr,而是直接在回调函数体内调用A成员函数。
class B
{
public:
typedef std::function<void(weak_ptr<A>)> Callback;
void exeFunc()
{
if (cb_) {
//cb_();
cb_(a); // 在执行回调cb_的地方,直接传入弱引用a作为参数
}
}
void tie(shared_ptr<A> obj)
{
a = obj;
}
...
private:
weak_ptr<A> a;
Callback cb_;
};
class A : public enable_shared_ptr<A>
{
public:
void func()
{
b->tie(shared_from_this()); // 提前bind 包裹A的shared_ptr,传递给B的weak_ptr
b->setCallback([](weak_ptr<A> a) {
shared_ptr<A> guard = a.lock(); // 提升为shared_ptr<A>
if (guard) {
guard->onMessage();
}
});
}
...
};
4)还有一种改法,改动很小,但不安全。即直接将A的this指针,通过b::setCallback传递给b::cb_,这样做的前提是我们能确信A对象的生命周期长于b,至少要长于b的cb_回调;否则,b的cb_回调时,会发生空指针异常。
// 不安全做法
b->setCallback(std::bind(&A::onMessage, this)); // unsafe
shared_ptr的解引用不等于原生指针的解引用
首先,明白一点:shared_ptr的解引用,类似于原生指针的解引用,都是得到指针所指的对象。
但是,指针的解引用有一个前提,那就是指针本身不能为空,否则可能导致程序崩溃。
如下面程序会异常退出:
int main()
{
std::shared_ptr<int> pp = nullptr;
std::cout << *pp << std::endl;
return 0;
}
shared_ptr(包括unique_ptr)是有作用域的,超出作用域或者手动调用reset、std::move,可能导致所指对象已释放或者移交给别的智能指针,也就是指针为空。
而原生指针不存在作用域的问题,只有调用delete才会释放对象。
也就是说,当不确定shared_ptr是否空时,如果需解引用,需要先进行空指针检查(这点类似于原生指针)。
可以看一下GNU/GCC如何实现shared_ptr的解引用(operator*)。本质上,还是调用的.get()获得原生指针后,再解引用。
using element_type = _Tp;
element_type&
operator*() const noexcept
{
__glibcxx_assert(_M_get() != nullptr);
return *_M_get();
}
unique_ptr
release 不会释放内存
release 只会放弃所有权,不会释放内存资源;
reset 既放弃所有权,还会释放内存资源(调用删除器)。如果有参数,还会接管参数对应的新资源。
#include <iostream>
#include <memory>
using namespace std;
class A
{
public:
A()
{
cout << "Create A object" << endl;
}
~A()
{
cout << "Destroy A object" << endl;
}
};
int main()
{
unique_ptr<A> p(new A);
p.release();
return 0;
}
运行结果:
Create A object
可以看到,并没有调用class A的析构函数。
注意到unique_ptr
如果想要释放内存,请调用reset
。