• C++11小结:使用智能指针的坑


    shared_ptr

    不能访问一个空智能指针所接管对象的数据成员或函数成员

    当不能确定一个智能指针释放已经释放接管的内存时,需要对其进行空指针判断。因为决不能不能访问一个空智能指针对象类型的数据成员或函数成员,否则可能会造成程序崩溃。

    这点对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
    

    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::release()返回值是T*,调用release放弃所有权后,可以将内存空间交给别人来接管。

    如果想要释放内存,请调用reset

  • 相关阅读:
    Spring_HelloWorld
    【日记想法】2017年终总结
    【运维技术】从零开始搭建开发使用的Kafka环境
    【运维技术】windows安装apache服务器,实现域名对应端口的解析跳转
    【软件安装】Xshell + XFtp
    【运维技术】node项目使用strongloop进行部署相关教程
    【运维技术】CentOS7上从零开始安装LAMP安装织梦DedeCMS教程
    【运维技术】CentOS7上从零开始安装阿里RocketMQ版本:release-4.0.1【亲测哈哈】
    【运维技术】VM虚拟机上使用centos7安装docker启动gogs服务教程【含B站视频教程】
    【读书笔记】Junit实战
  • 原文地址:https://www.cnblogs.com/fortunely/p/16370208.html
Copyright © 2020-2023  润新知