彻底搞懂之C++智能指针

2022-09-09 15:32:48 浏览数 (1)

前言

在现代 c 编程中,标准库包含 智能指针,这些指针用于帮助确保程序不会出现内存和资源泄漏,并具有异常安全。

标准库智能指针分类

auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c 11支持,并且第一个已经被c 11弃用。所以我只说后3个。

  1. shared_ptr 采用引用计数的智能指针。 如果你想要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时),请使用该指针。 直至所有 shared_ptr 所有者超出了范围或放弃所有权,才会删除原始指针。 大小为两个指针;一个用于对象,另一个用于包含引用计数的共享控制块。 头文件:<memory>。 有关详细信息,请参阅 如何:创建和使用 Shared_ptr 实例 和 shared_ptr 类。
  2. unique_ptr 只允许基础指针的一个所有者。 除非你确信需要 shared_ptr,否则请将该指针用作 POCO 的默认选项。 可以移到新所有者,但不会复制或共享。 替换已弃用的 auto_ptr。 与 boost::scoped_ptr 比较。 unique_ptr 很小且高效;大小是一个指针,它支持用于从 c 标准库集合快速插入和检索的右值引用。 头文件:<memory>。 有关详细信息,请参阅 如何:创建和使用 Unique_ptr 实例 和 unique_ptr 类。
  3. weak_ptr 结合 shared_ptr 使用的特例智能指针。 weak_ptr 提供对一个或多个 shared_ptr 实例拥有的对象的访问,但不参与引用计数。 如果你想要观察某个对象但不需要其保持活动状态,请使用该实例。 在某些情况下,需要断开 shared_ptr 实例间的循环引用。 头文件:<memory>。 有关详细信息,请参阅 如何:创建和使用 Weak_ptr 实例 和 weak_ptr 类。

shared_ptr

shared_ptr 类型是 C 标准库中的一个智能指针,是为多个所有者可能必须管理对象在内存中的生命周期的方案设计的。 在您初始化一个 shared_ptr 之后,您可复制它,按值将其传入函数参数,然后将其分配给其他 shared_ptr 实例。 所有实例均指向同一个对象,并共享对一个“控制块”(每当新的 shared_ptr 添加、超出范围或重置时增加和减少引用计数)的访问权限。 当引用计数达到零时,控制块将删除内存资源和自身。

下图显示了指向一个内存位置的几个 shared_ptr 实例。

 原始用法:

代码语言:javascript复制
Object * obj = new ChildObject(9);//从heap分配原始父对象,必须手动触发析构, 但子对象不会释放
testObject(*obj);
printf("release9 %p n", obj);
delete obj;

当testObject()出现异常时,delete将不被执行,因此将导致内存泄露。

如何避免这种问题?有人会说,这还不简单,直接在throw exception(); 在catch中加上delete ps;不就行了。问题是很多人都会忘记在适当的地方加上delete语句,如果你要对一个庞大的工程进行review,看是否有这种潜在的内存泄露问题,那就是一场灾难! 这时我们会想:如果指向heap的内存也能像stack变量一样用完时被自动释放,那该有多好啊。

这正是 auto_ptr、unique_ptr和shared_ptr这几个智能指针背后的设计思想。我简单的总结下就是:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。

使用shared_ptr:

代码语言:javascript复制
{
    std::shared_ptr<Object> sObj = std::make_shared<ChildObject>(1);
    testObject(*sObj); //调用父对象
    //自动回收 
}

很简单对吧~

unique_ptr

unique_ptr不共享指针。 它不能复制到另一个 unique_ptr函数,由值传递给函数,或在任何需要复制副本的 C 标准库算法中使用。 只能移动 unique_ptr。 这意味着,内存资源所有权将转移到另一 unique_ptr,并且原始 unique_ptr 不再拥有此资源。 我们建议你将对象限制为由一个所有者所有,因为多个所有权会使程序逻辑变得复杂。 因此,当需要纯 C 对象的智能指针时,请使用make_unique帮助程序函数。

下图演示了两个 unique_ptr 实例之间的所有权转换。

unique_ptr 在 C 标准库的标头中 <memory> 定义。 它与原始指针一样高效,可在 C 标准库容器中使用。 将实例添加到 unique_ptr C 标准库容器是有效的,因为移动构造函数 unique_ptr 无需复制操作。

unique_ptr 是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:

1、拥有它指向的对象

2、无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作

3、保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象

用法:

代码语言:javascript复制
std::unique_ptr<int>p1(new int(5));
std::unique_ptr<int>p2=p1;// 编译会出错
std::unique_ptr<int>p3=std::move(p1);// 转移所有权,那块内存归p3所有, p1成为无效的针.
p3.reset();//释放内存.
p1.reset();//无效

share_ptr和unique_ptr的例子:

代码语言:javascript复制
#include <iostream>
#include <string>
using namespace std;

namespace Test
{
    #define formatBool(b) ((b) ? "true" : "false")
    class Object
    {
    protected:
        int id;

    public:
        using pointer = std::shared_ptr<Object>;
        virtual std::string version() const {
            return "1.0.0";
        }; 
        Object(int _id):id(_id){
            cout << "nnew parent Object id:" << id  << endl;
        };
        virtual ~Object(){//释放时,首先是派生,然后是基类。必须将基类析构函数设为虚基类, 防止delete 子对象时不会调用父析构函数,导致内存泄露
            delete parent_str_ptr;
            cout << "delete parent Object id:" << id  << endl;
        };
        virtual std::string debug() const
        {
            auto str = std::string( "debug Object id:"   std::to_string(id) );
            return str;
        }

    private:
        std::string *parent_str_ptr = new std::string("parent_str_ptr memory leak");                                          
    };
    class ChildObject : public Object
    {
    public:
        ChildObject(int _id):Object(_id)
        {
            std::cout << "new ChildObject id:" << (id) << "n";
        }

        ~ChildObject()
        {
            delete str_ptr;
            std::cout << "delete ChildObject id:" << id << "n";
        }
        virtual std::string version() const {
            return "2.0.0";
        }; 
    private:
        std::string *str_ptr = new std::string("memory leak");  

    };

    void testObject(const Object &obj)
    {
        std::cout << obj.debug() << " version:"<< obj.version() << "n";
    }

    void testCase()
    {
        {
            std::shared_ptr<Object> sObj = std::make_shared<ChildObject>(1);
            testObject(*sObj); //调用父对象
            //自动回收 
        }

        {
            std::unique_ptr<Object> obj = std::make_unique<ChildObject>(2);
            testObject(*obj);
            auto obj2 = std::move(obj);//转移所有权到obj2

            printf("obj:%s obj2:%s n", formatBool(!!obj), formatBool(!!obj2));

            testObject(*obj2);//调用父对象

            obj2.release();//手动释放后, obj, obj2指向的对象已经被回收, 不会触发自动回收
            printf("obj2.release, obj:%s obj2:%s n", formatBool(!!obj), formatBool(!!obj2));
        }

        {
            std::unique_ptr<ChildObject> obj = std::make_unique<ChildObject>(3);// 使用make_unique
            testObject(*obj);
            printf("release3 %s n", formatBool(!!obj));
        }
        {
            std::unique_ptr<ChildObject> obj(new ChildObject(4));//使用new
            testObject(*obj);
            printf("release4 %s n", formatBool(!!obj));
        }
        {
            // std::unique_ptr<ChildObject> obj(ChildObject(5));//使用stack对象,这是错误的用法, error: no matching constructor for initialization of 'std::unique_ptr<Object>'
            // printf("release5 %d n", !!obj);
        }
        {
            std::unique_ptr<Object> obj = std::make_unique<ChildObject>(6);//用父对象, 会触发析构
            testObject(*obj);
            printf("release6 %s n", formatBool(!!obj));
        }
        {
            ChildObject obj = ChildObject(7);//从stack分配原始对象, 会触发析构
            testObject(obj);
            printf("release7 %p n", &obj);
        }
        {
            ChildObject * obj = new ChildObject(8);//从heap分配原始对象, 必须手动触发析构
            testObject(*obj);
            printf("release8 %p n", obj);
            delete obj;
        }
        {
            Object * obj = new ChildObject(9);//从heap分配原始父对象,必须手动触发析构
            testObject(*obj);
            printf("release9 %p n", obj);
            delete obj;
        }
        {
            Object * obj = new Object(10);//从heap分配原始父对象,必须手动触发析构
            testObject(*obj);
            printf("release10 %p n", obj);
            delete obj;
        }
        {
            std::shared_ptr<Object> obj = std::make_unique<ChildObject>(11);//指向父对象, 会释放子对象
            testObject(*obj);
            printf("release11 %s n", formatBool(!!obj));
        }
        // {
        //     std::unique_ptr<Object> obj = std::make_shared<ChildObject>(11);//error: no viable conversion from 'shared_ptr<Test::ChildObject>' to 'std::unique_ptr<Object>'
        //     testObject(*obj);
        //     printf("release11 %s n", formatBool(!!obj));
        // }
    }
}

int main(int argc, char **argv)
{
    Test::testCase();

    return EXIT_SUCCESS;
}
代码语言:javascript复制
#  c   -std=c  14 -o a share_ptr.cpp; ./a
new parent Object id:1
new ChildObject id:1
debug Object id:1 version:2.0.0
delete ChildObject id:1
delete parent Object id:1

new parent Object id:2
new ChildObject id:2
debug Object id:2 version:2.0.0
obj:false obj2:true 
debug Object id:2 version:2.0.0
obj2.release, obj:false obj2:false 

new parent Object id:3
new ChildObject id:3
debug Object id:3 version:2.0.0
release3 true 
delete ChildObject id:3
delete parent Object id:3

new parent Object id:4
new ChildObject id:4
debug Object id:4 version:2.0.0
release4 true 
delete ChildObject id:4
delete parent Object id:4

new parent Object id:6
new ChildObject id:6
debug Object id:6 version:2.0.0
release6 true 
delete ChildObject id:6
delete parent Object id:6

new parent Object id:7
new ChildObject id:7
debug Object id:7 version:2.0.0
release7 0x7ff7bfcf3488 
delete ChildObject id:7
delete parent Object id:7

new parent Object id:8
new ChildObject id:8
debug Object id:8 version:2.0.0
release8 0x7fcaef705ba0 
delete ChildObject id:8
delete parent Object id:8

new parent Object id:9
new ChildObject id:9
debug Object id:9 version:2.0.0
release9 0x7fcaef705ba0 
delete ChildObject id:9
delete parent Object id:9

new parent Object id:10
debug Object id:10 version:1.0.0
release10 0x7fcaef705ba0 
delete parent Object id:10

new parent Object id:11
new ChildObject id:11
debug Object id:11 version:2.0.0
release11 true 
delete ChildObject id:11
delete parent Object id:11

weak_ptr

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

最佳设计是避免在任何时候都能实现指针的共享所有权。 但是,如果您必须有实例的 shared_ptr 共享所有权,请避免它们之间存在循环引用。 如果无法避免循环引用,或者出于某种原因更可取,则使用 weak_ptr 向一个或多个所有者提供对另 shared_ptr 一个的弱引用。 通过使用 weak_ptr ,可以创建一个 shared_ptr 联接到一组现有相关实例的,但前提是基础内存资源仍有效。 weak_ptr本身并不参与引用计数,因此它无法阻止引用计数转到零。 但是,你可以使用 weak_ptr 来尝试获取用于初始化的的新副本 shared_ptr 。 如果已删除内存,则的 bool 运算符将 weak_ptr 返回 false 。 如果内存仍有效,新的共享指针会递增引用计数,并保证只要 shared_ptr 变量保持在范围内,内存就有效。weak_ptr是弱智能指针对象,它不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的智能指针。将一个weak_ptr绑定到一个shared_ptr对象,不会改变shared_ptr的引用计数。一旦最后一个所指向对象的shared_ptr被销毁,所指向的对象就会被释放,即使此时有weak_ptr指向该对象,所指向的对象依然被释放。

例子:

代码语言:javascript复制
#include <iostream>
#include <memory>

class A;

class B
{
public:
    ~B()
    {
        std::cout << "B destory, a_ptr use_count:" << a_ptr.use_count() << "n";
    }

    //    std::shared_ptr<A> a_ptr; //它会造成循环引用
    std::weak_ptr<A> a_ptr;//它不会循环引用
};

class A
{
public:
    ~A()
    {
        std::cout << "A destory, b_ptr use_count:" << b_ptr.use_count() << "n";
    }

    // std::shared_ptr<B> b_ptr;//它会造成循环引用
    std::weak_ptr<B> b_ptr;//它不会循环引用
};

int main()
{
    std::shared_ptr<A> a(new A());
    std::shared_ptr<B> b(new B());
    a->b_ptr = b;
    b->a_ptr = a;

    std::cout << "A:" << a.use_count() << "n";
    std::cout << "B:" << b.use_count() << "n";
}
// * 运行结果:
// A:2
// B:2

如何选择智能指针

(1)如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:

  • 有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;
  • 两个对象包含都指向第三个对象的指针;
  • STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。

(2)如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器在那个,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())。例如,可在程序中使用类似于下面的代码段。

  (3) 基于性能考虑:

1、unique_ptr独占对象的所有权,由于没有引用计数,因此性能较好

2、shared_ptr共享对象的所有权,但性能略差

3、weak_ptr配合shared_ptr,解决循环引用的问题

       由于性能问题,那么可以粗暴的理解:优先使用unique_ptr。但由于unique_ptr不能进行复制,因此部分场景下不能使用的。

智能指针的错误用法

1、使用智能指针托管的对象,尽量不要在再使用原生指针

很多开发同学(包括我在内)在最开始使用智能指针的时候,对同一个对象会混用智能指针和原生指针,导致程序异常。

代码语言:javascript复制
void incorrect_smart_pointer1()
{
    A *a= new A();
    std::unique_ptr<A> unique_ptr_a(a);

    // 此处将导致对象的二次释放
    delete a;
}

2、不要把一个原生指针交给多个智能指针管理

如果将一个原生指针交个多个智能指针,这些智能指针释放对象时会产生对象的多次销毁

代码语言:javascript复制
void incorrect_smart_pointer2()
{
    A *a= new A();
    std::unique_ptr<A> unique_ptr_a1(a);
    std::unique_ptr<A> unique_ptr_a2(a);// 此处将导致对象的二次释放
}

3、尽量不要使用 get()获取原生指针

代码语言:javascript复制
void incorrect_smart_pointer3()
{
    std::shared_ptr<A> shared_ptr_a1 = std::make_shared<A>();

    A *a= shared_ptr_a1.get();

    std::shared_ptr<A> shared_ptr_a2(a);// 此处将导致对象的二次释放

    delete a;// 此处也将导致对象的二次释放
}

4、不要将 this 指针直接托管智能指针

代码语言:javascript复制
class E
{
    void use_this()
    {
        //错误方式,用this指针重新构造shared_ptr,将导致二次释放当前对象
        std::shared_ptr<E> this_shared_ptr1(this);
    }
};

std::shared_ptr<E> e = std::make_shared<E>();

5、智能指针只能管理堆对象,不能管理栈上对象

栈上对象本身在出栈时就会被自动销毁,如果将其指针交给智能指针,会造成对象的二次销毁

代码语言:javascript复制
void incorrect_smart_pointer5()
{
    int int_num = 3;
    std::unique_ptr<int> int_unique_ptr(&int_num);
}

如何优化

  1. 内存占用高 shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。 因此相比于 unique_ptr, shared_ptr 的内存占用更高
  2. 原子操作性能低 考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。
  3. 使用移动优化性能 shared_ptr 在性能上固然是低于 unique_ptr。而通常情况,我们也可以尽量避免 shared_ptr 复制。 如果,一个 shared_ptr 需要将所有权共享给另外一个新的 shared_ptr,而我们确定在之后的代码中都不再使用这个 shared_ptr,那么这是一个非常鲜明的移动语义。 对于此种场景,我们尽量使用 std::move,将 shared_ptr 转移给新的对象。因为移动不用增加引用计数,性能比复制更好。

汇总 

智能指针能更安全的回收内存,它能防止:

  1. 忘记delete造成的内存泄露

  2. delete了,又被访问到了,比如并发时,导致“野指针”的危险情况

  3. delete了,又被delete了,导致重复回收,导致报错中断程序

总的来说,一般推荐用智能指针,性能要求很高性,可以用裸指针,但要十分小心。

参考

https://docs.microsoft.com/zh-cn/cpp/cpp/smart-pointers-modern-cpp?view=msvc-170

https://www.zhihu.com/question/319277442/answer/2384378560

https://www.cyhone.com/articles/right-way-to-use-cpp-smart-pointer/

https://juejin.cn/post/6844904198962675719

0 人点赞