C++ 2.0新特性——智能指针

智能指针

一、裸指针与智能指针

使用智能指针就是为了克服裸指针的一系列缺点:
1、裸指针在声明中并没有指出指向的是单个对象还是一个数组。
2、裸指针在声明中也没有提示在使用完指向的对象之后,是否需要析构它。即在声明中看不出指针是否拥有其指向的对象。
3、即使知道指向的对象,也不可能知道怎样析构才是合适的。是调用delete还是放到专门的用于析构的函数里面。
4、即使知道需要使用delete,匹配对像“delete”和数组“delete[]”也会存在未定义行为。
5、上述都清除,要保证析构在所有代码路径上都执行一次仍然有难度。只要有一条路径未执行,则会导致资源泄露,执行多次也会产生未定义行为。
6、无法检测出指针是否空悬,即它所指向的内存是否已经不再持有指针本应该指向的对象,若一个对象已经被析构,而某指针依然指向它,则会产生空悬指针。

智能指针: 对裸指针进行包装。通过保证对象在适当的时机被析构来防止资源泄露。

二、unique_ptr管理具备专属所有权的资源

1、unique_ptr是小巧、高效、具备只移类型的智能指针,对托管资源实施专属所有权语义。

  1. 只移:不允许复制。若复制一个unique_ptr则会得到两个指向同一资源的智能指针,其中一个修改资源,则另一个会产生未定义行为,造成资源泄露,这个特性会反映在编译阶段,IDE会编译不通过,而auto则不能在编译期检查这个问题。

2、 指针析构时,默认是通过unique_ptr内部的裸指针实施delete完成。当然也可以自定义析构函数,例如:

class A{};
auto del = [](A* a) {//自定义析构函数
	//log
	delete a;
};
unique_ptr<A, decltype(del)> makeA() {  }//使用自定义析构函数

3、 使用unique_ptr转换成shared_ptr很容易实现,在工厂模式常用作工厂函数的返回类型。

unique_ptr<A, decltype(del)> makeA() {  }
shared_ptr<A> sh = makeA();

三、shared_ptr管理具备共享所有权的资源

1、shared_ptr提供方便的手段,实现了任意资源在共享所有权语义下进行生命周期管理的垃圾回收。通过引用计数来管理共享资源,当 引用计数为0时,调用析构函数释放资源。
引用计数: 一个与资源关联的值,用来记录跟踪指向该资源的shared_ptr数量。会带来一下性能影响:

  1. shared_ptr尺寸是裸指针的两倍:内部包含一个指向该资源的裸指针和指向该资源引用计数的裸指针。
  2. 引用计数的内存必须动态分配。引用计数与被指向的对象关联,但该对象并不知情,因此对象没有存储引用计数的位置。
  3. 引用计数的增减必须是原子操作。

注:每个shared_ptr管理的对象都有一个控制块,除包含引用计数外,还包含自定义析构器的一个复制、弱计数、其他数据(自定义删除器、分配器等)

2、shared_ptr会带来控制块的开销,并要求原子化引用计数操作。控制块创建遵循的规则:

  1. make_shared总是创建一个控制块。make_shared会生产出一个指向的新对象,因此调用make_shared时,不会有针对该对象的控制块存在。
  2. 从unique_ptr出发创建shared_ptr,会创建一个控制块。
  3. shared_ptr构造函数使用裸指针作为实参调用时,会创建控制块。

从同一个裸指针出发构造不止一个shared_ptr会出现未定义的行为。

auto pw=new Widget;
shared_ptr<Widget> s1(pw);//创建第一个控制块
shared_ptr<Widget> s2(pw);//创建第二个控制块

此时pw就要两个引用计数,在第二次析构时会出现未定义行为,可以在第一次使用new Widget,第二次则s2(s1)调用复制构造函数,避免这个问题。

3、当希望一个托管到shared_ptr的类能安全地由this指针创建一个shared_ptr时,将为继承而来的基类提供一个模板——enable_shared_from_this(基类模板)。

class Widget:public enable_shared_from_this<Widget>{
public:
	void process{
		//Widgets.emplace_back(this);//出现未定义行为:在已经指向Widget类型的对象的成员函数外部再套了一层shared_ptr.
		Widgets.emplace_back(shared_from_this());
	}
};

四、类似shared_ptr但有可能指向的指针为空时,使用weak_ptr

weak_ptr:像shared_ptr那样运作,又不影响其指向的引用计数,即可以处理指向的对象可能已被析构的情况,是shared_ptr的一种补充。

weak_ptr是否失效的试验:

auto spw=make_shared<Widget>();
weak_ptr<Widget> wpw(spw);

shared_ptr<Widget> spw1=wpw.lock();//若wpw失效,则spw1为空
//auto spw1=wpw.lock();//auto版本

//shared_ptr作为实参构造
shared_ptr<Widget> spw3(wpw);//若wpw失效,则抛出bad_weak_ptr异常

weak_ptr可被用到缓存、观察者模式、避免指针环路的场景。

可以通过使用weak_ptr的expired()成员函数来检查它指向的资源是否有效:

weak_ptr<Widget> wp;
if(!wp.expired()){//wp指向的对象时有效的
	shared_ptr<Widget> spw=wp.lock();//weak_ptr不提供指针的解引用操作(*  ->),若要使用它观察的资源,必须先调用lock()成员函数获取一个shared_ptr实例。
}

五、优先使用make_unique和make_shared,而非直接使用new

C++14才支持make_unique.
使用make创建智能指针的对比:
1、代码冗余

auto upw1(make_unique<Widget>());
unique_ptr<Widget> upw2(new Widget);

auto spw1(make_shared<Widget>());
shared_ptr<Widget> spw2(new Widget);

使用new版本创建对象的类型重复写了两遍,代码冗余。

2、异常安全
定义一

//依据某种优先级处理Widget
void processWidget(sharde_ptr<Widget> spw,int priority)

优先级计算由函数int computePriority() 实现。

则调用函数:

processWidget(shared_ptr<Widget>(new Widget),computePriority));

会发生潜在的资源泄露。

运行期:传递给函数的实参必须在函数调用发起之前完成评估求值,因此先new Widget,再shared_ptr,再computePriority。
编译期:new Widget必须在shared_ptr之前,但computePriority可在任意顺序。
**极端情况下:**先new Widget,再computePriority,最后shared_ptr,一旦运行期computePriority发送异常,第一步分配的Widget就会被泄露。而make_shared则不会。

3、性能提升。
使用new会进行两次内存分配(多了new这一次),make只有一次。

不适用make的场景
1、希望自定义析构器的智能指针
2、由于make会向对象的构造函数完美转发形参,使用的是圆括号,因此若需要使用大括号初始化对象,则需要用new。

特殊地,针对make_shared不适用的场景还包括:
1、自定义内存管理的类
2、内存紧张的类,对象很大
3、存在weak_ptr

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dailingGuo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值