C/C++ | 每日一练 (6)

发布于:2025-03-17 ⋅ 阅读:(24) ⋅ 点赞:(0)

💢欢迎来到张胤尘的技术站
💥技术如江河,汇聚众志成。代码似星辰,照亮行征程。开源精神长,传承永不忘。携手共前行,未来更辉煌💥

C/C++ | 每日一练 (6)

题目

什么是智能指针?智能指针和普通指针的区别是什么?有哪些常用的智能指针?说一下底层如何实现的?

参考答案

普通指针存在的问题?

普通指针(如 C/C++ 中的 int*char* 等)是编程中非常灵活的工具,但同时也带来了许多潜在问题。这些问题主要源于指针的低级特性和对资源管理的直接依赖。

内存泄漏

内存泄漏是指动态分配的内存没有被正确释放,导致程序占用的内存不断增加,最终可能导致程序性能下降甚至崩溃。比如使用 malloc 或者 new 分配内存后,必须在合适的时候使用 free 或者 delete 释放内存。如果忘记释放内存,就会导致内存泄漏。例如:

void leakMemory() {
    int* ptr = new int[1000]; // 分配内存
    // 忘记释放内存
}

int main() {
    leakMemory();	// 函数调用
    return 0;
}

当程序每次调用 leakMemory 函数时,都会分配1000个 int 的内存,但这些内存永远不会被释放。

内存泄漏的对程序的危害:

  • 性能下降:随着程序运行时间的增加,内存泄漏会导致可用内存逐渐减少,程序运行速度变慢。
  • 系统崩溃:如果内存泄漏严重,可能会耗尽系统资源,导致程序崩溃甚至整个系统崩溃。
  • 资源浪费:未释放的内存无法被系统或其他程序使用,造成资源浪费。
悬空指针

另外,还有一种无效的指针——悬空指针,在使用时可能导致未定义行为甚至程序崩溃。

悬空指针是指指针曾经指向一个有效的内存位置,但该内存已被释放或回收,导致指针变得无效。尽管指针仍然保存着原来的地址,但访问该地址会产生未定义行为,因为该地址可能已经被分配给其他对象或成为不可访问的区域。例如:

void danglingPointer()
{
    int *ptr = new int(10);
    delete ptr; // 释放内存
    *ptr = 20;  // 通过悬空指针访问内存,未定义行为
}
int main()
{
    danglingPointer();	// 函数调用
    return 0;
}
指针被重复释放

重复释放是指对同一块内存调用多次 deletefree。这可能导致程序崩溃或内存损坏。例如:

int *doubleFree()
{
    int *ptr = new int(10);
    delete ptr; // 第一次释放
    return ptr;
}

int main()
{
    int *p = doubleFree();

    // free(): double free detected in tcache 2
    // Aborted (core dumped)
    delete p; // 第二次释放,未定义行为
    return 0;
}

另外,在使用普通指针时如果不注意可能还会有一些其他更为严重的问题产生,这里就不再一一列举。

说到这里有些同学可能会有疑问:当程序员的能力足够强、足够的仔细是不是就可以避免这些问题?这么说也很有道理,确实可以避免许多常见的问题,比如内存泄漏、悬空指针等。然而,我认为 “人非圣贤,孰能无过”,即使是能力最强、最谨慎的程序员也难以完全避免错误,尤其是在特别复杂的项目或团队协作环境中。使用普通指针仍然存在一些难以克服的局限性,这些局限性使得智能指针和其他现代C++ 特性成为更好的选择。

那么,接下来就针对 C++ 标准库中的智能指针进行深度讲解。

智能指针

智能指针是 C++ 标准库中的一种类模板,用于自动管理动态分配的内存。它们可以防止内存泄漏和悬挂指针问题,并且提供了异常安全性。

C++11 标准库引入了三种智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr

本文章只讨论 C++11 标准库中的智能指针,Boost 库中提供的智能指针本文章不再讨论。另外std::auto_ptrC++11 中被废弃,在 C++17 中被移除。本文章也不再讨论。

另外,本篇文章的所有底层源码均来源于 libstdc++ ,其他平台的源码实现可能会有些出入,请注意甄别。

std::unique_ptr

std::unique_ptrC++11 引入的一种智能指针,表示对资源(通常是动态分配的内存)的独占所有权。它通过移动语义(而不是拷贝语义)来转移资源的所有权,确保同一时刻只有一个 unique_ptr 可以管理某个资源。

#include <memory>
#include <iostream>

int main()
{
    std::unique_ptr<int> p(new int(10));
    std::cout << *p << std::endl;	// 10

    // cannot be referenced -- it is a deleted function
    // std::unique_ptr<int> p1 = p;
    // p = p1;
}

在源码中,std::unique_ptr 因为禁用了拷贝构造函数和赋值运算符函数,导致 std::unique_ptr 不支持拷贝语义。如下所示:

// Disable copy from lvalue.
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;

虽然 std::unique_ptr 禁用了拷贝构造和赋值操作运算符,但是提供了移动拷贝构造和移动赋值操作运算符,也就是说 std::unique_ptr 支持了移动语义。如下所示:

// Move constructor.
unique_ptr(unique_ptr&&) = default;
unique_ptr& operator=(unique_ptr&&) = default;

注意:std::unique_ptr 模板化的移动拷贝构造和移动赋值操作运算符源码这里不再列举,感兴趣的同学自行查看源代码。

#include <memory>
#include <iostream>

int main()
{
    std::unique_ptr<int> p(new int(10));
    std::unique_ptr<int> p1(new int(21));
    std::cout << *p << std::endl;  // 10
    std::cout << *p1 << std::endl; // 21

    std::unique_ptr<int> p3(std::move(p)); // 移动构造

    std::unique_ptr<int> p4;
    p4 = std::move(p1); // 移动赋值操作运算符

    std::cout << *p3 << std::endl; // 10
    std::cout << *p4 << std::endl; // 21

    return 0;
}
底层结构

std::unique_ptr 的实现中,__uniq_ptr_data 是一个底层数据结构,用于封装和管理 std::unique_ptr 的底层指针和删除器。

为了可以更好的理解下面的源码,这里先解释一下删除器:删除器是一个非常重要的组件,它定义了如何释放 std::unique_ptr 所管理的资源。另外,删除器的作用不仅仅是简单地释放内存,它还可以执行更复杂的清理操作,比如关闭文件句柄、释放网络连接、销毁自定义对象等。实际开发过程中,通过自定义删除器,std::unique_ptr 能够灵活地管理各种类型的资源,而不仅仅是动态分配的内存。

在下面的 std::unique_ptr 模板类中,类型 _Tp 表示底层指针的泛型参数,而 _Dp 表示所使用的删除器,如果未指定删除器则使用默认删除器 default_delete<_Tp>。如下所示:

template <typename _Tp, typename _Dp = default_delete<_Tp>>
class unique_ptr
{
    // ...
	
    __uniq_ptr_data<_Tp, _Dp> _M_t;
    
    // ...
};

以上代码只是部分截取,请注意甄别。

在上述结构体中, _M_t 是最为核心的成员属性,该属性是一个包含了两个部分的模板结构体,如下所示:

template <typename _Tp, typename _Dp,
    bool = is_move_constructible<_Dp>::value,
    bool = is_move_assignable<_Dp>::value>
struct __uniq_ptr_data : __uniq_ptr_impl<_Tp, _Dp>
{
    using __uniq_ptr_impl<_Tp, _Dp>::__uniq_ptr_impl;
    __uniq_ptr_data(__uniq_ptr_data&&) = default;
    __uniq_ptr_data& operator=(__uniq_ptr_data&&) = default;
};

__uniq_ptr_data 又继承自 __uniq_ptr_impl__uniq_ptr_impl 也是一个模板类,用于封装和管理 std::unique_ptr 的指针和删除器。它是 std::unique_ptr 的底层实现细节,隐藏了指针和删除器的管理逻辑,使得 std::unique_ptr 的接口更加简洁和安全。源码如下所示:

template <typename _Tp, typename _Dp>
class __uniq_ptr_impl
{
    template <typename _Up, typename _Ep, typename = void>
    struct _Ptr
    {
		using type = _Up*;
    };

    template <typename _Up, typename _Ep>
    struct
    _Ptr<_Up, _Ep, __void_t<typename remove_reference<_Ep>::type::pointer>>
    {
        using type = typename remove_reference<_Ep>::type::pointer;
    };

public:
 	// ...

    pointer&   _M_ptr() { return std::get<0>(_M_t); }
    pointer    _M_ptr() const { return std::get<0>(_M_t); }
    _Dp&       _M_deleter() { return std::get<1>(_M_t); }
    const _Dp& _M_deleter() const { return std::get<1>(_M_t); }

	// ...

private:
	tuple<pointer, _Dp> _M_t;
};

以上代码只是部分截取,请注意甄别。

从上述代码中可以看出,__uniq_ptr_impl 又使用 tuple 存储底层指针和删除器,同时对外也提供了底层指针 pointer 和删除器 _Dp 的访问函数。

常用操作

下面针对 std::unique_ptr 中常用函数的底层源码进行分析。

释放所有权

release 函数释放 std::unique_ptr 当前管理的指针。

pointer release() noexcept
{ 
    return _M_t.release(); 
}

调用 _M_trelease 函数,取到底层指针 __p,直接将底层指针置为 nullptr,最终返回当前管理的指针。如下所示:

pointer release() noexcept
{
    pointer __p = _M_ptr();
    _M_ptr() = nullptr;
    return __p;
}

如果将 std::unique_ptr 的底层指针置为 nullptr,表示它不再管理任何资源。

重置

reset 函数用于替换 std::unique_ptr 当前管理的底层指针。

void reset(pointer __p = pointer()) noexcept
{
	static_assert(__is_invocable<deleter_type&, pointer>::value,
      "unique_ptr's deleter must be invocable with a pointer");
	_M_t.reset(std::move(__p));
}

调用 _M_treset 函数,取到底层指针 __old_p,将新的指针 __p 赋值底层指针。给如下所示:

void reset(pointer __p) noexcept
{
    const pointer __old_p = _M_ptr();
    // 新的指针赋值给 unique_ptr
    _M_ptr() = __p;
    // 如果当前指针不为空
    if (__old_p)
        // 调用删除器释放当前指针
    	_M_deleter()(__old_p);
}

如果当前底层指针 __old_p 不为空,则首先获取到删除器。给如下所示:

// 获取取到删除器
const _Dp& _M_deleter() const { return std::get<1>(_M_t); }

然后,通过调用删除器释放当前指针(下面这个是默认删除器的释放指针的代码)。给如下所示:

template<typename _Tp>
struct default_delete
{
    // ...
    
    void operator()(_Tp* __ptr) const
    {
        static_assert(!is_void<_Tp>::value,
              "can't delete pointer to incomplete type");
        static_assert(sizeof(_Tp)>0,
              "can't delete pointer to incomplete type");
        delete __ptr;
    }
};

以上代码只是部分截取,请注意甄别。

获取原始指针

get 函数返回 std::unique_ptr 当前管理的底层指针。它不会转移所有权,只是提供对底层指针的访问。

pointer get() const noexcept
{ 
    return _M_t._M_ptr();
}

调用 _M_t_M_ptr 函数,取到底层指针 pointer,直接返回该指针。给如下所示:

pointer _M_ptr() const { return std::get<0>(_M_t); }
交换

swap 函数用于交换两个 std::unique_ptr 的底层指针和删除器。

void swap(unique_ptr& __u) noexcept
{
    static_assert(__is_swappable<_Dp>::value, "deleter must be swappable");
    _M_t.swap(__u._M_t);
}

调用 _M_tswap 函数,底层使用的是标准库的 std::swap 函数,然后交换底层指针和删除器。给如下所示:

void swap(__uniq_ptr_impl& __rhs) noexcept
{
    using std::swap;
    swap(this->_M_ptr(), __rhs._M_ptr());
    swap(this->_M_deleter(), __rhs._M_deleter());
}
std::shared_ptr

std::shared_ptr 也是 C++11 引入的一种智能指针,用于通过引用计数机制共享对象的所有权。它允许多个 std::shared_ptr 实例共享同一个对象,并在最后一个 std::shared_ptr 被销毁时自动销毁对象。

#include <memory>
#include <iostream>

int main()
{
    std::shared_ptr<int> p(new int(10));
    std::cout << *p << std::endl; // 10

    std::shared_ptr<int> p1;
    std::shared_ptr<int> p2 = p;	// 拷贝构造

    p1 = p2;	// 赋值运算符构造

    std::cout << *p << std::endl;  // 10
    std::cout << *p1 << std::endl; // 10
    std::cout << *p2 << std::endl; // 10
}

std::shared_ptrstd::unique_ptr 的其中之一的区别就是允许共享同一个对象的所有权,在源码中 std::shared_ptr 开放了拷贝构造函数和赋值运算符函数,也就是说支持拷贝语义。如下所示:

shared_ptr(const shared_ptr&) noexcept = default;
shared_ptr& operator=(const shared_ptr&) noexcept = default;

注意:std::shared_ptr 模板化的拷贝构造和赋值操作运算符源码这里不再列举,感兴趣的同学自行查看源代码。

同样的 std::shared_ptr 也开放了移动拷贝构造函数和移动赋值运算符函数,支持移动语义。如下所示:

shared_ptr(shared_ptr&& __r) noexcept
	: __shared_ptr<_Tp>(std::move(__r)) { }

shared_ptr& operator=(shared_ptr&& __r) noexcept
{
    this->__shared_ptr<_Tp>::operator=(std::move(__r));
    return *this;
}

注意:std::shared_ptr 模板化的移动拷贝构造和移动赋值操作运算符源码这里不再列举,感兴趣的同学自行查看源代码。

#include <memory>
#include <iostream>

int main()
{
    std::shared_ptr<int> p(new int(10));
    std::cout << *p << std::endl; // 10

    std::shared_ptr<int> p1;
    std::shared_ptr<int> p2 = std::move(p);

    p1 = std::move(p2);

    std::cout << *p1 << std::endl; // 10
}
底层结构

std::shared_ptr 是通过继承一个内部模板类 __shared_ptr<_Tp> 来实现的。这种设计允许 std::shared_ptr 继承底层的资源管理和引用计数逻辑,同时提供标准库的接口。如下所示:

template<typename _Tp>
class shared_ptr : public __shared_ptr<_Tp>
{
    // ...
}

以上代码只是部分截取,请注意甄别。

__shared_ptr<_Tp> 类中封装了重要的两个成员属性:_M_ptr_M_refcount

  • _M_ptr:指向被管理底层指针的指针。
  • _M_refcount:引用计数器,用于跟踪资源的引用计数。
template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr 
    : public __shared_ptr_access<_Tp, _Lp>
{
	// ...
private:
    element_type*     _M_ptr;         // Contained pointer.
    __shared_count<_Lp>  _M_refcount;    // Reference counter.
};

以上代码只是部分截取,请注意甄别。

在引用计数器 _M_refcount 中,又封装了引用计数逻辑的类 __shared_count。如下所示:

template<_Lock_policy _Lp>
class __shared_count
{
    // ...
private:
    _Sp_counted_base<_Lp>*  _M_pi;
};

以上代码只是部分截取,请注意甄别。

_Sp_counted_base 也是一个模板类,封装了实际的引用计数逻辑。它包含两个原子计数器:

  • _M_use_count:记录强引用计数(即 std::shared_ptr 的个数)。
  • _M_weak_count:记录弱引用计数(即 std::weak_ptr 的个数)。
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base
    : public _Mutex_base<_Lp>
{
	// ...
private:
    _Atomic_word  _M_use_count;     // #shared
    _Atomic_word  _M_weak_count;    // #weak + (#shared != 0)
};

以上代码只是部分截取,请注意甄别。


接下来,从整个 std::shared_ptr 的生命周期,深入了解 std::shared_ptr 是如何管理引用计数器的。

  • 当一个新的 std::shared_ptr 被创建时,_M_use_count_M_weak_count 默认都是 1。
explicit shared_ptr(_Yp* __p) : __shared_ptr<_Tp>(__p) { }

调用 __shared_ptr<_Tp>(__p) 构造函数,如下所示:

template<typename _Yp, typename = _SafeConv<_Yp>>
explicit
__shared_ptr(_Yp* __p)
: _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type())
{
    static_assert( !is_void<_Yp>::value, "incomplete type" );
    static_assert( sizeof(_Yp) > 0, "incomplete type" );
    _M_enable_shared_from_this_with(__p);
}

首先 _M_ptr(__p) 初始化数据指针,紧接着,调用 _M_refcount(__p, typename is_array<_Tp>::type()) 构造函数,如下所示:

template<typename _Ptr>
explicit __shared_count(_Ptr __p) : _M_pi(0)
{
    __try
    {
		_M_pi = new _Sp_counted_ptr<_Ptr, _Lp>(__p);
    }
    __catch(...)
    {
        delete __p;
        __throw_exception_again;
    }
}

调用 _M_pi 的构造函数,先初始化父类 _Sp_counted_base,当父类初始化完毕后再初始化 _Sp_counted_ptr,如下所示:

_Sp_counted_base() noexcept
      : _M_use_count(1), _M_weak_count(1) { }
  • 每当共享一个 std::shared_ptr 指针时,_M_use_count 会增加。

下面以赋值操作运算符为例。其他情况基本类似,这里不再赘述。

shared_ptr& operator=(const shared_ptr&) noexcept = default;

std::shared_ptr 使用了 default 的方式让编译器自动生成代码,这里可以查看官方文档:

cppreference.com

Shares ownership of the object managed by r. If r manages no object, *this manages no object too. Equivalent to shared_ptr®.swap(*this).

根据标准库的文档,拷贝赋值运算符的行为等价于以下代码:

shared_ptr(r).swap(*this);

首先构造一个临时的 std::shared_ptr 对象,它共享 r 的所有权。然后,通过 swap 方法交换临时对象和当前对象的内容。

template<typename _Yp,
	       typename = _Constructible<const shared_ptr<_Yp>&>>
shared_ptr(const shared_ptr<_Yp>& __r) noexcept
    : __shared_ptr<_Tp>(__r) { }

调用 __shared_ptr 的带参构造函数,如下所示:

template<typename _Yp, typename = _Compatible<_Yp>>
__shared_ptr(const __shared_ptr<_Yp, _Lp>& __r) noexcept
	: _M_ptr(__r._M_ptr), _M_refcount(__r._M_refcount) { }

其中,_M_refcount 又调用了 __shared_count 的拷贝构造函数,如下所示:

 __shared_count(const __shared_count& __r) noexcept
  : _M_pi(__r._M_pi)
{
    if (_M_pi != nullptr)
    	_M_pi->_M_add_ref_copy();
}

在该函数中当 _M_pi != nullptr 时,调用 _M_add_ref_copy 函数,在该函数内采用原子操作增加强引用计数器的值,如下所示:

void _M_add_ref_copy()
{ 
    __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1);
}

最后的交换逻辑参考 常用操作 小结中的内容,这里不再赘述。

  • 当一个 std::shared_ptr 被销毁时,_M_use_count 引用计数会减少。如果引用计数变为零,说明没有 std::shared_ptr 再持有该对象,此时对象会被销毁。

__shared_count 对象封装了引用计数的逻辑。当 std::shared_ptr 被销毁时,触发析构逻辑,如下所示:

 ~__shared_count() noexcept
{
    if (_M_pi != nullptr)
    	_M_pi->_M_release();
}

调用了 _M_release 函数,该函数内对引用计数器进行了减一的操作,当引用计数器为0时,触发资源回收的逻辑。如下所示:

void _M_release() noexcept
{
    // ...
    if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
    {
       // ...
        _M_dispose();

        // ...
        
        if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1)
          {
			// ...
        	_M_destroy();
          }
    }
}

以上代码只是部分截取,请注意甄别。

__exchange_and_add_dispatch 函数分别对 _M_use_count_M_weak_count 进行了减一操作,当 _M_use_count 值为 0 时调用 _M_dispose 函数;当 _M_weak_count 值为 0 时调用 _M_destroy 函数。如下所示:

virtual void _M_dispose() noexcept { delete _M_ptr; }
virtual void _M_destroy() noexcept { delete this; }

到目前为止,我们还未对 弱引用计数器 的作用进行说明。另外,从上面的描述中我们可以知道,强引用计数器的作用是为了跟踪 std::shared_ptr 管理的底层指针被多少个其他的 std::shared_ptr 共享。那么在这里我先抛出一个问题:在下面的代码示例中,class Aclass B 两个类的对象通过 std::shared_ptr 智能指针相互引用时,在程序结束时是否可以正常的将引用计数减少到0?

#include <memory>

class B;  // 前置声明

class A {
public:
    std::shared_ptr<B> b_ptr;
};

class B {
public:
    std::shared_ptr<A> a_ptr;
};

int main() {
    std::shared_ptr<A> aptr = std::make_shared<A>();
    std::shared_ptr<B> bptr = std::make_shared<B>();

    aptr->b_ptr = bptr;  // A 持有 B 的共享指针
    bptr->a_ptr = aptr;  // B 持有 A 的共享指针

    return 0;
}

答案:在这个例子中,AB 通过 std::shared_ptr 相互引用,形成一个循环依赖。当程序结束时,AB 的引用计数都为1,无法减少到0,因此不会触发删除器,导致内存泄漏

为什么?如何解决?解决办法是否和弱引用计数相关?这些悬念会在 std::weak_ptr 章节中一一解答。

常用操作
获取引用计数

use_count 函数返回当前 std::shared_ptr 管理的底层指针的引用计数,即有多少个 std::shared_ptr 实例共享同一个底层指针。

long use_count() const noexcept
{ 
    return _M_refcount._M_get_use_count(); 
}

调用 _M_refcount_M_get_use_count 函数,如果 std::shared_ptr 没有引用任何指针,则 _M_pi 对象为 nullptr 则返回 0,否则调用 _M_pi 对象的 _M_get_use_count 获取强引用计数值。如下所示:

long _M_get_use_count() const noexcept
{ 
    return _M_pi ? _M_pi->_M_get_use_count() : 0; 
}

紧接着,在 _M_get_use_count 中使用原子操作读取 _M_use_count 强引用计数器的值。如下所示:

long _M_get_use_count() const noexcept
{
    // No memory barrier is used here so there is no synchronization
    // with other threads.
    return __atomic_load_n(&_M_use_count, __ATOMIC_RELAXED);
}

其中,__ATOMIC_RELAXED 是最弱的内存顺序模型,仅保证操作的原子性,不提供任何内存顺序保证。

重置

reset 函数用于释放当前 std::shared_ptr 管理的指针。

void reset() noexcept
{ 
    __shared_ptr().swap(*this); 
}

reset 函数中使用默认构造创建了 __shared_ptr 临时对象,这个临时对象不管理任何资源(即它的引用计数为 0)。然后调用了 swap 函数将当前 std::shared_ptr 对象的内容与临时对象交换。交换后,当前 std::shared_ptr 的内容被清空。同时,临时对象接管了当前 std::shared_ptr 的资源,但由于临时对象即将被销毁,它的析构函数就会自动释放这些资源。

获取原始指针

get 函数返回 std::shared_ptr 管理的底层指针,但不会转移所有权。

element_type* get() const noexcept
{ 
    return _M_ptr; 
}
交换

swap() 函数用于交换两个 std::shared_ptr 的管理底层指针和引用器。

void swap(__shared_ptr<_Tp, _Lp>& __other) noexcept
{
    std::swap(_M_ptr, __other._M_ptr);
    _M_refcount._M_swap(__other._M_refcount);
}

使用标准库中的 swap 函数对底层指针进行交换,然后调用了 _M_swap 函数交换引用计数器。如下所示:

 void _M_swap(__shared_count& __r) noexcept
{
    _Sp_counted_base<_Lp>* __tmp = __r._M_pi;
    __r._M_pi = _M_pi;
    _M_pi = __tmp;
}
检测是否唯一

unique() 函数用于检查当前 std::shared_ptr 是否是其管理底层指针的唯一所有者。

bool unique() const noexcept
{ 
    return _M_refcount._M_unique(); 
}

调用 _M_unique 函数,返回一个 bool 值。如果 _M_get_use_count 函数的返回值等于1返回 true 否则返回 false。如下所示:

bool _M_unique() const noexcept
{ 
    return this->_M_get_use_count() == 1; 
}

_M_get_use_count 函数中调用了 _M_pi_M_get_use_count 方法,返回底层指针的强引用计数值。具体 _M_get_use_count 方法内容,请参考 获取引用计数 函数解析。

判断相等性

下面给出一段代码,该代码中判断了两个 std::shared_ptr 的相等性,如下所示:

#include <memory>
#include <iostream>

int main()
{
    std::shared_ptr<int> p(new int(10));
    std::shared_ptr<int> p1 = p;

    std::shared_ptr<int> p2(new int(10));

    std::cout << (p == p1) << std::endl; // 1
    std::cout << (p == p2) << std::endl; // 0
}

对象相等性判断的依据是 == 运算符重载函数的判断逻辑,函数如下所示:

template<typename _Tp, typename _Up>
_GLIBCXX_NODISCARD inline bool
operator==(const shared_ptr<_Tp>& __a, const shared_ptr<_Up>& __b) noexcept
{ return __a.get() == __b.get(); }

在该函数中,分别对两个 std::shared_ptr 调用了 get 函数,判断两个底层管理的指针是否相等,又因为指针的相等性是根据指针的地址是否相等来确定的,所以两个 std::shared_ptr 是否相等就是判断分别引用的底层指针地址是否相等。

std::weak_ptr

std::weak_ptrC++11 标准库中的一种智能指针,用于解决 std::shared_ptr 在使用过程中可能出现的循环引用问题,同时允许对资源进行“弱引用”。与 std::shared_ptr 不同,std::weak_ptr 不会增加资源的强引用计数,因此不会阻止资源的销毁。

我们重新看一下 std::shared_ptr 章节中给出的异常代码,如下所示:

#include <memory>

class B;  // 前置声明

class A {
public:
    std::shared_ptr<B> b_ptr;
};

class B {
public:
    std::shared_ptr<A> a_ptr;
};

int main() {
    std::shared_ptr<A> aptr = std::make_shared<A>();
    std::shared_ptr<B> bptr = std::make_shared<B>();

    aptr->b_ptr = bptr;  // A 持有 B 的共享指针
    bptr->a_ptr = aptr;  // B 持有 A 的共享指针

    return 0;
}

接下来,根据每行代码分析一下,为什么会出现在程序结束时,引用计数无法到 0 的问题。

  • 首先创建 aptr_M_use_count_M_weak_count 都是 1。
  • 紧接着创建 bptr_M_use_count_M_weak_count 也都是 1。
  • 当执行到 aptr->b_ptr = bptr 时,bptr_M_use_count 引用计数器增长变成了 2。
  • 同理当执行到 bptr->a_ptr = aptr 时,aptr_M_use_count 引用计数器也增长变成了 2。
  • 当程序 main 函数结束时,对函数栈中的栈对象依次进行析构:
    • 首先析构 bptr,将 bptr_M_use_count 引用计数器减少变成了 1,因为 _M_use_count 不是 0 无法触发 _M_dispose 函数的执行。
    • 然后析构 aptr,将 aptr_M_use_count 引用计数器也减少变成了 1,因为 _M_use_count 同样不是 0 也无法触发 _M_dispose 函数的执行。
  • 最终,两个 std::shared_ptr 所管理的底层指针都无法得到正确的资源释放,形成内存泄漏问题。

为了解决这个问题,就可以使用 std::weak_ptr 来打破循环引用。因为 std::weak_ptr 不会增加强引用计数,所以可以安全地访问资源。如下所示:

class B;  // 前置声明

class A {
public:
    std::weak_ptr<B> b_ptr;  // 使用 weak_ptr
};

class B {
public:
    std::weak_ptr<A> a_ptr;
};

int main() {
    std::shared_ptr<A> aptr = std::make_shared<A>();
    std::shared_ptr<B> bptr = std::make_shared<B>();

    aptr->b_ptr = bptr;  // A 持有 B 的弱引用
    bptr->a_ptr = aptr;  // B 持有 A 的弱引用

    return 0;  // 程序结束时,资源可以正确释放
}
  • 当执行到 aptr->b_ptr = bptr 时,bptr_M_use_count 引用计数器不会增长,但是会对 _M_weak_count 引用计数器增长,变成了 2。
  • 同理当执行到 bptr->a_ptr = aptr 时,aptr_M_use_count 引用计数器也不会增长,同样会对 _M_weak_count 引用计数器增长,变成了 2。
  • main 函数结束时,触发析构逻辑:
    • 首先析构 bptr
      • bptr_M_use_count 减少1,变为 0;因为 _M_use_count 为 0,触发 _M_dispose 函数,释放 B 的资源。
      • bptr_M_weak_count 减少1,变为1。
    • 然后析构 aptr
      • aptr_M_use_count 减少1,变为 0;因为 _M_use_count 为 0,触发 _M_dispose 函数,释放 A 的资源。
      • aptr_M_weak_count 减少1,变为1。
  • 当最后一个 std::weak_ptr 被析构时,_M_weak_count 减少到 0,触发 _M_destroy 函数的执行。至此内存都被正常释放,无内存泄漏问题。

接下来,我们一起深入 std::weak_ptr 中探究其背后的原理。

std::weak_ptr 支持拷贝语义和移动语义。如下所示:

weak_ptr(const weak_ptr&) noexcept = default;
weak_ptr& operator=(const weak_ptr& __r) noexcept = default;
weak_ptr(weak_ptr&&) noexcept = default;
weak_ptr& operator=(weak_ptr&& __r) noexcept = default;

注意:std::weak_ptr 模板化的拷贝构造、移动构造函数和移动、赋值操作运算符源码这里不再列举,感兴趣的同学自行查看。

底层结构

首先 std::weak_ptr 继承自 __weak_ptr<Tp>。如下所示:

 template<typename _Tp>
class weak_ptr : public __weak_ptr<_Tp>
{
    // ...
}

以上代码只是部分截取,请注意甄别。

__weak_ptrstd::weak_ptr 的底层实现类,用于管理对资源的弱引用。在 __weak_ptr 类中有两个核心的成员属性 _M_ptr_M_refcount。如下所示:

template<typename _Tp, _Lock_policy _Lp>
class __weak_ptr
{
	// ...
private:
    element_type*	 _M_ptr;         // Contained pointer.
    __weak_count<_Lp>  _M_refcount;    // Reference counter.
}

以上代码只是部分截取,请注意甄别。

__weak_count 中又封装了对 _Sp_counted_base 的引用计数器的管理,确保引用计数的生命周期被正确跟踪。如下所示:

template<_Lock_policy _Lp>
class __weak_count
{
    // ...

private:
    _Sp_counted_base<_Lp>*  _M_pi;
};

以上代码只是部分截取,请注意甄别。


  • 当创建一个 std::weak_ptr 时,调用其构造函数,同时也调用 __weak_ptr 的构造函数。如下所示:
weak_ptr(const shared_ptr<_Yp>& __r) noexcept
	: __weak_ptr<_Tp>(__r) { }
__weak_ptr(const __shared_ptr<_Yp, _Lp>& __r) noexcept
	: _M_ptr(__r._M_ptr), _M_refcount(__r._M_refcount) { }

初始化数据指针 _M_ptr,如果当前对象确实管理了一个有效的资源则增加弱引用计数 _M_weak_count。如下所示:

__weak_count(const __shared_count<_Lp>& __r) noexcept
	: _M_pi(__r._M_pi)
{
    if (_M_pi != nullptr)
    	_M_pi->_M_weak_add_ref();
}

调用 _M_weak_add_ref 函数增加弱引用计数 _M_weak_count。这个方法是线程安全的,确保在多线程环境中正确地更新弱引用计数。如下所示:

void _M_weak_add_ref() noexcept
{ 
     __gnu_cxx::__atomic_add_dispatch(&_M_weak_count, 1); 
}
  • 当销毁或者或重置 std::weak_ptr 时,__weak_count 的析构函数会被调用。如下所示:
~__weak_count() noexcept
{
    if (_M_pi != nullptr)
    	_M_pi->_M_weak_release();
}

_M_weak_release 函数中会调用 __exchange_and_add_dispatch 函数将 _M_weak_count 弱引用计数器减一,如果当弱引用计数为 0 时,则调用 _M_destroy 方法。如下所示:

void _M_weak_release() noexcept
{
    // ...
    if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1)
    {
        // ...
        
        _M_destroy();
    }
}

以上代码只是部分截取,请注意甄别。

_M_destroy_Sp_counted_base 类中的一个关键方法,它确保在引用计数为 0 时,资源被正确释放。

virtual void _M_destroy() noexcept
{ 
    delete this; 
}
常用操作
检查引用对象是否被销毁

expired 函数用于检查 std::weak_ptr 所引用的对象是否已经被销毁。如果 std::weak_ptr 所引用的对象已经被销毁,则返回 true;否则返回 false

bool expired() const noexcept
{ 
	return _M_refcount._M_get_use_count() == 0; 
}

具体 _M_get_use_count 方法内容, 请参考 获取引用计数 函数解析。

获取 std::shared_ptr

lock 函数尝试获取一个 std::shared_ptr,指向 std::weak_ptr 所引用的对象。如果对象仍然存在,返回一个 std::shared_ptr,共享对象的所有权;如果对象已经被销毁,返回一个空的 std::shared_ptr

shared_ptr<_Tp> lock() const noexcept
{ 
	return shared_ptr<_Tp>(*this, std::nothrow); 
}

直接调用了 std::shared_ptr 的构造函数,紧接着又调用了 __shared_ptr 的构造函数。如下所示:

shared_ptr(const weak_ptr<_Tp>& __r, std::nothrow_t) noexcept
    : __shared_ptr<_Tp>(__r, std::nothrow) { }

在该函数中,首先从 __r 的引用计数中初始化当前 std::shared_ptr 的引用计数。然后设置数据指针,当_M_get_use_count 返回非零值(即资源仍然存在),则 _M_ptr 被设置为 __r._M_ptr;否则就是资源已经被销毁 _M_ptr 被设置为 nullptr

__shared_ptr(const __weak_ptr<_Tp, _Lp>& __r, std::nothrow_t) noexcept
	: _M_refcount(__r._M_refcount, std::nothrow)
{
	_M_ptr = _M_refcount._M_get_use_count() ? __r._M_ptr : nullptr;
}
重置

reset 函数将 std::weak_ptr 置为空,表示不再引用任何对象,并不会影响引用计数。

void reset() noexcept
{ 
	__weak_ptr().swap(*this); 
}

首先调用默认构造函数创建 std::weak_ptr 对象,然后调用 swap 函数进行交换。

交换

swap 函数用于交换两个 std::weak_ptr 的内容,并不会影响引用计数。

void swap(__weak_ptr& __s) noexcept
{
    std::swap(_M_ptr, __s._M_ptr);
    _M_refcount._M_swap(__s._M_refcount);
}

调用标准库中的 swap 函数交换底层数据指针,然后再调用 _M_swap 函数交换引用计数器。如下所示:

void _M_swap(__weak_count& __r) noexcept
{
    _Sp_counted_base<_Lp>* __tmp = __r._M_pi;
    __r._M_pi = _M_pi;
    _M_pi = __tmp;
}
获取引用计数

use_count 函数返回 std::weak_ptr 所引用的对象的引用计数,表示有多少个 std::shared_ptr 共享该对象,如果对象已经被销毁,返回0。

long use_count() const noexcept
{ 
    return _M_refcount._M_get_use_count(); 
}

_M_get_use_count 函数中判断 _M_pi 是否为 nullptr 如果为空则表示没有管理任何资源直接返回 0;否则调用 _M_get_use_count 函数获取引用计数器的值。如下所示:

long _M_get_use_count() const noexcept
{ 
    return _M_pi != nullptr ? _M_pi->_M_get_use_count() : 0; 
}

通过原子操作获取关联的强引用计数器的值。如下所示:

long _M_get_use_count() const noexcept
{
	return __atomic_load_n(&_M_use_count, __ATOMIC_RELAXED);
}

其中,__ATOMIC_RELAXED 是最弱的内存顺序模型,仅保证操作的原子性,不提供任何内存顺序保证。

🌺🌺🌺撒花!

如果本文对你有帮助,就点关注或者留个👍
如果您有任何技术问题或者需要更多其他的内容,请随时向我提问。

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到