C++智能指针2

本文详细介绍了C++中的std::shared_ptr和std::weak_ptr,重点讲解了它们的工作原理、如何避免引用计数导致的死锁问题,以及在处理循环引用时std::weak_ptr的作用。
摘要由CSDN通过智能技术生成

std::shared_ptrstd::weak_ptr

我们前一篇博客提到了std::unique_ptr,我们称呼他是一个独占性质的管理资源的资源管理器。现在,我们来看一下std::shared_ptr和为了解决std::shared_ptr存在的潜在缺陷(本质上是引用计数的缺陷)而派生的std::weak_ptr

这次为了更好的展示,我们使用一个demo资源类:

class Special
{
public:
    Special() {
        std::cout << "Create Class Instances Special!" << std::endl;
    }
​
    ~Special() {
        std::cout << "delete Class Instances Special!" << std::endl;
    }
private:
    int sources;
};

std::shared_ptr 入门

结合上一篇博客,我们知道,智能指针可以自动的在资源的声明周期结束之后进行析构。std::shared_ptr作为智能指针的一种,自然也是如此:

int main()
{
    std::shared_ptr<Special> shared(new Special);
}

没有任何意外:

Visual Leak Detector read settings from: D:\VLD\Visual Leak Detector\vld.ini
Visual Leak Detector Version 2.5.1 installed.
Create Class Instances Special!
delete Class Instances Special!
No memory leaks detected.
Visual Leak Detector is now exiting.

但是首先,我们看到的是:shared_ptr是可以shared它的资源的。代价就是use_count++

int main()
{
    std::shared_ptr<Special> shared(new Special);
    std::shared_ptr<Special> other_shared(shared);
    std::cout << "Current sources is handling for " << shared.use_count() << " times\n";
}
Visual Leak Detector read settings from: D:\VLD\Visual Leak Detector\vld.ini
Visual Leak Detector Version 2.5.1 installed.
Create Class Instances Special!
delete Class Instances Special!
No memory leaks detected.
Visual Leak Detector is now exiting.

可以看到,使用这个指针来指向资源,不发生拷贝行为,相反,只是将它的use_count(引用计数,用来记载有多少个指针此时正在把控这个资源)增加,(这里不放源码了,这里的shared_ptr实现是继承了_ptr_base的,这里是更改了父类的计数)

Visual Leak Detector read settings from: D:\VLD\Visual Leak Detector\vld.ini
Visual Leak Detector Version 2.5.1 installed.
Create Class Instances Special!
Current sources is handling for 2 times
delete Class Instances Special!
No memory leaks detected.
Visual Leak Detector is now exiting.

你可以看到,我们在第一行指定了一个资源管理器管理一个资源:new Special这样一个右值。在第二行又要求另一个资源管理器管理同一个资源(如你所见,就是调用拷贝函数)。我们的程序仍然正确的释放了资源,这是因为shared或者是other_shared被释放的时候,当use_count不是0的时候就减去一个use_count,减到0的时候就会自动释放

    void _Decref() noexcept { // decrement use count
        if (_MT_DECR(_Uses) == 0) { 
        // #define _MT_DECR(x) _INTRIN_ACQ_REL(_InterlockedDecrement)(reinterpret_cast<volatile long*>(&x)), 也就是原子的减,调用的是CPU命令当中集成的原子减指令,这是为了防止形成竞态
            _Destroy(); // 删除资源
            _Decwref();
        }
    }

其他的部分让我们看看API就好了:

std::shared_ptr's API

构造

当然可以生成默认的构造:此时此刻,我们的std::shared_ptr就是空的,不托管任何资源

std::shared_ptr<Special> shared;

或者是托管一个裸指针:

std::shared_ptr<Special> shared(new Special);

或者是调用拷贝函数,

    std::shared_ptr<Special> shared(new Special);
    std::shared_ptr<Special> other_shared(shared);

亦或者是移动函数:

    std::shared_ptr<Special> shared(new Special);
    std::shared_ptr<Special> other_shared(std::move(shared));
    std::cout << "Current sources is handling for " << shared.use_count() << " times by shared\n";
    std::cout << "Current sources is handling for " << other_shared.use_count() << " times by other_shared\n";

结合你对移动构造的认识,你马上就会意识到:调用移动函数本质上就是更换资源托管器。这也正是它的作用。

我们得到shared_ptr的另一种更加可行的方式是:make_shared

std::make_shared<Special>(/*nullptr or set nothing to get a default shared_ptr*/);
std::make_shared<Special>(new Special); // wrapped a raw pointer
std::make_shared<Special>(other_shared); // copy a shared_ptr

通过这种方式也可以获得shared_ptr

这里插一句:使用这些智能指针访问就跟我们使用裸指针一样,使用->访问资源,.在这里则是表示对资源管理器自身进行操作。

修改器

就是这两个:

reset替换所管理的对象 (公开成员函数)
swap交换所管理的对象 (公开成员函数)

这里跟unique_ptr在功能上类似,这里如果只是希望查看这两个函数可以做什么的可以参考我的上一篇博客:C++ 智能指针-CSDN博客。这里只是给出Demo。相信可以一目了然:

reset空

void showUseCount_And_Reset()
{
    // 创建一个智能指针,让他托管
    std::shared_ptr<Special> spPtr(new Special);
    std::cout << spPtr.use_count() << std::endl;
    spPtr->setSources(100);
    spPtr.reset(); // set as nullptr, in other words, sources are released
    std::cout << spPtr.use_count() << std::endl;
}
Visual Leak Detector read settings from: D:\VLD\Visual Leak Detector\vld.ini
Visual Leak Detector Version 2.5.1 installed.
Create Class Instances Special!
1
delete Class Instances Special!
The sources of 00000159ADF00A60's value is:> 100
0
No memory leaks detected.
Visual Leak Detector is now exiting.

reset另一个资源

void showUseCount_And_Reset()
{
    // 创建一个智能指针,让他托管
    std::shared_ptr<Special> spPtr(new Special);
    std::cout << spPtr.use_count() << std::endl;
    spPtr->setSources(100); 
    spPtr.reset(new Special);
    spPtr->setSources(200);
    std::cout << spPtr.use_count() << std::endl;
}
Visual Leak Detector read settings from: D:\VLD\Visual Leak Detector\vld.ini
Visual Leak Detector Version 2.5.1 installed.
Create Class Instances Special!
1
Create Class Instances Special!
delete Class Instances Special!
The sources of 000002403FB20AE0's value is:> 100
1
delete Class Instances Special! // reset here
The sources of 000002403FB20DA0's value is:> 200
No memory leaks detected.
Visual Leak Detector is now exiting.
void showUseCount_And_Reset()
{
    std::shared_ptr<Special> sp(new Special);
    sp->setSources(100);
    // 创建多个资源管理同时管理同一个资源:
    std::shared_ptr<Special> sp2 = sp;
    std::shared_ptr<Special> sp3 = sp;
    std::cout << sp.use_count() << std::endl;
    std::cout << sp2.use_count() << std::endl;
    std::cout << sp3.use_count() << std::endl;
​
    // 资源发生变动
    sp.reset(new Special);
    sp->setSources(200);
    std::cout << "sp2:> " << sp2->getSources() << std::endl;
    std::cout << "sp3:> " << sp3->getSources() << std::endl; 
}
Visual Leak Detector read settings from: D:\VLD\Visual Leak Detector\vld.ini
Visual Leak Detector Version 2.5.1 installed.
Create Class Instances Special!
3
3
3
Create Class Instances Special!
sp2:> 100
sp3:> 100
delete Class Instances Special!
The sources of 0000023F58B505E0's value is:> 100
delete Class Instances Special!
The sources of 0000023F58B501E0's value is:> 200
No memory leaks detected.
Visual Leak Detector is now exiting.

这个例子存在输出的竞态,实际上,仍然是释放了资源之后再去接受新的资源。其他的获取器API同unique_ptr差不多,这里不再赘述了

weak_ptr入门

引用计数存在天然的缺陷!

我们知道:我们现在做出的假定是:资源管理器自己不会称为一个被管理的资源。什么意思呢?我们看看一个资源管理的逻辑图就知道了:

这是unique_ptr的资源逻辑管理示意图:我们看到了资源和管理器是一一映射的

ManagerA < - > A
ManagerB < - > B
ManagerC < - > C

这是shared_ptr的资源逻辑管理示意图:我们看到了资源和管理器是多对一映射的。

ManagerA < - > A
ManagerB < - > A
ManagerC < - > A

换而言之,我们的资源逻辑图不会出现一个环状的结构。什么是一个环状的结构呢?我们来看一个例子:

ManagerA < - > ManagerB
ManagerB < - > ManagerA

这个步骤单纯的依靠管理器本身初始化做不到,需要我们手动的构造以下场景:

class Special_HolderII;
​
class Special_HolderI
{
public:
    std::shared_ptr<Special_HolderII> sp;
    ~Special_HolderI() {
        std::cout << "Special Holder I is released" << std::endl;
    }
};
​
class Special_HolderII
{
public:
    std::shared_ptr<Special_HolderI> sp;
    ~Special_HolderII() {
        std::cout << "Special Holder II is released" << std::endl;
    }
};

你可以看到,我们的管理对象里包含了对方!这里就是导致潜在漏洞的点!我们初始化:

    std::shared_ptr<Special_HolderI>    sh1(new Special_HolderI);
    std::shared_ptr<Special_HolderII>   sh2(new Special_HolderII);

上面的代码首先声明了两个独立的shared_ptr,分别托管了这样的资源示意图:

两个圆圈就是两个shared_ptr,现在他们分别托管Special_HolderISpecial_HolderII,这两个资源管理器本身没有耦合!现在为止,我们在外部操作了两个智能指针托管资源让他们的引用计数为1

    // I
    std::cout << "sh1's use_count: " << sh1.use_count() << std::endl;
    std::cout << "sh2's use_count: " << sh2.use_count() << std::endl;

现在,我们操作资源,让资源耦合资源管理器,以一种奇怪的方式再次增加:

    sh1->sp = sh2;
    sh2->sp = sh1;

这是在干什么?仔细思考走完上面这两步的后果:我们的sh1托管一个资源管理器(他是属于sh1的)被赋值以sh2他所托管的对象(第一句的作用),而sh2托管的Special_HolderI类型的对象是就是sh1(第二句的作用)

同理:我们的sh2托管一个资源管理器(他是属于sh2的)被赋值以sh1他所托管的对象(第二句的作用),而sh1托管的Special_HolderII类型的对象是就是sh2(第一句的作用)

等等,这是不同的智能指针托管同一个对象。所以,我们一经发现这是同一个对象,不会释放自己handle的资源而是简单的增加引用计数。

现在构成了这样的一个图:

    // II
    std::cout << "sh1's use_count: " << sh1.use_count() << std::endl; // 2
    std::cout << "sh2's use_count: " << sh2.use_count() << std::endl; // 2

还不懂??有点绕?我再重复一次!第一次我们的外部智能指针分别托管了对方类型的资源,让资源的引用数为1了。第二次我们操纵他们所托管的资源内部的智能指针托管外部指针托管的对象(包含了他们自身的资源)。这样,我们走一次逻辑链就会发现,第一次我们在资源管理器的层次(资源外部)上让A托管了B的资源,B托管了A的资源。第二次我们的赋值,则是在资源层次让资源内部的智能指针重复我们所作的事情。也就是在资源层次上让A托管了B的资源,B托管了A的资源。强行让编译器认为我们是在操作不同的指针指向同一个资源而不是实际上的同一个(同一个指针多次指向同一个资源当然不会增加引用计数)

这也被叫做shared_ptr的死锁。当我们释放的时候,触发了sh1和sh2的析构,进而准备释放成员。

现在Extern SH1和Extern SH2释放,引用计数被减1了,但是还有资源内部耦合没有被解除:对象是动态分配的,而对象本身又含有shared_ptr指针,释放对象需要shared_ptr的释放使引用计数减为零,而shared_ptr的释放又需要对象的释放,两者互相等待对方先释放,往往是两者都无法释放。

Visual Leak Detector read settings from: D:\VLD\Visual Leak Detector\vld.ini
Visual Leak Detector Version 2.5.1 installed.
sh1's use_count: 1
sh2's use_count: 1
sh1's use_count: 2
sh2's use_count: 2
WARNING: Visual Leak Detector detected memory leaks!
---------- Block 2 at 0x0000000007774F60: 24 bytes ----------
  Leak Hash: 0x386B91B3, Count: 1, Total 24 bytes
  Call Stack (TID 28472):
  Data:
    70 DE F3 EF    F7 7F 00 00    01 00 00 00    01 00 00 00     p....... ........
    90 B6 77 07    C1 01 00 00                                   ..w..... ........
​
​
---------- Block 4 at 0x00000000077752C0: 24 bytes ----------
  Leak Hash: 0x1142308C, Count: 1, Total 24 bytes
  Call Stack (TID 28472):
  Data:
    A0 DE F3 EF    F7 7F 00 00    01 00 00 00    01 00 00 00     ........ ........
    20 BD 77 07    C1 01 00 00                                   ..w..... ........
​
​
---------- Block 1 at 0x000000000777B690: 16 bytes ----------
  Leak Hash: 0x9A5D80CF, Count: 1, Total 16 bytes
  Call Stack (TID 28472):
  Data:
    20 BD 77 07    C1 01 00 00    C0 52 77 07    C1 01 00 00     ..w..... .Rw.....
​
​
---------- Block 3 at 0x000000000777BD20: 16 bytes ----------
  Leak Hash: 0xE93075EF, Count: 1, Total 16 bytes
  Call Stack (TID 28472):
  Data:
    90 B6 77 07    C1 01 00 00    60 4F 77 07    C1 01 00 00     ..w..... `Ow.....
​
​
Visual Leak Detector detected 4 memory leaks (288 bytes).
Largest number used: 288 bytes.
Total allocations: 288 bytes.
Visual Leak Detector is now exiting.

这样就泄漏了。

如何解决呢?答案是使用weak_ptr

根本原因在于:我们总是强耦合的管理资源,匆匆的宣布自己负责托管它。但是事实上过于急躁的宣布自己的所属权可能会导致死锁。

我们试想:如果我们可以在我们真正需要访问并且资源的时候在增加引用计数,而只是声明我跟资源有关系的时候不增加引用计数,这样我们就回避了过早的增加计数导致死锁的问题了。

weak_ptr正是这样的:

class Special_HolderII;
​
class Special_HolderI
{
public:
    std::shared_ptr<Special_HolderII> sp;
    ~Special_HolderI() {
        std::cout << "Special Holder I is released" << std::endl;
    }
};
​
class Special_HolderII
{
public:
    std::weak_ptr<Special_HolderI> sp;
    ~Special_HolderII() {
        std::cout << "Special Holder II is released" << std::endl;
    }
};
​
​
int main()
{
    std::shared_ptr<Special_HolderI>    sh1(new Special_HolderI);
    std::shared_ptr<Special_HolderII>   sh2(new Special_HolderII);
    // I
    std::cout << "sh1's use_count: " << sh1.use_count() << std::endl;
    std::cout << "sh2's use_count: " << sh2.use_count() << std::endl;
    sh1->sp = sh2;
    sh2->sp = sh1; // 这里只是声明我很资源有关系但是可能不打算使用,不增加计数
    // II
    std::cout << "sh1's use_count: " << sh1.use_count() << std::endl;
    std::cout << "sh2's use_count: " << sh2.use_count() << std::endl;
}
Visual Leak Detector read settings from: D:\VLD\Visual Leak Detector\vld.ini
Visual Leak Detector Version 2.5.1 installed.
sh1's use_count: 1
sh2's use_count: 1
sh1's use_count: 1
sh2's use_count: 2
Special Holder I is released
Special Holder II is released
No memory leaks detected.
Visual Leak Detector is now exiting.

值得注意的是:weak_ptr因为只是声明有关系,没办法真正有权利访问资源,需要使用API进行转化。这里就开始介绍:

std::weak_ptr's API

std::weak_ptr支持拷贝和移动,以及从一个强管理的shared_ptr中派生,但是不支持默认的构造。也就是说它完全是shared_ptr的附属物,依靠shared_ptr生存。

使用lock来获取真正可以管理的实例对象:

创建共享被管理对象的所有权的新 std::shared_ptr 对象。若无被管理对象,即 *this 为空,则返回的 shared_ptr 也为空。相当于返回 expired() ? shared_ptr<T>() : shared_ptr<T>(*this),原子地执行。

使用expire来检查我们的weak_ptr是否合法!

等价于 use_count() == 0。可能仍未对被管理对象调用析构函数,但此对象的析构已经临近(或可能已发生)。

使用weak_ptr的场景是:当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

这是因为:weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,

参考网站:

cppreference.com

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值