【C++】二十二、智能指针

一、智能指针的概念

用关键字new开辟堆内存,需要delete手动释放。这一特性可以会出现:

  • 忘记释放
  • 代码调整

等问题导致堆内存出现内存泄露的问题。为了解决内存泄露的问题。

最开始效仿Java中的垃圾回收机制:在后台启动一个守护进程,检测对象的生存周期是否结束,结束则系统释放内存,未结束不释放。
但是 存在缺点:开启守护进程,影响程序执行效率,C/C++主要做服务器,要求高效率,所以不采用Java的垃圾回收机制。

最后 结合堆,栈,对象的特性想出了自主内存回收机制:智能指针。

  • 栈:系统开辟,系统释放
  • 堆:人为开辟,人为释放
  • 对象:生成对象时开辟内存,调用构造;销毁对象时调用析构函数,释放内存。

现在想要实现人为开辟,系统释放,可以让对象中的指针指向开辟的内存,当对象销毁时,系统自动调用析构,析构中释放内存。实现了人为开辟,系统释放。

【1. 智能指针主要思想:】

  • 对象有一个指针,指向这块内存。
  • 析构函数中实现释放堆内存,对象销毁时,系统自动调用析构函数,释放开辟的内存。

实现了人为开辟,系统释放,即只需要new开辟,不需要自己调用delete。

【2. 智能指针的分类:】

使用系统的智能指针,需要添加头文件:

# include<memory>

根据智能指针对内存的特性,将智能指针进行分类:

【C++98标准库:】

  • auto_ptr:内存的所有权唯一,最后只有一个指针指向此块内存。存在所有权失效问题。

【 带标志位的智能指针:】

不属于任何库,保证内存的释放权唯一,最后只有一个指针拥有释放权。存在释放权失效问题。

【 C++11标准库:】

  • unique_ptr:只允许一个指针指向该内存,至始至终。存在从代码层面让多个指针指向一块内存,内存被多次释放的问题。
  • shared_ptr:通过引用计数的方式实现了多个指针指向同一块内存,强指针。存在强指针互相引用,导致内存泄露的问题。
  • weak_ptr:为了解决强指针互相引用的问题,不能单独使用,需要配合强指针一起使用。

C++11标准后已经将auto_ptr摒弃了,即存在但是不建议使用。

下面我们讲解所有智能指针的具体实现原理,因为使用智能指针指向的类型不确定,所以使用模板实现代码。

二、auto_ptr

(一)基本概念

auto_ptr智能指针是C++98标准库中的智能指针。它要求内存的所有权唯一,即当前内存块只允许有一个指针指向,当新指针指向时将旧指针释放对内存块的指向,新指针指向该内存块,获得所有权,如下图:

在这里插入图片描述

所以 需要在有新指针指向内存时,进行所有权转移,故实现构造,赋值重载函数需要进行转移处理。

(二)实现原理

使用模板实现auto_ptr模板类,私有成员变量为指针,主要成员函数如下:

【1. 所有权转移函数:】

使用此函数实现内存块的所有权唯一,写在类私有访问限定符下。实现当新指针指向时,旧指针释放对内存块的指向,新指针指向该内存块。

函数流程:

  • 定义变量保存旧指针,将旧指针断开
  • 返回变量

代码如下:

T* Release()//新指针获得所有权函数
{
	T* temp = mptr;//保存旧指针的指向
	mptr = NULL;//断开旧指针指向内存的连接
	return temp;//将旧指针原来指向的内存地址返回,让新指针指向
}

当发生拷贝构造或赋值操作时,需要调用此函数。

【2. 拷贝构造函数:】

用旧对象拷贝出一个新对象,故旧指针失效,新指针指向,调用所有权转移函数即可,代码如下:

Auto_ptr(Auto_ptr<T>& rhs)//rhs为已经存在的对象,让新对象指针指向它指向的的内存,它断开指向,保证所有权唯一
{
	std::cout<<"Auto_ptr(&rhs)"<<std::endl;
	mptr = rhs.Release();
}

【3. 赋值运算符重载函数:】

旧对象将值赋给已经存在的对象:

  • 那么已经存在的对象需要先释放原来指向的内存,原来指针指向的内存有多个指针或一个指针指向,多个此时mptr=NULL,一个mptr=地址;delete NULL是正确的,所以可以写为delete mptr
  • 调用所有权转移函数

代码如下:

    Auto_ptr<T> operator=(Auto_ptr<T>& rhs)//赋值运算符重载
	{
		if(this != &rhs)
		{
			std::cout<<"Auto_ptr(operator=)"<<std::endl;
			delete mptr;
			mptr=rhs.Release();
		}
		return *this;
	}

【4. 重载、*,->运算符:】

智能指针称为称为面向对象的指针,也是指针,应该具备指针的基本功能,所以需要重载这两个运算符:

	T* operator->()
	{
		return mptr;
	}
	T& operator*()
	{
		return *mptr;
	}

【5. 构造,析构函数:】

构造函数对成员变量指针初始化;析构函数中delete释放指针指向的内存。

    Auto_ptr(T* ptr)
		:mptr(ptr)
	{
		std::cout<<"Auto_ptr()"<<std::endl;
	}
	~Auto_ptr()
	{
		delete mptr;
	}

auto_ptr指针模板类:Auto_ptr源码实现完成,给出主函数和测试类,进行测试:

class Test
{
public:
	void show()
	{
		std::cout<<"Hello"<<std::endl;
	}
};
int main()
{
	//智能指针应该可以和普通指针的使用一样
	//1. 开辟内存int *p = new int(10);
	Auto_ptr<int> pa1 = new int(10);
	//2. 解引用赋值*p=20
	*pa1=20;
	std::cout<<"*pa1="<<*pa1<<std::endl;
	//3. 指向对象,调用函数如:Test* ptest = new Test();ptest->show();
	Auto_ptr<Test>pa2=new Test();
	pa2->show();
}

运行结果如下:

在这里插入图片描述

实现的Auto_ptr基本普通指针的功能,也实现了人为开辟,系统释放的自主内存回收功能,

(三)缺陷

auto_ptr智能指针需要保证所有权唯一,这种处理方式,会带来问题,如:

//4.缺陷,智能指针提前失效问题
Auto_ptr<int> pa3 = new int(10);
Auto_ptr<int> pa4 = pa3;//此时pa3指针已经失效为NULL
*pa4=30;//成功,因为此时它已经获得了内存的所有权
*pa3=40;//失败,此时pa3为NULL,对保留区操作,崩溃
return 0;

只允许有一个指针指向内存,那么此时pa3的指针指向空,对其操作,是对保留区操作,程序崩溃。

所以缺陷就是:因为内存所有权的转移,导致智能指针提前失效,即指针提前被置为NULL,导致无法操作。

所以C++11标准后,auto_ptr指针不建议使用,被摒弃。

三、带标志位的智能指针

(一)基本概念

这个智能指针不是C++库中的指针,称为带标志位的智能指针。它的思想是: 所有权不唯一, 释放权唯一。

允许多个指针指向同一个内存,但是只有一个指针拥有对内存的释放权,所以我们可以使用一个标识flag标志这个指针是否具有对内存的释放权:

  • flag标识释放权,true有释放权。
  • false没有。只有为true才可以进行内存释放。

那么当有新指针指向内存时,即存在拷贝构造,赋值运算符重载函数被调用时,它们都是用自身的值来创建或赋值一个相同值的对象,所以:

  • 如果旧指针有释放权,则转移的新指针有。
  • 如果没有,那么转移的新指针也没有。

总结来说,当新指针被指向内存时:

  1. 旧指针将其释放权标识(不管旧指针有没有转移权),直接赋给新指针即可。
  2. 将旧智能指针的释放权标识置为false

如下图:
在这里插入图片描述
代码表示:

new.flag=old.flag;
old.flag=false;

这样解决了auto_ptr指针所有权失效的问题。

(二)实现原理

设计Smart_ptr智能指针类时,私有成员有指针,标志位flag,主要成员函数为:

【1. 构造,析构:】

  • 构造函数进行指针和标志位的初始化。
  • 析构函数:只有拥有释放权的对象结束时才释放内存,即flag为true时,其他对象,将指针置为空即可。

代码:

   Smart_ptr(T* ptr)
   	 :mptr(ptr),flag(true)
   {
   	std::cout<<"Smart_ptr()"<<std::endl;
   }
   ~Smart_ptr()
   {
   	if(flag)//判断是否是最后一个指针
   	{
   		delete mptr;
   		std::cout<<"~Smart_ptr()"<<std::endl;
   	}
   	mptr = NULL;
   }

【2. 拷贝构造:】

旧对象生成新对象:

  • 将指针赋给新对象的指针。
  • 旧对象进行释放权转移,转移给新对象的指针上,自己为false。
    Smart_ptr(Smart_ptr<T>& rhs)
	{
		mptr = rhs.mptr;
		flag = rhs.flag;//旧指针将自己的释放权给新指针。
		rhs.flag = false;//取消旧指针释放权
	}

【3. 赋值运算符重载:】

用旧对象对另一个旧对象的指针进行赋值:

  • 被赋值的对象对原来指向的内存取消释放权,调用析构。
  • 指向新内存块。
  • 旧指针将自己的释放权给新指针。
  • 取消旧指针释放权。

代码:

   Smart_ptr<T>& operator=(Smart_ptr<T>& rhs)
	{
		if(this != &rhs)
		{
			this->~Smart_ptr();//对原来指向的内存取消释放权,调用析构
			mptr = rhs.mptr;//指向新内存块
			flag = rhs.flag;//旧指针将自己的释放权给新指针。
			rhs.flag = false;//取消旧指针释放权
		}
		return *this;
	}

带有标志位的Smart_ptr模板类Smart_ptr源码设计完成,给出主函数测试:

int main()
{
	Smart_ptr<int> s1 = new int(10);
	*s1=20;
	std::cout<<*s1<<std::endl;
	Smart_ptr<int> s2 = s1;
	*s2=30;
	*s1=40;//解决了auto_ptr所有权失效的问题
}

我们可以看调试代码查看逻辑是否正确:

在这里插入图片描述
在这里插入图片描述
运行结果:
在这里插入图片描述

(三)缺陷

带标识的智能指针需要保证释放权唯一,这种处理机制,如果使用不当,会出现缺陷。如下面这种情况,程序就会出现问题:

在这里插入图片描述

所以缺陷就是:因为内存释放权的转移,导致智能指针提前失效,即指针指向的空间被提前释放,无法对指针操作。

四、unique_ptr

(一)基本概念

前面两个指针都会存在因为权限转移导致程序出错的情况,为了解决这种问题,设计不允许权限转移的指针,这就是unique_ptr指针。

unique_ptr: 要求至始至终内存所有权唯一,不允许权限转移(禁止)只有一个对象指向一块内存,在设计点就限制了。和auto_ptr是有区别的,auto_ptr允许权限转移,转移后只有一个对象指向内存。

在拷贝构造,赋值运算符重载时会产生权限转移,将这两个函数写到私有下,就不会出现权限转移的情况。

(二)实现原理

unique_ptr智能指针的实现,相对比较简单,只需要将拷贝构造,赋值运算符重载函数的声明写到私有下即可,那么代码如下:

# include<iostream>
//3.前两个智能指针都是因为权限转移出现的问题,现在设计一个不允许转移权限的智能指针,禁止多个指针指向同一块内存
//只有一个指针指向这块内存,将拷贝构造函数赋值运算符写为私有即可,这就是unique_ptr
template<typename T>
class Unique_ptr
{
public:
	Unique_ptr(T* ptr)
		:mptr(ptr)
	{
		std::cout<<"Unique_ptr()"<<std::endl;
	}
	T* operator->()
	{
		return mptr;
	}
	T& operator*()
	{
		return *mptr;
	}
	~Unique_ptr()
	{
		delete mptr;
		std::cout<<"~Unique_ptr()"<<std::endl;
	}

private:
	Unique_ptr(Unique_ptr<T>& rhs);//私有拷贝构造
	Unique_ptr<T>& operator=(Unique_ptr<T>& rhs);//私有赋值
	T* mptr;
};
int main()
{
	Unique_ptr<int> u1=new int(10);
	*u1=20;
	//Unique_ptr<int> u2=u1;//禁止
	std::cout<<*u1<<std::endl;
}

运行结果:
在这里插入图片描述

(三)缺陷

unique_ptr虽然在设计层面,不允许多个智能指针指向同一块内存,但是可以通过代码,让多个智能指针指向同一个内存,那么就会出现一块内存被释放多次的问题。 如:

*int* a=new int;
Unique_ptr<int> u2(a);
Unique_ptr<int> u3(a);//程序崩溃,a内存被释放多次

此时u2和u3都指向a内存块,a内存块被释放多次,程序崩溃。

所以可见:让一个指针指向一个内存块的设计是不现实的,需要设计出允许多个指针指向同一块内存,且不出现权限转移问题的智能指针。

五、shared_ptr

(一)基本概念

需要实现:

  • 多个指针指向同一块内存
  • 不采取权限转移的方法

我们可以采取写时拷贝中使用的引用计数的办法实现,即记录每一块内存被指向的次数。

这就是我们最常用的指针,shared_ptr,称为带引用计数的智能指针,或称为强智能指针。思想:允许多个智能指针对象指向一块内存,最后一个对象释放该堆内存,采取引用计数实现。

(二)实现原理

概念,分析:

  • 需要用一个数据结构保存内存地址和计数信息,所以设计一个管理数据结构的类。
  • 数据结构中的每一行记录一个内存块的地址和指向内存的指针个数,可以用结点类表示。
  • 智能指针类,用来实现智能指针的操作。

【1. 数据结构的设计】

采取引用计数办法记录内存块被指向的次数,那么就需要使用数据结构来保存:

  • 每一块内存的地址
  • 每一个内存地址的指针计数,即当前有几个指针指向这块内存。

我们可以使用数组,map等结构实现,采取数组思想实现,如:

在这里插入图片描述

那么该结构体的每一行就是结点,提供管理类对该结构体进行操作,智能指针每指向或释放一块内存,在此结构体上进行信息的更新,所以需要和管理类进行通信。

【2. 实现智能指针的类间关系:】

C++类间通信,提供接口实现即可。实现shared_ptr智能指针,需要设计三个类,三个类提供数据结构,接口实现通信。那么三个类的作用是

  1. 结点类负责初始化结构体每一行的信息,将地址置为NULL,计数初始化为0。
  2. 管理结构体类负责开辟Node[x]大小的结构体,对结构体进行地址,计数添加,计数减少,查找,获得计数等管理操作。
  3. 智能指针类实现指针的基本操作,当指针开辟内存,释放内存时需要调用管理结构体类的函数。

那么三个类的关系为:
在这里插入图片描述

从上图可以看出,核心是实现结构体管理类,因为智能指针的函数基本都需要调用管理类的方法

【3. 结点类】

将结点类写为结构体管理类的私有成员,结点类中:

  • 成员变量:内存地址,计数。
  • 成员函数:构造函数:对两个成员变量进行初始化。

结点类代码如下:

class Node//一条记录
	{
	public:
		Node(void* a = NULL,int c = 0)
			:addr(a),count(c)
		{}
		void* addr;
		int count;
	};

【4. 结构体管理类:】

结构体管理类进行结构体大小的申请,对其进行操作,因为操作的结构体类型是固定的,地址是void*,计数为int,所以不需要写为模板类:

成员变量:

  • 开辟一定数量的结点,如开辟10个结点大小的结构体,Node total[10];表示可以记录10个内存块的信息。
  • 结构体可以提供下标来访问,所以定义变量current,代表现在里面的有效元素个数,如current=1,表示记录了一个内存信息,因为数组从0开始,所以下一个内存信息放入current下标1中。

成员函数:

  • 查找地址函数写为私有的,不允许外界使用,遍历查找结构体,判断此内存块地址是否存在。

    //查找当前数据结构中是否存在该地址
     int find(void* ptr)
     {
     	for(int i = 0;i<10;i++)
     	{
     		if(ptr == total[i].addr)
     			return i;
     	}
     	return -1;
     }
    
  • 获取内存块的计数情况先查找,有返回对应结点的count即可,没有返回-1

    //3.获得内存块的引用计数
     int getc(void* ptr)
     {
     	int i = find(ptr);
     	if(i < 0)
     	{
     		return -1;
     	}
     	else
     	{
     		return total[i].count;
     	}
     }
    
  • 增加函数。

  • 删除函数。

【4-1 成员函数增加函数的实现:】

当智能指针开辟内存块或拷贝构造,赋值操作,就要调用此函数,将内存块的地址信息更新到结构体中。

增加函数主要实现流程:

  • 查找地址内存块是否存在存在表示指针不是第一次指向,直接将对应的**内存块计数++**即可。
  • 不存在,将地址放入current下标的结构体,计数置为1,current++。

那么代码如下:

//1.添加一个内存块信息或计数
	void add(void* ptr)
	{
		int i = find(ptr);//查找该内存块是否存在
		if(i != -1)//表示存在
		{
			total[i].count++;//对应内存块计数++即可
		}
		else//不存在
		{
			total[current].addr = ptr;//将地址放入结构体
			total[current].count = 1;//计数为1
			current++;
		}
	}

【4-3 成员函数删除函数的实现:】

当智能指针对象结束时,调用析构函数,需要释放内存,或将引用计数- -,调用此函数,将计数- -,此函数实现流程:

  • 判断地址内存块是否存在不存在抛出异常
  • 存在,如果此时计数大于0,计数- -

注意: 当计数为0时,不允许进行- -操作,不清空那一行的信息,所以结构体可以保存的内存块地址信息,如果Node[10],那么就只能记录10块内存的地址信息。

代码如下:

//2. 减少计数,当计数变为0,表示最后一个指针对此内存块指向结束,我们并不清空地址记录,只是不允许减了
	//所以结构体只能保存10块内存地址信息
	void del(void* ptr)
	{
		int i = find(ptr);
		if(i < 0)//表示没找到,抛出异常
		{
			throw std::exception("ptr error");
		}
		else
		{
			if(total[i].count>0)
			{
				total[i].count--;
			}
		}
	}

【5. 智能指针Shared_ptr类:】

因为不知道指向什么类型的变量,所以写为模板类。实现指针的基本操作,调用结构体管理类函数:

成员变量:

  • 静态结构体管理类对象,所有对象都可以看见,方便进行管理类函数的调用,需要在类外初始化。
  • 指针
private:
	T* mptr;
	static Manage manage;
};
template<typename T>
Manage Shared_ptr<T>::manage;

主要成员函数:

【1. 构造函数:】

  • 得到内存地址。
  • 指向一块内存调用管理类添加函数将内存地址添加到管理结构体中。

代码为:

   Shared_ptr(T* ptr = NULL)
		:mptr(ptr)
	{
		manage.add(mptr);
	}

【2. 拷贝构造函数:】

  • 获取旧对象的指针
  • 调用管理类添加函数将内存地址添加到管理结构体中。
    Shared_ptr(Shared_ptr<T>& rhs)
	{
		mptr = rhs.mptr;
		manage.add(mptr);
	}

【 3. 赋值重载函数:】

  • 调用析构函数处理指针原来指向的内存块;
  • 得到新指针的地址。
  • 调用管理类添加函数将内存地址添加到管理结构体中。
   Shared_ptr<T> operator=(Shared_ptr<T>& rhs)//赋值,先析构旧内存块,再指向新的
	{
		if(this != &rhs)
		{
			this->~Shared_ptr();//处理旧指针
			mptr=rhs.mptr;
			manage.add(mptr);
		}
		return *this;
	}

【 4. 析构函数:】

  • 调用管理类删除函数,减少内存地址计数;
  • 调用管理类获取计数函数,得到当前内存块的计数,如果此时内存计数为0,表示当前指针为最后一个指针,delete释放内存块。
  • 如果不为0,表示还有其他指针指向该内存,置空当前指针即可。
~Shared_ptr()
	{
		manage.del(mptr);//减少内存地址计数
		if(manage.getc(mptr) == 0)//如果此时内存计数为0,表示当前指针为最后一个指针,释放内存块
		{
			delete mptr;
		}
		else//否则,置为NULL即可。
		{
			mptr=NULL;
		}
	}

整合代码Shared_ptr源码,主函数测试:

int main()
{
	int* a = new int(10);
	Shared_ptr<int> s1(a);
	Shared_ptr<int> s2(a);
	Shared_ptr<int> s3(a);
	Shared_ptr<int> s4 = new int(20);
	Shared_ptr<int> s5=s4;
	s1=s5;
	s5.showma();
}

主函数操作:s1,s2,s3指向内存块a,s4申请新内存块,通过拷贝构造s5指向新内存块,通过赋值运算符重载s1指向新内存块,所以第一块内存现在有2个指针指向,第二块3个指针。

运行结果验证如下:

在这里插入图片描述
和我们分析的一样。

(三)缺陷

shared_ptr是强指针,和引用计数具有强关联性,只要存在指针指向,引用计数就++。这就会导致程序出现问题, 如下面一段代码:

class B;
class A
{
public:
	A()
	{
		std::cout<<"A()"<<std::endl;
	};
	~A()
	{
		std::cout<<"~A()"<<std::endl;
	}
	Shared_ptr<B> spa;//自己实现的shared_ptr
};
class B
{
public:
	B()
	{
		std::cout<<"B()"<<std::endl;
	};
	~B()
	{
		std::cout<<"~B()"<<std::endl;
	}
	Shared_ptr<A> spb;
};
int main()
{
	//缺陷,强指针内部的互相引用
	Shared_ptr<A> pa = new A();//A内部存在强指针
	Shared_ptr<B> pb = new B();//B也存在
	pa->spa=pb;//互相指向
	pb->spb=pa;//析构只会析构pa,此时A生成的堆内存有2个指针指向,但是只会析构pa,内存块不能被释放,因为计数为1
}

按照代码设计,应该输出A,B构造,B,A析构。运行程序:

在这里插入图片描述
和我们想的不一样,它并没有释放申请的内存,原因就是发生了强指针相互引用,我们分析上面的代码:
在这里插入图片描述
释放时,销毁了pb和pa的指向,此时没有释放内存,因为还有别的指针指向,此时就形成了内部指针的相互指向,无法释放内存块。

核心原因:强指针的互相指向,导致内存无法释放

六、weak_ptr

(一)基本概念

为了解决强智能指针相互引用的问题。如果在互相指向时引用计数不加1,那么就不会出现互相指向无法释放的问题了。需要使用到弱指针weak_ptr:

  • 不能单独使用,必须结合强智能指针一起使用。因为弱智能指针的析构不释放内存,导致内存泄露,故需要结合强指针一起使用。
  • 在拷贝构造函数,赋值运算符重载函数的实现中不改变计数值,不使用计数计数,只是让指针指向即可。

(二)实现原理

不使用引用计数技术,那么:

【成员变量:】

只需要一个指针即可。

【成员函数:】

  1. 拷贝构造函数:浅拷贝,直接指向

    Weak_ptr(Weak_ptr<T>& rhs)
    {
    	mptr=rhs.mptr;
    }
    
  2. 弱指针给弱指针赋值的赋值运算符重载:浅拷贝:

    Weak_ptr<T> operator= (Weak_ptr<T>& rhs)//弱指针=弱指针赋值
    {
    	if(this != &rhs)
    	{
    		mptr=rhs.mptr;
    	}
    	return *this;
    }
    
  3. 强指针给弱指针赋值的运算符重载:不能直接访问Shared_ptr强指针的的私有成员,所以从公有接口获取,强指针提供一个常方法getptr返回自己的地址指向,弱指针直接接收即可。

    //Shared_ptr类中使用
    T* getptr()const//给弱指针用
    {
    	return mptr;
    }
    Weak_ptr<T> operator= (const Shared_ptr<T>& rhs)//弱指针=强指针赋值,不能直接访问Shared_ptr的私有成员,所以从公有接口获取
    {
    	mptr=rhs.getptr();//常对象只能调用常方法,所以getptr需要用const修饰
    	return *this;
    }
    
  4. 析构函数:里面什么也不写,故不能单独使用,因为它不释放内存。

    ~Weak_ptr()
     {}
    

将上面强指针缺陷代码中A,B类的成员变量改为弱指针,那么此时代码分析为:

在这里插入图片描述

整合代码 Weak_ptr源码 程序运行如下:

在这里插入图片描述
可以成功释放A,B内存,弱指针解决了强指针相互指向的问题。

(三)缺陷

必须和强指针配合使用,不能单独使用,因为只开辟了内存,析构中没有释放。

加油哦!🥘。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值