c++智能指针

1、std::auto_ptr

        std::auto_ptr真正容易让人误用的地方是其不常用的复制语义,即当复制一个std::auto_ptr对象时(拷贝复制或operator =复制),原对象所持有的堆内存对象也会转移给复制出来的对象。示例代码如下:

//测试拷贝构造函数
    std::auto_ptr<int> sp1(new int(8));
    std::auto_ptr<int> sp2(sp1);
    if (sp1.get() != NULL)
    {
        std::cout << " sp1 is not empty" << std::endl;
    }
    else {
        std::cout << " sp1 is  empty" << std::endl;
    }

    if (sp2.get() != NULL)
    {
        std::cout << " sp2 is not empty" << std::endl;
    }
    else {
        std::cout << " sp2 is  empty" << std::endl;
    }

    //测试赋值构造
    std::auto_ptr<int> sp3(new int(8));
    std::auto_ptr<int> sp4;
    sp4 = sp3;
    if (sp3.get() != NULL)
    {
        std::cout << " sp3 is not empty" << std::endl;
    }
    else {
        std::cout << " sp3 is  empty" << std::endl;
    }

    if (sp4.get() != NULL)
    {
        std::cout << " sp4 is not empty" << std::endl;
    }
    else {
        std::cout << " sp4 is  empty" << std::endl;
    }

上述代码中分别利用拷贝构造(sp1=>sp2)和赋值构造(sp3=>sp4)来创建新的std::auto_ptr对象,因此sp1 持有的堆对象被转移给sp2,sp3持有的堆对象被转移给sp4。因此输出为:

sp1 is  empty
sp2 is not empty
sp3 is  empty
sp4 is not empty

例如:

std::vector<std::auto_ptr<int>> myvectors;

当用算法对容器操作的时候(如最常见的容器元素遍历),很难避免不对容器中的元素实现赋值传递,这样便会使容器中多个元素被置为空指针,这不是我们希望看到的,可能会造成一些意想不到的错误。

2、std::unique_ptr

std::unique_ptr对其持有的堆内存具有唯一拥有权,也就是说引用计数永远是1,std::unique_ptr对象销毁时会释放其持有的堆内存。可以使用以下方式初始化一个std::unique_ptr对象:

//1
std::unique_ptr<int> sp1(new int(123));
//2
std::unique_ptr<int> sp2;
sp2.reset(new int(123));
//3
std::unique_ptr<int> sp3 = std::make_unique<int>(123);

应该尽量使用初始化方式3的方式去创建一个std::unique_ptr而不是方式1和2,因为形式3更安全。

std::unique_ptr禁止复制语义,为了达到这个效果,std::unique_ptr类的拷贝构造函数和赋值运算符(operator =)被标记为 =delete

template <class T>
class unique_ptr
{
 //省略其他代码...
 
 //拷贝构造函数和赋值运算符被标记为delete
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;
};

因此,下列代码是无法通过编译的:

 std::unique_ptr<int> sp1(new int(123));

    //报错
    std::unique_ptr<int> sp2(sp1);

    std::unique_ptr<int> sp3;
    //报错
    sp3 = sp2;

禁止复制语义也存在特例,即可以通过一个函数返回一个std::unique_ptr。

既然std::unique_ptr不能复制,那么如何将一个std::unique_ptr对象持有的堆内存转移给另外一个呢?答案是使用移动构造,示例代码如下:

 std::unique_ptr<int> sp1(std::make_unique<int>(123));

    std::unique_ptr<int> sp2(std::move(sp1));

    std::unique_ptr<int> sp3;
    sp3 = std::move(sp2);

以上代码利用std::move将sp1持有的堆内存(值为123)转移给sp2,再把sp2转移给sp3。最后,sp1和sp2不再持有堆内存的引用,变成一个空的智能指针对象。并不是所有的对象的std::move操作都有意义,只有实现了移动构造函数(Move Constructor)或移动赋值运算符(operator =)的类才行,而std::unique_ptr正好实现了这二者。

3、std::shared_ptr

        std::unique_ptr对其持有的资源具有独占性,而std::shared_ptr持有的资源可以在多个std::shared_ptr之间共享,每多一个std::shared_ptr对资源的引用,资源引用计数将增加1,每一个指向该资源的std::shared_ptr对象析构时,资源引用计数减1,最后一个std::shared_ptr对象析构时,发现资源计数为0,将释放其持有的资源。多个线程之间,递增和减少资源的引用计数是安全的。(注意:这不意味着多个线程同时操作std::shared_ptr引用的对象是安全的)。std::shared_ptr提供了一个use_count()方法来获取当前持有资源的引用计数。除了上面描述的,std::shared_ptr用法和std::unique_ptr基本相同。

初始化std::shared_ptr的示例:

//初始化方式1
    std::shared_ptr<int> sp1(new int(123));

    //初始化方式2
    std::shared_ptr<int> sp2;
    sp2.reset(new int(123));

    //初始化方式3
    std::shared_ptr<int> sp3;
    sp3 = std::make_shared<int>(123);
class A
{
public:
    A()
    {
        std::cout << "A constructor" << std::endl;
    }

    ~A()
    {
        std::cout << "A destructor" << std::endl;
    }
};
int main()
{
    {
        //初始化方式1
        std::shared_ptr<A> sp1(new A());

        std::cout << "use count: " << sp1.use_count() << std::endl;

        //初始化方式2
        std::shared_ptr<A> sp2(sp1);
        std::cout << "use count: " << sp1.use_count() << std::endl;

        sp2.reset();
        std::cout << "use count: " << sp1.use_count() << std::endl;

        {
            std::shared_ptr<A> sp3 = sp1;
            std::cout << "use count: " << sp1.use_count() << std::endl;
        }

        std::cout << "use count: " << sp1.use_count() << std::endl;
    }
return 0;
}
  • 上述代码sp1构造时,同时触发对象A的构造,因此A的构造函数会执行;

  • 此时只有一个sp1对象引用sp1 new出来的A对象(为了叙述方便,下文统一称之为资源对象A),因此count第一次打印出来的引用计数值为1

  • 利用sp1拷贝一份sp2,导致count第二次打印出来的引用计数为2

  • 调用sp2的reset()方法,sp2释放对资源对象A的引用,因此coun第三次打印的引用计数值再次变为1

  • 利用sp1再次创建sp3,因此count第四次打印的引用计数变为2

  • sp3出了其作用域被析构,资源A的引用计数递减1,因此count第五次打印的引用计数为1

  • sp1出了其作用域被析构,在其析构时递减资源A的引用计数至0,并析构资源A对象,因此类A的析构函数被调用。

输出结果:

A constructor
use count: 1
use count: 2
use count: 1
use count: 2
use count: 1
A destructor

4、std::weak_ptr

std::weak_ptr是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只是提供了对其管理的资源的一个访问手段,引入它的目的为协助std::shared_ptr工作。

std::weak_ptr可以从一个std::shared_ptr或另一个std::weak_ptr对象构造,std::shared_ptr可以直接赋值给std::weak_ptr ,也可以通过std::weak_ptrlock()函数来获得std::shared_ptr。它的构造和析构不会引起引用计数的增加或减少。std::weak_ptr可用来解决std::shared_ptr相互引用时的死锁问题,即两个std::shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0, 资源永远不会释放。

//创建一个std::shared_ptr对象
    std::shared_ptr<int> sp1(new int(123));
    std::cout << "use count: " << sp1.use_count() << std::endl;

    //通过构造函数得到一个std::weak_ptr对象
    std::weak_ptr<int> sp2(sp1);
    std::cout << "use count: " << sp1.use_count() << std::endl;

    //通过赋值运算符得到一个std::weak_ptr对象
    std::weak_ptr<int> sp3 = sp1;
    std::cout << "use count: " << sp1.use_count() << std::endl;

    //通过一个std::weak_ptr对象得到另外一个std::weak_ptr对象
    std::weak_ptr<int> sp4 = sp2;
    std::cout << "use count: " << sp1.use_count() << std::endl;
use count: 1
use count: 1
use count: 1
use count: 1

无论通过何种方式创建std::weak_ptr都不会增加资源的引用计数,因此每次输出引用计数的值都是1。

既然,std::weak_ptr不管理对象的生命周期,那么其引用的对象可能在某个时刻被销毁了,如何得知呢?std::weak_ptr提供了一个expired()方法来做这一项检测,返回true,说明其引用的资源已经不存在了;返回false,说明该资源仍然存在,这个时候可以使用std::weak_ptr 的lock()方法得到一个std::shared_ptr对象然后继续操作资源。

std::weak_ptr类没有重写operator->operator方法,因此不能像std::shared_ptr或std::unique_ptr一样直接操作对象,同时std::weak_ptr类也没有重写operator bool()操作,因此也不能通过std::weak_ptr对象直接判断其引用的资源是否存在。

之所以std::weak_ptr不增加引用资源的引用计数来管理资源的生命周期,是因为即使它实现了以上说的几个方法,调用它们仍然是不安全的,因为在调用期间,引用的资源可能恰好被销毁了,这样可能会造成比较棘手的错误和麻烦。

因此,std::weak_ptr的正确使用场景是那些资源如果可用就使用,如果不可用则不使用的场景,它不参与资源的生命周期管理。例如,网络分层结构中,Session对象(会话对象)利用Connection对象(连接对象)提供的服务来进行工作,但是Session对象不管理Connection对象的生命周期,Session管理Connection的生命周期是不合理的,因为网络底层出错会导致Connection对象被销毁,此时Session对象如果强行持有Connection对象则与事实矛盾。

std::weak_ptr的应用场景,经典的例子是订阅者模式或者观察者模式中。这里以订阅者为例来说明,消息发布器只有在某个订阅者存在的情况下才会向其发布消息,而不能管理订阅者的生命周期。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值