三种智能指针
概述:是什么,及其参与的生态位
C++ 11后,标准库(std)中新加入了四种指针:unique_ptr
,shared_ptr
,weak_ptr
,auto_ptr
(最后一种在C++17
后被废弃因而本文不做任何涉及)。它们对应的传统位置是C++的裸指针(和前者相比,纯粹的一个T* ptr
没有被封装,故得名)。它们的创建与删除操作对应了裸指针的new
,delete
操作。也就是说,它的核心任务是在堆上人为分配资源,但同时利用了栈与作用域的特性,实现了更安全的资源分配。
安全在哪?
- 裸指针指针每次创建都需要手动调用
delete
删除。这可能会有以下问题:
- 忘记
delete
- 当程序逻辑很复杂时,找不对调用
delete
的时机 delete
错东西(指针指向发生变化后原来指向的内存找不回来却也不能再被分配,或如链表等在删除时少删了几个结点)
- 既然程序结束时一定其创建的资源会被卸载,不能不管它吗?
- 显然不能。程序存在逻辑漏洞
- 对于长期运行程序,没有靠这个卸载的机会
- 这种结束程序强行截断可能导致有些模块逻辑没闭环,留下可以被外界接入的契机
-
程序发生意外时,可能有些内存被强制卸载为下次启动留下bug(类似上一条)
-
而相对的,智能指针能只管创建,而可以在其所属作用域结束后自动卸载。这样就不用考虑忘记或删错东西的情况。同时它还能在遭遇意外时也自动执行
delete
,这也就是“异常安全”保证(异常不是形容词) -
如果有些对象生存周期和程序本身一样(或中途创建,到程序结束时才被卸载),那它和等到主函数结束让系统自动删有区别吗?
- 显然有的。让程序出发与结束时同状态是正常设计该有的事,而不能将这种是交由系统的强行执行(就像重写构造、析构函数)
- 对于这些资源,是“它们的生存周期本来就该到程序结束”,这和“忘了删”形式上一样,本质上不同
- 需要考虑到将来这些资源需要提前被卸载的可能。用智能指针是能让自己有掌控它被卸载的时机的(这个可能体现在运行逻辑设计上而不是非要自己明显地意识到)
该何时用智能指针?
从目前学到的来看,应该是“尽量用”。
也就是:在要用指针是首先考虑用unique_ptr
,如果其在功能上不够或在含义上有更契合的,则考虑后两种智能指针,总之不是裸指针
智能指针(尤其unique_ptr
)的开销是很小的,故不用在这方面节省性能
简单对比三种智能指针
首先在OOP情境下许多模块类需要进行实例化,但它一般一个程序内实例化一次就够了。另一个例子是状态机的状态信息,需要在模块间传来传去,但即使被修改替换也始终有且只有一个
这个时候如其现实意义,需要一个独一无二的指针调用它(函数直接传值显然不合理就算了)
-
unique_ptr
如其字面意思,是独一无二的。
-
如何保证独一无二?
创建时只能链接一个对象(裸指针也是)
不能进行再复制与赋值操作(该类的赋值操作符
=
在std中被人为禁用了)
所以在创建后,对指针本身的操作其实是所有权的交移
-
但有时存在需要多个模块调用同一个模块对象的情况(比如多个材质在用同一款纹理),这时需要能够多指针共享同一份资源
-
shared_ptr
为了恰好使指针指向的资源被用完后才卸载,其会有一个计数系统。指向同一内存的不同指针共用一个计数。新的
shared_ptr
指向该内存时它们共有的计数会加一,当某个shared_ptr
被自动“卸载”(在这里因为内存未被释放,只是它自身离开作用域了)后计数会减一。当计数为零时说明这份内存已经没有被shared_ptr
指着了(且也没办法再被重新指向了),此时这份内存才会被真正卸载
但存在交叉引用的情况(内存互相调用),涉及此情景时会有双方计数最终减不到零的情况出现
同时也存在创建指针只为了临时查看其状态的情景
所以有了weak_ptr
weak_ptr
它可以从shared_ptr
与weak_ptr
上赋值得到,但不存在类似std::make_weak_ptr
的生成方式(因为它本质是一个不计数的shared_ptr
)
它的创建与卸载不会影响shared_ptr
的计数系统
智能指针的使用
unique_ptr<>
class CustomedClass(){
public:
CustomedClass(int num)
:num_(num){
}
private:
int num_;
}
int main(){
int num_i = 1;
//创建智能指针时要用std::make_xx的指令(一般类的赋值等操作全被禁了,用提供的就够)
std::unique_ptr<CustomedClass> ptr = std::make_unique<CustomedClass>(num_i);
//unique_ptr无赋值、复制的概念,只能转移所有权,用std::move()
std::unique_ptr<CustomedClass> ptr2 = std::move(ptr);
return 0;
}
shared_ptr<>
与weak_ptr<>
std::shared_ptr<MyClass> shared1 = std::make_shared<CustomedClass>();
std::shared_ptr<MyClass> shared2 = shared1;
std::weak_ptr<MyClass> weak1 = shared1;
因为这两种设计出来就是为了分享的,故也只需要简单赋值即可得到新的指针
背景:智能指针的简单实现及更深一步的理解
前置概念
作用域(Scope)
在C++中这个概念很直观,指的是被一对花括号包起来的内部区域,花括号可以是来自对象、函数、判断、循环等,甚至可以是单独出现的花括号
class ClassA{
//Scope
}
{
//And this can also be scope
}
生存期(Life Time)
- 栈作用域生存期
如果某一物被创建在栈上,当它离开其被创建时所属的作用域时,其就会被自动销毁
class ClassA{
ClassA(){
std::cout << "Constructed." << std::endl;
}
~ClassA(){
std::cout << "Destoryed" << std::endl;
}
}
int main(){
{
ClassA obj; //在这里会调用构造函数
} //在这里因为离开作用域而调用析构函数
}
- 堆作用域生存期
比栈更简单些,其正常来讲会一直存活直到被我们指定销毁(因程序结束时还没被销毁而被强制回收不算正常情况)
- 核心功能的实现
template<typename T>
class ScopePtr(){
public:
ScopePtr(T* ptr)
:ptr_(ptr){
}
~ScopePtr(){
delete ptr_;
}
private:
T* ptr_; //其实最核心的只有这里:在析构时删掉分配在堆上的ptr
}
int main(){
ScopePtr<int> s_ptr(new int(42)); //这样就创建了一个智能指针
}
其实就是将分配到堆上的指针包到栈里,这样当栈上的智能指针离开作用域被删除后会连带着将包含的堆上的指针也删掉。
但是相较于只靠栈,它还能实现在函数内创建指针后将它再传出去这种堆专属的功能
template<typename T>
ScopePtr<T> create_scope_ptr(T data){
return ScopePtr<T>(new T(data));
}
如果仅是栈的话,由于内部生成的裸指针所指向的内存出域后被销毁,而返回值即使是该函数内部生成的指针的复制,因为指向的同一块内存,而该内存被销毁了,也就不能用了
但智能指针从函数传出时先进行类复制,默认构造函数将内部成员ptr_
原封不动拷过去,相当于内存被销毁,但“内存的内存”没事。
拓展:RAII
RAII(Resoure Acquisition Is Initialization)其强调的是在作用域开始时分配资源,在离开作用域时释放资源(这里的离开包含因异常离开等情况,所以程序要有耐造性)
更原教旨地讲,它强调的是在构造函数中进行资源分配,在析构函数中进行资源释放
其目的是优化内存管理
说些什么吧!