【C++11上手篇】叁、智能指针知多少

564 阅读4分钟

众所周知,C++指针是一把双刃剑,使用时存在的问题有:

  • 忘记释放或发生异常引起内存泄露
  • 重复释放同一块内存
  • 出现野指针 为了给我们提供更好的内存管理,c++11新增了以下这3种智能指针。 智能指针.png

原理

智能指针的原理比较简单,它是一个资源管理类,封装了原始指针,内部维护着一份引用计数,记录着当前有多少个类对象共享同一份内存。由于智能指针本身是栈上对象,当作用域结束时会调用析构函数,此时引用计数减1,当计数为0时,则释放原始指针指向的堆内存。

使用

shared_ptr

shared_ptr的特点是共享所有权,即同一时刻可以有多个shared_ptr指向同一块堆内存。shared_ptr内部维护着两个指针,一个原始指针指向共享对象,另一个指针指向control block,它管理着引用计数等共享数据,本身也是在堆内存中。 智能指针2.png 当存在多个shared_ptr共享一个堆内存对象时: 智能指针3.png

shared_ptr使用std::make_shared来构造对象,构造一个shared_ptr有几种方式:

int main() {
    //构造, use_count()输出引用计数
    std::shared_ptr<int> p1 = std::make_shared<int>(1);
    std::cout << p1.use_count() << std::endl; // 1
    
    //拷贝构造,引用计数+1
    std::shared_ptr<int> p2 = p1;
    std::cout << p1.use_count() << std::endl; // 2
    std::cout << p2.use_count() << std::endl; // 2

    //移动构造,p1的所有权给p3,总的引用计数不变
    auto p3 = std::move(p1);
    std::cout << p1.use_count() << std::endl; // 0
    std::cout << p3.use_count() << std::endl; // 2
}

unique_ptr

unique_ptr的特点是独占所有权,即同一时刻只有一个unique_ptr指向这块堆内存。所以unique_ptr不支持拷贝语义,只支持移动语义。


//C++11 没有提供 std::make_unique,自行实现
template<typename T, typename ...Args>
std::unique_ptr<T> make_unique( Args&& ...args ) {
  return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
}

int main() {
    //构造
    std::unique_ptr<int> p1 = make_unique<int>(1);

    // 非法,unique_ptr无拷贝构造
    // std::unique_ptr<int> p2 = p1;

    //移动构造,p1的所有权给p2
    auto p2 = std::move(p1);
}

weak_ptr

weak_ptr一般作为shared_ptr的辅助指针,当 weak_ptr 指针的指向和某一 shared_ptr 指针相同时,并不会改变shared_ptr所指堆内存空间的引用计数。 其中一个使用场景是解决shared_ptr存在的循环引用导致的内存泄露问题。 智能指针5.png 当shared_ptr指针析构释放T对象时,weak_ptr指针还可以继续观察保留下来的control block数据。 智能指针6.png

int main()
{
    std::shared_ptr<int> p1 = std::make_shared<int>(1);
    std::weak_ptr<int> p2(p1);
    std::cout << p2.use_count() << std::endl; //输出1

    // 如果weak_ptr已经过期(指针为空,或者指向的堆内存已经被释放),则lock函数会返回一个空的shared_ptr指针
    // 反之,此函数返回一个和weak_ptr指向相同的shared_ptr指针
    std::cout << *(p2.lock()) << std::endl;
    return 0;
}

性能对比

  • 内存占用:shared_ptr是原始指针大小的两倍,而unique_ptr默认情况和原始指针相同。
  • 运行速度:考虑到线程安全,shared_ptr中引用计数是原子操作,相比unique_ptr慢。

一般情况优先考虑使用unique_ptr,比如在忘记漏写delete或者出现异常安全的场景上非常适合,如果需要有多个对象同时管理同一块内存时,则使用shared_ptr。

异常安全的例子如下,如果handle方法出现异常,那么后面的delete方法将不会执行,而使用unique_ptr的方式就不需要去try-catch主动delete。

void func()
{
    Test* test = new Test();
    test->handle();
    delete test;
}

实践踩坑

智能指针虽好,但用的不好还是免不了踩坑。下面列举几个例子

额外拷贝&非法引用

class Test
{
public:
    void print()
    {
        std::cout << "call print: " << this << std::endl;
    }
    ~Test()
    {
        std::cout << "destroy: " << this << std::endl;
    }
};

std::shared_ptr<Test> GetTest()
{
    return std::make_shared<Test>();
}

int main()
{
    //方式1 额外拷贝
    auto test1 = *GetTest();
    test1.print();

    //方式2 非法引用
    auto& test2 = *GetTest();
    test2.print();

    //方式3
    auto test2 = GetTest();
    test2->print();
    // return 0;
}

代码中有三种方式接收getTest方法返回的shared_ptr对象。

方式1的输出是:

destroy: 0x7ff6c6c05a08
call print: 0x7ffee29d0c88
destroy: 0x7ffee29d0c88

日志显示destroy了两次,是不同的对象。方式1的问题在于操作符*返回的是一个引用,使用auto会调用一次拷贝构造函数生成一个新的实例,造成了额外的拷贝开销。

方式2的输出是:

destroy: 0x7fa741405a08
call print: 0x7fa741405a08

日志显示在调用print前对象已经destroy了、原因是使用auto&虽然减少了额外拷贝,但是操作符*返回的结果并没有用智能指针接收,所以函数GetTest返回后之智能指针就销毁掉了,造成了非法引用。

方式3才是正确的使用姿势。

重复释放同一块内存

//复用之前例子的Test类
static void fun2(Test* rawTest)
{
    std::shared_ptr<Test> testObj2(rawTest);
    testObj2->print();
}
static void fun1()
{
    auto testObj = std::make_shared<Test>();
    fun2(testObj.get());
}

int main(){
    fun1();
}

这里fun1新建一个shared_ptr对象,通过get()把原始指针传递给fun2,fun2又新建一个shared_ptr对象,导致出现两个独立非共享的shared_ptr,即此时存在两个control block,析构时原始指针指向的对象会被delete两次导致崩溃。 智能指针4.png

同样的case也经常出现在下面这个例子中,比如一个类的成员函数想获得指向自身的shared_ptr

class Test
{
public:
    void print()
    {
        std::cout << "call print: " << this << std::endl;
    }
    ~Test()
    {
        std::cout << "destroy: " << this << std::endl;
    }
    std::shared_ptr<Test> GetTestPtr() 
    {
        return std::shared_ptr<Test>(this);
    }
};

int main()
{
    auto testObj1 = std::make_shared<Test>();
    auto testObj2 = testObj1->GetTestPtr();
}

原因同上个例子,这里恰当的方式是继承自C++提供的std::enable_shared_from_this,调用shared_from_this返回shared_ptr

class Test: public std::enable_shared_from_this<Test>
{
public:
    void print()
    {
        std::cout << "call print: " << this << std::endl;
    }
    ~Test()
    {
        std::cout << "destroy: " << this << std::endl;
    }
    std::shared_ptr<Test> GetTestPtr() 
    {
        return shared_from_this();
    }
};

总结

C++11引入智能指针,是为了方便管理一个对象的生命期,在实际使用中,我们应该具体情况具体分析,避免不分场景只使用shared_ptr的情况。以上就是所有内容,大家有兴趣可以尝试实现一个shared_ptr智能指针。

扫码_搜索联合传播样式-白色版.png