C++11中的智能指针unique_ptr、shared_ptr和weak_ptr详解

目录

1、引言

2、什么是智能指针?

3、在Visual Studio中查看智能指针的源码实现

4、独占式指针unique_ptr

4.1、查看unique_ptr的源码实现片段

4.2、为什么unique_ptr的拷贝构造函数和复制函数被delete了?(面试题)

4.3、使用unique_ptr独占式智能指针的实例

5、共享式指针shared_ptr 

5.1、查看shared_ptr的源码实现片段

5.2、shared_ptr的类图说明

5.3、shared_ptr循环引用问题(面试题)

5.4、使用shared_ptr的实例

6、弱指针weak_ptr

6.1、查看weak_ptr的源码实现片段

6.2、使用weak_ptr的实例

7、使用智能指针是否会影响到程序的执行效率?

8、有必要学习C++11标准中的新特性

9、最后


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具案例集锦(正在更新中...)https://blog.csdn.net/chenlycly/category_12279968.htmlC/C++基础与进阶(正在更新中...)https://blog.csdn.net/chenlycly/category_11931267.html       C++11引入了unique_ptr、shared_ptr和weak_ptr三个智能指针,给我们编写C++代码带来了很大的便利,今天我们就来详细讲讲这三个智能指针的相关内容。

1、引言

       在C++程序中,大部分异常问题都是与内存相关的,都是内存操作异常引起的。对于动态申请的堆内存,C++没有内存回收机制,动态申请的内存需要程序员自己去释放。如果不去释放,则会造成内存泄漏。内存泄漏是个很常见的问题,在日常开发过程中会时不时地遇到。发生内存泄漏的代码如果频繁执行,则会导致持续的内存泄漏,长时间运行之后就会使得程序占用的虚拟内存接近或超过进程的虚拟内存上限,就会导致Out of memory内存耗尽的异常,引发程序发生闪退或崩溃。

以32位程序为例,程序启动时系统会给程序进程分配4GB的虚拟内存空间,其中内核态和用户态内存各占一半,即用户态内存为2GB,如果用户态的代码占用的用户态虚拟内存达到或超过2GB,就会触发Out of memory内存耗尽的异常。

       C++11标准引入三个智能指针:unique_ptr、shared_ptr和weak_ptr,使用这些智能指针就能很好地解决内存泄漏的问题。

2、什么是智能指针?

       其实早在1998年发布的C++98标准中就引入了智能指针auto_ptr,不过auto_ptr有一些缺陷,现在已经被废弃了。

       C++智能指针是存储指向动态分配(堆)对象指针的类,用于C++类对象的生存期的控制,确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每引用C++对象1次,内部引用计数就加1;智能指针每析构1次,内部的引用计数就减1,当引用计数减为0时,就会删除指向的C++类对象(释放类对象的堆内存)。

        C++11标准引入三个智能指针unique_ptr、shared_ptr和weak_ptr,位于C++标准STL库中,在 <memory> 头文件中的 std 命名空间中定义的。

这个地方需要注意一下,有的朋友可能会以为STL只包含vector、list和map等容器,其实大家平常使用的字符串类string、输入输出iostream、unique_ptr等智能指针,都是STL标准模板库中的。STL模板库不仅仅包含容器和迭代器。C++标准库主要由C库、C++库和STL标准模板库构成,其中STL标准模板库在C++标准库中比重占了80%左右,即C++标准库中一大半都是STL库。

       C++11标准中主要使用unique_ptr和shared_ptr两智能指针,weak_ptr则主要用来辅助shared_ptr,避免出现循环引用问题的。在使用unique_ptr和shared_ptr两个智能指针类时,可以使用指针运算符(-> 和 *)访问指向的对象,因为智能指针类重载了->和*运算符,以返回指向的对象(指针)。

_NODISCARD add_lvalue_reference_t<_Ty> operator*() const
    {    // return reference to object
        return (*get());
    }

_NODISCARD pointer operator->() const noexcept
    {    // return pointer to class object
        return (this->_Myptr());
    }

_NODISCARD pointer get() const noexcept
    {    // return pointer to object
        return (this->_Myptr());
    }

3、在Visual Studio中查看智能指针的源码实现

        C++11引入的unique_ptr、shared_ptr和weak_ptr,可以直接在Visual Studio中查看源码实现。这三个智能指针位于STL库中,而Visual Studio采用的是P.J. STL版本。

       注意一下,要在Visual Studio中查看unique_ptr和shared_ptr实现源码,需要使用Visual Studio 2015或以上版本,低版本的Visual Studio可能不支持C++11标准或者支持了部分C++11新特性。比如Visual Studio 2010中只支持了部分C++11的新特性,比如匿名函数(lamda表达式),但不支持unique_ptr和shared_ptr智能指针(会显示未定义)。

       目前,STL主要有5个版本:

1)HP 原始版本
       HP STL 是 Alexandar Stepanov(STL 标准模板库之父)在惠普 Palo Alto 实验室工作时,与 Meng Lee 合作完成的。本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一要遵守的是,必修在文件中加上HP的版本声明和运用权限声明。 
2)P. J. 实现版本
       由 P. J. Plauger 开发,继承自 HP 版本,该版本不开源,不能公开、修改或贩卖。该版本不开源也是合法的,因为HP没要求强迫要求其衍生产品必须开源。该版本被微软 Visual C++ 采用,缺陷是,可读性比较低,符号命名也比较怪异。但我们在Visual Studio中阅读该版本的实现源码时,感觉还好,也没传说中那么难读。
3)RW 实现版本
       由 Rouge Wage 公司开发,继承自 HP 版本,被Borland公司的 C+ + Builder 采用。该版本也不是开源的,不能公开、修改或贩卖,这个版本的可读性还不错。
由 Rouge Wage 公司开发,继承自 HP 版本,被Borland公司的 C+ + Builder 采用。该版本也不是开源的,不能公开、修改或贩卖,这个版本的可读性还不错。值得一提的是,尽管 Rouge Wave STL 的性能不是很好,但 C++ Builder 对 C++ 语言标准的支持还算不错,所以在一定程度上使 Rouge Wave STL 的表现得以改善。但遗憾的是,由于 Rouge Wave STL 长期没有更新且不完全符合标准,因此 Rouge Wave STL 在 6.0 版本时改用了 STLport 版本(之后的版本也都采用了 STLport),不过考虑到和之前版本的兼容,6.0 版本中依旧保留了 Rouge Wave STL。
4)SGI 实现版本
        由 Silicon Graphics Computer Systems,Inc 公司开发,继承自 HP 版本。该版本被 Linux GCC 采用,可移植性好, 在 Linux 平台上的性能非常出色。该版本是开源的,可公开、修改甚至贩卖。 无论是符号命名,还是编程风格,这个版本的可读性非常高。如果大家要学习 STL源码,推荐大家看这个版本的源码实现。侯捷老师的经典书籍《STL源码剖析》,也是基于这个版本展开的。
5)STLport 实现版本
        为了使 SGI STL 的基本代码都适用于 VC++ 和 C++ Builder 等多种编译器,俄国人 Boris Fomitchev 建立了一个 free 项目来开发 STLport,此版本 STL 是开放源码的。由于 Rouge Wave STL 长期没有更新且不完全符合标准,因此 Rouge Wave STL 在 6.0 版本时改用了 STLport 版本,之后的版本也都采用了 STLport。

4、独占式指针unique_ptr

        unique_ptr是独占式智能指针,它拥有对其所指向对象的唯一所有权。unique_ptr 指针被销毁时,它所指向的对象也会被销毁。由于其具有独占性,所以unique_ptr 不能被拷贝,只能被转移所有权。将对象的所有权转移到新的unique_ptr对象中,原先的unique_ptr对象不再指向原来的对象。

4.1、查看unique_ptr的源码实现片段

        可以在Visual Studio中输入unique_ptr,然后go到unique_ptr的定义处查看unique_ptr的源码实现,都是基于模板实现的:(下面给出部分unique_ptr的源码)

    // CLASS TEMPLATE unique_ptr SCALAR
template<class _Ty,
    class _Dx>    // = default_delete<_Ty>
    class unique_ptr
        : public _Unique_ptr_base<_Ty, _Dx>
    {    // non-copyable pointer to an object
public:
    typedef _Unique_ptr_base<_Ty, _Dx> _Mybase;
    typedef typename _Mybase::pointer pointer;
    typedef _Ty element_type;
    typedef _Dx deleter_type;

    using _Mybase::get_deleter;

    template<class _Dx2 = _Dx,
        _Unique_ptr_enable_default_t<_Dx2> = 0>
        constexpr unique_ptr() noexcept
            : _Mybase(pointer())
        {    // default construct
        }

    template<class _Dx2 = _Dx,
        _Unique_ptr_enable_default_t<_Dx2> = 0>
        constexpr unique_ptr(nullptr_t) noexcept
            : _Mybase(pointer())
        {    // null pointer construct
        }

    unique_ptr& operator=(nullptr_t) noexcept
        {    // assign a null pointer
        reset();
        return (*this);
        }

    template<class _Dx2 = _Dx,
        _Unique_ptr_enable_default_t<_Dx2> = 0>
        explicit unique_ptr(pointer _Ptr) noexcept
            : _Mybase(_Ptr)
        {    // construct with pointer
        }
        
        // ...(余下源码省略)
    }

源码就不在此解读了,感兴趣可以去详细看看。源码中到处都是模板,需要有一定的模板编程基础才能看懂。

4.2、为什么unique_ptr的拷贝构造函数和复制函数被delete了?(面试题)

       现在C++岗位面试时,很多公司都喜欢问C++11新标准中的若干特性,所以大家在面试前需要详细看看C++11新特性。

       之前有个同事出去面试时,就被问到为什么unique_ptr的拷贝构造函数和复制函数被delete了?在unique_ptr的实现代码中可以看到:

unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;

其实很简单,因为unique_ptr是独占式的,不能进行拷贝,只能进行对象所有权的转移。拷贝构造函数与赋值函数进行的是拷贝操作,所以要将这两个函数禁用掉。

注意此处在函数后面添加“= delete”,是C++11新标准中新增的,是用来禁用对应的函数的。

4.3、使用unique_ptr独占式智能指针的实例

        使用unique_ptr智能指针的简单示例如下: 

#include <iostream>
#include <memory>

class LargeObject
{
public:
    void DoSomething(){}
};

void SmartPointerDemo()
{
    // Create the object and pass it to a smart pointer
    std::unique_ptr<LargeObject> pLarge(new LargeObject());

    //Call a method on the object
    pLarge->DoSomething();

    // Free the memory before we exit function block.
    pLarge.reset();

    // Do some other work...
}

5、共享式指针shared_ptr 

        shared_ptr是共享式智能指针,一个对象可以被多个shared_ptr共享。每个shared_ptr内部都维护一个引用计数,当引用计数为 0 时,所指向的对象会被销毁。std::shared_ptr 可以被拷贝和移动。

5.1、查看shared_ptr的源码实现片段

       可以在Visual Studio中输入shared_ptr,然后go到shared_ptr的定义处查看shared_ptr的源码实现,都是基于模板实现的:(下面给出部分shared_ptr的源码)

    // CLASS TEMPLATE shared_ptr
template<class _Ty>
    class shared_ptr
        : public _Ptr_base<_Ty>
    {    // class for reference counted resource management
private:
    using _Mybase = _Ptr_base<_Ty>;

public:
    using typename _Mybase::element_type;

#if _HAS_CXX17
    using weak_type = weak_ptr<_Ty>;
#endif /* _HAS_CXX17 */

    constexpr shared_ptr() noexcept
        {    // construct empty shared_ptr
        }

    constexpr shared_ptr(nullptr_t) noexcept
        {    // construct empty shared_ptr
        }

    template<class _Ux,
        enable_if_t<conjunction_v<conditional_t<is_array_v<_Ty>, _Can_array_delete<_Ux>, _Can_scalar_delete<_Ux>>,
            _SP_convertible<_Ux, _Ty>>, int> = 0>
        explicit shared_ptr(_Ux * _Px)
        {    // construct shared_ptr object that owns _Px
        _Setp(_Px, is_array<_Ty>{});
        }

    template<class _Ux,
        class _Dx,
        enable_if_t<conjunction_v<is_move_constructible<_Dx>,
            _Can_call_function_object<_Dx&, _Ux *&>,
            _SP_convertible<_Ux, _Ty>>, int> = 0>
        shared_ptr(_Ux * _Px, _Dx _Dt)
        {    // construct with _Px, deleter
        _Setpd(_Px, _STD move(_Dt));
        }

    template<class _Ux,
        class _Dx,
        class _Alloc,
        enable_if_t<conjunction_v<is_move_constructible<_Dx>,
            _Can_call_function_object<_Dx&, _Ux *&>,
            _SP_convertible<_Ux, _Ty>>, int> = 0>
        shared_ptr(_Ux * _Px, _Dx _Dt, _Alloc _Ax)
        {    // construct with _Px, deleter, allocator
        _Setpda(_Px, _STD move(_Dt), _Ax);
        }

    template<class _Dx,
        enable_if_t<conjunction_v<is_move_constructible<_Dx>,
            _Can_call_function_object<_Dx&, nullptr_t&>
        >, int> = 0>
        shared_ptr(nullptr_t, _Dx _Dt)
        {    // construct with nullptr, deleter
        _Setpd(nullptr, _STD move(_Dt));
        }

    template<class _Dx,
        class _Alloc,
        enable_if_t<conjunction_v<is_move_constructible<_Dx>,
            _Can_call_function_object<_Dx&, nullptr_t&>
        >, int> = 0>
        shared_ptr(nullptr_t, _Dx _Dt, _Alloc _Ax)
        {    // construct with nullptr, deleter, allocator
        _Setpda(nullptr, _STD move(_Dt), _Ax);
        }

    template<class _Ty2>
        shared_ptr(const shared_ptr<_Ty2>& _Right, element_type * _Px) noexcept
        {    // construct shared_ptr object that aliases _Right
        this->_Alias_construct_from(_Right, _Px);
        }

    shared_ptr(const shared_ptr& _Other) noexcept
        {    // construct shared_ptr object that owns same resource as _Other
        this->_Copy_construct_from(_Other);
        }
        
        // ...(余下源码省略)
}

5.2、shared_ptr的类图说明

       shared_ptr类内部实现类图如下:

shared_ptr类继承于_Ptr_base类,_Ptr_base类内部包含了指向外部对象的成员_Ptr和_Ref_count_base计数对象。根据外部调用shared_ptr的哪个构造函数,确定到底是new出_Ref_count_obj、_Ref_count或者_Ref_count_resource中哪个计数对象。类图很重要,类图可以表明各个相关类的关系,无论是写设计文档,还是学习源码,都需要使用到类图。

5.3、shared_ptr循环引用问题(面试题)

       使用shared_ptr可能会出现循环引用问题,场景是两个类中都包含了指向对方的shared_ptr对象,这样会导致new出来的两个类没有走析构,引发内存泄漏问题。

       循环引用问题的示意图如下:

相关代码如下:

#include <iostream>
#include<memory>
 
using namespace std;
 
class B;
class A{
    public:
    shared_ptr<B> bptr;
    ~A(){cout<<"~A()"<<endl;}
}
 
class B
{
    public:
    shared_ptr<A> aptr;
    ~B( ){cout<<"~B()"<<endl;}
}

 

int main() {
    shared_ptr<A> pa(new A()); // 引用加1
    shared_ptr<B> pb(new B()); // 引用加1
    pa->bptr = pb; // 引用加1
    pa->aptr = pa; // 引用加1
    return 0;
}

       执行到上述return 0这句代码时,指向A和B两个对象的引用计数都是2。当退出main函数时,先析构shared_ptr<B> pb对象,B对象的引用计数减1,B对象的引用计数还为1,所以不会delete B对象,不会进入B对象析构函数,所以B类中的shared_ptr<A> aptr成员不会析构,所以此时A对象的引用计数还是2。当析构shared_ptr<A> pa时,A的引用计数减1,A对象的引用计数变为1,所以不会析构A对象。所以上述代码会导致A和B两个new出的对象都没释放,导致内存泄漏。

       为了解决上述问题,引入了weak_ptr,可以将类中包含的shared_ptr成员换成weak_ptr,如下:

相关代码如下:

#include <iostream>
#include<nemory>
 
using namespace std;
 
class B;
class A{
    public:
    weak_ptr<B> bptr;  // 使用weak_ptr替代shared_ptr
    ~A(){cout<<"~A()"<<endl;}
}
 
class B
{
    public:
    weak_ptr<A> aptr; // 使用weak_ptr替代shared_ptr
    ~B( ){cout<<"~B()"<<endl;}
}

int main() {
    shared_ptr<A> pa(new A());
    shared_ptr<B> pb(new B());
    pa->bptr = pb;
    pa->aptr = pa;
    return 0;
}

5.4、使用shared_ptr的实例

        使用shared_ptr的实例代码如下:

#include <iostream>
#include <memory>
 
int main() 
{ 
    std::shared_ptr<int> sharedPtr1 = std::make_shared<int>(10);
    std::shared_ptr<int> sharedPtr2 = sharedPtr1;
    std::cout << *sharedPtr1 << std::endl; // 输出 10
    std::cout << *sharedPtr2 << std::endl; // 输出 10
 
    // 使用智能指针自动管理内存,不需要手动释放
    // 智能指针会自动更新引用计数
    sharedPtr1.reset();
 
    // 只有当所有引用都被释放后,内存才会被自动释放
    std::cout << "sharedPtr2 use count: " << sharedPtr2.use_count() << std::endl; // 输出 1
}

6、弱指针weak_ptr

        weak_ptr是为了配合shared_ptr而引入的一种智能指针,它不具有普通指针的行为,没有重载*和->两个操作符,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。

        weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源 (也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr 获得一个可用的shared_ptr对象,从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。

6.1、查看weak_ptr的源码实现片段

       到Visual Studio中可以看到weak_ptr的源码实现:

// CLASS TEMPLATE weak_ptr
template<class _Ty>
    class weak_ptr
        : public _Ptr_base<_Ty>
    {    // class for pointer to reference counted resource
public:
    constexpr weak_ptr() noexcept
        {    // construct empty weak_ptr object
        }

    weak_ptr(const weak_ptr& _Other) noexcept
        {    // construct weak_ptr object for resource pointed to by _Other
        this->_Weakly_construct_from(_Other);
        }

    template<class _Ty2,
        enable_if_t<_SP_pointer_compatible<_Ty2, _Ty>::value, int> = 0>
        weak_ptr(const shared_ptr<_Ty2>& _Other) noexcept
        {    // construct weak_ptr object for resource owned by _Other
        this->_Weakly_construct_from(_Other);
        }

    template<class _Ty2,
        enable_if_t<_SP_pointer_compatible<_Ty2, _Ty>::value, int> = 0>
        weak_ptr(const weak_ptr<_Ty2>& _Other) noexcept
        {    // construct weak_ptr object for resource pointed to by _Other
        this->_Weakly_construct_from(_Other.lock());
        }

    weak_ptr(weak_ptr&& _Other) noexcept
        {    // move construct from _Other
        this->_Move_construct_from(_STD move(_Other));
        }

    template<class _Ty2,
        enable_if_t<_SP_pointer_compatible<_Ty2, _Ty>::value, int> = 0>
        weak_ptr(weak_ptr<_Ty2>&& _Other) noexcept
        {    // move construct from _Other
        this->_Weakly_construct_from(_Other.lock());
        _Other.reset();
        }

    ~weak_ptr() noexcept
        {    // release resource
        this->_Decwref();
        }

    weak_ptr& operator=(const weak_ptr& _Right) noexcept
        {    // assign from _Right
        weak_ptr(_Right).swap(*this);
        return (*this);
        }

    template<class _Ty2>
        weak_ptr& operator=(const weak_ptr<_Ty2>& _Right) noexcept
        {    // assign from _Right
        weak_ptr(_Right).swap(*this);
        return (*this);
        }
        
        // ...(余下源码省略)
}

6.2、使用weak_ptr的实例

#include <iostream>  
using namespace std;  
#include <memory>  
  
class B;  
  
class A  
{  
public:  
    weak_ptr<B> ptrA_B;  // 弱类型指针
public:  
    A()  
    {  
        cout << "调用class A的构造函数" << endl;  
    }  
    ~A()  
    {  
        cout << "调用class A的析构函数" << endl;  
    }  
};  
  
class B  
{  
public:  
    weak_ptr<A> ptrB_A;  // 弱类型指针
public:  
    B()  
    {  
        cout << "调用class B的构造函数" << endl;  
    }  
    ~B()  
    {  
        cout << "调用class B的析构函数" << endl;  
    }  
};  
  
int main()  
{  
    shared_ptr<A> ptrA = make_shared<A>();
    shared_ptr<B> ptrB = make_shared<B>();  
    ptrA->ptrA_B = ptrB;  
    ptrB->ptrB_A = ptrA;  
}  

上述代码运行的输出结果是:

调用class B的构造函数
调用class A的构造函数
调用class A的析构函数
调用class B的析构函数

7、使用智能指针是否会影响到程序的执行效率?

       使用智能指针不会影响到程序的执行效率。智能指针的设计原则是在内存和性能上尽可能高效。 例如,unique_ptr中的唯一数据成员是封装的指针。 从内存占用上看,unique_ptr 与该指针的大小完全相同,不是四个字节就是八个字节。从性能上看,使用重载了 * 和 ->运算符的智能指针访问封装指针的速度不会明显慢于直接访问原始指针的速度。

8、有必要学习C++11标准中的新特性

       2011年发布的C++11新标准,给C++引入了大量的新特性,是C++发展史上一次里程碑式的更新,开启了现代C++的时代!unique_ptr、shared_ptr和weak_ptr智能指针就是在这次更新中引入的!

       为什么说我们很有必要学习C++11标准中的新特性呢?主要从两点来看。一方面,现在很多公司在招聘C++开发人员时会频繁地问到C++11的新特性另一方面,很多开源代码在频繁地使用C++11的新特性,比如很多公司都在用的开源WebRTC库,就大量地使用到了C++11及C++14中的新特性,我们要阅读这些开源代码,必须要了解C++新特性。

       C++新标准引入的新特性,使得C++变得更加灵活,但也使得C++变得更加臃肿,更加难以驾驭!C++这些新特性,能真正驾驭的人并不多,所以为了保证代码的可读性与可维护性,在一般公司的项目代码中C++新特性用的并不多,一般只会用到少部分特性,比如一些新增的关键字和匿名函数(lamda表达式)等。

9、最后

       本文详细地介绍了C++11标准中引入的智能指针unique_ptr、shared_ptr和weak_ptr基础部分的内容,希望能给大家提供的一定的借鉴和参考。后面将介绍在代码中如何有效地使用这些智能指针。

猜你喜欢

转载自blog.csdn.net/chenlycly/article/details/130918547