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

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

原文链接: https://www.cnblogs.com/fortunely/p/16370208.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

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

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/402535

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月21日 上午11:06
下一篇 2023年4月21日 上午11:06

相关推荐