上一章:智能指针 (3)
有了智能指针的定义,我们现在来讲讲智能指针如何使用优势以及一些问题。
1,unique_ptr
具有拥有语义的类成员变量
传统情况下,具有拥有语义类成员变量可使用:普通成员,普通指针。
普通成员变量, 需要在头文件里面包含所拥有成员的头文件,这会增加编译的复杂度。
//Test1.h
#include "Test1.h"
class Test1
{
Type1 ownedByTest1;
public:
Test1();
~Test1();
};
//Test1.cpp
#include "Test.h"
#include "Type1.h"
Test1::Test1()
{}
Test1:: ~Test1()
{}
普通指针, 只需要前置声明,在cpp文件里面包含成员的头文件即可,这样不会增加编译的复杂度,只需要增加链接即可,但是这种裸指针,必须在函数析构时显示的删除,在项目庞大,结构复杂的情况下,程序员就有可能犯错忘记删除资源,造成资源泄露。
//Test2.h
class Type2;
class Test2
{
Type2* ownedByTest2;
public:
Test2();
~Test2();
};
//Test2.cpp
#include "Test.h"
#include "Type2.h"
Test2::Test2()
: ownedByTest2(new Type2)
{}
Test2:: ~Test2()
{
delete ownedByTest2;
}
unique_ptr综合了普通成员变量和指针成员变量的优点,又没有他们的缺点。
(1) unique_ptr较普通成员,主要是通过前置声明的方式减少头文件包含。
(2) unique_ptr普通指针,可以去掉析构时显式delete成员。
// Test3.h
#include <memory>
class Type3;
class Test3
{
std::unique_ptr<Type3> ownedByTest3;
};
// Test3.cpp
Test3::Test3()
: ownedByTest3(new Type3)
{}
Test3::~Test3()
{}
函数参数传递
其意义是通过move语意,把内存的所有权转移。
对于C++11之前,auto_ptr也有这个功能,只是auto_ptr语义没有那么明确。造成很多没有理解正确的人滥用auto_ptr,造成程序不可预期的结果。
C++11之后,auto_ptr被deprecated了,取而代之的是unique_ptr,而对于unique_ptr,其语义非常明确,必须要通过std::move 进程所有权转移。
void doSomething(std::unique_ptr<Type> type)
{
// do something
}
void moveOnwerShip(std::unique_ptr<Type> type)
{
doSomething(type); // 错误, 编译无法通过
doSomething(std::move(type)); // 正确, OwnerShip转移
}
2, shared_ptr
具有关联属性的类成员变量,即有多个对象都关联此成员。
具有关联属性的类成员变量可使用:引用, 普通指针。
class Test1
{
Type1& type1_;
public:
Test1(Type1& type1);
};
Test1::Test1(Type1& type1)
:type1_(type1)
{}
class Test2
{
Type2* type2_;
public:
Test2(Type1* type2);
};
Test2::Test2(Type2* type2)
:type2_(type2)
{}
使用引用和指针,要非常注意的是在类对象的析构顺序,如果用这些引用或指针的对象被析构掉后,那么这些指针或引用就无效了,如果此时被引用关联的对象再去使用,就会产生资源错误(比如段错误)。
要解决类对象析构顺序问题,就必须用到一种技术——引用计数(比如Com指针),而C++11之后,标准库里面的shared_ptr为我们提供了这一解决方案。
(1) shared_ptr较引用,可以不用考虑引用被关联对象析构的顺序。也就是说,对于引用被关联对象,必须在引用对象析构前析构。否则引用对象一旦析构,医用就即可失效,引用使用就会引起内存错误。而shared_ptr只有在引用计数为0(即最后一份引用被销毁)时,才会去真正销毁资源。
// 只有在其他地方的share_ptr 都被销毁了,在Test析构的时候才会去销毁Type资源。
class Test
{
std::shared_ptr<Type> type_;
public:
Test(std::shared_ptr<Type> type);
};
Test::Test(std::shared_ptr<Type> type)
:type_(type)
{}
(2) shared_ptr较普通指针,普通指针存在和引用同样的问题,因此shared_ptr有着同样的优势。
函数参数传递
其传递具有引用或指针传递的优势,并且无需担心对象是否有效,因为shared_ptr所指向的对象一直都是有效的(只要初始化时是有效的),share_ptr每次赋值,只是引用计数+1,销毁时,只是引用计数-1,因此参数传递时,没有太多的额外开销。
3, 作用域问题
如果在某个作用域里面,有多个分支要手动销毁对象,那么智能指针便是最好的选择,不会因为漏写而产生内存泄漏。
比如如下代码,非常容易出错,若以后case还要增加,则情况会变得更加复杂。
void mutiStatesReturn(const State& state)
{
Type* type = new Type();
switch(state)
{
case STATE1:
handle1();
delete type;
return;
case STATE2:
handle2(type);
break;
default:
break;
}
handle3(type);
delete type;
}
如果用share_ptr, 则情况就会变得简单多,无需考虑type的资源释放问题:
void mutiStatesReturn(const State& state)
{
std::shared_ptr<Type> type(new Type());
switch(state)
{
case STATE1:
handle1();
return;
case STATE2:
handle2(type);
break;
default:
break;
}
handle3(type);
}
4, shared_ptr存在的问题
(1)首先就是环形引用问题,即A1引用A2,A2引用A3 ..... Ax引用A1,这个问题不在累赘,解决方案是用weak_ptr.
(2)正所谓成也萧何,败也萧何,shared_ptr为了解决对象销毁的顺序问题,但是也正是它可以解决这个问题,导致它被滥用。
比如有三个对象同时引用了一个shared_ptr, 那么通过代码走读,我们很容易就能知道他们之间的关系,谁先创立,谁后销毁。
但是试想一下,如果有30个对象同时引用一个shared_ptr,那么,很难搞清楚个中关系,这个对维护的人来说,简直就是shit。
在无shared_ptr时代,一般建立对象有着严格的顺序,如下方式
启动模块1
@启动子模块11
@@创建对象111
@@创建对象112
......
@启动子模块12
......
启动模块2
@启动子模块21
.......
@启动子模块22
......
层次非常分明,因为如果随意创建,并且有多对象引用同一资源,那么必然会造成资源多次释放。
而有了智能指针之后,很多人就不遵循层次化的原则, 写的人轻松,看的人头大骂娘。
因此个人观点是,即使有引用计数的智能指针,还是要有明确的先后次序,能用引用代替shared_ptr,则应该代替,shared_ptr到处存, 单例到处创建要不得(单例这个问题,以后有机会专门讲讲, 它最大的缺点就是初始化和销毁顺序不明确)。