Modern C++ 智能指针

Why?

原始指针存在缺陷,不符合现代编程语言的需要。
原始指针的缺陷:

  • 指针指向一片内存,使用者无法得知到底是指向了什么,是数组还是对象?
  • 使用完指针是否需要销毁?什么时候销毁?如何进行销毁?并没有一个统一的定论。
    • 如果一个指针指向的地址没有被释放指针就被销毁了,会内存泄漏;
    • 如果指针指向的内存已经被释放了,指针没有被销毁,会产生悬空指针;
    • 如果指针指向的内存已经被释放,在销毁该指针时,又释放了一回,会二次释放,未定义行为,程序崩溃。
    • 如果指针指向了一个数组,使用了delete而非delete[]进行释放,会产生未定义行为,后果未知。

总之,原始指针过于简陋,虽然强大,但是需要考虑的太多了,对菜鸡不友好。
智能指针(smart pointers)是解决这些问题的一种办法。智能指针包裹原始指针,它们的行为看起来像被包裹的原始指针,但避免了原始指针的很多陷阱。

What

在C++11的智能指针之前,C++98中,智能指针通过一个模板类型auto_ptr来实现。auto_ptr以对象的方式管理堆分配的内存,并在适当的时间(比如析构)​,释放所获得的堆内存。
但是auto_ptr存在一些问题,在C++17中该方式已经被废弃。

在C++11中存在四种智能指针:std::auto_ptrstd::unique_ptrstd::shared_ptrstd::weak_ptr。都是被设计用来帮助管理动态对象的生命周期,在适当的时间通过适当的方式来销毁对象,以避免出现资源泄露或者异常行为。std::auto_ptr在C++17中已经弃用,尽量不要用,除非要与C++98代码兼容。

std::unique_ptr

指针独享它所指向的对象,在指针销毁时,对象一同被释放。

#include <iostream>
#include <memory>

class Test {
    public:
        Test() :number(0) {
            std::cout << "Test()" << std::endl;
        }
        ~Test() {
            std::cout << "~Test()" << std::endl;
        }
        int getNumber() {
            return number;
        }
    private:
        int number;
};


void print(std::unique_ptr<Test> ptr) {
    std::cout << ptr.get()->getNumber() << std::endl;

}
void print1(std::unique_ptr<Test> &ptr) {
    std::cout << ptr.get()->getNumber() << std::endl;
}
int main() {

    Test t; //不会内存泄漏,局部变量离开作用域会自动调用析构函数
    Test *t1 = new Test(); //会内存泄漏,new出来的对象存储在堆上,需要手动delete
    Test *t2 = new Test();
    std::unique_ptr<Test> ptr1(t2); //不会内存泄漏,unique_ptr会自动调用析构函数
    std::unique_ptr<Test> ptr(new Test()); //不会内存泄漏,unique_ptr会自动调用析构函数
    std::unique_ptr<Test> ptr2 = std::make_unique<Test>(); //C++14引入的std::make_unique,更加安全
    std::cout << ptr1.get()->getNumber() << std::endl; //使用时需要通过get()获取指针,然后就跟正常指针一样了
    print(std::move(ptr)); //传递时需要使用std::move,因为unique_ptr是不可拷贝的
    print1(ptr1); //传递时需要使用引用,因为unique_ptr是不可拷贝的
    return 0;
}

因为只有一个智能指针指向对象,所以unique_ptr智能指针的赋值函数,拷贝构造函数,都被禁用了,构造函数添加了explicit关键字修饰,不能使用转换函数,即:不能隐式转换构造unique_ptr指针,

原因也很好理解,如果有一个原始指针ptr,如果允许进行隐式转换构造,那么就可以根据这个原始指针隐式构造出来多个unique_ptr,这就不符合一个独享的原则了。

想要将unique_ptr转移,要么使用引用,要么使用std::move进行移动。
std::unique_ptr的常见用法是作为继承层次结构中对象的工厂函数返回类型。

std::shared_ptr

共享指针,允许多个指针指向同一个对象,当最后一个shared_ptr被销毁时自动进行内存释放。std::shared_ptr通过引用计数(reference count)来确保它是否是最后一个指向某种资源的指针,引用计数关联资源并跟踪有多少std::shared_ptr指向该资源。
引用计数会对性能有一定影响:

  • std::shared_ptr大小是原始指针的两倍,因为它内部包含一个指向资源的原始指针,还包含一个指向资源的控制块的原始指针。

    • 控制块的大小和位置都是不确定的,没有一个明确的规定非要放在哪里,有可能放到堆上面。
    • 在这里插入图片描述
  • 递增递减引用计数必须是原子性的,因为多个reader、writer可能在不同的线程。比如,指向某种资源的std::shared_ptr可能在一个线程执行析构(于是递减指向的对象的引用计数),在另一个不同的线程,std::shared_ptr指向相同的对象,但是执行的却是拷贝操作(因此递增了同一个引用计数)。原子操作通常比非原子操作要慢,所以即使引用计数通常只有一个word大小,你也应该假定读写它们是存在开销的。

shared_ptr和unique_ptr都支持自定义销毁器(仿函数或者匿名函数),不同的是unique_ptr把销毁器也看作指针类型的一部分,而shared_ptr不会,因此shared_ptr有更高的灵活性:

auto loggingDel = [](Widget *pw)        //自定义删除器
                  {                     //(和条款18一样)
                      makeLogEntry(pw);
                      delete pw;
                  };

std::unique_ptr<                        //删除器类型是
    Widget, decltype(loggingDel)        //指针类型的一部分
    > upw(new Widget, loggingDel);
std::shared_ptr<Widget>                 //删除器类型不是
    spw(new Widget, loggingDel);        //指针类型的一部分

不了解仿函数和匿名函数的可以看之前的这篇文章:Modern C++:函数的发展:从函数指针到匿名函数

当创造一个新的shared_ptr指针时,我们是不知道是否有其他的shared_ptr的,自然也就不知道是否有与该对象对应的control block,因此控制块的创建有以下规则:

  • 使用make_shared时,会创建控制块,因为make_shared是在创建一个新的对象。
  • 将unique_ptr转化为shared_ptr时会创建控制块,因为unique_ptr指向的对象不需要控制块,转为shared_ptr需要新建控制块,并将原来的unique_ptr置为null
  • 使用已有shared_ptr构造新的shared_ptr时不会新建控制块。
  • 使用原始指针创建shared_ptr时,会产生未定义行为,因为我们无法从原始指针得知指向的对象是否已经有了控制块。
#include <iostream>
#include <memory>

class Test {
    public:
        Test() :number(0) {
            std::cout << "Test()" << std::endl;
        }
        ~Test() {
            std::cout << "~Test()" << std::endl;
        }
        int getNumber() {
            return number;
        }
    private:
        int number;
};

int main(){
    std::shared_ptr<Test> ptr1 = std::make_shared<Test>(); //最推荐的初始化方式
    std::shared_ptr<Test> ptr2(new Test()); //也不是不可以
    std::shared_ptr<Test> ptr3 = ptr1; //shared_ptr可以拷贝
    Test *t = new Test();
    std::shared_ptr<Test> ptr4(t); //shared_ptr可以接受裸指针,但是会导致未定义行为,不推荐
    std::unique_ptr<Test> ptr5 = std::make_unique<Test>();
    std::shared_ptr<Test> ptr6 = std::move(ptr5); //unique_ptr可以转换为shared_ptr,原来的unique_ptr会变成nullptr
    std::cout << "Unique ptr has been moved:" << ptr5.get() << std::endl;
    std::cout << ptr1.get()->getNumber() << std::endl;

}

std::weak_ptr

在C++11标准中,除了unique_ptr和shared_ptr,智能指针还包括了weak_ptr这个类模板。weak_ptr的使用更为复杂一点,它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存。而使用weak_ptr成员lock,则可返回其指向内存的一个shared_ptr对象,且在所指对象内存已经无效时,返回指针空值(nullptr,请参见7.1节)​。这在验证share_ptr智能指针的有效性上会很有作用.

  • std::weak_ptr的潜在使用场景包括:缓存、观察者列表、打破std::shared_ptr环状结构。
    • shared_ptr环状结构:即存在两个对象,互相持有对方的智能指针,如果使用shared_ptr,会因为相互持有,导致无法释放,内存泄漏。
#include <iostream>
#include <memory>

class Test {
    public:
        Test() :number(0) {
            std::cout << "Test()" << std::endl;
        }
        ~Test() {
            std::cout << "~Test()" << std::endl;
        }
        int getNumber() {
            return number;
        }
    private:
        int number;
};

int main(){
    std::shared_ptr<Test> ptr1 = std::make_shared<Test>();
    std::weak_ptr<Test> ptr2 = ptr1; //weak_ptr是shared_ptr的弱引用,不会增加引用计数
    if(!ptr2.expired()) { //判断weak_ptr是否指向了有效对象
        std::shared_ptr<Test> ptr3 = ptr2.lock(); //通过weak_ptr获取shared_ptr
        std::cout << ptr3.get()->getNumber() << std::endl;
    }else{
        std::cout << "ptr2 is expired" << std::endl;
    }
    return 0;
}

但是在考虑多线程时,有可能判断过weak_ptr的有效性后在另一个线程中对指向的对象进行了析构,所以更常见的做法是:

int main(){
int main(){
    std::shared_ptr<Test> ptr1 = std::make_shared<Test>();
    std::weak_ptr<Test> ptr2 = ptr1; //weak_ptr是shared_ptr的弱引用,不会增加引用计数
    std::shared_ptr<Test> ptr3 = ptr2.lock(); //通过weak_ptr获取shared_ptr
    if(ptr3) { //判断weak_ptr是否指向了有效对象
        std::cout << ptr3.get()->getNumber() << std::endl;
    }else{
        std::cout << "ptr2 is expired" << std::endl;
    }

    ptr1.reset(); //释放shared_ptr
    ptr3.reset();

    //或者
    try
    { 
        std::shared_ptr<Test> ptr4(ptr2); //如果已经析构,会抛出bad_weak_ptr异常
    }
    catch(const std::exception& e)
    {
        std::cerr << e.what() << '\n';
    }
    
    return 0;
}
}

使用建议

  • 如果不确定要使用哪种智能指针好,优先选用unique_ptr,性能和原始指针差不多,且从unique_ptr升级成shared_ptr很容易,反之不行。
  • 创建智能指针时,尽量使用make_xxx,如果不行再使用new,实在不行最后才选择使用原始指针,但是shared_ptr不要使用原始指针,会导致未定义行为。

参考文献:
《Effective Modern C++》
《深入理解C++11:C++11新特性解析与应用》

  • 27
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值