【C++进阶】智能指针

1. 智能指针介绍

C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理,程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露、二次释放、程序发生异常时内存泄露等问题。

智能指针是一种抽象的数据类型,使用它我们可以以一种普通的指针的形式将其用作诸如文件处理,网络套接字等的内存管理,还可以执行自动销毁,引用计数等许多功能。智能指针和普通指针的区别在于智能指针实际上是对普通指针加了一层封装机制,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。

相比于普通指针,智能指针有如下优点:

  • 自动释放内存

  • 防止内存泄漏

  • 防止空悬指针

    空悬指针(Dangling Pointer)是指一个指针的指向对象已被删除,那么就成了空悬指针,使用空悬指针是一种非常危险的行为,因为它可能会导致程序崩溃或者产生不可预知的结果

  • 可以进行引用计数

C++里面的四个智能指针:auto_ptr、unique_ptr、shared_ptr、weak_ptr,其中第一个已经被C++11弃用,后三个是C++11支持,unique_ptr是C++11新增加的一个类模板,它提供了独占式拥有权语义;shared_ptr则提供了共享式拥有权语义;weak_ptr则是为了配合shared_ptr而引入的一种弱化的智能指针。

2. shared_ptr

共享的智能指针,用于管理动态分配的内存。它是一个类模板,可以将其视为一个包装器,它将指向动态分配内存的原始指针封装在一起,并提供了自动内存管理。与其他智能指针不同,shared_ptr使用引用计数技术来跟踪有多少个对象共享相同的指针。当最后一个对象离开作用域时,引用计数将减少并且如果没有其他对象共享相同的指针,则释放内存。

简单来说,shared_ptr包含了两个部分:

  • 一个指向堆上创建的对象的裸指针:raw_ptr
  • 一个指向内部隐藏的、共享的管理对象:share_count_object

它的特点:

  • 它们允许多个智能指针共享同一个对象
  • 它们使用引用计数来跟踪有多少个智能指针共享同一个对象
  • 当最后一个智能指针离开作用域时,它会自动删除所管理的对象
  • 它们可以与STL容器一起使用

常用的函数:

  • reset():释放所拥有的对象并接管新对象
  • use_count():返回与此共享所有权的智能指针数量,也就是当前这个堆上对象被多少对象引用了,简单来说就是引用计数
  • get():返回原始指针
  • operator*():解引用原始指针
  • operator->():访问原始指针成员

① 初始化

通过构造函数、std::shared_ptr辅助函数和reset方法来初始化shared_ptr

#include <iostream>
#include <memory>

int main()
{
    
    
    std::shared_ptr<int> p1(new int(1));
    std::shared_ptr<int> p2 = p1;
    std::shared_ptr<int> p3;
    p3.reset(new int(1)); // reset括号内有值表示接管新对象,也就是给指针赋值,若无值,则表示释放该指针所拥有的对象

    std::cout << p1.use_count() << std::endl;
    std::cout << p3.use_count() << std::endl;

    return 0;
}

stdout:

2
1

虽然通过如上类的方式可以构造shared_ptr,但这样显然不够高效,这里更推荐使用make_shared

auto p = std::make_shared<int>(100);
// 相当于
std::shared_ptr<int> p(new int(100));

切记不能将一个原始指针直接赋值给一个智能指针,它们类型并不一样,只能通过构造函数和辅助方法来初始化

对于一个未初始化的智能指针,可以通过reset方法来初始化,当智能指针有值的时候调用reset会引起引用计数减1

另外智能指针可以通过重载的bool类型操作符来判断是否为空

#include <iostream>
#include <memory>

int main()
{
    
    
    std::shared_ptr<int> p1(new int(1));
    std::shared_ptr<int> p2 = p1;
    std::cout << "origin" << std::endl;
    std::cout << "p1 use count: " << p1.use_count() << std::endl;
    std::cout << "p2 use count: " << p2.use_count() << std::endl;

    p1.reset();
    std::cout << "\np1 reset" << std::endl;
    std::cout << "p1 use count: " << p1.use_count() << std::endl;
    std::cout << "p2 use count: " << p2.use_count() << std::endl;
    if (!p1)
        std::cout << "p1 is empty" << std::endl;
    if (!p2)
        std::cout << "p2 is empty" << std::endl;

    p2.reset();
    std::cout << "\np2 reset" << std::endl;
    std::cout << "p1 use count: " << p1.use_count() << std::endl;
    std::cout << "p2 use count: " << p2.use_count() << std::endl;
    if (!p1)
        std::cout << "p1 is empty" << std::endl;
    if (!p2)
        std::cout << "p2 is empty" << std::endl;

    return 0;
}

② 获取原始指针

当需要获取原始指针时,可以通过get()方法来返回原始指针:

#include <iostream>
#include <memory>

int main()
{
    
    
    auto p = std::make_shared<int>(100);
    int *op = p.get();
    // delete op; // forbidden

    return 0;
}

谨慎使用p.get()的返回值,如果你不知道其危险性则永远不要调用get()函数
p.get()的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发生,如果一定要使用请遵守一下规定:

  • 不要保存p.get()的返回值,无论是保存为裸指针还是shared_ptr都是错误的
  • 保存为裸指针不知什么时候就会变成空悬指针,保存为shared_ptr则产生了独立指针
  • 不要delete p.get()的返回值,智能指针会自动进行释放,就会导致对一块内存delete两次的错误

裸指针:裸指针是指没有被智能指针管理的指针,也就是没有使用智能指针进行初始化的指针。裸指针不会自动释放内存,需要手动释放(使用delete和free函数,然后将其置为NULL,防止出现空悬指针)。在使用时要特别注意,避免出现内存泄漏等问题。

③ 指定删除器

如果用shared_ptr管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器

当p的引用计数为0时,会自动调用删除器Destructor来释放对象的内存,删除器也可以是一个lambda表达式,上下两种写法是等同的

#include <iostream>
#include <memory>

void Destructor(int *p)
{
    
    
    std::cout << "call destructor function" << std::endl;
    delete p;
}

int main()
{
    
    
    std::shared_ptr<int> p1(new int(10), Destructor);

    std::shared_ptr<int> p2(new int(100), [](int *p) {
    
    
        std::cout << "cal lambda destructor function" << std::endl;
        delete p;
    });

    return 0;
}

在这里插入图片描述
观察到释放顺序是从后往前的,这类似于栈结构,如果引用计数同时为零,则后进的先释放

指定删除器的正确使用姿势,当我们用shared_ptr管理动态数组时(注意不是普通的数组),需要指定删除器,因为shared_ptr的默认删除器不支持数组对象

std::shared_ptr<int> ap(new int[10], [](int *p) {
    
    
	std::cout << "delete array pointer" << std::endl;
	delete []p;
});

④ 不要用一个原始指针初始化多个shared_ptr

如:

int *ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr);

当一个shared_ptr对象被销毁时,它会尝试删除它所管理的指针,如果有多个shared_ptr对象管理同一个指针,则会导致重复删除同一块内存,从而导致未定义的行为。

如果要将一个原始指针转换为shared_ptr,可以使用以下方法之一:

  • 使用std::make_shared()函数创建新的shared_ptr对象
  • 使用std::shared_ptr构造函数创建新的shared_ptr对象
  • 使用std::shared_ptr::reset()函数将原始指针赋值给现有的shared_ptr对象

⑤ 不要在函数实参中创建shared_ptr

如:

void function(shared_ptr<int>(new int), f());

因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右。所以,可能的过程是先new int,然后调用f(),如果恰好f()发生异常,而shared_ptr还没有创建,则int内存泄漏了。正确的写法应该是先创建智能指针,再进行函数调用,代码如下:

auto p = std::make_shared(new int);
void function(p, f());

⑥ 通过shared_from_this()返回this指针

不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,这样可能会导致重复析构,如:

#include <iostream>
#include <memory>

using namespace std;

class A
{
    
    
public:
    shared_ptr<A> getSelf()
    {
    
    
        return shared_ptr<A>(this);
    }
    ~A()
    {
    
    
        cout << "destructor" << endl;
    }
};

int main()
{
    
    
    shared_ptr<A> p1(new A);
    shared_ptr<A> p2 = p1->getSelf();

    return 0;
}

在这里插入图片描述
可以发现析构函数被调用了两次,它是由于用同一个指针(this)构造了两个智能指针p1和p2,而他们之间是没有任何关系的,在离开作用域之后this将会被构造的两个智能指针各自析构,所以导致重复析构的错误。

当一个类的对象被std::shared_ptr管理时,如果需要在该类的成员函数中返回一个指向该对象的std::shared_ptr,则可以使用std::enable_shared_from_this1。这个类提供了一个名为shared_from_this()的函数,它返回一个指向当前对象的std::shared_ptr。这样做是为了避免创建多个std::shared_ptr对象来管理同一个对象,从而导致未定义的行为。

如果要使用shared_from_this()函数,需要满足:

  • 该类继承自std::enable_shared_from_this
  • 已经创建了一个std::shared_ptr对象来管理该类的实例
  • 正在调用shared_from_this()函数来获取指向该实例的另一个std::shared_ptr对象

正确使用的例子:

#include <iostream>
#include <memory>

class A : public std::enable_shared_from_this<A>
{
    
    
public:
    std::shared_ptr<A> getSelf()
    {
    
    
        return shared_from_this();
    }
    ~A()
    {
    
    
        std::cout << "destructor" << std::endl;
    }
};

int main()
{
    
    
    auto p1 = std::make_shared<A>();
    std::shared_ptr<A> p2 = p1->getSelf();

    return 0;
}

stdout:

destructor

⑦ 避免循环引用

智能指针的循环引用问题是一个常见的问题,当两个或多个对象相互持有对方的std::shared_ptr时,就会出现循环引用的情况,这会导致内存泄漏,因为这些对象将永远不会被销毁。

#include <iostream>
#include <memory>

class A;
class B;

class A
{
    
    
public:
    std::shared_ptr<B> bp;
    ~A()
    {
    
    
        std::cout << "A destructor" << std::endl; // 释放成员变量的操作在析构函数之后
    }
};

class B
{
    
    
public:
    std::shared_ptr<A> ap;
    ~B()
    {
    
    
        std::cout << "B destructor" << std::endl;
    }
};

int main()
{
    
    
    std::shared_ptr<A> pp;
    {
    
    
        std::shared_ptr<A> p1(new A);
        std::shared_ptr<B> p2(new B);

        p1->bp = p2;
        p2->ap = p1;

        pp = p1;
        p2->ap.reset(); // 需要手动释放才行
    }

    std::cout << pp.use_count() << std::endl; // 循环引用导致p1、p2退出了作用域后都没有发生析构

    return 0;
}

循环引用导致ap和bp的引用计数为2,在离开作用域之后,ap和bp的引用计数减为1,并不回减为0,导致两个指针都不会被析构,产生内存泄漏。

解决这个问题的方法是使用std::weak_ptr,std::weak_ptr是一种特殊类型的指针,它可以指向一个由std::shared_ptr管理的对象,但不会增加该对象的引用计数。因此,当所有std::shared_ptr都释放了它们对该对象的所有权时,该对象将被销毁。

3. unique_ptr

unique_ptr是C++11引入的智能指针之一,它是一个独享被管理对象指针所有权的智能指针。unique_ptr对象包装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。unique_ptr具有->和*运算符重载符,因此它可以像普通指针一样使用。

特点:

  • unique_ptr不共享它的指针,它无法复制到其他unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法
  • 支持移动语义,因此可以将所有权从一个unique_ptr转移到另一个unique_ptr,使用std::move
  • 支持自定义删除器,与shared_ptr有区别

① std::move

unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了,本身就会变为NULL

#include <iostream>
#include <memory>

int main()
{
    
    
    std::unique_ptr<int> p1(new int);
    // std::unique_ptr<int> p2 = p1; // error
    std::unique_ptr<int> p2 = std::move(p1);
    if (p1 == nullptr)
        std::cout << "p1 is null" << std::endl;
    if (p2 == nullptr)
        std::cout << "p2 is null" << std::endl;

    return 0;
}

stdout:

p1 is null

② std::make_unique

std::make_shared是c++11的一部分,但std::make_unique不是,它是在c++14里加入标准库的

它是一函数模板,用于创建一个指向动态分配对象的unique_ptr,使用它可以避免使用new和delete操作符

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args);

其中T是要分配的对象类型,Args是传递给T构造函数的参数。

使用make_unique时,不需要显式地指定所需对象的大小,因为make_unique会自动推断所需大小。此外,make_unique还可以防止内存泄漏和空悬指针等问题。

#include <iostream>
#include <memory>

class A
{
    
    
};

int main()
{
    
    
    auto p1(std::make_unique<A>());
    std::unique_ptr<A> p2(new A); // 两者是等价的

    return 0;
}

使用new的版本重复了被创建对象的键入,但是make_unique函数则没有。重复类型违背了软件工程的一个重要原则:应该避免代码重复,代码中的重复会引起编译次数增加,导致目标代码膨胀。在支持c++14的环境中更推荐使用std::make_unique。

③ 指向数组

因为unique_ptr使用了模板特化,使其支持数组类型。另一方面,shared_ptr没有这个特性,因为它使用引用计数来管理内存,由于数组的大小在运行时才能确定,因此无法在引用计数中进行跟踪。

#include <iostream>
#include <memory>

int main()
{
    
    
    std::unique_ptr<int []> p1(new int[10]);
    p1[9] = 9;
    // std::shared_ptr<int []> p2(new int[10]); // error
    std::shared_ptr<int> p2(new int[10]); // correct

    return 0;
}

④ 指定删除器

unique_ptr需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器

使用时需要注意如下几点:

  • 删除器必须是可调用的对象,可以是函数指针、函数对象或lambda表达式等
  • 如果删除器是一个函数对象,则它必须实现operator()
  • 如果删除器是一个类,则它必须实现operator()或转换为函数指针
  • 如果删除器是一个成员函数,则它必须是非静态的,并且第一个参数必须是指向要删除的对象的指针
#include <iostream>
#include <memory>

struct MyDelete
{
    
    
    void operator()(int *p)
    {
    
    
        std::cout << "delete" << std::endl;
        delete p;
    }
};


int main()
{
    
    
    std::unique_ptr<int, void (*)(int *)> p(new int(1), [](int *p) {
    
    
        delete p;
    });

    std::unique_ptr<int, MyDelete> p2(new int(1)); // 这里不能使用make_unique进行构造

    return 0;
}

此外,如果使用有状态的删除器,则需要注意内存开销。使用无状态的函数对象作为std::unique_ptr的删除器不会占用额外的内存空间;而使用函数指针或有状态的函数对象则会增加std::unique_ptr对象的大小,其中std::function的内存开销最大,应尽量避免使用。

⑤ 选择

如果需要共享指针,希望多个智能指针管理同一个资源,使用std::shared_ptr;如果不需要共享指针,只有一个智能指针管理资源或者管理数组,则使用std::unique_ptr。

std::unique_ptr是一个独享所有权的智能指针,它提供了严格意义上的所有权。它取代了C++98中的auto_ptr。unique_ptr对象包装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。

std::shared_ptr是一个共享所有权的智能指针,多个shared_ptr可以同时拥有同一个对象。当最后一个shared_ptr被销毁时,它所拥有的对象才会被销毁。

4. weak_ptr

share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

weak_ptr 是一种不控制对象生命周期的智能指针,它指向一个 shared_ptr 管理的对象,进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,它只可以从一个shared_ptr 或另一个 weak_ptr 对象构造,它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

weak_ptr没有重载操作符*->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中管理的资源是否存在。weak_ptr还可以返回this指针和解决循环引用的问题。

特点:

  • weak_ptr是对对象的一种弱引用,它不会增加对象的use_count,weak_ptr和shared_ptr可以相互转化,shared_ptr可以直接赋值给weak_ptr,weak_ptr也可以通过调用lock函数来获得shared_ptr。
  • weak_ptr指针通常不单独使用,只能和 shared_ptr 类型指针搭配使用。将一个weak_ptr绑定到一个shared_ptr上时,需要使用shared_ptr的成员函数lock()来获得一个可用的shared_ptr。

常用的函数:

  • expired():判断所指向的对象是否已经被释放
  • lock():返回一个指向所指向对象的shared_ptr

① use_count()

通过use_count()方法获取当前观察资源的引用计数

#include <iostream>
#include <memory>

int main()
{
    
    
    std::shared_ptr<int> p(new int(1));
    std::shared_ptr<int> pp = p;
    std::weak_ptr<int> wp(p);
    std::cout << wp.use_count() << std::endl;

    return 0;
}

stdout:

2

② expired()

通过expired()方法判断所观察资源是否已经释放,0表示未释放,1表示已释放

#include <iostream>
#include <memory>

int main()
{
    
    
    std::shared_ptr<int> p(new int(1));
    std::weak_ptr<int> wp(p);

    std::cout << wp.expired() << std::endl;
    p.reset();
    std::cout << wp.expired() << std::endl;

    return 0;
}

stdout:

0
1

③ lock()

使用lock()方法可以获取指向被管理对象的shared_ptr,从而安全地使用被管理对象,而不会出现空指针异常

具体来说,使用lock()方法有以下好处:

  • 避免悬空指针:当使用shared_ptr的时候,如果有多个shared_ptr指向同一个对象,当其中一个shared_ptr释放掉对该对象的引用时,其他shared_ptr仍然可能指向该对象,导致悬空指针问题。而使用weak_ptr的lock()方法可以避免这个问题,因为weak_ptr只是对对象的一个弱引用,不会影响对象的生命周期。
  • 避免空指针异常:当使用shared_ptr的时候,如果一个shared_ptr为空指针,那么对该指针进行操作就会导致空指针异常。而使用lock()方法可以避免这个问题,因为weak_ptr的lock()方法会返回一个shared_ptr,如果weak_ptr已经失效或者指向的对象已经被释放,那么返回的shared_ptr将是一个空指针,可以避免空指针异常。
  • 提高程序安全性:lock()方法可以提高程序的安全性,因为它可以防止对已经被释放的对象进行访问,从而避免了一些潜在的程序崩溃或者内存泄漏问题。
#include <iostream>
#include <memory>

std::weak_ptr<int> gw;

void f()
{
    
    
    if (gw.expired())
        std::cout << "gw has expired" << std::endl;
    else
    {
    
    
        auto sp = gw.lock();
        std::cout << *sp << std::endl;
    }
}

int main()
{
    
    
    {
    
    
        auto sp = std::make_shared<int>(10);
        gw = sp;
        f();
    }
    f(); // sp已经被释放

    return 0;
}

stdout:

10
gw has expired

④ shared_ptr中返回this指针的问题

在shared_ptr中不能直接将this指针返回shared_ptr,需要通过派生std::enable_shared_from_this类,并通过其方法shared_from_this来返回指针,原因是std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来管理this智能指针,调用shared_from_this()方法会调用内部这个weak_ptr的lock()方法,将所管理的shared_ptr返回。

对于之前那个例子:

#include <iostream>
#include <memory>

using namespace std;

class A
{
    
    
public:
    shared_ptr<A> getSelf()
    {
    
    
        return shared_ptr<A>(this);
    }
    ~A()
    {
    
    
        cout << "destructor" << endl;
    }
};

int main()
{
    
    
    shared_ptr<A> p1(new A);
    shared_ptr<A> p2 = p1->getSelf();

    return 0;
}

在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()是内部的weak_ptr调用lock()方法之后返回的智能指针,在离开作用域之后,spy的引用计数减为0,A对象会被析构,不会出现A对象被析构两次的问题。

需要注意的是,获取自身智能指针的函数仅在shared_ptr的构造函数被调用之后才能使用,因为enable_shared_from_this内部的weak_ptr只有通过shared_ptr才能构造。

⑤ 循环引用问题

在shared_ptr中智能指针循环引用的问题,因为智能指针的循环引用会导致内存泄漏,可以通过weak_ptr解决该问题,只要将A或B的任意一个成员变量改为weak_ptr即可,如下:

#include <iostream>
#include <memory>

class A;
class B;

class A
{
    
    
public:
    std::weak_ptr<B> bp;
    ~A()
    {
    
    
        std::cout << "A destructor" << std::endl;
    }
};

class B
{
    
    
public:
    std::shared_ptr<A> ap;
    ~B()
    {
    
    
        std::cout << "B destructor" << std::endl;
    }
};

int main()
{
    
    
    {
    
    
        std::shared_ptr<A> p1(new A);
        std::shared_ptr<B> p2(new B);

        p1->bp = p2;
        p2->ap = p1;
    }

    std::cout << "program end" << std::endl;

    return 0;
}

stdout:

B destructor
A destructor
program end

这样在对B的成员赋值,即执行p2->ap = p1;时,由于ap是weak_ptr,它并不会增加引用计数,所以p1的引用计数仍然会是1,在离开作用域之后,p1的引用计数为减为0,A指针会被析构,析构后其内部的bp的引用计数会被减为1,然后在离开作用域后p2引用计数又从1减为0,B对象也被析构,不会发生内存泄漏。

⑥ weak_ptr在使用前需检查合法性

shared_ptr sp_okweak_ptr wp属于不同作用域

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

int main()
{
    
    
    weak_ptr<int> wp;
    {
    
    
        shared_ptr<int> sp(new int(1));
        wp = sp;
        shared_ptr<int> sp_ok = wp.lock();
    }
    shared_ptr<int> sp_null = wp.lock();
    
    if (wp.expired())
        cout << "shared_ptr has destroyed" << endl;
    else
        cout << "shared_ptr is alive" << endl;

    return 0;
}

stdout:

shared_ptr has destroyed

因为上述代码中sp和sp_ok离开了作用域,其容纳的int对象已经被释放了,得到了一个容纳NULL指针的sp_null对象。

shared_ptr sp_okweak_ptr wp属于同一个作用域

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

int main()
{
    
    
    weak_ptr<int> wp;
    shared_ptr<int> sp_ok;
    {
    
    
        shared_ptr<int> sp(new int(1));
        wp = sp;
        sp_ok = wp.lock();
    }

    if (wp.expired())
        cout << "shared_ptr has destroyed" << endl;
    else
        cout << "shared_ptr is alive" << endl;

    return 0;
}

stdout:

shared_ptr is alive

在使用weak_prt前需要调用expired()函数判断一下。因为weak_prt还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息。直到最后一个weak_ptr对象被析构,这块“堆”存储块才能被回收,否则weak_ptr无法知道自己所容纳的那个指针资源的当前状态。

5. 智能指针安全性问题

引用计数本身是安全的,至于智能指针是否安全需要结合实际使用分情况讨论

① 多线程代码操作同一个shared_ptr对象

此时是不安全的

比如std::thread的回调函数,是一个lambda表达式,其中引用捕获了一个shared_ptr

std::thread td([&sp1]()){
    
    ....});

又或者通过回调函数的参数传入的shared_ptr对象,参数类型引用

void fn(shared_ptr<A> &sp) {
    
    
...
}
..
std::thread td(fn, sp1); 

这时候必然不是线程安全的

② 多线程代码操作不同shared_ptr对象

这里指的是管理的数据是同一份,而shared_ptr不是同一个对象,比如多线程回调的lambda的是按值捕获的对象

std::thread td([sp1]()){
    
    ....});

另个线程传递的shared_ptr是值传递,而非引用

void fn(shared_ptr<A> sp) {
    
    
...
}
..
std::thread td(fn, sp1); 

这时候每个线程内看到的sp,他们所管理的是同一份数据,用的是同一个引用计数,但是各自是不同的对象,当发生多线程中修改sp指向的操作的时候,是不会出现非预期的异常行为的,也就是说,如下操作是安全的。

void fn(shared_ptr<A>sp) {
    
    
...
  if(..){
    
    
   sp = other_sp;
  } else {
    
    
    sp = other_sp2;
  }
}

需要注意所管理数据的线程安全性问题

显而易见,所管理的对象必然不是线程安全的,必然 sp1、sp2、sp3智能指针实际都是指向对象A,三个线程同时操作对象A,那对象的数据安全必然是需要对象A自己去保证。

6. 智能指针实践

  • RAII(资源获取即初始化):智能指针可以用来管理动态分配的内存,确保在对象的生命周期结束时,分配的内存得到正确的释放,避免内存泄漏
  • 异常安全:使用智能指针可以避免因为异常导致资源没有正确释放的问题,从而提高程序的健壮性
  • 简化代码:使用智能指针可以避免手动管理内存的复杂性,简化代码的编写和维护
  • 防止空悬指针:智能指针可以在指针指向的对象被释放后,将指针置为 nullptr,避免出现空悬指针的问题
  • 共享所有权:shared_ptr 可以被多个指针共享,从而实现共享所有权,避免了多个指针对同一对象进行释放的问题
  • 避免深拷贝:使用 unique_ptr 可以避免深拷贝的问题,因为 unique_ptr 只能被一个指针拥有,所以不存在多个指针拥有同一对象的情况。

猜你喜欢

转载自blog.csdn.net/weixin_52665939/article/details/129735651
今日推荐