CPP11特性之资源管理指针

智能指针在c++03的时候就被引入了,就是auto_ptr,但是因为原始指针所有权问题,auto_ptr被废弃掉了,取而代之的是c++11引入的智能指针——shared_ptr / weak_ptr / unique_ptr,在c++11新特性中,智能指针应该是我使用频率最高的内容,合理的使用智能指针,可以有效的帮我们管理好对象的生命周期,不再被内存泄漏的问题所困扰。

简介

需要引用头文件memory

shared_ptr

他是一个强指针,只要任何一个shared_ptr没有释放,那么原始指针就不会被释放,即每个shared_ptr都会影响着原始指针的生命周期

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct Item
{
int val = 1234;

Item()
{
std::cout << "Item constructor" << std::endl;
}

~Item()
{
std::cout << "Item destructor" << std::endl;
}
};

int main()
{
std::shared_ptr<Item> item1(new Item()); // 用构造函数初始化一个智能指针
std::shared_ptr<Item> item2 = std::make_shared<Item>(); // 用make_shared初始化一个智能指针
std::shared_ptr<Item> item3(item1); // 用拷贝构造函数初始化一个智能指针
std::shared_ptr<Item> item4 = item1; // 用复制函数初始化一个智能指针
std::cout << item1.use_count() << std::endl;
std::cout << item2.use_count() << std::endl;
return 0;
}

// 输出
Item constructor
Item constructor
3
1
Item destructor
Item destructor

上面的程序中,1,3,4共用一个原始指针,所以一共构造了两个item对象,此时item1的引用计数为3

shared_ptr应该是三个智能指针中使用最广泛的一个了,但是shared_ptr有一个非常致命的问题,那就是在循环引用的场景,是无法进行释放的,这种情况下最好的方式就是使用一个weak_ptr来打破循环引用

  • 循环引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    #include <iostream>
    #include <memory>
    using namespace std;


    class A;
    class B;

    class A {
    public:
    std::shared_ptr<B> bptr;
    ~A() {
    cout << "A is deleted" << endl; // 析构函数后,才去释放成员变量
    }
    };

    class B {
    public:
    std::shared_ptr<A> aptr;
    ~B() {
    cout << "B is deleted" << endl; // 析构函数后,才去释放成员变量
    }
    };

    int main()
    {
    std::shared_ptr<A> pa;

    {
    std::shared_ptr<A> ap(new A);
    std::shared_ptr<B> bp(new B);
    ap->bptr = bp;
    bp->aptr = ap;
    }
    return 0;
    }

    这种情况下,他们的引用计数都为2

    在作用域内ap和bp的引用计数都为2,但是当它们退出循环的时候,ap的引用计数减1,bp的引用计数也减1,但它们依旧不为0,引用计数均为1。
    对ap来说:只有调用了A的析构函数,才会去释放它的成员变量bptr。何时会调用A的析构函数呢?就是ap的引用计数为0
    对于bp来说,只有调用了B的析构函数,才会去释放它的成员变量aptr。同样是bp的引用计数都为0的时候才能析构。

    现在,对于ap和bp来说,它们都拿着对方的share_ptr(有点类似于死锁的现象),没法使得ab和bp的引用计数为0。那么A和B的对象均无法析构。于是造成了内存泄漏。

    ap和bp退出作用域了,为什么不会调用析构函数呢?
    ap和bp是创建在栈上的,而不是A或者B对象的本身,ap、bp退出作用域,只是ap和bp本身释放了,只会使得,A、B对象的引用计数-1,调用析构函数,是要A或B的对象,的引用计数为0才能执行析构函数。

weak_ptr

weak_ptr顾名思义,就是一个弱引用,更像是一个观察者,和shared_ptr最大的区别就是,weak_ptr是不会增加引用计数的, 也就是说weak_ptr不会影响原始对象的生命周期。

1
2
3
4
5
6
7
int main()
{
std::shared_ptr<Item> item1(new Item());
std::weak_ptr<Item> item2 = item1;
std::cout << item1.use_count() << std::endl; // 输出1
return 0;
}

我想应该很多人都会认为weak_ptr存在的意义只是为了解决shared_ptr循环引用不释放的问题,我之前也一直这么认为,但是当你真正用到的时候,才发现weak_ptr还有一些更有用的作用。

我们考虑这样一个场景,我们需要异步执行某些工作,这个时候一般需要把任务需要的一些资源封装起来起来放到线程池里,但是有的时候这个任务我们并不一定是执行的,比如任务在线程池的队列里超过一定时间还没有执行,这个任务就没有意义了,并且对应的资源需要及时释放,这个时候我们希望的是这个任务不再被执行了,如果任务封装的时候持有的是shared_ptr,那么这个时候我们是无法释放资源的,如果这个时候我们封装的是一个weak_ptr,那么问题就解决了。但是weak_ptr有一个问题就是在使用过程中资源有可能被释放,所以我们在使用weak_ptr的时候,需要将weak_ptr转成一个shared_ptr。

weak_ptr虽然不会增加引用计数,但是它也是和shared_ptr共享同一个引用计数的,所以weak_ptr是可以直接转为shared_ptr的,同样,shared_ptr也是可以很方便的转换为一个weak_ptr的。如下代码所示。如果这个时候资源已经释放了,那么lock()返回的shared_ptr就会是未初始化的状态了。

1
2
3
4
5
6
7
8
void fun(std::weak_ptr<Item> item)
{
const std::shared_ptr<Item>& lock = item.lock();
if (lock)
{
std::cout << lock->val << std::endl;
}
}

unique_ptr

unique_ptr和shared_ptr的一个最大的区别就是,每个unique_ptr对原始指针是独享的,通过禁用拷贝语义来保证无法被“share”,但是可以通过移动语义来转交所有权。转交所有权之后,原来的unique_ptr就变为空指针了。

1
2
3
4
5
6
int main()
{
std::unique_ptr<Item> item1(new Item());
std::unique_ptr<Item> item2 = std::move(item1);
return 0;
}