14.C++中的智能指针


欢迎访问个人网络日志🌹🌹知行空间🌹🌹


1.背景

在C++中,动态内存的管理使用的是newdelete运算符,手动的分配和释放堆上的内存。

动态内存的使用很容易出问题,因为确保在正确的时候释放内存是极其困难的。

为更容易也更安全的使用动态内存,C++11标准库中提供了两种智能指针shared_ptrunique_ptr。智能指针与常规指针行为类似,但两者的重要区别是智能指针能自动释放他所指向的内存。两种智能指针的主要区别在于管理底层指针的方式,shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占"所指向的对象。

类似于vector等容器,标准库中提供的两种智能指针也都是模板。因此,在创建智能指针的时候,必须提供指针可以指向的类型。

#include <memory>
shared_ptr<int> p1;
unique_ptr<int> p2;

智能指针包含在头文件memory中,如上默认初始化的智能指针中保存着一个空指针。

p.get()返回p中保存的指针。要小心使用,若智能指针释放了对象,get返回的指针所指向的对象就消失了。

2.shared_ptr

shared_ptr正如其名,允许多个指针指向同一个对象;其采用引用计数的方式来管理内存的释放,当其检查到指针的引用数为0时会自动释放对象所分配的内存。

2.1创建shared_ptr并初始化

  • 使用make_shared函数

最安全的分配和使用动态内存的方式是调用make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向对象的shared_ptr

shared_ptr<int> p1 = make_shared<int>(4);
  • 结合new来使用

可以使用new返回的指针来初始化智能指针。

shared_ptr<int> p1(new int(2));
std::cout << *p1 << std::endl;

接受指针参数的智能指针构造函数是explicit的,因此,不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式。

shared_ptr<int> p1 = new int(2); // error
shared_ptr<int> p1(new int(2)); // right

不推荐混合使用普通指针和智能指针,使用make_shared能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。

看两个例子:

内存被释放两次:

int *p = new int(2);
shared_ptr<int> p1(p);
std::cout << *p1 << std::endl;
shared_ptr<int> p2(p);
std::cout << *p2 << std::endl;

// free(): double free detected in tcache 2
// Aborted (core dumped)

内存被意外释放:

#include<iosteam>
#include<memory>

using namespace std;

void log(shared_ptr<int> p)
{

} // `shared_ptr`按值传递,离开作用域,内存被销毁

int main(int argc, char **argv) {
    int *p = new int(2);
    log(shared_ptr<int>(p));
    cout << *p << endl; // p所指向的内存已被销毁,0
}

2.2不要使用‵get`初始化另一个智能指针

get返回了指向智能指针管理对象的内置指针,提供这个函数,主要是为了向不能使用智能指针的代码中传递一个内置指针。

不能delete get返回的指针。

但同样使用get返回的指针给其他智能指针赋值,有可能会导致内存的意外释放。

2.3shared_ptr计数器增减

每个shared_ptr都有一个关联的计数器,通常称为引用计数器,无论何时拷贝一个shared_ptr,或使用shared_ptr赋值时,计数器都会递增,被赋值的shared_ptr的计数器会减1,当计数器小于1时,shared_ptr中所指向的内存会被释放。如下:

#include <memory>
#include <iostream>
using namespace std;

int main(int argc, char **argv)
{
     auto p1 = make_shared<int>(1);
     auto q(p1);
     cout << p1.use_count() << endl;
     cout << q.use_count() << endl;
     auto r = make_shared<int>(2);
     auto r1 = r;
     cout << r.use_count() << endl;
     cout << r1.use_count() << endl;
     r = q;
     cout << r.use_count() << endl;
     cout << r1.use_count() << endl;
     cout << q.use_count() << endl;
     cout << p1.use_count() << endl;

     return 0;
}

// 2
// 2
// 2
// 2
// 3
// 1
// 3
// 3

2.4shared_ptr与多线程

shared_ptr的引用计数本身是安全无锁的,但对象的读写不是,shared_ptr有两个数据成员,读写操作不能原子化。shared_ptr的线程安全级别和内建类型/标准容器/string等一样,如果要从多个线程读写同一个shared_ptr对象,需要加锁。

3.unique_ptr

shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。

有个小插曲,‵C++11‵标准中没有提供类似make_shared的函数,到C++14中才给出make_unqiue函数用来创建unique_ptr类型的对象,据说是因为C++11的标准制定者忘了*_*

test.cpp:88:14: note: ‘std::make_unique’ is only available from C++14 onwards
test.cpp:88:26: error: expected primary-expression before ‘int’

unique_ptr拥有它指向的对象,unique_ptr不支持普通的赋值和复制操作。

auto q = make_unique<int>(10);
auto p = q; // error
unique_ptr<int> uq(q); // error

可以通过.release/.reset函数将指针所有权从一个unique_ptr转移给另一个unique_ptr.

auto q = make_unique<int>(10);
unique_ptr<int> p(q.release()); // 释放q的所有权并交给p,类似复制
auto q1 = make_unique<int>(20);
p.reset(q1.release()); // 释放p原来指向的内存并将q1的所有权给p,类似赋值
cout << *p << endl;
cout << *q << endl; // q已为空,error

例外:可以拷贝或赋值一个将要被销毁的unique_ptr

// 复制
unique_ptr<int> clone(int p)
{
     return unique_ptr<int>(new int(p));
}
// 局部变量
unique_ptr<int> clone(int p)
{    
     unique_ptr<int> q(new int(p));
     return q;
}

4.weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr所指向的对象。

weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

auto q = make_shared<int> (10);
cout << q.use_count() << endl;
weak_ptr<int> wq(q);
cout << q.use_count() << endl;
// 1
// 1

一旦最后一个指向对象的shared_ptr被销毁,对象的内存就会被释放,即使有weak_ptr指向对象,对象也还是会被释放,这也正是weak_ptr这种智能指针“弱”共享对象的特点。

由于对象可能不存在,不能使用weak_ptr直接访问对象,而必须调用weak_ptrlock函数。lock函数检查weak_ptr指向的对象是否存在。如果存在,lock返回一个指向共享对象的shared_ptr

auto q = make_shared<int> (10);
cout << q.use_count() << endl;
weak_ptr<int> wq(q);
if(shared_ptr<int> sp = wq.lock())
     cout << *sp << endl;
cout << q.use_count() << endl;
// 1
// 10
// 1

避免shared_ptr出现相互引用,导致对象无法析构,内存无法释放的问题

#include <iostream>
#include <memory>

// 前置声明
class A;

class B
{
     public:
     ~B()
     {
          std::cout << "B destructor\n";
     }
     std::shared_ptr<A> ptr;
};

class A
{
     public:
     ~A()
     {
          std::cout << "A destructor\n";
     }
     std::shared_ptr<B> ptr;
};

int main()
{
    { // 作用域开始
        std::shared_ptr<A> aPtr = std::make_shared<A>();
        std::shared_ptr<B> bPtr = std::make_shared<B>();

        aPtr->ptr = bPtr;
        bPtr->ptr = aPtr;
    } // 作用域结束

    std::cout << "end\n";

    return 0;
}

创建aPtr和bPtr时,各自的引用计数会变成1,接着2个赋值语句,又把各自的引用计数加了1,就都变成了2,然后离开作用域,会减1,这样各自的引用计数还保持1,这样就无法释放内存空间,就不会去执行A和B的析构函数。而weak_ptr不增加引用计数,只需要将A/B类中的shared_ptr替换成weak_ptr即可。2

参考资料

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值