浅谈shared_ptr及shared_ptr涉及到的循环引用问题

 起初在C++标准库里面是没有智能指针的,直到C++11才加入了shared_ptr和unique_ptr以及weak_ptr。

最早的智能指针在Boost库里面,Boost库是为C++标准库提供扩展的一些C++程序的总称,由Boost社区组织开发维护。


   “智能”指针看上去是指针,实际上是附加了语义的对象。shared_ptr与scoped_ptr一样包装了new操作符在堆上分配的动态对象,但它实现的是引用计数型的智能指针 ,可以被自由地拷贝和赋值,在任意的地方共享它,当没有代码使用(引用计数为0)它时才删除被包装的动态分配的对象。shared_ptr也可以安全地放到标准容器中,并弥补了auto_ptr因为转移语义而不能把指针作为STL容器元素的缺陷。任何情况下都不要使用auto_ptr。


一、模拟实现shared_ptr的基本功能:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

template<class T>
class SharpedPtr
{
public:
    SharpedPtr(T* ptr = 0)//构造函数
        :_ptr(ptr)
        , _pCount(NULL)
    {
        if (ptr)
        {
            _pCount = new int(1);
        }
    }
    SharpedPtr(const SharpedPtr<T>& ap)//拷贝构造
        :_ptr(ap._ptr)
        , _pCount(ap._pCount)
    {
        if (_ptr)
        {
            ++UseCount();
        }
    }
    //ap1 = ap2;赋值运算符重载
    //①ap1内部封装的指针为空
    //②ap1独占一块空间
    //③ap1与其他对象共享一块空间
    SharpedPtr& operator=(const SharpedPtr<int>& ap)
    {
        if (this != &ap)
        {
            if (_ptr)
            {
                Release();
            }
            //ap1与其他对象共享一块空间
            _ptr = ap._ptr;
            _pCount = ap._pCount;
            ++UseCount();
        }
        return *this;
    }
    //析构函数
    ~SharpedPtr()
    {
        Release();
    }
    //检查引用计数并释放空间
    void Release()
    {
        if (0 == --UseCount())
        {
            delete _pCount;
            _pCount = NULL;
            delete _ptr;
            _ptr = NULL;
        }
    }
    //获取引用计数
    int& UseCount()
    {
            return *_pCount;
    }
//为了使其更像一个指针,所完成的基本操作
    T* operator->()
    {
        return _ptr;
    }

    T& operator*()
    {
        return *_ptr;
    }
private:
    T* _ptr;
    int* _pCount;
};
void TestSharpedPtr()
{
    SharpedPtr<int> ap1(new int(10));
    SharpedPtr<int> ap2(new int(20));
    SharpedPtr<int> ap3(ap1);
    SharpedPtr<int> ap4;
    ap4 = ap1;
    *ap1 = 1;
    *ap2 = 2;
    *ap3 = 3;
    *ap4 = 4;
}
int main()
{
    TestSharpedPtr();
    return 0;
}

虽然我们上面的程序看起来还可以,但实际上还存在以下问题:
但是还存在以下问题:
  1. 线程安全问题:

  shared_ptr 本身不是 100% 线程安全的。它的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。shared_ptr 的线程安全级别和内建类型、标准库容器、string 一样,即:

  • 一个 shared_ptr 实体可被多个线程同时读取;
  • 两个的 shared_ptr 实体可以被两个线程同时写入,“析构”算写操作;
  • 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁。
2.对于管理的特定类型的指针的释放不合理造成的程序崩溃:

我们都会有一种普遍的认知,shared_ptr智能指针只能用来管理动态开辟的空间,实际不然,shared_ptr是用来管理资源的,这里的资源并不仅仅是动态开辟的空间,还有例如:打开文件,其维护一个文件指针,那么在程序快要结束时,需要关闭文件;
还有就是使用malloc来动态开辟的空间,需要使用free来释放,否则,不管管理什么资源,都使用delete来释放空间,有时就会使得程序崩溃。
所以呢,我们就需要为其定制删除器
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
//为解决文件指针
struct Fclose
{
    void operator()(FILE*& fp)
    {
        cout<< "Fclose()" << endl;
        fclose(fp);
        fp = NULL;
    }
};
//为解决malloc开辟的空间
template<class T>
struct Free
{
    void operator()(T*& p)
    {
        free(p);
        p = NULL;
    }
};
//一般情况下(使用new动态开辟的空间)
template<class T>
class Delete
{
public :
    void operator()(T*& p)
    {
        delete p;
        p = NULL;
    }
};
template<class T, class Destory = Delete<T> >
class SharedPtr
{
public :
    SharedPtr(T* ptr = 0)//构造函数
        :_ptr(ptr)
        ,_pCount(NULL)
    {
        if (_ptr)
        {
            _pCount = new int(1);
        }
    }
    SharedPtr(const SharedPtr<T>& sp)//拷贝构造函数
        :_ptr(sp._ptr)
        , _pCount(sp._pCount)
    {
        if (_ptr)
        {
            ++GetRef();
        }
    }
    //sp1 = sp2
    SharedPtr<T>& operator=(const SharedPtr<T>& sp)
    {
        //可有三种情况:①sp1._ptr = NULL  ②sp1的引用计数为1  ③sp1的引用计数大于1
        if (this != &sp)
        {
            Release();
            _ptr = sp._ptr;
            _pCount = sp._pCount;
            ++GetRef();
        }
        return *this;
    }
    //辅助函数
    void Release()
    {
        if (_ptr && --GetRef() == 0)
        {
            Destory()(_ptr);
            delete _pCount;
            _pCount = NULL;
        }
    }
 //析构函数
    ~SharedPtr()
    {
        Release();
    }

    int& GetRef()
    {
        return *_pCount;
    }
private:
    T* _ptr;
    int* _pCount;
};
void Test2()
{
    FILE* sp1 = fopen("test.txt", "rb");
    SharedPtr<FILE, Fclose> sp2(sp1);

    int* sp3 = (int*)malloc(sizeof(int));
    SharedPtr<int, Free<int> >sp4(sp3);

    int* sp5 = new int(1);
    SharedPtr<int> sp6(sp5);
}

int main()
{
    Test2();
    return 0;
}

3.循环引用问题;
先来看下面一个示例:

下面的代码使用的都是c++标准库里的智能指针,源码也是。boost库的智能指针和c++标准库中的智能指针在底层实现机制是一样的。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include"boost/shared_ptr.hpp"


struct Node
{
    Node(int value)
    :_value(value)
    {
        cout << "Node()" << endl;
    }
    ~Node()
    {
        cout << "~Node()" << endl;
    }
    shared_ptr<Node> _prev;
    shared_ptr<Node> _next;
    int _value;
};

void Test2()
{
    shared_ptr<Node> sp1(new Node(1));
    shared_ptr<Node> sp2(new Node(2));
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;
    sp1->_next = sp2;
    sp2->_prev = sp1;
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;
}

int main()
{
    Test2();
    return 0;
}

可以看出,在这里我们使用了双向链表来说明这个问题,并且双向链表中的指针都是使用shared_ptr来维护的,在这里我使用的是标准库里的 shared_ptr智能指针。

从下图可以看出,构造的sp1和sp2在出它们的作用域时(即Test2())都没有被析构,从而造成了内存泄漏。



  由于先构造的后释放,后构造的先释放可知,先释放的是sp2,那么因为它的引用计数为2,减去1之后就成为了1,不能释放空间,因为还有其他的对象在管理这块空间。但是sp2这个变量已经被销毁,因为它是栈上的变量,但是sp2管理的堆上的空间并没有释放。
接下来释放sp1,同样,先检查引用计数,由于sp1的引用计数也是2,所以减1后成为1,也不会释放sp1管理的动态空间。

通俗点讲:就是sp2要释放,那么必须等p1释放了,而sp1要释放,必须等sp2释放,所以,最终,它们两个都没有释放空间。

所以,造成了内存泄漏。

下面让我们一起来剖析源码是怎样实现这种情况的?




上图只给出了简单的过程,下面贴出调用过程完整的源码:

shared_ptr() _NOEXCEPT
        {   // construct empty shared_ptr
        }

    _Ptr_base()
        : _Ptr(0), _Rep(0)
        {   // construct
        }
上面的构造函数连续调用了两次,构造节点内的_prev和_next。

template<class _Ux>
        explicit shared_ptr(_Ux *_Px)
        {   // construct shared_ptr object that owns _Px
        _Resetp(_Px);
        }

_Ptr_base()
        : _Ptr(0), _Rep(0)
        {   // construct
        }


    template<class _Ux>
        void _Resetp(_Ux *_Px)
        {   // release, take ownership of _Px
        _TRY_BEGIN  // allocate control block and reset
        _Resetp0(_Px, new _Ref_count<_Ux>(_Px));
        _CATCH_ALL  // allocation failed, delete resource
        delete _Px;
        _RERAISE;
        _CATCH_END
        }


_Ref_count(_Ty *_Px)
        : _Ref_count_base(), _Ptr(_Px)
        {   // construct
        }

_Ref_count_base()
        {   // construct
        _Init_atomic_counter(_Uses, 1);
        _Init_atomic_counter(_Weaks, 1);
        }


template<class _Ux>
        void _Resetp0(_Ux *_Px, _Ref_count_base *_Rx)
        {   // release resource and take ownership of _Px
        this->_Reset0(_Px, _Rx);
        _Enable_shared(_Px, _Rx);
        }
    };

    void _Reset0(_Ty *_Other_ptr, _Ref_count_base *_Other_rep)
        {   // release resource and take new resource
        if (_Rep != 0)
            _Rep->_Decref();//没有执行
        _Rep = _Other_rep;
        _Ptr = _Other_ptr;
        }

inline void _Enable_shared(const volatile void *, const volatile void *)
    {   // not derived from enable_shared_from_this; do nothing
    }


所以,构造智能指针sp1主要完成两件事情:

1、给引用计数开辟空间,并初始化uses和weaks均为1
2、将节点的空间交给_Rep来管理,将引用计数的空间交给_Ptr来管理。
sp2和sp1的构造过程相同。



同样:下面贴出上述过程的源码:
_Ty *operator->() const _NOEXCEPT
        {   // return pointer to resource
        return (this->_Get());
        }

_Myt& operator=(const _Myt& _Right) _NOEXCEPT
        {   // assign shared ownership of resource owned by _Right
        shared_ptr(_Right).swap(*this);
        return (*this);
        }
shared_ptr(const _Myt& _Other) _NOEXCEPT
        {   // construct shared_ptr object that owns same resource as _Other
        this->_Reset(_Other);
        }

_Ptr_base()
        : _Ptr(0), _Rep(0)
        {   // construct
        }

template<class _Ty2>
        void _Reset(const _Ptr_base<_Ty2>& _Other)
        {   // release resource and take ownership of _Other._Ptr
        _Reset(_Other._Ptr, _Other._Rep);
        }

void _Reset(_Ty *_Other_ptr, _Ref_count_base *_Other_rep)
        {   // release resource and take _Other_ptr through _Other_rep
        if (_Other_rep)
            _Other_rep->_Incref();
        _Reset0(_Other_ptr, _Other_rep);
        }


void _Incref()
        {   // increment use count
        _MT_INCR(_Mtx, _Uses);
        }

void _Reset0(_Ty *_Other_ptr, _Ref_count_base *_Other_rep)
        {   // release resource and take new resource
        if (_Rep != 0)
            _Rep->_Decref();
        _Rep = _Other_rep;
        _Ptr = _Other_ptr;
        }

上述调用过程,主要完成:

1.将sp2管理的引用计数中的uses加至2
2.sp1中的_next节点中的_Rep管理sp2的引用计数,_next节点中的_Ptr管理sp2节点


sp2->_prev = sp1;
其实调用过程同sp1->_next = sp2调用过程,主要完成:

1.将sp1管理的引用计数中的uses加至2
2.sp2中的_prev节点中的_Rep管理sp1的引用计数,_prev节点中的_Ptr管理sp1节点.

总体结构图如下:


                     图1


下面我们一起来看看析构的过程:

先析构sp2,下面是析构sp2的过程,可以看出没有释放空间(节点空间和引用计数空间),sp2析构完成之后,再去析构sp1,sp1的析构过程和sp2的析构过程相同,所以由此可知,一共有四块动态开辟的空间,但是都没有释放。

这里只贴出sp2的析构过程。
  ~shared_ptr() _NOEXCEPT
        {   // release resource
        this->_Decref();
        }

void _Decref()
        {   // decrement reference count
        if (_Rep != 0)
            _Rep->_Decref();
        }


void _Decref()
        {   // decrement use count
        if (_MT_DECR(_Mtx, _Uses) == 0)//uses为2,经过这条语句之后减为1
            {   // destroy managed resource, decrement weak reference count
            _Destroy();//不执行
            _Decwref();//不执行
            }
        }

由此引入weak_ptr,可以这么说,weak_ptr是为了shared_ptr而生,weak_ptr就是为解决shared_ptr的循环引用问题而产生的。
它必须依赖于shared_ptr,不能直接管理一块动态开辟的空间。

-------------------------------------------------------------------------
下面让我们一起来看weak_ptr是如何来解决循环引用的。

因为下面的过程和上面的过程大多数相同,所以在这里就不详细讲解了。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include"boost/shared_ptr.hpp"

struct Node
{
    Node(int value)
    :_value(value)
    {
        cout << "Node()" << endl;
    }
    ~Node()
    {
        cout << "~Node()" << endl;
    }
   weak_ptr<Node> _prev;
   weak_ptr<Node> _next;
    int _value;
};
void Test2()
{
    shared_ptr<Node> sp1(new Node(1));
    shared_ptr<Node> sp2(new Node(2));
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;
    sp1->_next = sp2;
    sp2->_prev = sp1;
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;
}

int main()
{
    Test2();
    return 0;
}

使用源码分析上述过程:

先来看构造sp1和sp2的过程:
shared_ptr<Node> sp1(new Node(1));
shared_ptr<Node> sp2(new Node(2));
Node(int value)
    :_value(value)
    {
        cout << "Node()" << endl;
    }
//与上面的区别在这里(Node里的_prev和_next分别为weak_ptr智能指针)
weak_ptr() _NOEXCEPT
        {   // construct empty weak_ptr object
        }
_Ptr_base()
        : _Ptr(0), _Rep(0)
        {   // construct
        }
上面过程同样连续执行两次(构造_prev和_next)
下面过程也是执行两次(构造sp1和sp2),和上面的过程相同
template<class _Ux>
        explicit shared_ptr(_Ux *_Px)
        {   // construct shared_ptr object that owns _Px
        _Resetp(_Px);
        }
_Ptr_base()
        : _Ptr(0), _Rep(0)
        {   // construct
        }
template<class _Ux>
        void _Resetp(_Ux *_Px)
        {   // release, take ownership of _Px
        _TRY_BEGIN  // allocate control block and reset
        _Resetp0(_Px, new _Ref_count<_Ux>(_Px));
        _CATCH_ALL  // allocation failed, delete resource
        delete _Px;
        _RERAISE;
        _CATCH_END
        }
_Ref_count(_Ty *_Px)
        : _Ref_count_base(), _Ptr(_Px)
        {   // construct
        }
_Ref_count_base()
        {   // construct
        _Init_atomic_counter(_Uses, 1);
        _Init_atomic_counter(_Weaks, 1);
        }

template<class _Ux>
        void _Resetp0(_Ux *_Px, _Ref_count_base *_Rx)
        {   // release resource and take ownership of _Px
        this->_Reset0(_Px, _Rx);
        _Enable_shared(_Px, _Rx);
        }
    };

void _Reset0(_Ty *_Other_ptr, _Ref_count_base *_Other_rep)
        {   // release resource and take new resource
        if (_Rep != 0)
            _Rep->_Decref();
        _Rep = _Other_rep;
        _Ptr = _Other_ptr;
        }
inline void _Enable_shared(const volatile void *, const volatile void *)
    {   // not derived from enable_shared_from_this; do nothing
    }


sp1->_next = sp2;
sp2->_prev = sp1;
源码分析上面两个语句的执行过程:
 
sp1->_next = sp2;
  _Ty *operator->() const _NOEXCEPT
        {   // return pointer to resource
        return (this->_Get());
        }
template<class _Ty2>
        weak_ptr& operator=(const shared_ptr<_Ty2>& _Right) _NOEXCEPT//_Right就是sp2
        {   // assign from _Right
        this->_Resetw(_Right);
        return (*this);
        }
template<class _Ty2>
        void _Resetw(const _Ptr_base<_Ty2>& _Other)//_Other为sp2
        {   // release weak reference to resource and take _Other._Ptr
        _Resetw(_Other._Ptr, _Other._Rep);
        }
template<class _Ty2>
        void _Resetw(_Ty2 *_Other_ptr, _Ref_count_base *_Other_rep)
        {   // point to _Other_ptr through _Other_rep
        if (_Other_rep)//增加sp2的弱引用计数,sp2.weaks加1 = 2
            _Other_rep->_Incwref();
        if (_Rep != 0)//sp1->next._Rep为0
            _Rep->_Decwref();//不执行
        _Rep = _Other_rep;//sp1->_next._Rep = sp2._Rep
        _Ptr = _Other_ptr;//sp1->_next._Ptr = sp2._Ptr
        }
void _Incwref()
        {   // increment weak reference count
        _MT_INCR(_Mtx, _Weaks);//将weaks从1修改为2
        }
sp2->_prev = sp1;
_Ty *operator->() const _NOEXCEPT
        {   // return pointer to resource
        return (this->_Get());
        }
template<class _Ty2>//_Right就是sp1
        weak_ptr& operator=(const shared_ptr<_Ty2>& _Right) _NOEXCEPT
        {   // assign from _Right
        this->_Resetw(_Right);
        return (*this);
        }
template<class _Ty2>//_Other为sp1
        void _Resetw(const _Ptr_base<_Ty2>& _Other)
        {   // release weak reference to resource and take _Other._Ptr
        _Resetw(_Other._Ptr, _Other._Rep);
        }
template<class _Ty2>
        void _Resetw(_Ty2 *_Other_ptr, _Ref_count_base *_Other_rep)
        {   // point to _Other_ptr through _Other_rep
        if (_Other_rep)//增加sp1的弱引用计数,sp1.weaks加1 = 2
            _Other_rep->_Incwref();
        if (_Rep != 0)//sp2->prev._Rep为0
            _Rep->_Decwref();//不执行
        _Rep = _Other_rep;//sp2->_prev._Rep = sp1._Rep
        _Ptr = _Other_ptr;//sp2->_prev._Ptr = sp1._Ptr
        }
void _Incwref()
        {   // increment weak reference count
        _MT_INCR(_Mtx, _Weaks);
        }

                            图2


分析图2与图1的区别:

1)sp1和sp2节点内的_prev和_next均为弱引用计数(_weak_ptr)
2)sp1和sp2的引用计数中的_weaks为2,_uses为1,和上面相反。

源码分析析构过程:

   
//析构sp2
~shared_ptr()
        {   // release resource
        this->_Decref();
        }
void _Decref()
        {   // decrement reference count
        if (_Rep != 0)//sp2._Rep不为0
            _Rep->_Decref();
        }
void _Decref()
        {   // decrement use count
        if (_MT_DECR(_Mtx, _Uses) == 0)//sp2.uses从1减为0
            {   // destroy managed resource, decrement weak reference count
            _Destroy();
            _Decwref();
            }
        }
//释放节点空间(清理节点内资源(包括sp2->next和sp2->prev))
virtual void _Destroy()
        {   // destroy managed resource
        delete _Ptr;
        }
-------------------------------------------------------------------------------------
下面都是清理sp2节点内的资源
~Node()
    {
        cout << "~Node()" << endl;
    }
~weak_ptr()//清理sp2._next(弱智能指针)
        {   // release resource
        this->_Decwref();
        }
void _Decwref()
        {   // decrement weak reference count
        if (_Rep != 0)//sp2->next._Rep为0
            _Rep->_Decwref();//不执行
        }
~weak_ptr()//清理sp2._prev
        {   // release resource
        this->_Decwref();
        }
void _Decwref()/sp2->prev._Rep不为0(即就是sp1的引用计数空间存在)
        {   // decrement weak reference count
        if (_Rep != 0)/
            _Rep->_Decwref();
        }
void _Decwref()//sp2->prev._Weaks从2减为1(即就是将sp1._Weaks减为1)
        {   // decrement weak reference count
        if (_MT_DECR(_Mtx, _Weaks) == 0)
            _Delete_this();//不执行
        }
调用operator delete释放节点sp2的空间
void _Decwref()//释放sp2引用计数空间
        {   // decrement weak reference count
        if (_MT_DECR(_Mtx, _Weaks) == 0)//sp2._weaks从2减为1
            _Delete_this();//不执行(也就是没有释放sp2的引用计数)
        }
----------------------------------------------------------------------------
析构sp1
~shared_ptr()
        {   // release resource
        this->_Decref();
        }
void _Decref()//减sp1的引用计数
        {   // decrement reference count
        if (_Rep != 0)//sp1._Rep不为0(sp1的引用计数不为空)
            _Rep->_Decref();m
        }
void _Decref()
        {   // decrement use count
        if (_MT_DECR(_Mtx, _Uses) == 0)//sp1._uses从1减为0
            {   // destroy managed resource, decrement weak reference count
            _Destroy();//释放节点空间
            _Decwref();//释放sp1的引用计数
            }
        }
-------------------------------------------------------------------------------
清理sp1节点内资源
virtual void _Destroy()
        {   // destroy managed resource
        delete _Ptr;//释放sp1节点的空间(需要去清理节点内的资源(包括sp1->_next和sp1->_prev))
        }
~weak_ptr()//清理sp1->next
        {   // release resource
        this->_Decwref();
        }
void _Decwref()
        {   // decrement weak reference count
        if (_MT_DECR(_Mtx, _Weaks) == 0)//sp1->next._weaks从1减为0(即就是sp2._weaks减为0)
            _Delete_this();//执行
        }
virtual void _Delete_this()//释放了sp1->_next._Rep空间(即就是sp2的引用计数空间)
        {   // destroy self
        delete this;
        }
~weak_ptr()//清理sp1._prev
        {   // release resource
        this->_Decwref();
        }
void _Decwref()
        {   // decrement weak reference count
        if (_Rep != 0)//sp1->_prev._Rep为空
            _Rep->_Decwref();//不执行
        }
调用operator delete释放节点sp1空间
-----------------------------------------------------------------------------------------------------------
释放sp1引用计数空间
void _Decwref()
        {   // decrement weak reference count
        if (_MT_DECR(_Mtx, _Weaks) == 0)//sp1._weaks从1减为0
            _Delete_this();
        }

virtual void _Delete_this()//释放sp1引用计数的空间
        {   // destroy self
        delete this;
        }

分析不同之处:
1)执行下面两条语句
sp1->_next = sp2;
 sp2->_prev = sp1;
都是将对方的_weaks加至2

2)释放节点空间(关注uses的大小,若可以减为0,则释放节点空间)
释放引用计数空间(关注_weaks的大小,若可以减为0,则释放引用计数的空间)
从而解决了循环引用的问题。

下面用图简单说明下析构过程:

sp2的析构过程:




sp1的析构过程:


对于c++标准库里shared_ptr和weak_ptr的继承关系简单图示:





总结:

在以下情况时使用 shared_ptr :
  • 当有多个使用者使用同一个对象,而没有一个明显的拥有者时

  • 当要把指针存入标准库容器时

  • 当要传送对象到库或从库获取对象,而没有明确的所有权时

  • 当管理一些需要特殊清除方式的资源时

    通过定制删除器的帮助。



由于看的源码太少,所以文中对于源码的理解不到位的地方还请各位大神指正!


  • 49
    点赞
  • 107
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值