C++智能指针学习——小谈引用计数

这篇具有很好参考价值的文章主要介绍了C++智能指针学习——小谈引用计数。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录
  • 前言
  • 控制块简介
  • 共享控制块
  • 引用计数与弱引用计数创建过程
    • __shared_ptr
    • __shared_count
    • _Sp_counted_base
  • 弱引用计数增加过程
    • 再谈共享控制块
    • __weak_count
  • 引用计数增加过程
  • 弱引用计数的减少过程
    • 弱引用计数减为0
  • 引用计数的减少过程
    • 引用计数减为0
  • 参考文章

前言

本文结合源码讨论std::shared_ptr和std::weak_ptr的部分底层实现,然后讨论引用计数,弱引用计数的创建和增减。
文章中尽可能的先阐述原理,然后再贴上代码。如果有不想看代码的,直接略过代码即可。
本文涉及的源码均出自gcc 9.4.0版本

控制块简介

控制块是shared_ptrweak_ptr中的重要组成,主要用于管理资源的引用计数和生命周期。这个机制允许智能指针安全地共享和管理同一个对象,同时自动释放不再需要的资源。

控制块包含以下部分:

  • 引用计数
  • 弱引用计数
  • 分配器
  • 删除器

本文讨论的引用计数和弱引用计数的创建、加减、销毁,与控制块密切相关。

共享控制块

首先我们要知道,当创建一个std::shared_ptr指向某个对象时,会生成一个控制块来存储该对象的引用计数和其他管理信息。如果基于这个std::shared_ptr再创建一个或多个std::weak_ptr,那么这些std::weak_ptr将也指向这个控制块。

示意图大概长这样:
C++智能指针学习——小谈引用计数

引用计数与弱引用计数创建过程

在谈引用计数和弱引用计数的创建时,其实就是讨论控制块的创建。

我们知道std::weak_ptr是被设计用来解决std::shared_ptr智能指针可能导致的循环引用问题。一个有效的std::weak_ptr对象一般是通过std::shared_ptr构造的或者是通过拷贝(移动)其他std::weak_ptr对象得到的,std::weak_ptr对象的构造不涉及控制块的创建。

因此在讨论引用计数、弱引用计数的创建时,我们是去分析std::shared_ptr的源码

__shared_ptr

__shared_ptrstd::shared_ptr的核心实现,它位于shared_ptr_base.h中。

__shared_ptr在构造实例时都会构造一个_M_refcount,它的类型为__shared_count<_Lp>

//file: shared_ptr_base.h
template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr : public __shared_ptr_access<_Tp, _Lp>
{
public:
	using element_type = typename remove_extent<_Tp>::type;
	//默认构造
	constexpr __shared_ptr() noexcept
      : _M_ptr(0), _M_refcount()
      { }
	...	
	//有删除器和分配器的构造
	template<typename _Yp, typename _Deleter, typename _Alloc,
	       typename = _SafeConv<_Yp>>
	__shared_ptr(_Yp* __p, _Deleter __d, _Alloc __a)
	: _M_ptr(__p), _M_refcount(__p, std::move(__d), std::move(__a))
	{
	  static_assert(__is_invocable<_Deleter&, _Yp*&>::value,
	      "deleter expression d(p) is well-formed");
	  _M_enable_shared_from_this_with(__p);
	}
private:
	...
	element_type*        _M_ptr;         // Contained pointer.
    __shared_count<_Lp>  _M_refcount;    // Reference counter.	
};

__shared_count

在创建__shared_count对象时,也会创建一个指向控制块的指针(_Sp_counted_base类型的指针)。控制块用来管理引用计数。

代码中的_Sp_counted_ptr_Sp_counted_deleter就是_Sp_counted_base的派生类。

//file: shared_ptr_base.h
template<_Lock_policy _Lp>
class __shared_count
{
public:
	//默认构造
	__shared_count(_Ptr __p) : _M_pi(0)
	{
	    __try
	    {
	        _M_pi = new _Sp_counted_ptr<_Ptr, _Lp>(__p);
	    }
	    __catch(...)
	    {
	        delete __p;
	        __throw_exception_again;
	    }
	}
	//带分配器和删除器的构造
	template<typename _Ptr, typename _Deleter, typename _Alloc,
	       typename = typename __not_alloc_shared_tag<_Deleter>::type>
	__shared_count(_Ptr __p, _Deleter __d, _Alloc __a) : _M_pi(0)
	{
	    typedef _Sp_counted_deleter<_Ptr, _Deleter, _Alloc, _Lp> _Sp_cd_type;
	    __try
	    {
	        typename _Sp_cd_type::__allocator_type __a2(__a);
	        auto __guard = std::__allocate_guarded(__a2);
	        _Sp_cd_type* __mem = __guard.get();
	        ::new (__mem) _Sp_cd_type(__p, std::move(__d), std::move(__a));
	        _M_pi = __mem;
	        __guard = nullptr;
	    }
	    __catch(...)
	    {
	        __d(__p); // Call _Deleter on __p.
	        __throw_exception_again;
	    }
	}
private:
	friend class __weak_count<_Lp>;
	_Sp_counted_base<_Lp>*  _M_pi;
};

_Sp_counted_base

_Sp_counted_base负责管理引用计数和弱引用计数,其中

  • _M_use_countshared_ptr的计数,就是引用计数,表示有多少个shared_ptr对象共享同一个内存资源。
  • _M_weak_countweak_ptr的计数,也就是弱引用计数,表示有多少个weak_ptr对象引用同一个资源。

我们可以看到在_Sp_counted_base的初始化列表中,初始化了_M_use_count_M_weak_count为1,完成了引用计数和弱引用计数的创建和初始化。

//file: shared_ptr_base.h
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base : public _Mutex_base<_Lp>
{
public:
    _Sp_counted_base() noexcept : _M_use_count(1), _M_weak_count(1) { }
	...	
private:
	_Atomic_word  _M_use_count;     // #shared
	_Atomic_word  _M_weak_count;    // #weak + (#shared != 0)
};

这里再简单提一下_Sp_counted_base_Sp_counted_ptr_Sp_counted_deleter的关系与各自的功能。

  • _Sp_counted_base是一个抽象基类,定义并管理了引用计数与弱引用记数。
  • _Sp_counted_ptr继承自_Sp_counted_base,主要是使用默认的分配策略和删除策略管理资源对象。
  • _Sp_counted_deleter继承自_Sp_counted_base,主要是使用用户提供的分配器和删除器管理资源对象。

因为_Sp_counted_base是抽象基类无法被实例化,所以使用的是其派生类_Sp_counted_ptr_Sp_counted_deleter对象来管理引用计数、弱引用计数、分配器、删除器。这个对象就是我们常说的控制块。

_Sp_counted_base还有一个派生类_Sp_counted_ptr_inplace,适合使用std::make_shared的场景,此处不过多讨论)

弱引用计数增加过程

再谈共享控制块

在上面的引用计数与弱引用计数创建过程中,我们提到:

一个有效的std::weak_ptr对象一般是通过std::shared_ptr构造的或者是通过拷贝(移动)其他std::weak_ptr对象得到的

对应的__weak_count__shared_count对象也具有上述关系。

查看源码,我们可以发现,__weak_count__shared_count都有一个指向控制块的多态指针。

	_Sp_counted_base<_Lp>*  _M_pi;

__weak_count中并没有使用new或者类似操作让_M_pi指向一块新的内存(控制块)。追根溯源,__weak_count中多态指针指向的控制块的来源就是__shared_count。代码中是通过在__weak_count构造函数和重载的赋值运算符中给多态指针_M_pi初始化和赋值实现的。以此实现了weak_ptrshared_ptr共享控制块的功能。

__weak_count

弱引用计数的增加可以分为下面几种情况:

  • 通过std::shared_ptr构造std::weak_ptr
  • 通过std::weak_ptr构造std::weak_ptr
  • 通过std::shared_ptrstd::weak_ptr赋值
  • 通过std::weak_ptrstd::weak_ptr赋值

其实本质是靠调用_M_weak_add_ref()增加的弱引用计数,详情见__weak_count的源码:

//file: shared_ptr_base.h
template<_Lock_policy _Lp>
class __weak_count
{
public:
	...
	//通过__shared_count构造
    //和一个已存在的__shared_count对象共享控制块,并更新控制块的弱引用计数
    __weak_count(const __shared_count<_Lp>& __r) noexcept
     : _M_pi(__r._M_pi)
    {
    	//若入参的多态指针不为空
        //弱引用计数++(增加_Sp_counted_base对象的_M_weak_count)
		if (_M_pi != nullptr)
			_M_pi->_M_weak_add_ref();
	}
	
	//通过__weak_count拷贝构造
    //和传入的__weak_count对象就共享同一个控制块,并更新控制块的弱引用计数
    __weak_count(const __weak_count& __r) noexcept
     : _M_pi(__r._M_pi)
    {
		if (_M_pi != nullptr)
			_M_pi->_M_weak_add_ref();
    }
    
	//通过__shared_count给__weak_count赋值
	__weak_count& operator=(const __shared_count<_Lp>& __r) noexcept
    {
    	_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
    	//新对象弱引用计数++
		if (__tmp != nullptr)
	  		__tmp->_M_weak_add_ref();
	  	//原对象弱引用计数--
		if (_M_pi != nullptr)
	  		_M_pi->_M_weak_release();
	  	//指向新对象的控制块
		_M_pi = __tmp;
		return *this;
	}

	//通过__weak_count给__weak_count赋值
	__weak_count& operator=(const __weak_count& __r) noexcept
    {
		_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
		if (__tmp != nullptr)
	  		__tmp->_M_weak_add_ref();
		if (_M_pi != nullptr)
	  		_M_pi->_M_weak_release();
		_M_pi = __tmp;
		return *this;
    }
    ...
private:
	friend class __shared_count<_Lp>;
	_Sp_counted_base<_Lp>*  _M_pi;
};

引用计数增加过程

引用计数的增加可以分为下面几种情况:

  • 通过std::shared_ptr构造std::shared_ptr
  • 通过std::shared_ptrstd::shared_ptr赋值
  • std::weak_ptr升级为std::shared_ptr

本质是靠调用_M_add_ref_copy()_M_add_ref_lock增加的引用计数,详情见__shared_count的源码:

//file: shared_ptr_base.h
template<_Lock_policy _Lp>
class __shared_count
{
public:
	//拷贝构造
    __shared_count(const __shared_count& __r) noexcept
     : _M_pi(__r._M_pi)
    {
		if (_M_pi != 0)
			_M_pi->_M_add_ref_copy();
    }

	//拷贝赋值
	__shared_count& operator=(const __shared_count& __r) noexcept
    {
		_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
		if (__tmp != _M_pi)
	  	{
	    	if (__tmp != 0)
	      		__tmp->_M_add_ref_copy();
	    	if (_M_pi != 0)
	      		_M_pi->_M_release();
	    	_M_pi = __tmp;
	    }
		return *this;
	}
	
	//转换构造
	//weak_ptr使用lock()时会调用此构造函数
	explicit __shared_count(const __weak_count<_Lp>& __r) 
	 : _M_pi(__r._M_pi)
    {
    	if (_M_pi != nullptr)
			_M_pi->_M_add_ref_lock();//引用计数++,具体实现依赖于锁策略
      	else
			__throw_bad_weak_ptr();
    }
private:
	friend class __weak_count<_Lp>;
	_Sp_counted_base<_Lp>*  _M_pi;
};

弱引用计数的减少过程

弱引用计数的减少可以分为下面几种情况:

  • std::weak_ptr析构
  • std::weak_ptr对象被覆盖(赋值操作覆盖原std::weak_ptr

本质是靠调用_M_weak_release()减少弱引用计数:

//file: shared_ptr_base.h
template<_Lock_policy _Lp>
class __weak_count
{
public:
	//析构
	~__weak_count() noexcept
    {
      if (_M_pi != nullptr)
        _M_pi->_M_weak_release();
    }
	//转换赋值
	__weak_count& operator=(const __shared_count<_Lp>& __r) noexcept
	{
    	_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
        if (__tmp != nullptr)
          	__tmp->_M_weak_add_ref();
        if (_M_pi != nullptr)
        	_M_pi->_M_weak_release();
        _M_pi = __tmp;
	    return *this;
	}
	//拷贝赋值
	__weak_count& operator=(const __weak_count& __r) noexcept
	{
      	_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
        if (__tmp != nullptr)
          	__tmp->_M_weak_add_ref();
        if (_M_pi != nullptr)
          	_M_pi->_M_weak_release();
        _M_pi = __tmp;
    	return *this;
    }
    //移动赋值
    __weak_count& operator=(__weak_count&& __r) noexcept
    {
    	if (_M_pi != nullptr)
        	_M_pi->_M_weak_release();
      	_M_pi = __r._M_pi;
            __r._M_pi = nullptr;
      	return *this;
    }

private:
	friend class __shared_count<_Lp>;
	_Sp_counted_base<_Lp>*  _M_pi;
};

然后在这里对std::weak_ptr::reset()说明一下:它是用来重置 std::weak_ptr 的。调用 reset() 会使std::weak_ptr不再指向它原本观察的对象。

它也会减少原对象的弱引用计数(本质是通过调用的析构函数使得弱引用计数减少)

//file: shared_ptr_base.h
void reset() noexcept
{ 
	__weak_ptr().swap(*this); 
}

弱引用计数减为0

在上面提到:弱引用计数的减少是通过调用_M_weak_release()实现的。通过分析_M_weak_release()的代码我们可以知道,_M_weak_release()中主要做了:

  • 对弱引用计数做减1操作并
  • 判断弱引用计数减1后是否为0,若为0则调用_M_destroy()删除控制块。
//file: shared_ptr_base.h
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base : public _Mutex_base<_Lp>
{
    //控制块的弱引用计数为0时,销毁自身
    virtual void _M_destroy() noexcept
    { delete this; }
    
    //弱引用计数--
    //当弱引用计数变为0,销毁控制块
	void _M_weak_release() noexcept
    {
    	// Be race-detector-friendly. For more info see bits/c++config.
        _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
        //减少弱引用计数,并返回-1之前的值
	    if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1)
	    {
        	_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
	        if (_Mutex_base<_Lp>::_S_need_barriers)
	        {
	        // See _M_release(),
	        // destroy() must observe results of dispose()
		    	__atomic_thread_fence (__ATOMIC_ACQ_REL);
	        }
	        _M_destroy();
	    }
    }
};

引用计数的减少过程

引用计数的减少可以分为下面几种情况:

  • std::shared_ptr析构
  • std::shared_ptr对象被覆盖(赋值操作覆盖原std::shared_ptr

本质是靠调用_M_release()减少弱引用计数

//file: shared_ptr_base.h
template<_Lock_policy _Lp>
class __shared_count
{
public:
	//析构
	~__shared_count() noexcept
	{
        if (_M_pi != nullptr)
        	_M_pi->_M_release();
    }
    //拷贝赋值
    __shared_count& operator=(const __shared_count& __r) noexcept
    {
		_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
        if (__tmp != _M_pi)
        {
          	if (__tmp != 0)
            	__tmp->_M_add_ref_copy();
          	if (_M_pi != 0)
            	_M_pi->_M_release();
          	_M_pi = __tmp;
        }
	    return *this;
	}
private:
	friend class __weak_count<_Lp>;
	_Sp_counted_base<_Lp>*  _M_pi;
};

引用计数减为0

上面提到:引用计数的减少是通过调用_M_release()实现的。通过分析_M_release()的代码我们可以知道,_M_release()中主要做了

  • 对引用计数做减1操作并
  • 判断引用计数减1后是否为0,若为0则调用_M_dispose()释放其所管理的内存资源
  • 若引用计数减1后为0,则还会对弱引用计数做一次减1操作并
  • 判断弱引用计数减1后是否为0,若为0则调用_M_destroy()删除控制块。
//file: shared_ptr_base.h
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base : public _Mutex_base<_Lp>
{
	//当前对象的引用计数为0时,释放管理的资源
    //纯虚函数,取决于释放策略,由派生类实现
    virtual void _M_dispose() noexcept = 0;

    //当前对象的弱引用计数为0时,销毁自身
    virtual void _M_destroy() noexcept
    { delete this; }
	
	void _M_release() noexcept
    {
    	// Be race-detector-friendly.  For more info see bits/c++config.
        _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
        //减少引用计数,并返回-1之前的值
        //如果引用计数为0,则释放管理的资源
	    if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
	    {
        	_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
	        _M_dispose();
          	// There must be a memory barrier between dispose() and destroy()
          	// to ensure that the effects of dispose() are observed in the
          	// thread that runs destroy().
          	// See http://gcc.gnu.org/ml/libstdc++/2005-11/msg00136.html
          	if (_Mutex_base<_Lp>::_S_need_barriers)
          	{
            	__atomic_thread_fence (__ATOMIC_ACQ_REL);
          	}

          	// Be race-detector-friendly.  For more info see bits/c++config.
          	_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
          	//减少弱引用计数,并返回-1之前的值
          	//如果弱引用计数为0,则销毁控制块自身
	        if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1)
          	{
            	_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
	          	_M_destroy();
          	}
        }
	}
};

这里再说明一下为什么__shared_count要在引用计数减为0时还要对弱引用计数做减1操作:
__shared_count构造的同时,也会构造一个控制块对象,其中引用计数和弱引用计数一同被初始化为1。这意味着,即使最后一个std::weak_ptr被销毁了,但若其对应的std::shared_ptr还至少存在一个,那么弱引用计数就不会被减少至0(代码中的注释也是这么提示的)。

//file: shared_ptr_base.h
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base : public _Mutex_base<_Lp>
{
	_Atomic_word  _M_use_count;     // #shared
	_Atomic_word  _M_weak_count;    // #weak + (#shared != 0)
};

std::shared_ptr对象存在的情况下,所有相关std::weak_ptr对象被销毁后,控制块仍存在,且其中的弱引用计数为1,此时在销毁最后一个std::shared_ptr对象时,除了要减少引用计数为0,释放管理的内存资源,还要把最后一个弱引用计数减少为0,销毁控制块。

std::weak_ptr对象存在的情况下,所有相关std::shared_ptr对象都被销毁后,①std::shared_ptr管理的内存资源会被释放(因为引用计数为0,_M_dispose()被调用)②弱引用计数不为0,控制块仍然存在(直到最后一个std::weak_ptr对象被销毁,控制块才会被销毁)

参考文章

1.C++2.0 shared_ptr和weak_ptr深入刨析
2.智能指针std::weak_ptr文章来源地址https://www.toymoban.com/news/detail-844186.html

到了这里,关于C++智能指针学习——小谈引用计数的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++】C++ 引用详解 ⑦ ( 指针的引用 )

    指针的引用 效果 等同于 二级指针 , 因此这里先介绍 二级指针 ; 使用 二级指针 作为参数 , 可以实现如下功能 : 动态内存管理 : 借助二级指针 , 可以在函数中分配或释放内存 ; 如 : 创建一个动态数组或调整现有数组的大小 , 在函数中需要一个指向指针的指针作为参数 , 以便修

    2024年02月11日
    浏览(37)
  • C++【4】指针与引用;数组指针;指针数组

    /*     指针变量作为函数参数         函数的参数可以是指针类型,它的作用是将一个变量的地址传送到另一个函数中。         指针变量作为函数参数与变量本身作为函数参数不同,变量作函数参数传递的是具体值。         而指针作为函数参数传递的是内存的地址

    2024年02月07日
    浏览(35)
  • 【C++】引用与指针

    专栏放在【 C++知识总结 】,会持续更新,期待支持 🌹 引用的概念 在C++中,引用的本质其实就是给一个已经存在的变量 ”起别名“ 。也就是说, 引用与它所引用的对象共用一块空间 。( 同一块空间的多个名字 ) 就比如说,李逵又叫黑旋风,而黑旋风就是指李逵本人,只

    2024年02月02日
    浏览(43)
  • C++的指针和引用

    C++中内存单元内容和地址 内存由很多的内存单元组成,这些内存单元用于存放各种类型数据; 计算机对内存的每个内存单元都进行了编号,这个编号就称为内存地址,地址决定了内存单元在内存中的位置; 记住这些内存单元地址不方便,因此C++语言的编译器让我们通过名字

    2024年02月06日
    浏览(46)
  • c++的引用和指针

    我们要清楚的知道,使用指针和引用都可以的传入函数的main函数的变量在局部函数改变值时,main函数里面相应的变量也会改变值。但他俩的方式不同。 我们先来说指针,指针传入局部参数时,他会在创建个局部指针变量,然后把传入的地址赋值给局部的指针变量,然后修改

    2024年02月09日
    浏览(48)
  • C++[第五章]--指针和引用

    引用就是别名,引用定义时必须初始化: int a; int b=a; //b即为a的别名 如果不是形参,必须初始化,引用某一变量 指针和c一样; this指针 在类的成员函数中使用,表示当前对象; C++11 新增了一种引用 类型 引用名 = 右值表达式; 引入右值引用如: 编译器允许我们为 num 左值建立

    2024年02月15日
    浏览(40)
  • C++拾遗(四)引用与指针

    引用和指针是两种不同的概念,尽管它们在某些方面有一些相似之处,但它们在功能和用途上是有所区别 引用:引用是别名,是对已存在变量的另一个称呼,一旦一个变量被引用,就不能再被引用其他变 量。 int a = 10; int ref = a;   这里, ref  是  a  的引用,它们引用的是同

    2024年01月25日
    浏览(55)
  • C++中const,指针和引用

    在线C/C++编译器,可以试着运行代码。 在C语言中,const修饰的量称为 常变量 (在编译过程中,const就是当成变量的编译生成指令的),不可以直接修改它的值,但是可以 通过地址进行修改其对应的值 。并且const修饰的变量可以不进行初始化,编译器最后默认赋值为0。 然而在

    2024年02月13日
    浏览(40)
  • 【C++初阶(三)】引用详解(对比指针)

    💓博主CSDN主页:杭电码农-NEO💓   ⏩专栏分类:C++初阶之路⏪   🚚代码仓库:NEO的学习日记🚚   🌹关注我🫵带你学习排序知识   🔝🔝 C语言中有一利器: 指针 而C++中增加了另一利器: 引用 这两个板块的存在 极大了提升了C/C++的可用性! 本篇文章将给大家详细讲解引用 并

    2024年02月12日
    浏览(47)
  • C++中的引用及指针变量

    目录 1.1 C++中的引用 1.2 C++中的指针变量(pointer) 1.1 C++中的引用 C++中的引用(reference)是一种特殊的变量,它是某个已存在变量的另一个名字。引用变量与指针变量类似,但引用变量必须在声明时进行初始化,并且一旦引用变量与某个变量关联起来,就无法再与其他变量关

    2024年01月20日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包