智能指针之使用

上一章:智能指针 (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 
class Type3;
class Test3
{
    std::unique_ptr 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)
{
    // do something
}
void moveOnwerShip(std::unique_ptr 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_;
public:
    Test(std::shared_ptr type);
};

Test::Test(std::shared_ptr 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(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到处存, 单例到处创建要不得(单例这个问题,以后有机会专门讲讲, 它最大的缺点就是初始化和销毁顺序不明确)。

下一章: 容器 - vector (1)
目录

你可能感兴趣的:(智能指针之使用)