前面提到的两种智能指针,设计思想都是独占资源。shared_ptr的设计思想是共享资源。一个指针可以被多个shared_ptr管理。采用引用计数法控制析构函数的执行,当shared_ptr发生赋值和拷贝的时候,引用计数加一。当shared_ptr发生析构的时候,引用计数减一。这个看起来特别像jvm里面的引用计数法,实则不然。后文会对原理有详细介绍。
一、shared_ptr的初始化
int main ( )
{
shared_ptr<Test> sp1(new Test); //构造函数初始化
shared_ptr<Test> sp2; //reset初始化
sp2.reset(new Test);
shared_ptr<Test> sp3 = make_shared<Test>(); //make_shared初始化
shared_ptr<Test> sp4(sp1); //拷贝构造函数初始化
shared_ptr<Test> sp5; //赋值运算符重载初始化
sp5 = sp1;
shared_ptr<Test> sp6(move(sp1)); //移动构造函数初始化
shared_ptr<Test> sp7; //右值赋值运算符重载初始化
sp7 = move(sp2);
}
主要有三种初始化方法:
(1)构造函数初始化;
(2)对于已经构造的shared_ptr对象,可以采用reset的方法重新托管新的内存。
(3)使用make_shared方法初始化,<>中记录托管的指针类型,()中表示托管类型的构造函数参数。后面会指出,优先使用方法(3)
(4)(5)(6)(7)后文有详细解释。
二、引用计数的使用
可以使用use_count函数可以获得当前托管指针的引用计数。
int main ( )
{
shared_ptr<Test> sp1(new Test);
cout << "sp1 use_count() = " << sp1.use_count() << endl<<endl;
shared_ptr<Test> sp2(sp1);
cout << "sp1 use_count() = " << sp1.use_count() << endl;
cout << "sp2 use_count() = " << sp2.use_count() << endl<<endl;
shared_ptr<Test> sp3;
sp3 = sp1;
cout << "sp1 use_count() = " << sp1.use_count() << endl;
cout << "sp2 use_count() = " << sp2.use_count() << endl;
cout << "sp3 use_count() = " << sp3.use_count() << endl<<endl;
sp1.reset();
cout << "sp1 use_count() = " << sp1.use_count() << endl;
cout << "sp2 use_count() = " << sp2.use_count() << endl;
cout << "sp3 use_count() = " << sp3.use_count() << endl<<endl;
sp2= nullptr;
cout << "sp1 use_count() = " << sp1.use_count() << endl;
cout << "sp2 use_count() = " << sp2.use_count() << endl;
cout << "sp3 use_count() = " << sp3.use_count() << endl<<endl;
}
输出结果:
Test start...
sp1 use_count() = 1
sp1 use_count() = 2
sp2 use_count() = 2
sp1 use_count() = 3
sp2 use_count() = 3
sp3 use_count() = 3
sp1 use_count() = 0
sp2 use_count() = 2
sp3 use_count() = 2
sp1 use_count() = 0
sp2 use_count() = 0
sp3 use_count() = 1
Test end...
对于拷贝构造函数与赋值之后,引用计数加一,这个绝对没问题,但是如果是移动构造函数与右值赋值运算符重载呢?
int main ( )
{
shared_ptr<Test> sp1(new Test);
cout << "sp1 use_count() = " << sp1.use_count() << endl<<endl;
shared_ptr<Test> sp2(move(sp1));
cout << "sp1 use_count() = " << sp1.use_count() <<" ptr= "<<sp1.get()<< endl;
cout << "sp2 use_count() = " << sp2.use_count() <<" ptr= "<<sp2.get()<< endl<<endl;
shared_ptr<Test> sp3;
sp3 = move(sp2);
cout << "sp2 use_count() = " << sp2.use_count() <<" ptr= "<<sp2.get()<< endl;
cout << "sp3 use_count() = " << sp3.use_count() <<" ptr= "<<sp3.get()<< endl<<endl;
}
运行结果:
Test start...
sp1 use_count() = 1
sp1 use_count() = 0 ptr= 0
sp2 use_count() = 1 ptr= 0x1ed7a6a18e0
sp2 use_count() = 0 ptr= 0
sp3 use_count() = 1 ptr= 0x1ed7a6a18e0
Test end...
可以看出,对于移动构造函数与右值赋值运算符重载,引用计数不变,被move的智能指针放弃了对于实际指针的托管。不看源码也能猜到,是在shared_ptr自定义的移动构造函数与右值赋值运算符重载函数中做的事情。
看到这里可能会发现,漏掉了一个关键的函数:release()。实际上,在shared_ptr中没有实现release函数,为什么呢?release函数所做的事情是:转移被托管的指针。转移给谁了不确定,可能是另一个shared_ptr对象,也可能是裸指针。这就不太好控制引用计数了。
引用计数总结如下:
(1)构造shared_ptr对象时,传入一个非空指针,引用计数初始化=1;
(2)根据已有的shared_ptr对象进行拷贝构造、赋值操作的时候,引用计数+1;
(3)当shared_ptr转而管理其他指针时(一般通过reset函数),或者直接置为nullptr时,引用计数-1;
(4)根据已有shared_ptr对象(代号a)的右值进行移动构造函、右值赋值的时候,对象a托管的指针置为空指针,原来的指针的引用计数不变。
三、引用计数的实现原理
在古早版本的java语言中,采用引用计数法进行垃圾回收,基本原理和shared_ptr类似,但是不一样的地方在于,java语言中的计数是放在每个对象里面,有jvm维护一个用户不可感知的计数变量。由于C++语言历史包袱过于沉重,很难从编译器层面做出改变,shared_ptr实际上是编码层面的设计,由shared_ptr对象维护计数变量。
初学者容易产生误解,这个计数变量应该是静态变量?当然不可能是静态变量,因为一个实例化后的shared_ptr可能指向多个指针,不可能统一计数。
它的做法非常简单,shared_ptr对象维护一个被托管指针的同时,维护一个int指针,记录被托管指针被引用的次数。如果有多个shared_ptr托管这个指针的时候,他们都可以修改这个int值(因为是指针),修改之后也可以被其他对象感知到。
(1)通过有参构造函数初始化的时候,给指针变量赋初值,初始化记录引用次数的int指针。
(2)根据shared_ptr A,进行拷贝构造的时候,构造shared_ptr B,B记录的被托管指针赋值为A所记录的,同时,B的计数指针被赋值为A的计数指针,并且指针指向的int值加一。
(3)赋值运算符重载操作同上,但是注意一点,如果判断A和B托管的指针是同一个,函数立刻终止。如果不是同一个,A的托管的原指针对应的计数-1,如果减一后等于0,触发析构函数。
(4)根据shared_ptr A,进行移动构造的时候,构造shared_ptr B,B记录的被托管指针赋值为A所记录的,同时,B的计数指针被赋值为A的计数指针。之后A托管的指针置为null,A记录的引用计数指针也置为null。右值赋值同理。
(5)当shared_ptr A调用reset重置的时候,放弃原来被托管指针,原引用计数-1,如果减一后=0,触发析构函数。置为nullptr同理。
上述方案,其实是虚假的引用计数法,与java的引用计数法完全不能相提并论,有一个非常非常致命的缺陷,后面会提到。
四、自己实现shared_ptr。
template<typename Ty> //Ty是被托管的指针类型
class my_shared_ptr{
private:
Ty * ty; //被托管的指针
int * count;
void release(){
(*count)--;
if((*count)==0)
{
if(ty)
{
delete ty;
ty=NULL;
}
if(count)
{
delete count;
count=NULL;
}
}
}
public:
explicit my_shared_ptr(Ty * ty1=0):ty(ty1){
if(ty!= nullptr){
count = new int(1);
}
}
~my_shared_ptr(){ //析构函数中释放资源
(*count)--;
if((*count)==0)
{
if(ty)
{
delete ty;
ty=NULL;
}
if(count)
{
delete count;
count=NULL;
}
}
}
Ty * get(){ //返回被托管的指针
return ty;
}
void reset(Ty * ty2){ //重置被托管的指针
if(ty!=ty2){
release();
ty=ty2;
if(ty!= nullptr){
count = new int(1);
}
}
}
Ty * operator ->(){ //重载->和*运算符,让智能指针用起来像指针
return ty;
}
Ty & operator * (){
return *(ty);
}
my_shared_ptr(my_shared_ptr && ty1){ //移动构造函数
release();
ty = ty1.ty;
count = ty1.count;
ty1.ty= nullptr;
ty1.count = nullptr;
}
my_shared_ptr& operator = (my_shared_ptr && ty1 ){ //移动赋值运算符重载
release();
ty = ty1.ty;
count = ty1.count;
ty1.ty= nullptr;
ty1.count = nullptr;
return *this;
}
my_shared_ptr& operator = (my_shared_ptr & ty1 ) { //左值赋值运算符重载
release();
ty = ty1.ty;
count = ty1.count;
(*count)++;
return *this;
}
my_shared_ptr(my_shared_ptr & ty1) { //左值拷贝构造函数
ty = ty1.ty;
count = ty1.count;
(*count)++;
}
};
五、shared_ptr作为形参
前文提到了,auto_ptr作为形参是一件非常危险的事情,因为其资源转移的特点,会导致访问非法内存。shared_ptr不用考虑这个,看代码:
void my_ptr_test(shared_ptr<Test> ptr) {
cout<<"when call my_ptr_test, use_count = "<<ptr.use_count()<<endl;
}
int main ( )
{
shared_ptr<Test> test(new Test);
cout<<"before call my_ptr_test, use_count = "<<test.use_count()<<endl;
my_ptr_test(test);
cout<<"after call my_ptr_test, use_count = "<<test.use_count()<<endl;
}
运行结果:
Test start...
before call my_ptr_test, use_count = 1
when call my_ptr_test, use_count = 2
after call my_ptr_test, use_count = 1
Test end...
显然,赋值给形参以后,仍然出现了引用计数+1,但是函数执行完毕,形参销毁,引用计数-1.所以这个过程程序员完全可以不care,编译器已经帮我们处理好了。
六、shared_ptr的缺陷
(1)循环引用导致无法释放内存
class Girl;
class Boy {
public:
Boy() {
cout << "Boy Start" << endl;
}
~Boy() {
cout << "~Boy End" << endl;
}
void setGirlFriend(shared_ptr<Girl> _girlFriend) {
this->girlFriend = _girlFriend;
}
private:
shared_ptr<Girl> girlFriend;
};
class Girl {
public:
Girl() {
cout << "Girl Start" << endl;
}
~Girl() {
cout << "~Girl End" << endl;
}
void setBoyFriend(shared_ptr<Boy> _boyFriend) {
this->boyFriend = _boyFriend;
}
private:
shared_ptr<Boy> boyFriend;
};
void useTrap() {
shared_ptr<Boy> spBoy(new Boy());
shared_ptr<Girl> spGirl(new Girl());
// 陷阱用法
spBoy->setGirlFriend(spGirl);
spGirl->setBoyFriend(spBoy);
// 此时boy和girl的引用计数都是2
}
int main(void) {
useTrap();
return 0;
}
可以使用weak_ptr解决这个问题。下一篇博客有详细介绍。
(2)多次释放内存
循环引用是引用计数法的自身缺陷,所以在现在版本的jvm中已经抛弃了这种垃圾回收机制。但是多次释放内存完全是shared_ptr的设计缺陷,因此,shared_ptr使用的引用计数法是虚假的引用计数法。
int main(void) {
Test * test = new Test;
shared_ptr<Test> ptr1(test);
shared_ptr<Test> ptr2(test);
}
运行结果:
Test start...
Test end...
Test end...
进程已结束,退出代码-1073740940 (0xC0000374)
为什么程序会崩溃?因为一个指针被毫不相关的两个shared_ptr托管,它们之间没有拷贝、赋值等关系,所以会各自引用计数。ptr1析构的时候,看到自身引用计数是1,减为零,然后delete test。ptr2析构的时候,仍然看到自身引用计数是1,再次delete test。注意到第二次test也不是空指针,是一个正常的地址,但是指向的对象已经被delete了,不能再次delete,否则就会引发程序崩溃。
根本原因,shared_ptr的引用计数由智能指针管理,正常应该由堆上的对象管理。
那该怎么办呢?最好的办法,如第一章所讲,使用make_shared方法初始化智能指针对象。
int main(void) {
shared_ptr<Test> ptr1 = make_shared<Test>();
shared_ptr<Test> ptr2 = make_shared<Test>();
}
简单地说,就是不要手动管理指针,这样一来就会避免一个指针被托管给多个毫不相关的shared_ptr。