初识智能指针
- 前言
- 一.智能指针的介绍
- 二.智能指针基本原理
- 1.变量的作用域
- 2.RAII
- 3.类的拷贝控制
- 4.引用计数
- 三.智能指针具体使用
- 1.unique_ptr
- 使用场景
- 2.shared_ptr
- 使用场景
- 3.weak_ptr
- 四.结尾
前言
本文主要介绍智能指针用法和场景,不会很深。
后续会讲解智能指针使用的进阶知识、智能指针的实现原理以及智能指针源码剖析和模仿实现。
一.智能指针的介绍
c/c++语言可以灵活的操控指针,但是在操作不当的时候会导致内存泄露或者内存越界,而智能指针使用了RAII、引用计数等方法,来解决这个问题。
智能指针可以保证内存在不需要使用的时候自动释放,在需要使用的时候不会释放,防止内存泄露;同时,在有些情景下智能指针在使用的时候开销几乎和裸指针一样。
指针指针主要有三种unique_ptr,sharead_ptr,weak_ptr(auto_ptr在较新的标准中被删除)
unique_ptr保证指针只会被一个对象拥有,无人拥有时清除内存;shared_ptr通过引用计数保证指针在有人使用时不会删除内存,在无人使用的时候会自动清理内存;weak_ptr只会指向内存,并感知内存是否可用,不会引用技术。
这里讲得很粗糙,接下来细讲。
二.智能指针基本原理
前面几点会讲解一些前置知识,虽然简单且无聊,但是很重要
(也可以先看用法再回来看这个)
1.变量的作用域
大家都知道栈变量离开作用域后会自动销毁,但是堆内存需要手动管理,如
void func()
{
int a[100];
int *p = new int(10);
}
函数func结束后,a数组的内存已经释放,而p变量(int*类型)虽然释放,但是其指向的内存不会释放。
那有什么办法让堆内存像栈内存一样自动释放?有办法,就是使用栈变量来管理堆内存,同时利用cpp的析构函数,在栈变量析构的时候处理堆内存。
简单举例如下
//管理int*指针的类
class mzx_smart_int_ptr{
int* ptr;//要管理的指针
mzx_smart_int_ptr(){
ptr = new int;
}
~mzx_smart_int_ptr(){
delete ptr;//析构时释放内存
}
};
//作用域
{
mzx_smart_int_ptr p;
}//离开时p析构,释放内存
可以看到,栈变量析构时,堆内存跟着释放了。
虽然例子不太实际,但是智能指针基本上是基于这个原理
2.RAII
RAII大概是指创建变量时就完成初始化,销毁时释放资源;这种技术使用在智能指针是为了防止野指针。了解一下就行,后面细讲。
3.类的拷贝控制
通过delete删除类的拷贝构造、拷贝赋值等函数,限制类的复制,保证类内的资源不会随意共享(删除这些函数不一定使用delete,也有其他办法)。这与unique_ptr的实现有关系,unique_ptr包含一个指针,且要求对于一个指针只能有一个unique_ptr包含,就是资源不能共享(可以移动,即可以使用移动语义完成赋值和构造)
4.引用计数
shared_ptr在析构时,不会直接释放内存,而是通过引用计数来确定没用智能指针指向这块内存时再释放内存。
简单实现举例
template<typename T>
class s_ptr{
T* ptr;
int* counter;//计数器
s_ptr(T* p){
ptr = p;
counter = new int(1);
}
s_ptr(const s_ptr& p){
ptr = p.ptr;
counter = p.counter;
*counter++;
}
~s_ptr(){
*counter--;
if(*counter == 0){
delete ptr;
}
ptr = nullptr;
counter = nullptr;
}
};
//只是简单举例,很多细节没完善,先掌握思想就行
s_ptr用一个计数器来确定有多少个智能指针拥有该内存,创建时初始化计数器为1,若是拷贝构造,直接获取传入类的计数器,同时计数器加一,最后实现内存控制。细节没完善,但是思想差不多。
三.智能指针具体使用
1.unique_ptr
智能指针是一个模板类,类中含有对应对像的指针(其实还有其他东西,但是我们一步步来),通过将指针封装,保证指针在正确的时候析构。简单来说就是让一个变量来管理一块资源。首先介绍unique_ptr的使用。unique_ptr,顾名思义,一块资源只有一个unique_ptr能操控,不能直接赋值给其他智能指针,但是可以使用std::move(ptr)将一个指针的资源所有权交给另一个指针,当前智能指针滞空。
具体API:(暂时先不介绍删除器)
//有两种定义,单个对象和对象数组管理是分开的
template<
class T,
class Deleter = std::default_delete<T>
> class unique_ptr;
template <
class T,
class Deleter
> class unique_ptr<T[], Deleter>;
#include<iostream>
#include<memory>
using namespace std;
int main(){
unique_ptr<int> p0;//默认构造
//使用get函数获取保存的指针,但是要慎用很危险
cout<<"p0 own pointer : "<<p0.get()<<endl<<endl;
unique_ptr<int> p1(new int(11));
cout<<"p1 own pointer : "<<p1.get()<<endl;
cout<<"*p1 : "<<*p1<<endl<<endl;;//重载了*运算符,使其能够像指针一样
//同样对于可以使用->的对象,也可以使用p1->func(),这里就不举例了
// unique_ptr<int> error_p0 = p1;错误用法
unique_ptr<int> move_p1 = std::move(p1);
cout<<"after move p1 to move_p1 : "<<endl;
cout<<"p1 own pointer : "<<p1.get()<<endl;
cout<<"move_p1 own pointer : "<<move_p1.get()<<endl<<endl;
p1.swap(move_p1);
cout<<"after swap p1 and move_p1 : "<<endl;
cout<<"p1 own pointer : "<<p1.get()<<endl;
cout<<"move_p1 own pointer : "<<move_p1.get()<<endl<<endl;
p1.release();//释放资源
cout<<"p1 own pointer : "<<p1.get()<<endl<<endl;
//可以近似等价于 p2(new int(10))
//但是推荐使用make_unique,因为更安全,将指针创建和销毁全部交给智能指针实现
//并且在shared_ptr当中,make写法会使得程序更高效
unique_ptr<int> p2 = make_unique<int>(10);
int* pint= new int(11);//不推荐
p2.reset(pint);
cout<<"*p2 : "<<*p2<<endl<<endl;
}
运行结果:
p0 own pointer : 0
p1 own pointer : 0x30000272c0
*p1 : 11
after move p1 to move_p1 :
p1 own pointer : 0
move_p1 own pointer : 0x30000272c0
after swap p1 and move_p1 :
p1 own pointer : 0x30000272c0
move_p1 own pointer : 0
p1 own pointer : 0
*p2 : 11
如果还不太清楚,可以自己去尝试使用一下,具体使用方法就是让智能指针管理一个创建和销毁的时候都会打印值的类,然后去看类的资源是什么时候进行销毁和创建的
使用场景
主要使用在资源不需要共享的场景,比如申请后,使用一会就退出作用域释放(多线程里的锁),具体场景下一章细讲。
2.shared_ptr
shared_ptr没有拷贝控制,表明资源可以在多个共享智能指针之间共享,需要用引用计数(会增加成本),当然,有要问,那是不是shared_ptr可以替代unique_ptr了?不是,unique_ptr在有些时候开销基本和裸指针一样,但是shared_ptr开销总是大于(甚至远大于)裸指针,这是因为他需要引用计数器,且为了保证线程安全,还使用了速度较慢的原子变量。
具体内容:
//模板里面没有删除器,删除器在控制块中,后面会讲,现在先不管删除器是什么
template< class T > class shared_ptr;
#include<memory>
#include<iostream>
using namespace std;
//由于前面已经介绍了unique_ptr的用法,所以这里不会太详细,主要展示一些简单基础用法
//使用宏定义和函数简化后续展示过程的代码
#define SP shared_ptr
#define SPI SP<int>
void showSPI(const SPI& ptr,string name = ""){
if(name != ""){
cout<<name<<endl;
}
cout<<"ptr address : "<<ptr.get()<<endl;
cout<<"*ptr val : ";
if(ptr)cout<<*ptr<<endl;
else cout<<"ptr is nullptr"<<endl;
cout<<"ptr counter : "<<ptr.use_count()<<endl;
cout<<endl;
}
int main(){
cout<<"1"<<endl;
SPI s;
showSPI(s);
cout<<"before carete s1 : "<<endl;
s.reset(new int(128));
showSPI(s,"s");
SPI s1 = s;
cout<<"after create s1 :"<<endl;
showSPI(s,"s");
showSPI(s1,"s1");
{
cout<<"after create s2 : "<<endl;
SPI s2 = s;
showSPI(s,"s");
showSPI(s1,"s1");
showSPI(s2,"s2");
//离开作用域,s2销毁
}
cout<<"after destory s2 : "<<endl;
showSPI(s,"s");
showSPI(s1,"s1");
//move就不再展示了,可以自己按照这个模板进行拓展实验
//判断是否只有一个智能指针在管理这个资源
if(s.unique() == false){
cout<<"false";
}
}
1
ptr address : 0
*ptr val : ptr is nullptr
ptr counter : 0
before carete s1 :
s
ptr address : 0x3000028ec0
*ptr val : 128
ptr counter : 1
after create s1 :
s
ptr address : 0x3000028ec0
*ptr val : 128
ptr counter : 2
s1
ptr address : 0x3000028ec0
*ptr val : 128
ptr counter : 2
after create s2 :
s
ptr address : 0x3000028ec0
*ptr val : 128
ptr counter : 3
s1
ptr address : 0x3000028ec0
*ptr val : 128
ptr counter : 3
s2
ptr address : 0x3000028ec0
*ptr val : 128
ptr counter : 3
after destory s2 :
s
ptr address : 0x3000028ec0
*ptr val : 128
ptr counter : 2
s1
ptr address : 0x3000028ec0
*ptr val : 128
ptr counter : 2
false
可以看到,多个智能指针可以共享管理一个资源,然后通过引用计数防止资源提前析构,当计数为0时会释放资源(代码没展示,可以将int换为类,让类在析构时打印东西),每次拷贝构造和拷贝赋值都会将指针计数器加一,而移动不会,因为移动不会增加资源拥有者的数量,交换也同理。此外,创建最好使用make_shared< T >(args…)。
使用场景
主要使用在资源需要被共享的场景,或者资源需要被长期保存的场景,还是那句话,后面细嗦。
3.weak_ptr
其实weak_ptr我也用的很少,是最近才去细看相关用法和场景的。weak_ptr一般和共享智能指针一起使用,我们知道共享智能指针会使用引用计数来控制资源,每次拷贝都会增加计数,但是weak_ptr不会,weak_ptr只会持有指针,并“观察”指针计数,也就是说,weak_ptr持有资源不会印象资源的析构,资源析构时不考虑weak_ptr的数量,那有什么作用呢?解决shared_ptr的循环引用导致的内存泄露。
直接上代码:
#include<memory>
#include<iostream>
using namespace std;
struct node{
node(int val_ = 0):val(val_){}
shared_ptr<node> next;
int val;
~node(){
cout<<"析构 "<<val<<endl;
}
};
using pNode = shared_ptr<node>;
int main(){
{
pNode node1 = make_shared<node>();
pNode node2 = make_shared<node>();
node1->next = node2;
node2->next = node1;
pNode node3(new node(128));
cout<<"准备离开作用域"<<endl;
}
cout<<"已经离开作用域"<<endl;
}
准备离开作用域
析构 128
已经离开作用域
可以看到,node3正常析构,但是node1 、node2 没有正常析构,这是因为在准备离开作用域的时候,node1的资源被node1和node2->next持有,node2的资源被node2和node->next持有,计数均为2,node2先开始析构,计数减少1,但是不为0,资源不释放,而资源是node类型,node类型中的next指向了node1,故而node2对应资源不释放会导致指向node1的一个智能指针不会释放,所以在node1析构的时候,会发现,自己持有的资源计数减少1后计数任然不为0,最后导致两个资源都会释放,发生资源泄露。
所以,这里使用weak_ptr来替换next,使得node1->next只是在能使用node2资源,但是无法影响node2资源的管理,node2->next同理
struct node{
node(int val_ = 0):val(val_){}
weak_ptr<node> next;
int val;
~node(){
cout<<"析构 "<<val<<endl;
}
};
此外介绍一下weak_ptr的一些用法(简单介绍,和前面差不多)
#define WPI weak_ptr<int>
SPI sp;
WPI wp = sp;
wp.lock();//若wp指向的资源可以使用
//就返回有效shared_ptr,否则返回空的shared_ptr
*wp.lock();//使用wp
//*wp无效
wp.expired();//若能够资源使用,返回false,否则返回true
wp.use_count();//打印引用计数
wp.reset();//取消“观察”
其实weak_ptr和shared_ptr的使用还有很多细节,但是这里就不展示了,这个后面会讲。
四.结尾
智能指针还有很多东西没写出来(主要是我也还在学习),虽然基础用法很简单,但是在一些场景的使用会有难度,比如设计一个线程安全的oberserver(具体参照陈硕《Liunx多线程服务端编程》),后面会慢慢介绍,同时会讲一下智能智能指针的实现。下一次主要是讲智能指针的一些注意事项,会有一点难度,主要是根据effective modern c++。
讲一下这两天的学习经历吧,感觉最近基本没学什么,项目也没写,印象深一点的内容就是effective modern c++(感觉这本翻译比同系列另外两本好),内容很新,很是现代c++的学习,虽然书中会有一些让人感到陌生的语法,但是绝非作者秀技,而是确实需要。如果能读完并吃透,对c++的学习肯定有很大的帮助,书中很多技巧可以帮助你编写出安全、易扩展的代码,并且让你的代码很优雅,但是初学者可能会感觉很困难,因为他往往会在将某技巧时,提到另一个技巧,你翻过去看另外一个,又发现他又需要另外一个知识点,甚至出现循环引用。
这几天学习有点累,但是收获还是不少,主要基本养成了两天一跑的好习惯。后面会继续搓项目博客不会更太勤了。