笔记2:智能指针

16 篇文章 1 订阅

       我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。

在C++中,动态内存的管理是用一对运算符完成的:new和delete,new:在动态内存中为对象分配一块空间并返回一个指向该对象的指针,delete:指向一个动态独享的指针,销毁对象,并释放与之关联的内存。

动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。

为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同,shared_ptr允许多个指针指向同一个对象(共享模式)unique_ptr“独占”所指向的对象。标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。

auto_ptr

(C++98的方案,C++11已经抛弃)采用所有权模式。

--------------------------------------------------------------------------------------------------------------------------------------

unique_ptr

某个时刻只能有一个unique_ptr指向一个给定对象,由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作

unique_ptr 支持的操作:

unique_ptr<T, D>  u         释放它的指针, u会使用一个类型为D的可调用对象来释放它的指针。

unique_ptr <T, D> u(d)      空unique_ptr ,指向类型为T的对象,用类型D的对象d代替delete

u = nullptr                 释放u指向的对象,将u置空。

u.release()                 u放弃对指针的控制权,返回当前保存的指针,并将 u 置空

u.reset(q)                  释放u指向的对象,如果提供了内置指针q,令u指向这个对象,否则将u 置空。

虽然我们不能拷贝或者赋值unique_ptr,但是可以通过调用release或reset将指针所有权从一个(非const)unique_ptr转移给另一个unique_ptr

unique_ptr<string> p2(p1.release());   //将所有权从p1(指向string Stegosaurus)转移给p2    //release将p1置为空
p2.reset(p3.release());    //将所有权从p3转移到p2    //reset释放了p2原来指向的内存。

reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。
调用release会切断unique_ptr和它原来管理的的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。
不能拷贝unique_ptr有一个例外:

我们可以拷贝或赋值一个将要被销毁的unique_ptr,最常见的例子是从函数返回一个unique_ptr。

--------------------------------------------------------------------------------------------------------------------------------------

shared_ptr

shared_ptr特有支持的操作:

make_shared<T>(args)    返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化该对象.

shared_ptr<T>p(q)       p是shared_ptr的拷贝,该操作会递增q中的计数器,q中的指针必须能转换为 T*。

p = q                   p 和 q 都是shared_ptr,所保存的指针必须能相互转换,该操作会递减p的引用计 
                        数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放。

p.swap()                交换两个 shared_ptr 对象(即交换所拥有的对象)

p.reset()               放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少

p.get()                 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.

p.unique()              若p.use_count() 等于 1,返回true(独占所有权),否则返回false。

p.use_count()           返回与p共享对象的智能指针数量。

        我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数器,无论何时我们拷贝一个shared_ptr,计数器都会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减,一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。

        shared_ptr自动销毁所管理的对象
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象(通过析构函数),析构函数一般用来释放对象所分配的资源。shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占用的内存。当动态对象不再被使用时,shared_ptr类还会自动释放动态对象。

        shared_ptr和new结合使用
如果我们不初始化一个智能指针,它就会被初始化成一个空指针,接受指针参数的智能指针是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须直接初始化形式来初始化一个智能指针。当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。不要混合使用普通指针和智能指针如果混合使用的话,智能指针自动释放之后,普通指针有时就会变成悬空指针,也不要使用get初始化另一个智能指针或为智能指针赋值

shared_ptr<int> p1 = new int(1024);//错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024));//正确:使用了直接初始化形式

智能指针和异常
如果使用智能指针,即使程序块过早结束,智能指针也能确保在内存不再需要时将其释放,sp是一个shared_ptr,因此sp销毁时会检测引用计数,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放。

使用我们自己的释放操作
默认情况下,shared_ptr假定他们指向的是动态内存,因此当一个shared_ptr被销毁时,会自动执行delete操作,为了用shared_ptr来管理一个connection,我们必须首先必须定义一个函数来代替delete。这个删除器函数必须能够完成对shared_ptr中保存的指针进行释放的操作。

智能指针陷阱:
(1)不使用相同的内置指针值初始化(或reset)多个智能指针。
(2)不delete get()返回的指针
(3)不使用get()初始化或reset另一个智能指针
(4)如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
(5)如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

****************************补充1:为什么不能检测shared_ptr是否为NULL?************************************

因为这就是错的。shared_ptrnullptr相等,并不代表所引用的对象不存在了。

shared_ptr允许有一个“empty”状态,代表shared_ptr<T>对象的实例本身存在,但并不指向一个有效的Tshared_ptr重载了===来表示以下意义:

  • shared_ptrnullptr判等,代表判断是否处在empty状态;

  • shared_ptr被赋值为nullptr,不代表shared_ptr实例本身没了,而是把这个shared_ptr实例的状态改为empty。

  • 一个已经处于empty状态的shared_ptr仍可以被继续赋值成为一个有效、有值的shared_ptr

如果有两个shared_ptr同时指向同一个T在内存中的实例,那么任意一个shared_ptrnullptr判等成功,都不能说明指向的对象已经不存在(被销毁)了!

任何一个具体用例中,都必然有多个shared_ptr指向同一个实例。所以通过判空某 1 个shared_ptr去确定被指向的对象是否已被销毁这个在语义和实效上都绝无任何正确的可能!

**************************************补充2:shared_ptrweak_ptr?**************************************

shared_ptr就意味着你的引用和原对象是一个强联系。你的引用不解开,原对象就不能销毁。滥用强联系,这在一个运行时间长、规模比较大,或者是资源较为紧缺的系统中,极易造成隐性的内存泄漏,这会成为一个灾难性的问题。

更糟的是,滥用强联系可能造成循环引用的灾难。即:B持有指向A内成员的一个shared_ptrA也持有指向B内成员的一个shared_ptr,此时AB的生命周期互相由对方决定,事实上都无法从内存中销毁。
——这还仅仅是一个简单的情况。如果存在间接的强引用,或者是多于两个实例之间的强引用,这个相关的bug解起来将是灾难性的。

shared_ptr是C++内存管理机制的一种放松。但放松绝不意味着可以滥用,否则最后的结局恐怕不会比裸指针到处申请了不释放更好。

必须明确:从语义上来说,shared_ptr代表了一种对生命周期的自动推断。其本质的意义是:A持有Bshared_ptr,代表B的生命周期反而完全覆盖了A。以树形结构的层级来理解,指针持有者是下级,指针指向的目标反而才是上级——下级短命,上级长存;上级不存,下级焉附。

从这个意义上,有些关系你用shared_ptr就表示不了了:

  • 隶属关系中,祖先到子孙的关系。比如一个对象容器,具体对象找其隶属的容器可以用shared_ptr,但是容器去找具体的对象则不行,因为容器不能要求对象生存的比自己更久。

  • 生命周期没有本质关联的两个无关对象。例如一个全局事件管理器(订阅者模型),事件订阅者构造时把自己注册进来,析构时把自己解注册掉。管理器要维护到达这个对象的指针(从而发送消息),但绝对不允许染指对象的生命周期,对象的析构需要无视订阅者的存在,只由其他业务所必须的强引用来控制。

这种时候就是weak_ptr的用处。weak_ptr提供一个(1)能够确定对方生存与否(2)互相之间生命周期无干扰(3)可以临时借用一个强引用(在你需要引用对方的短时间内保证对方存活)的智能指针。

weak_ptr要求程序员在运行时确定生存并加锁,这也是逻辑上必须的本征复杂度。

--------------------------------------------------------------------------------------------------------------------------------------

weak_ptr
weak_ptr是一种不控制所指向对象生存周期的智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。weak_ptr 支持拷贝或赋值, 但不会影响对应的 shared_ptr 内部对象的计数.

由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否存在。如果存在,lock返回一个指向共享对象的shared_ptr,如果不存在,lock将返回一个空指针。

weak_ptr支持的操作

weak_ptr<T>  w        空weak_ptr 可以指向类型为T的对象

w.reset()             将w置为空

w.use_count()         与w 共享对象的shared_ptr 的数量

w.expired()           若 w.use_count() 为 0,返回true,否则返回false

w.lock()           若expired()为true,返回一个空shared_ptr,否则返回一个指向w的对象的shared_ptr
#include <iostream>
using namespace std;
 
class A;
class B;
 
class A{
public:
	~A(){ cout << "destroying A\n" ; }
	shared_ptr<B> pb;
};
/*
class B{
public:
	~B(){ cout << "destroying B\n" ; }
	shared_ptr<A> pa;
};
*/
class B{
public:
	~B(){ cout << "destroying B\n" << endl; }
	weak_ptr<A> pa;
};
 
void test(){
	shared_ptr<A> a(new A());
	shared_ptr<B> b(new B());
 
	a->pb = b;
	b->pa = a;
}
 
int main(){
	cout << "begin test...\n";
	test();
	cout << "end test\n";
}

-------------------------------------------------------------------------------------------------------------------------------------

scoped_ptr
scoped和weak_ptr的区别就是,给出了拷贝和赋值操作的声明并没有给出具体实现,并且将这两个操作定义成私有的,这样就保证scoped_ptr不能使用拷贝来构造新的对象也不能执行赋值操作,更加安全,但有了"++" " –"以及"*" "->"这些操作,比weak_ptr能实现更多功能。

参考博文:

https://blog.csdn.net/flowing_wind/article/details/81301001

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值