C++智能指针

CMakeLists.txt的

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。简要的说,智能指针利用了 C++ 的 RAII 机制,在智能指针对象作用域结束后,会自动做内存释放的相关操作,不需要我们再手动去操作内存。
在C++中,如果一个类被声明在栈空间,则在该函数执行完毕从栈空间弹出之后,类会自动调用析构函数。可是如果被显示声明在堆空间(使用new方法或者malloc方法),则需要显式调用析构函数才能进行析构。

C++ 中有四种智能指针:auto_pt、unique_ptr、shared_ptr、weak_ptr 其中后三个是 C++11 支持,第一个已经被 C++11 弃用且被 unique_prt 代替,不推荐使用。随着 C++11 标准的出现(最新标准是 C++20)。

#include <memory>

std::unique_ptr<T> :独占资源所有权的指针。

std::shared_ptr<T> :共享资源所有权的指针。

std::weak_ptr<T> :共享资源的观察者,需和 std::shared_ptr 一起使用,不影响资源的生命周期。

std::auto_ptr 已被废弃。

std::unique_ptr

作为对 std::auto_ptr 的改进,std::unique_ptr 对其持有的堆内存具有唯一拥有权,也就是 std::unique_ptr 不可以拷贝或赋值给其他对象,其拥有的堆内存仅自己独占,std::unique_ptr 对象销毁时会释放其持有的堆内存。

int main()
{
    //初始化方式1
    std::unique_ptr<int> up1(new int(123));
    //初始化方式2
    std::unique_ptr<int> up2;
    up2.reset(new int(123));
    //初始化方式3 (-std=c++14)
    std::unique_ptr<int> up3 = std::make_unique<int>(123);
}

使用 std::unique_ptr 自动管理内存。

std::unique_ptr<int> uptr = std::make_unique<int>(200);// 离开 uptr 的作用域的时候自动释放内存

令很多人对 C++11 规范不满的地方是,C++11 新增了 std::make_shared() 方法创建一个 std::shared_ptr 对象,却没有提供相应的 std::make_unique() 方法创建一个 std::unique_ptr 对象,这个方法直到 C++14 才被添加进来。当然,在 C++11 中你很容易实现出这样一个方法来:

template <typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts &&...params)
{
    return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
void runGame()
{  
  std::unique_ptr<Monster> monster1(new Monster());//monster1 指向 一个怪物  
  std::unique_ptr<Monster> monster2 = monster1;//Error!编译期出错,不允许复制指针指向同一个资源。     	std::unique_ptr<Monster> monster3 = std::move(monster1);//转移所有权给monster3.  
  monster1->doSomething();//Oops!monster1指向nullptr,运行期崩溃 
}

在这里插入图片描述

 虽然我们不能拷贝或者赋值unique_ptr,但是可以通过调用release或reset将指针所有权从一个(非const)unique_ptr转移给另一个unique

//将所有权从p1(指向string Stegosaurus)转移给p2
unique_ptr<string> p2(p1.release());//release将p1置为空
unique_ptr<string>p3(new string("Trex"));

//将所有权从p3转移到p2
p2.reset(p3.release());//reset释放了p2原来指向的内存

鉴于 std::auto_ptr 的前车之鉴,std::unique_ptr 禁止复制语义,为了达到这个效果,std::unique_ptr 类的拷贝构造函数和赋值运算符(operator =)被标记为 delete

template <class T>
class unique_ptr
{
    //省略其他代码...

    //拷贝构造函数和赋值运算符被标记为delete
    unique_ptr(const unique_ptr &) = delete;
    unique_ptr &operator=(const unique_ptr &) = delete;
};
因此,下列代码是无法通过编译的:

std::unique_ptr<int> up1(std::make_unique<int>(123));;

//以下代码无法通过编译
//std::unique_ptr<int> up2(up1);
std::unique_ptr<int> up3;
//以下代码无法通过编译
//up3 = up1;
禁止复制语义也存在特例,即可以通过一个函数返回一个 std::unique_ptr:

#include <memory>

std::unique_ptr<int> func(int val)
{
    std::unique_ptr<int> up(new int(val));
    return up;
}

int main()
{
    std::unique_ptr<int> up1 = func(123);

    return 0;
}

上述代码从 func 函数中得到一个 std::unique_ptr 对象,然后返回给 up1。

既然 std::unique_ptr 不能复制,那么如何将一个 std::unique_ptr 对象持有的堆内存转移给另外一个呢?答案是使用移动构造,示例代码如下:

int main()
{
    std::unique_ptr<int> up1(std::make_unique<int>(123));
    std::unique_ptr<int> up2(std::move(up1));
    std::cout << ((up1.get() == nullptr) ? "up1 is NULL" : "up1 is not NULL") << std::endl;
    
    std::unique_ptr<int> up3;
    up3 = std::move(up2);
    std::cout << ((up2.get() == nullptr) ? "up2 is NULL" : "up2 is not NULL") << std::endl;
    
    return 0;
}

运行结果

up1 is NULL

up2 is NULL

std::unique_ptr 不仅可以持有一个堆对象,也可以持有一组堆对象,示例如下:

int main()
{
    //创建10个int类型的堆对象
    //形式1
    std::unique_ptr<int[]> up1(new int[10]);
    //形式2
    std::unique_ptr<int[]> up2;
    up2.reset(new int[10]);
    //形式3
    std::unique_ptr<int[]> up3(std::make_unique<int[]>(10));

    for (int i = 0; i < 10; ++i)
    {
        up1[i] = i;
        up2[i] = i;
        up3[i] = i;
    }

    for (int i = 0; i < 10; ++i)
    {
        std::cout << up1[i] << ", " << up2[i] << ", " << up3[i] << std::endl;
    }

    return 0;
}

运行结果

0, 0, 0
1, 1, 1
2, 2, 2
3, 3, 3
4, 4, 4
5, 5, 5
6, 6, 6
7, 7, 7
8, 8, 8
9, 9, 9
 

std::unique_ptr 是 move-only 的。

{

    std::unique_ptr<int> uptr = std::make_unique<int>(200);

    std::unique_ptr<int> uptr1 = uptr;  // 编译错误,std::unique_ptr<T> 是 move-only 的

    std::unique_ptr<int> uptr2 = std::move(uptr);

    assert(uptr == nullptr);

}

std::unique_ptr 可以指向一个数组。

{

    std::unique_ptr<int[]> uptr = std::make_unique<int[]>(10);

    for (int i = 0; i < 10; i++) {

        uptr[i] = i * i;

    }  

    for (int i = 0; i < 10; i++) {

        std::cout << uptr[i] << std::endl;

    }  

}

自定义 deleter。

{

    struct FileCloser {

        void operator()(FILE* fp) const {

            if (fp != nullptr) {

                fclose(fp);

            }

        }  

    }; 

    std::unique_ptr<FILE, FileCloser> uptr(fopen("test_file.txt", "w"));

}

使用 Lambda 的 deleter。

{

    std::unique_ptr<FILE, std::function<void(FILE*)>> uptr(

        fopen("test_file.txt", "w"), [](FILE* fp) {

            fclose(fp);

        });

}

另外,std::unique_ptr 有几个常用函数如下:

void reset(pointer p = pointer())

释放当前由 unique_ptr(如果有)管理的指针并获得参数 p(参数 p 默认为 NULL)的所有权。如果 p 是空指针(例如默认初始化的指针),则 unique_ptr 变为空,调用后不管理任何对象。

pointer release()

返回管理的指针并将其替换为空指针, 释放其管理指针的所有权。这个调用并不会销毁托管对象,但是将 unique_ptr 对象管理的指针解脱出来。如果要强制销毁所指向的对象,请调用 reset 函数或对其执行赋值操作。

element_type* get()

返回存储的指针,不会使 unique_ptr 释放指针的所有权。因此,该函数返回的值不能于构造新的托管指针,如果为了获得存储的指针并释放其所有权,请调用 release。

void swap (unique_ptr& x)

将 unique_ptr 对象的内容与对象 x 进行交换,在它们两者之间转移管理指针的所有权而不破坏二者。

自定义智能指针对象持有的资源的释放函数

class Socket
{
public:
    Socket()
    {}

    ~Socket()
    {}

    //关闭资源句柄
    void close()
    {}
};

int main()
{
    auto deletor = [](Socket *pSocket)
    {
        //关闭句柄
        pSocket->close();
        //TODO: 你甚至可以在这里打印一行日志...
        delete pSocket;
    };

    std::unique_ptr<Socket, void (*)(Socket * pSocket)> upSocket(new Socket(), deletor);

    return 0;
}

自定义 std::unique_ptr 的资源释放函数其规则是:

std::unique_ptr<T, DeletorFuncPtr>

其中 T 是你要释放的对象类型,DeletorPtr 是一个自定义函数指针。上述代码 28 行表示 DeletorPtr 有点复杂(是 C++11 中的 Lambda 函数),我们可以使用 decltype(deletor) 让编译器自己推导 deletor 的类型,因此可以将 上 行代码修改为:

std::unique_ptr<Socket, decltype(deletor)> upSocket(new Socket(), deletor);
 

std::shared_ptr

std::unique_ptr 对其持有的资源具有独占性,而 std::shared_ptr 持有的资源可以在多个 std::shared_ptr 之间共享,每多一个 std::shared_ptr 对资源的引用,资源引用计数将增加 1,每一个指向该资源的 std::shared_ptr 对象析构时,资源引用计数减 1,最后一个 std::shared_ptr 对象析构时,发现资源计数为 0,将释放其持有的资源。多个线程之间,递增和减少资源的引用计数是安全的。(注意:这不意味着多个线程同时操作 std::shared_ptr 引用的对象是安全的)。std::shared_ptr 提供了一个 use_count() 方法来获取当前持有资源的引用计数。除了上面描述的,std::shared_ptr 用法和 std::unique_ptr 基本相同。

在这里插入图片描述

在这里插入图片描述

 

 

int main()
{
    //初始化方式1
    std::shared_ptr<int> sp1(new int(123));

    //初始化方式2
    std::shared_ptr<int> sp2;
    sp2.reset(new int(123));

    //初始化方式3
    std::shared_ptr<int> sp3;
    sp3 = std::make_shared<int>(123);

    return 0;
}

和 std::unique_ptr 一样,你应该优先使用 std::make_shared 去初始化一个 std::shared_ptr 对象。

#include <iostream>
#include <memory>

class A
{
public:
    A()
    { std::cout << "A constructor" << std::endl;}

    ~A()
    {std::cout << "A destructor" << std::endl;}
};

int main()
{
    {
        //初始化方式1
        std::shared_ptr<A> sp1(new A());
        std::cout << "use count: " << sp1.use_count() << std::endl;

        //初始化方式2
        std::shared_ptr<A> sp2(sp1);
        std::cout << "use count: " << sp1.use_count() << std::endl;

        sp2.reset();
        std::cout << "use count: " << sp1.use_count() << std::endl;
        {
            std::shared_ptr<A> sp3 = sp1;
            std::cout << "use count: " << sp1.use_count() << std::endl;
        }

        std::cout << "use count: " << sp1.use_count() << std::endl;
    }

    return 0;
}

A constructor
use count: 1
use count: 2
use count: 1
use count: 2
use count: 1
A destructor

上述代码 sp1 构造时,同时触发对象 A 的构造,因此 A 的构造函数会执行。

此时只有一个 sp1 对象引用  new 出来的 A 对象(为了叙述方便,下文统一称之为资源对象 A),因此后面打印出来的引用计数值为 1。

中间,利用 sp1 拷贝一份 sp2,导致代码 28 行打印出来的引用计数为 2。

调用 sp2 的 reset() 方法,sp2 释放对资源对象 A 的引用,因此代码 后面打印的引用计数值再次变为 1。

后面利用 sp1 再次 创建 sp3,因此打印的引用计数变为 2。

程序执行到 36 行}以后,sp3 出了其作用域被析构,资源 A 的引用计数递减 1,因此 代码 38 行打印的引用计数为 1。

程序执行到 39 行}以后,sp1 出了其作用域被析构,在其析构时递减资源 A 的引用计数至 0,并析构资源 A 对象,因此类 A 的析构函数被调用。

std::shared_ptr 其实就是对资源做引用计数——当引用计数为 0 的时候,自动释放资源。

{

    std::shared_ptr<int> sptr = std::make_shared<int>(200);

    assert(sptr.use_count() == 1);  // 此时引用计数为 1

  {  

        std::shared_ptr<int> sptr1 = sptr;

        assert(sptr.get() == sptr1.get());

        assert(sptr.use_count() == 2);   // sptr 和 sptr1 共享资源,引用计数为 2

    }  

    assert(sptr.use_count() == 1);   // sptr1 已经释放

}

// use_count 为 0 时自动释放内存

和 unique_ptr 一样,shared_ptr 也可以指向数组和自定义 deleter。

{

    // C++20 才支持 std::make_shared<int[]>

    // std::shared_ptr<int[]> sptr = std::make_shared<int[]>(100);

    std::shared_ptr<int[]> sptr(new int[10]);

    for (int i = 0; i < 10; i++) {

        sptr[i] = i * i;

    }  

    for (int i = 0; i < 10; i++) {

        std::cout << sptr[i] << std::endl;

    }  

}

自定义 deleter。

{

    struct FileCloser {

        void operator()(FILE* fp) const {

            if (fp != nullptr) {

                fclose(fp);

            }

      }  

    }; 

    std::unique_ptr<FILE, FileCloser> uptr(fopen("test_file.txt", "w"));

}

使用 Lambda 的 deleter。

{

    std::unique_ptr<FILE, std::function<void(FILE*)>> uptr(

        fopen("test_file.txt", "w"), [](FILE* fp) {

         std::cout << "close " << fp << std::endl;

            fclose(fp);

        });

}

void runGame()
{  
    std::shared_ptr<Monster> monster1(new Monster());  //计数加到1
 do{
       std::shared_ptr<Monster> monster2 = monster1; 	//计数加到2  
    }while(0);          
  //该栈退出后,计数减为1,monster1指向的堆对象仍存在
  std::shared_ptr<Monster> monster3 = monster1;   	//计数加到2
}//该栈退出后,shared_ptr都释放了,计数减为0,它们指向的堆对象也能跟着释放.

std::shared_ptr 有几个常用函数如下:

void swap (unique_ptr& x)

将 shared_ptr 对象的内容与对象 x 进行交换,在它们两者之间转移管理指针的所有权而不破坏或改变二者的引用计数。

void reset()

void reset (ponit p)

没有参数时,先将管理的计数器引用计数减一并将管理的指针和计数器置清零。有参数 p 时,先做面前没有参数的操作,再管理 p 的所有权和设置计数器。

element_type* get()

得到其管理的指针。

long int use_count()

返回与当前智能指针对象在同一指针上共享所有权的 shared_ptr 对象的数量,如果这是一个空的 shared_ptr,则该函数返回 0。如果要用来检查 use_count 是否为 1,可以改用成员函数 unique 会更快。

bool unique()

返回当前 shared_ptr 对象是否不和其他智能指针对象共享指针的所有权,如果这是一个空的 shared_ptr,则该函数返回 false。

element_type& operator\*()

重载指针的 * 运算符,返回管理的指针指向的地址的引用。

element_type* operator->()

重载指针的 -> 运算符,返回管理的指针,可以访问其成员。

explicit operator bool()

返回存储的指针是否已经是空指针,返回的结果与 get() != 0 相同。


std::enable_shared_from_this


实际开发中,有时候需要在类中返回包裹当前对象(this)的一个 std::shared_ptr 对象给外部使用,C++ 新标准也为我们考虑到了这一点,有如此需求的类只要继承自 std::enable_shared_from_this 模板对象即可。用法如下:
 

#include <iostream>
#include <memory>

class A : public std::enable_shared_from_this<A>
{
public:
    A()
    {std::cout << "A constructor" << std::endl;}

    ~A()
    {std::cout << "A destructor" << std::endl; }

    std::shared_ptr<A> getSelf()
    {     return shared_from_this(); }
};

int main()
{
    std::shared_ptr<A> sp1(new A());
    std::shared_ptr<A> sp2 = sp1->getSelf();
    std::cout << "use count: " << sp1.use_count() << std::endl;

    return 0;
}

上述代码中,类 A 的继承 std::enable_shared_from_this 并提供一个 getSelf() 方法返回自身的 std::shared_ptr 对象,在 getSelf() 中调用 shared_from_this() 即可。

std::enable_shared_from_this 用起来比较方便,但是也存在很多不易察觉的陷阱。

陷阱一:不应该共享栈对象的 this 给智能指针对象

假设我们将上面代码 main 函数 25 行生成 A 对象的方式改成一个栈变量,即:

//其他相同代码省略...

int main()
{
    A a;

    std::shared_ptr<A> sp2 = a.getSelf();

    std::cout << "use count: " << sp2.use_count() << std::endl;

    return 0;
}

运行修改后的代码会发现程序在 std::shared_ptr sp2 = a.getSelf() 产生崩溃。这是因为,智能指针管理的是堆对象,栈对象会在函数调用结束后自行销毁,因此不能通过 shared_from_this() 将该对象交由智能指针对象管理。

切记:智能指针最初设计的目的就是为了管理堆对象的(即那些不会自动释放的资源)。

陷阱二:避免 std::enable_shared_from_this 的循环引用问题
 

#include <iostream>
#include <memory>

class A : public std::enable_shared_from_this<A>
{
public:
    A()
    {
        m_i = 9;
        //注意:比较好的做法是在构造函数里面调用shared_from_this()给m_SelfPtr赋值
        //但是很遗憾不能这么做,如果写在构造函数里面程序会直接崩溃
        std::cout << "A constructor" << std::endl;
    }

    ~A()
    {
        m_i = 0;
        std::cout << "A destructor" << std::endl;
    }

    void func()
    {
        m_SelfPtr = shared_from_this();
    }

public:
    int m_i;
    std::shared_ptr<A> m_SelfPtr;
};

int main()
{
    {
        std::shared_ptr<A> spa(new A());
        spa->func();
    }

    return 0;
}

运行结果A constructor

我们发现在程序的整个生命周期内,只有 A 类构造函数的调用输出,没有 A 类析构函数的调用输出,这意味着 new 出来的 A 对象产生了内存泄漏!

我们来分析一下为什么 new 出来的 A 对象得不到释放。当程序执行到 39 行后,spa 出了其作用域准备析构,在析构时其发现仍然有另外的一个 std::shared_ptr 对象即 A::m_SelfPtr 引用了 A,因此 spa 只会将 A 的引用计数递减为 1,然后就销毁自身了。现在留下一个矛盾的处境:必须销毁 A 才能销毁其成员变量 m_SelfPtr,而销毁 m_SelfPtr 必须先销毁 A。这就是所谓的 std::enable_shared_from_this 的循环引用问题。我们在实际开发中应该避免做出这样的逻辑设计,这种情形下即使使用了智能指针也会造成内存泄漏。也就是说一个资源的生命周期可以交给一个智能指针对象,但是该智能指针的生命周期不可以再交给整个资源来管理。

std::weak_ptr

1、计数区域(SharedPtrControlBlock)结构体引进新的int变量weak_count,来作为弱引用计数。
2、每个weak_ptr都占指针的两倍空间,一个装着原始指针,一个装着计数区域的指针(和shared_ptr一样的成员)。
3、weak_ptr可以由一个shared_ptr或者另一个weak_ptr构造。
4、weak_ptr的构造和析构不会引起shared_count的增加或减少,只会引起weak_count的增加或减少

在这里插入图片描述

 

因为 weak_ptr 作为弱引用指针,其实现依赖于 Counter 计数器类和 shared_ptr 的赋值,所以先进行 Counter 计数器类和 share_ptr 的简单实现。

std::weak_ptr 要与 std::shared_ptr 一起使用。 一个 std::weak_ptr 对象看做是 std::shared_ptr 对象管理的资源的观察者,它不影响共享资源的生命周期:

如果需要使用 weak_ptr 正在观察的资源,可以将 weak_ptr 提升为 shared_ptr。

当 shared_ptr 管理的资源被释放时,weak_ptr 会自动变成 nullptr。\

void Observe(std::weak_ptr<int> wptr) {

    if (auto sptr = wptr.lock()) {

        std::cout << "value: " << *sptr << std::endl;

    } else {

        std::cout << "wptr lock fail" << std::endl;

    }

}

std::weak_ptr<int> wptr;

{

    auto sptr = std::make_shared<int>(111);

    wptr = sptr;

    Observe(wptr);  // sptr 指向的资源没被释放,wptr 可以成功提升为 shared_ptr

}

Observe(wptr);  // sptr 指向的资源已被释放,wptr 无法提升为 shared_ptr

std::weak_ptr 是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只是提供了对其管理的资源的一个访问手段,引入它的目的为协助 std::shared_ptr 工作。

std::weak_ptr 可以从一个 std::shared_ptr 或另一个 std::weak_ptr 对象构造,std::shared_ptr 可以直接赋值给 std::weak_ptr ,也可以通过 std::weak_ptr 的 lock() 函数来获得 std::shared_ptr。它的构造和析构不会引起引用计数的增加或减少。std::weak_ptr 可用来解决 std::shared_ptr 相互引用时的死锁问题(即两个std::shared_ptr 相互引用,那么这两个指针的引用计数永远不可能下降为 0, 资源永远不会释放)。

示例代码如下:

#include <iostream>
#include <memory>

int main()
{
    //创建一个std::shared_ptr对象
    std::shared_ptr<int> sp1(new int(123));
    std::cout << "use count: " << sp1.use_count() << std::endl;

    //通过构造函数得到一个std::weak_ptr对象
    std::weak_ptr<int> sp2(sp1);
    std::cout << "use count: " << sp1.use_count() << std::endl;

    //通过赋值运算符得到一个std::weak_ptr对象
    std::weak_ptr<int> sp3 = sp1;
    std::cout << "use count: " << sp1.use_count() << std::endl;

    //通过一个std::weak_ptr对象得到另外一个std::weak_ptr对象
    std::weak_ptr<int> sp4 = sp2;
    std::cout << "use count: " << sp1.use_count() << std::endl;

    return 0;
}

运行结果

use count: 1

use count: 1

use count: 1

use count: 1

无论通过何种方式创建 std::weak_ptr 都不会增加资源的引用计数,因此每次输出引用计数的值都是 1。

既然,std::weak_ptr 不管理对象的生命周期,那么其引用的对象可能在某个时刻被销毁了,如何得知呢?std::weak_ptr 提供了一个 expired() 方法来做这一项检测,返回 true,说明其引用的资源已经不存在了;返回 false,说明该资源仍然存在,这个时候可以使用 std::weak_ptr 的 lock() 方法得到一个 std::shared_ptr 对象然后继续操作资源,以下代码演示了该用法:

// tmpConn_ 是一个 std::weak_ptr<TcpConnection> 对象
// tmpConn_ 引用的TcpConnection已经销毁,直接返回
if (tmpConn_.expired())
    return;

std::shared_ptr<TcpConnection> conn = tmpConn_.lock();
if (conn)
{
    //对conn进行操作,省略...
}

有读者可能对上述代码产生疑问,既然使用了 std::weak_ptr 的 expired() 方法判断了对象是否存在,为什么不直接使用 std::weak_ptr 对象对引用资源进行操作呢?实际上这是行不通的,std::weak_ptr 类没有重写 operator-> 和 operator* 方法,因此不能像 std::shared_ptr 或 std::unique_ptr 一样直接操作对象,同时 std::weak_ptr 类也没有重写 operator! 操作,因此也不能通过 std::weak_ptr 对象直接判断其引用的资源是否存在:

#include <memory>

class A
{
public:
    void doSomething()
    {}
};

int main()
{
    std::shared_ptr<A> sp1(new A());
    std::weak_ptr<A> sp2(sp1);

    //正确代码
    if (sp1)
    {
        //正确代码
        sp1->doSomething();
        (*sp1).doSomething();
    }

    //正确代码
    if (!sp1)
    {}

    //错误代码,无法编译通过
    //if (sp2)
    //{
    //    //错误代码,无法编译通过
    //    sp2->doSomething();
    //    (*sp2).doSomething();
    //}

    //错误代码,无法编译通过
    //if (!sp2)
    //{
    //}

    return 0;
}

之所以 std::weak_ptr 不增加引用资源的引用计数不管理资源的生命周期,是因为,即使它实现了以上说的几个方法,调用它们也是不安全的,因为在调用期间,引用的资源可能恰好被销毁了,这会造成棘手的错误和麻烦。

因此,std::weak_ptr 的正确使用场景是那些资源如果可能就使用,如果不可使用则不用的场景,它不参与资源的生命周期管理。例如,网络分层结构中,Session 对象(会话对象)利用 Connection 对象(连接对象)提供的服务工作,但是 Session 对象不管理 Connection 对象的生命周期,Session 管理 Connection 的生命周期是不合理的,因为网络底层出错会导致 Connection 对象被销毁,此时 Session 对象如果强行持有 Connection 对象与事实矛盾。

std::weak_ptr 应用场景,经典的例子是订阅者模式或者观察者模式中。这里以订阅者为例来说明,消息发布器只有在某个订阅者存在的情况下才会向其发布消息,而不能管理订阅者的生命周期。

class Subscriber
{
};

class SubscribeManager
{
public:
    void publish()
    {
        for (const auto &iter : m_subscribers)
        {
            if (!iter.expired())
            {
                //TODO:给订阅者发送消息
            }
        }
    }

private:
    std::vector<std::weak_ptr<Subscriber>> m_subscribers;
};

另外,std::weak_ptr 有几个常用函数如下:

void swap (weak_ptr& x)

将当前 weak_ptr 对象的内容与 x 的内容交换。

void reset()

将当前 weak_ptr 对象管理的指针和计数器变成空的,就像默认构造的一样。

long int use_count()

返回与当前 weak_ptr 对象在同一指针上共享所有权的 shared_ptr 对象的数量。

bool expired()

检查是否过期,返回 weak_ptr 对象管理的指针为空,或者和他所属共享的没有更多 shared_ptr。lock 函数一般需要先调用 expired 判断,如果已经过期,就不能通过 weak_ptr 恢复拥有的 shared_ptr。此函数应返回与(use_count() == 0)相同的值,但是它可能以更有效的方式执行此操作。

shared_ptr<element_type> lock()

如果它没有过期,则返回一个 shared_ptr,其中包含由 weak_ptr 对象保留的信息。如果 weak_ptr 对象已经过期,则该函数返回一个空的 shared_ptr(默认构造一样)。因为返回的 shared_ptr 对象也算作一个所有者,所以这个函数锁定了拥有的指针,防止它被释放(至少在返回的对象没有释放它的情况下)。 此操作以原子方式执行。

关于循环引用

class Monster{
  //尽管父子可以互相访问,但是彼此都是独立的个体,无论是谁都不应该拥有另一个人的所有权。
  std::weak_ptr<Monster> m_father;    //所以都把shared_ptr换成了weak_ptr
  std::weak_ptr<Monster> m_son;      //同上

public:
  void setFather(std::shared_ptr<Monster>& father); 
  void setSon(std::shared_ptr<Monster>& son);    
  ~Monster(){std::cout << "A monster die!";}     
};

void runGame()
{  
    std::shared_ptr<Monster> father(new Monster());  
    std::shared_ptr<Monster> son(new Monster());  
    father->setSon(son);  
    son->setFather(father);
}

1、一开始:father指向的堆对象 shared计数为1,weak计数为1;son指向的堆对象 shared计数为1,weak计数为1;
2、son智能指针退出栈:son指向的堆对象 shared计数减为0,weak计数为1,释放son的堆对象;      father指向的堆对象 shared计数为1,weak计数减为0;
3、father智能指针退出栈:father指向的堆对象 shared计数减为0,weak计数为0;释放father的堆对象和father的计数区域;son指向的堆对象 shared计数为0,weak计数减为0;释放son的计数区域。
4、函数结束,释放行为正确。

weak_ptr没有重载 * 和 -> ,所以并不能直接使用资源。但可以使用lock()获得一个可用的shared_ptr对象,如果对象已经销毁,lock()会失败,返回一个空的shared_ptr


智能指针的大小


一个 std::unique_ptr 对象大小与裸指针大小相同(即 sizeof(std::unique_ptr) == sizeof(void)),而 std::shared_ptr 的大小是 std::unique_ptr 的一倍。以下是我分别在 Visual Studio 2019 和 gcc/g++ 4.8 上(二者都编译成 x64 程序)的测试结果:
 

#include <iostream>
#include <memory>
#include <string>

int main()
{
    std::shared_ptr<int> sp0;
    std::shared_ptr<std::string> sp1;
    sp1.reset(new std::string());
    std::unique_ptr<int> sp2;
    std::weak_ptr<int> sp3;

    std::cout << "sp0 size: " << sizeof(sp0) << std::endl;
    std::cout << "sp1 size: " << sizeof(sp1) << std::endl;
    std::cout << "sp2 size: " << sizeof(sp2) << std::endl;
    std::cout << "sp3 size: " << sizeof(sp3) << std::endl;

    return 0;
}

Visual Studio 2019 (32bit) 运行结果:

sp0 size:8
sp1 size:8
sp2 size:4
sp3 size:8

gcc/g++ (64bit) 运行结果:

sp0 size:16
sp1 size:16
sp2 size:8
sp3 size:16

在 32 位机器上,std_unique_ptr 占 4 字节,std::shared_ptr 和 std::weak_ptr 占 8 字节。

在 64 位机器上,std_unique_ptr 占 8 字节,std::shared_ptr 和 std::weak_ptr 占 16 字节。

也就是说,std_unique_ptr 的大小总是和原始指针大小一样,std::shared_ptr 和 std::weak_ptr 大小是原始指针的一倍。

智能指针使用注意事项

C++ 新标准提倡的理念之一是不应该再手动调用 delete 或者 free 函数去释放内存了,而应该把它们交给新标准提供的各种智能指针对象。

1、一旦一个对象使用智能指针管理后,就不该再使用原始裸指针去操作

#include <memory>

class Subscriber
{
};

int main()
{
    Subscriber *pSubscriber = new Subscriber();

    std::unique_ptr<Subscriber> spSubscriber(pSubscriber);

    delete pSubscriber;

    return 0;
}

这段代码利用创建了一个堆对象 Subscriber,然后利用智能指针 spSubscriber 去管理之,可以却私下利用原始指针销毁了该对象,这让智能指针对象 spSubscriber 情何以堪啊?

记住,一旦智能指针对象接管了你的资源,所有对资源的操作都应该通过智能指针对象进行,不建议再通过原始指针进行操作了。

当然,除了 std::weak_ptr 之外,std::unique_ptr 和 std::shared_ptr 都提供了获取原始指针的方法——get() 函数。

int main()
{
    Subscriber *pSubscriber = new Subscriber();

    std::unique_ptr<Subscriber> spSubscriber(pSubscriber);

    //pTheSameSubscriber和pSubscriber指向同一个对象
    Subscriber *pTheSameSubscriber = spSubscriber.get();

    return 0;
}

2、分清楚场合应该使用哪种类型的智能指针

通常情况下,如果你的资源不需要在其他地方共享,那么应该优先使用 std::unique_ptr,反之使用 std::shared_ptr,当然这是在该智能指针需要管理资源的生命周期的情况下;如果不需要管理对象的生命周期,请使用 std::weak_ptr。

3、认真考虑,避免操作某个引用资源已经释放的智能指针

#include <iostream>
#include <memory>

class T
{
public:
    void doSomething()
    {
        std::cout << "T do something..." << m_i << std::endl;
    }

private:
    int m_i;
};

int main()
{
    std::shared_ptr<T> sp1(new T());
    const auto &sp2 = sp1;

    sp1.reset();

    //由于sp2已经不再持有对象的引用,程序会在这里出现意外的行为
    sp2->doSomething();

    return 0;
}

上述代码中,sp2 是 sp1 的引用,sp1 被置空后,sp2 也一同为空。这时候调用 sp2->doSomething(),sp2->(即 operator->)在内部会调用 get() 方法获取原始指针对象,这时会得到一个空指针(地址为 0),继续调用 doSomething() 导致程序崩溃。

//连接断开
void MonitorServer::OnClose(const std::shared_ptr<TcpConnection> &conn)
{
    std::lock_guard<std::mutex> guard(m_sessionMutex);
    for (auto iter = m_sessions.begin(); iter != m_sessions.end(); ++iter)
    {
        //通过比对connection对象找到对应的session
        if ((*iter)->GetConnectionPtr() == conn)
        {
            m_sessions.erase(iter);
            //注意这里:程序在此处崩溃
            LOGI("monitor client disconnected: %s", conn->peerAddress().toIpPort().c_str());
            break;
        }
    }
}

该段程序会在代码 12 行处崩溃,崩溃原因是调用了 conn->peerAddress() 方法。为什么这个方法的调用可能会引起崩溃?现在可以一目了然地看出了吗?

崩溃原因是传入的 conn 对象和上一个例子中的 sp2 一样都是另外一个 std::shared_ptr 的引用,当连接断开时,对应的 TcpConnection 对象可能早已被销毁,而 conn 引用就会变成空指针(严格来说是不再拥有一个 TcpConnection 对象),此时调用 TcpConnection 的 peerAddress() 方法就会产生和上一个示例一样的错误。
4、作为类成员变量时,应该优先使用前置声明(forward declarations)

我们知道,为了减小编译依赖加快编译速度和生成二进制文件的大小,C/C++ 项目中一般在 *.h 文件对于指针类型尽量使用前置声明,而不是直接包含对应类的头文件。例如:

//Test.h
//在这里使用A的前置声明,而不是直接包含A.h文件
class A;

class Test
{
public:
    Test();
    ~Test();

private:
    A *m_pA;
};

同样的道理,在头文件中当使用智能指针对象作为类成员变量时,也应该优先使用前置声明去引用智能指针对象的包裹类,而不是直接包含包含类的头文件。

//Test.h
#include <memory>

//智能指针包裹类A,这里优先使用A的前置声明,而不是直接包含A.h
class A;

class Test
{
public:
    Test();
    ~Test();

private:
    std::unique_ptr<A> m_spA;
};

Modern C/C++ 已经变为 C/C++ 开发的趋势,希望能善用和熟练这些智能指针对象。

智能指针陷阱:

1、不使用相同的内置指针值初始化(或reset)多个智能指针。
2、不delete get()返回的指针
3、不使用get()初始化或reset另一个智能指针
4、如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
5、如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

小结


1)不要使用std::auto_ptr
2)当你需要一个独占资源所有权(访问权+生命控制权)的指针,且不允许任何外界访问,请使用std::unique_ptr
3)当你需要一个共享资源所有权(访问权+生命控制权)的指针,请使用std::shared_ptr
4)当你需要一个能访问资源,但不控制其生命周期的指针,请使用std::weak_ptr

推荐用法:一个shared_ptr和n个weak_ptr搭配使用 而不是n个shared_ptr。

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值