深入实践C++11智能指针

概念

  C/C++ 语言最为人所诟病的特性之一就是存在内存泄露问题,因此后来的大多数语言都提供了内置内存分配与释放功能,有的甚至干脆对语言的使用者屏蔽了内存指针这一概念。这里不置贬褒,手动分配内存与手动释放内存有利也有弊,自动分配内存和自动释放内存亦如此,这是两种不同的设计哲学。有人认为,内存如此重要的东西怎么能放心交给用户去管理呢?而另外一些人则认为,内存如此重要的东西怎么能放心交给系统去管理呢?在 C/C++ 语言中,内存泄露的问题一直困扰着广大的开发者,因此各类库和工具的一直在努力尝试各种方法去检测和避免内存泄露,如 boost,智能指针技术应运而生。

  智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。简要的说,智能指针利用了 C++ 的 RAII 机制,在智能指针对象作用域结束后,会自动做内存释放的相关操作,不需要我们再手动去操作内存。但是智能指针也不只是说的简简单单的自动释放内存这么简单,在使用上有一些坑需要注意,将会在下文中提到。

  C++ 中有四种智能指针:

  • auto_pt:已经被 C++11 弃用且被 unique_prt 代替
  • unique_ptr:C++11 支持
  • shared_ptr:C++11 支持
  • weak_ptr :C++11 支持

一、std::auto_ptr

  在这个年代讨论 std::auto_ptr 不免有点让人怀疑是不是有点过时了,确实如此,随着 C++11 标准的出现(最新标准是 C++20),std::auto_ptr 已经被彻底放弃,取而代之是 std::unique_ptr。基本用法如下代码所示:

#include <iostream>
#include <memory>
class A {
public:
    A() { std::cout << "A constructor" << std::endl; }
    ~A() { std::cout << "A destructor" << std::endl; }
    void test() { std::cout << "test" << std::endl; }
};

int main() {
    std::auto_ptr<A> a(new A());		// 初始化方式1
    std::auto_ptr<A> b;					// 初始化方式2
    b.reset(new A);
    return 0;
}

  编译时会警告:warning: 'template<class> class std::auto_ptr' is deprecated [-Wdeprecated-declarations]。说明 auto_ptr 已经过时了。

  智能指针对象 a 和 b 均持有一个在堆上分配 A 对象,这两块堆内存均可以在 a 和 b 释放时得到释放。这是基本用法。

  std::auto_ptr 真正让人容易误用的地方是其不常用的复制语义,即当复制一个 std::auto_ptr 对象时(拷贝复制或 operator= 复制),原对象所持有的堆内存对象也会转移给复制出来的对象。示例代码如下:

#include <iostream>
#include <memory>

class A {
public:
    A() { std::cout << "A constructor" << std::endl; }
    ~A() { std::cout << "A destructor" << std::endl; }
    void test() { std::cout << "test" << std::endl; }
};

int main() {
    // 测试拷贝构造
    std::auto_ptr<A> a(new A());
    std::auto_ptr<A> b(a);
    if (a.get() == nullptr) {
        std::cout << "a is empty." << std::endl;
    } else {
        std::cout << "a is not empty." << std::endl;
    }
    if (b.get() == nullptr) {
        std::cout << "b is empty." << std::endl;
    } else {
        std::cout << "b is not empty." << std::endl;
    }

    // 测试赋值构造
    std::auto_ptr<A> c(new A());
    std::auto_ptr<A> d;
    d = c;
    if (c.get() == nullptr) {
        std::cout << "c is empty." << std::endl;
    } else {
        std::cout << "c is not empty." << std::endl;
    }
    if (d.get() == nullptr) {
        std::cout << "d is empty." << std::endl;
    } else {
        std::cout << "d is not empty." << std::endl;
    }
    return 0;
}

// 执行后输出
A constructor
a is empty.
b is not empty.
A constructor
c is empty.
d is not empty.
A destructor
A destructor

  分析:上述代码中分别利用拷贝构造(a => b)和 赋值构造(c => d)来创建新的 std::auto_ptr 对象,因此 a 持有的堆对象被转移给 b ,c 持有的堆对象被转移给 d 。而 a 和 c 已经指向 nullptr,若现在再对 a 和 c 进行访问并操作,将会出现内存错误问题。

  由于 std::auto_ptr 这种不常用的复制语义,应该避免在 stl 容器中使用 std::auto_ptr,例如绝不应该写出如下代码:std::vector<std::auto_ptr<int>> v;

  当用算法对容器操作的时候(如最常见的容器元素遍历),很难避免不对容器中的元素实现赋值传递,这样便会使容器中多个元素被置为空指针,这不是我们想看到的,会造成很多意想不到的错误。

  以史为鉴,作为 std::auto_ptr 的替代者 std::unique_ptr 吸取了这个经验教训。正因为 std::auto_ptr 的设计存在如此重大缺陷,C++11 标准在充分借鉴和吸收了 boost 库中智能指针的设计思想,引入了三种类型的智能指针,即 std::unique_ptr、std::shared_ptr 和 std::weak_ptr

  boost 还有 scoped_ptr,C++11 并没有全部照搬,而是选择了三个最实用的指针类型。在 C++11 中可以通过 std::unique_ptr 达到与 boost::scoped_ptr 一样的效果。

所有的智能指针类(包括 std::unique_ptr)均包含于头文件 <memory> 中。

  正因为 std::auto_ptr 存在上述设计上的缺陷,在 C++11及后续语言规范中 std::auto_ptr 已经被废弃,你的代码不应该再使用它。

二、std::unique_ptr

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

可以使用以下方式初始化一个 std::unique_ptr 对象:

#include <iostream>
#include <memory>

class A {
public:
    A() { std::cout << "A constructor" << std::endl; }
    ~A() { std::cout << "A destructor" << std::endl; }
    void test() { std::cout << "test" << std::endl; }
};

int main() {
    // 初始化方式1
    std::unique_ptr<A> a(new A());

    // 初始化方式2
    std::unique_ptr<A> b;
    b.reset(new A());

    // 初始化方式3 (-std=c++14),这种方式执行了两次析构函数
    std::unique_ptr<A> c = std::make_unique<A>(A());
    return 0;
}

  应该尽量使用初始化方式 3 的方式去创建一个 std::unique_ptr 而不是方式 1 和 2,因为形式 3 更安全,原因 Scott Meyers 在其《Effective Modern C++》中已经解释过了,有兴趣的可以阅读此书相关章节。

  令很多人对 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)...));
}

  鉴于 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;
};

  因此,下列代码是无法通过编译的:

#include <iostream>
#include <memory>
class A {
public:
    A() { std::cout << "A constructor" << std::endl; }
    ~A() { std::cout << "A destructor" << std::endl; }
    void test() { std::cout << "test" << std::endl; }
};

int main() {
    std::unique_ptr<A> a(std::make_unique<A>(A()));;
// 拷贝构造无法通过编译
// std::unique_ptr<A> b(a);
    std::unique_ptr<A> c;
// 赋值运算无法通过编译
// c = a;
    return 0;
}

  禁止复制语义也存在特例,即可以通过一个函数返回一个 std::unique_ptr

#include <iostream>
#include <memory>
class A {
public:
    A() { std::cout << "A constructor" << std::endl; }
    ~A() { std::cout << "A destructor" << std::endl; }
    void test() { std::cout << "test" << std::endl; }
};

std::unique_ptr<A> func(A a) {
    std::unique_ptr<A> tmp(new A(a));
    return tmp;
}

int main() {
    std::unique_ptr<A> up1 = func(A());
    return 0;
}

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

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

#include <iostream>
#include <memory>

class A {
public:
    A() { std::cout << "A constructor" << std::endl; }
    ~A() { std::cout << "A destructor" << std::endl; }
    void test() { std::cout << "test" << std::endl; }
};

std::unique_ptr<A> func(A a) {
    std::unique_ptr<A> tmp(new A(a));
    return tmp;
}

int main(){
    std::unique_ptr<A> a(std::make_unique<A>(A()));
    std::unique_ptr<A> b(std::move(a));
    std::cout << ((a.get() == nullptr) ? "a is NULL" : "a is not NULL") << std::endl;
    std::unique_ptr<A> c;
    c = std::move(b);
    std::cout << ((b.get() == nullptr) ? "b is NULL" : "b is not NULL") << std::endl;
    return 0;
}
// 执行结果
A constructor
A destructor
a is NULL
b is NULL
A destructor

  以上代码利用 std::move 将 a 持有的堆内存转移给 b,再把 b 转移给 c 。最后,a 和 b 不再持有堆内存的引用,变成一个空的智能指针对象。并不是所有的对象的 std::move 操作都有意义,只有实现了移动构造函数或移动赋值运算符的类才行,而std::unique_ptr 正好实现了这二者,以下是实现伪码:

template <typename T, typename Deletor>
class unique_ptr {
    // 其他函数省略...
public:
    unique_ptr(unique_ptr &&rhs) {
        this->m_pT = rhs.m_pT;
        // 源对象释放
        rhs.m_pT = nullptr;
    }
    unique_ptr &operator=(unique_ptr &&rhs) {
        this->m_pT = rhs.m_pT;
        // 源对象释放
        rhs.m_pT = nullptr;
        return *this;
    }
private:
    T *m_pT;
};

  这是 std::unique_ptr 具有移动语义的原因。关于移动构造和 std::move 也是 C++11 的新特性,这里不详细说明。

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

#include <iostream>
#include <memory>
int main() {
    // 创建10个int类型的堆对象
    // 形式1
    std::unique_ptr<int[]> a(new int[10]);
    // 形式2
    std::unique_ptr<int[]> b;
    b.reset(new int[10]);
    // 形式3
    std::unique_ptr<int[]> c(std::make_unique<int[]>(10));
    for (int i = 0; i < 10; ++i) {
        a[i] = i;
        b[i] = i;
        c[i] = i;
    }
    for (int i = 0; i < 10; ++i) {
        std::cout << a[i] << ", " << b[i] << ", " << c[i] << std::endl;
    }
    return 0;
}

  std::shared_ptrstd::weak_ptr 也可以持有一组堆对象,用法与 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 进行交换,在它们两者之间转移管理指针的所有权而不破坏二者。

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

  默认情况下,智能指针对象在析构时只会释放其持有的堆内存(调用 delete或者 delete[]),但是假设这块堆内存代表的对象还对应一种需要回收的资源(如操作系统的套接字句柄、文件句柄等),我们可以通过自定义智能指针的资源释放函数。假设现在有一个 Socket 类,对应着操作系统的套接字句柄,在回收时需要关闭该对象,我们可以如下自定义智能指针对象的资源析构函数,这里以 std::unique_ptr 为例:

class Socket {
public:
    Socket() {}
    ~Socket() {}
    // 关闭资源句柄
    void close() {}
};
int main() {
    // Lambda 函数
    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 是你要释放的对象类型,deletor 是一个自定义函数指针。上述代码表示 deletor 有点复杂(是 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 基本相同。示例:

#include <iostream>
#include <memory>

class A {
public:
    A() { std::cout << "A constructor" << std::endl; }
    ~A() { std::cout << "A destructor" << std::endl; }
    void test() { std::cout << "test" << std::endl; }
};

int main() {
    // 初始化方式1
    std::shared_ptr<A> a(new A());
    // 初始化方式2
    std::shared_ptr<A> b;
    b.reset(new A());
    // 初始化方式3
    std::shared_ptr<A> c;
    c = std::make_shared<A>(A());
    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 constructor" << std::endl; }
    void test() { std::cout << "test" << std::endl; }
};

int main() {
    {
        // 初始化方式1
        std::shared_ptr<A> a(new A());
        std::cout << "use count: " << a.use_count() << std::endl;
        // 初始化方式2
        std::shared_ptr<A> b(a);
        std::cout << "use count: " << a.use_count() << std::endl;
        b.reset();
        std::cout << "use count: " << a.use_count() << std::endl;
        {
            std::shared_ptr<A> c = a;
            std::cout << "use count: " << a.use_count() << std::endl;
        }
        std::cout << "use count: " << a.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

常用函数

  • void swap (unique_ptr& x)

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

  • void reset()void reset (ponit p)

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

  • element_type* get()

  得到其管理的指针。

  • long 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> a(new A());
    std::shared_ptr<A> b = a->getSelf();
    std::cout << "use count: " << a.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 函数生成 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 对象得不到释放。当程序执行到 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

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

  std::weak_ptr 可以从一个 std::shared_ptr 或另一个 std::weak_ptr 对象构造,std::shared_ptr 可以直接赋值给 std::weak_ptr ,也可以通过 std::weak_ptrlock() 函数来获得 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_ptrlock() 方法得到一个 std::shared_ptr 对象然后继续操作资源,以下代码演示了该用法:

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

std::shared_ptr<TcpConnection> conn = tmpConn_.lock(); // 没过期就暂时锁上
if (conn) {
    // 对conn进行操作,省略...
}

  有的人可能对上述代码产生疑问,既然使用了 std::weak_ptrexpired() 方法判断了对象是否存在,为什么不直接使用 std::weak_ptr 对象对引用资源进行操作呢?实际上这是行不通的,std::weak_ptr 类没有重写 operator->operator* 方法,因此不能像 std::shared_ptrstd::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 的应用场景,经典的例子是订阅者模式或者观察者模式中。这里以订阅者为例来说明,消息发布器只有在某个订阅者存在的情况下才会向其发布消息,而不能管理订阅者的生命周期。

#include <vector>
#include <memory>
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;
};

常用函数

  • void swap (weak_ptr& x)

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

  • void reset()

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

  • long 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 对象也算作一个所有者,所以这个函数锁定了拥有的指针,防止它被释放(至少在返回的对象没有释放它的情况下)。 此操作以原子方式执行。

  • 智能指针的大小

  一个 std::unique_ptr 对象大小与裸指针大小相同(即 sizeof(std::unique_ptr<int>) == 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;
}
// std_unique_ptr 的大小总是和原始指针大小一样,std::shared_ptr 和 std::weak_ptr 大小是原始指针的一倍。

智能指针使用注意事项

  C++ 新标准提倡的理念之一是不应该再手动调用 delete 或者 free 函数去释放内存了,而应该把它们交给新标准提供的各种智能指针对象。C++ 新标准中的各种智能指针是如此的实用与强大,在现代 C++ 项目开发中,我们应该尽量去使用它们。智能指针虽然好用,但稍不注意,也可能存在许多难以发现的 bug,这里我根据经验总结了几条:

  • 一旦一个对象使用智能指针管理后,就不该再使用原始裸指针去操作
#include <memory>
class Subscriber {};
int main() {
    Subscriber *a = new Subscriber();
    std::unique_ptr<Subscriber> b(a);
    delete a;
    return 0;
}

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

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

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

#include <memory>
class Subscriber {};
int main() {
    Subscriber *a = new Subscriber();
    std::unique_ptr<Subscriber> b(a);
    // c 和 a 指向同一个对象
    Subscriber *c = b.get();
    return 0;
}
  • 分清楚场合应该使用哪种类型的智能指针

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

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

  前面的例子,一定让你觉得非常容易知道一个智能指针的持有的资源是否还有效,但是还是建议在不同场景谨慎一点,有些场景是很容易造成误判。例如下面的代码:

#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();             // T对象 在堆中的内存被释放
    sp2->doSomething();     // 导致了 sp2 已经不再持有对象的引用,程序会在这里出现意外的行为:Process finished with exit code -1073741819 (0xC0000005)
    return 0;
}

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

  你一定仍然觉得这个例子也能很明显地看出问题,ok,让我们把这个例子放到实际开发中再来看一下:

// 连接断开
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;
        }
    }
}

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

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

  • 作为类成员变量时,应该优先使用前置声明(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;
};

智能指针的简单实现

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

#include <iostream>
#include <atomic>

using namespace std;

template<typename T>
class WeakPtr;

template<typename T>
class SharedPtr;

template<typename T>
class Counter;

template<typename T>
class WeakPtr {
public:
    /*
     * 构造函数,用SharedPtr对象构造
     */
    explicit WeakPtr(SharedPtr<T> &s) : m_ptr(s.m_ptr), m_cnt(s.m_cnt) {
        m_cnt->m_weakCount++;
        cout << "S Construct W." << endl;
    }

    /*
     * 构造函数,用WeakPtr对象构造
     */
    WeakPtr(WeakPtr<T> &w) : m_ptr(w.m_ptr), m_cnt(w.m_cnt) {
        m_cnt->m_weakCount++;
        cout << "W Construct W." << endl;
    }

    ~WeakPtr() {
        release();
    }

    /*
     * 赋值构造函数,用另一个SharedPtr对象构造
     */
    WeakPtr<T> &operator=(SharedPtr<T> &s) {
        release();
        m_cnt = s.m_cnt;
        m_cnt->m_weakCount++;
        m_ptr = s.m_ptr;
        cout << "S Assign Construct W." << endl;
        return *this;
    }

    /*
     * 赋值构造函数,用另一个WeakPtr对象构造
     */
    WeakPtr<T> &operator=(const WeakPtr<T> &w) {
        if (this != &w) {
            release();
            m_cnt = w.m_cnt;
            m_cnt->m_weakCount++;
            m_ptr = w->m_ptr;
            cout << "W Assign Construct W." << endl;
        }
        return *this;
    }

    /*
     * WeakPtr通过lock函数获得SharedPtr
     */
    SharedPtr<T> &lock() {
        return SharedPtr<T>(*this);
    }

    /*
     * 检查SharedPtr是否已过期
     */
    bool expired() {
        if (m_cnt) {
            if (m_cnt->m_refCount > 0)
                return false;
        }
        return true;
    }

    WeakPtr() = delete;       // WeakPtr禁止默认构造,只能从SharedPtr或者WeakPtr构造
    T &operator*() = delete;  // WeakPtr禁止*
    T *operator->() = delete; // WeakPtr禁止->

private:
    void release() {
        if (m_cnt) {
            m_cnt->m_weakCount--;
            if (m_cnt->m_weakCount < 1 && m_cnt->m_refCount < 1) {
                delete m_cnt;
                m_cnt = nullptr;
                cout << "Delete Cnt." << endl;
            }
            cout << "WeakPtr Release." << endl;
        }
    }

private:
    T *m_ptr;
    Counter<T> *m_cnt;
};

template<typename T>
class SharedPtr {

    friend class WeakPtr<T>;

public:
    /*
     * 构造函数,用原生指针构造
     */
    explicit SharedPtr(T *ptr) : m_ptr(ptr), m_cnt(new Counter<T>) {
        if (ptr) {
            m_cnt->m_refCount = 1;
        }
        cout << "Ptr Construct S." << endl;
    }

    ~SharedPtr() {
        release();
    }

    /*
     * 拷贝构造函数,用另一个SharedPtr对象构造
     */
    SharedPtr(const SharedPtr &s) {
        m_ptr = s.m_ptr;
        s.m_cnt->m_refCount++;
        m_cnt = s.m_cnt;
        cout << "S Copy Construct S." << endl;
    }

    /*
     * 拷贝构造函数,用另一个WeakPtr对象构造
     * 为了WeakPtr对象调用自己的lock()方法将自己传进来构造一个SharedPtr返回
     */
    explicit SharedPtr(const WeakPtr<T> &w) {
        m_ptr = w.m_ptr;
        w.m_cnt->m_refCount++;
        m_cnt = w.m_cnt;
        cout << "W Copy Construct S." << endl;
    }

    /*
     * 赋值构造函数,用另一个SharedPtr对象构造
     */
    SharedPtr<T> &operator=(const SharedPtr<T> &s) {
        if (this != s) {
            this->release();
            m_ptr = s.m_ptr;
            s.m_cnt->m_refCount++;
            m_cnt = s.m_cnt;
            cout << "S Assign Construct S." << endl;
        }
        return *this;
    }

    T &operator*() {
        return *m_ptr;
    }

    T *operator->() {
        return m_ptr;
    }

protected:
    void release() {
        m_cnt->m_refCount--;
        if (m_cnt->m_refCount < 1) {
            delete m_ptr;
            m_ptr = nullptr;
            cout << "SharedPtr Delete Ptr." << endl;
            if (m_cnt->m_weakCount < 1) {
                delete m_cnt;
                m_cnt = nullptr;
                cout << "SharedPtr Delete Cnt." << endl;
            }
            cout << "SharedPtr Release." << endl;
        }
    }

private:
    T *m_ptr;
    Counter<T> *m_cnt;
};


/*
 * 计数器
 * Counter对象就是用来申请一块内存存储引用计数
 * m_refCount是SharedPtr的引用计数
 * m_weakCount是WeakPtr的引用计数
 * 当m_weakCount为0时删除Counter对象
 */
template<typename T>
class Counter {
    friend class SharedPtr<T>;

    friend class WeakPtr<T>;

public:
    Counter() : m_refCount(0), m_weakCount(0) {}

    virtual ~Counter() = default;

    Counter(const Counter &) = delete;

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

private:
    atomic_uint m_refCount;  // shared,原子操作
    atomic_uint m_weakCount; // weak,原子操作
};

  上面的实现可能不是非常严谨,仅实现了常用的的函数接口而已,但其主要的目的是为了更深刻的了解智能指针的原理,这样才能更有把握的使用智能指针,只有了解它的内部实现,对于使用中的一些坑才能有效避免。

原文地址:

https://blog.csdn.net/code_peak/article/details/119722167

posted @ 2023-10-18 23:19  黄河大道东  阅读(20)  评论(0编辑  收藏  举报