C++学习之路-运算符重载

为什么要对运算符进行重载:

C++预定义中的运算符的操作对象只局限于基本的内置数据类型,但是对于我们自定义的类型(类)是没有办法操作的。但是大多时候我们需要对我们定义的类型进行类似的运算(也就是对象之间的基本运算),这个时候就需要我们对这么运算符进行重新定义,赋予其新的功能,以满足自身的需求。

比如:对象之间的相加,相减、对象取反、判断两个对象是否相等

Point p1;
Point p2;
Point p3;
p3 = p1 + p2  // 看起来很不正常,但是只要我们对“+”进行运算符重载,就可以实现
if !p1 == p2: // 只要我们对 “!” 和 “==” 进行运算符重载,就可以实现
	// ...

C++运算符重载的实质:

运算符重载的实质就是函数重载或函数多态。运算符重载是一种形式的C++多态。目的在于让人能够用同名的函数来完成不同的基本操作。要重载运算符,需要使用被称为运算符函数的特殊函数形式,其运算符函数形式:operatorp(argument-list)// operator后面的’p’为要重载的运算符符号。

运算符重载,需要采用operator后面紧跟着运算符,大致定义形式如下:

<返回类型> operator <运算符符号>(<参数列表>)
{
    // do something
}

比如:

void operator+() { }
void operator-() { }
void operator==() { }
void operator!=() { }
void operator++() { }

下面我们通过例子,具体讲解各种运算符是如何重载的。

梳理各种运算符的重载

首先定义一个类Point,有两个私有的成员变量存放一个点的xy坐标值,一个打印函数,一个构造函数。

class Point
{
private:
	int m_x;
	int m_y;
public:
	Point(int x, int y):m_x(x),m_y(y){ }
	void display()
	{
		cout << "(" << m_x << "," << m_y << ")" << endl;
	}
};

加号 “+” 运算符重载

重载加号 “+” 目的就是想让两个对象可以直接相加,即实现这种操作:

Point p1(10,20);
Point p2(20,30);
Point p3 = (p1 + p2); // 对象直接相加

重载函数如下:

friend Point operator+(Point, Point); // 该函数在Ponit类中声明为友元函数,因此可以直接访问成员变量

Point operator+(Point p1, Point p2)
{
	return Point(p1.m_x + p2.m_x, p1.m_y + p2.m_y);
}

函数的返回值是Point类型的,operator后面跟着“+”,所以重载的是“+”。传入的两个参数分别是运算符左右两侧的变量,也就是两个Point对象。函数实现的功能是将两个对象的m_x变量相加,两个对象的m_y对象相加,然后返回一个临时对象。

运算符函数调用的本质

运算符重载函数本质上还是函数,所以下面两行代码本质上是一样的。我们进行运算符重载的目的也就是可以更加直观的,更加简便的,使得对象之间通过我们熟悉的基本运算符完成对象间的自定义运算操作。

总而言之,就是好看,代码可读性高。

Point p3 = p1 + p2;
Point p3 = operator+(p1, p2);

我们查看汇编也可以证明上述观点:Point p3 = p1 + p2;代码里可以看出call operator+的操作。

在这里插入图片描述

相加操作,一定还存在两者以上变量相加,假如想要三个对象一起相加,上述函数可以实现吗?答案是可以实现,但是原理是什么呢?

Point p1(10, 20);
Point p2(20, 30);
Point p3(30, 40);
Point p4 = p1 + p2 + p3;
p4.display(); // (60,90)

因为下面三行代码的原理,本质,以及结果都是一样的。

Point p4 = p1 + p2 + p3;
Point p4 = operator+(p1, p2) + p3;
Point p4 = operator+(operator+(p1, p2) , p3);

即先进行p1和p2的相加,然后返回一个临时Point对象,作为operator+函数的第一个形参再次传入,与p3进行相加。也就意味着,我们写两个重载之后的运算符就相当于调用两次重载函数一样。

“+”运算符重载函数的优化(完善),也是运算符重载过程中的注意点(非常重要!!!

我们之前在对象类型参数与对象类型返回值(博文)的那一节提到,当对象作为函数形式参数时,会产生一些不必要的中间对象(额外调用拷贝构造函数),所以函数的参数尽量使用引用类型:

friend Point operator+(Point &, Point &); // 该函数在Ponit类中声明为友元函数,因此可以直接访问成员变量

Point operator+(Point &p1, Point &p2)
{
	return Point(p1.m_x + p2.m_x, p1.m_y + p2.m_y);
}

其次,函数的参数尽量使用const &(const引用)的形式。为什么呢?因为const& 可以接受const参数和非const参数(之前的博文中有介绍:const引用作为函数形参时,可以接收“实参”的范围比较大,在面向对象中用处很多

所以,函数的形参,设置为const引用:

friend Point operator+(const Point &, const Point &); // 该函数在Ponit类中声明为友元函数,因此可以直接访问成员变量

Point operator+(const Point &p1, const Point &p2)
{
	return Point(p1.m_x + p2.m_x, p1.m_y + p2.m_y);
}

因为,有时候我的对象是const类型的。所以参数写成const更加的具有“鲁棒性”。

const Point p1(10, 20);

跟类有关的运算符函数,也可以重载为成员函数。只不过,函数的形式要发生一定的变化。因为成员函数由对象调用,因此形式参数相对于全局函数少一个参数

class Point
{
private:
	int m_x;
	int m_y;
public:
	Point(int x, int y):m_x(x),m_y(y){ }
	void display()
	{
		cout << "(" << m_x << "," << m_y << ")" << endl;
	}

	Point operator+(const Point &point)
	{
		// this 执行调用函数的对象,可以省略不写this
		return  Point(this->m_x + point.m_x, this->m_y + point.m_y);
	}
};

因此,对象之间相加的运算,也就是下面两种写法。原理本质是一模一样的。

p1 + p2;
p1.operator+(p2);

这样将运算符重载函数写在类内部有两个好处:函数书写更简单(参数少,不用设置为友元);本身运算符重载大多数就是为对象服务的,因此写在类内也好管理。

减号 “-” 运算符重载

重载减号 “-” 目的就是想让两个对象可以直接相减,即实现这种操作:

Point p1(10,20);
Point p2(20,30);
Point p3 = p1 - p2; // (-10,-10)

重载函数如下:

class Point
{
private:
	int m_x;
	int m_y;
public:
	Point(int x, int y):m_x(x),m_y(y){ }
	void display()
	{
		cout << "(" << m_x << "," << m_y << ")" << endl;
	}

	Point operator-(const Point &point)
	{
		return  Point(m_x - point.m_x, m_y - point.m_y);
	}
};

这跟重载加几乎相似,没什么问题。

运算符重载尽量也要遵循原本运算符的运算规则

这句话什么意思呢?

我们知道C++中有些表达式是可以被赋值的,有些表达式则不可以被赋值。比如,可以被赋值的表达式:

int a = 1;
int b = 0;
(a > b ? a : b) = 10; // 表达式是可以被赋值的

不可以被赋值的表达式:

a + b = 10;
a - b = 10;

也就是说,加减这种操作所得到的表达式是不可以被赋值的。那我们在重载对象之间的加减操作时也要遵循这样的原则,即不可以给对象加减之后的表达式赋值,即下列操作应该是不被允许的:

p1 - p2 = Point(10, 10);
p1 + p2 = Point(30, 30);

所以我们就要进一步完善刚才的重载函数。既然我们要求相加之后的返回值不可以被赋值,那就把返回值设置为const就可以了(因为想要为表达式赋值,那么表达式一定是可以修改的左值。但是const是不可以修改的,所以达到了我们的目的)

class Point{
	// ...
	const Point operator+(const Point &point)
	{
		return  Point(m_x + point.m_x, m_y + point.m_y);
	}
	
	const Point operator-(const Point &point)
	{
		return  Point(m_x - point.m_x, m_y - point.m_y);
	}
};

在这里插入图片描述

这样才合理。因为p1+p2返回的是一个临时对象,是一个匿名对象。为这个对象赋值没有意义,因为这个临时对象马上就会销毁。

但是,返回值是const类型之后,发现不可以连续调用了,即不可以连续相加或者连续相减了。这又是为什么呢?为什么返回值是const就不可以连续相加减了呢?

在这里插入图片描述

由于,上面的操作本质是这样调用重载函数的:

Point p4 = (p1.operator+(p2)).operator+(p3);

也就是说,先执行p1.operator+(p2),返回一个const对象,然后再通过这个const对象调用operator+()函数。但是,我们知道const对象只能调用const成员函数。所以无法继续调用非const类型的成员函数。

所以,想要遵循运算符基本规则的前提下,还可以满足连续相加减的操作,就必须也得将成员函数变成const成员函数,即下面写法:

class Point
{
	// ...
	const Point operator+(const Point &point)  const
	{
		return  Point(m_x + point.m_x, m_y + point.m_y);
	}

	const Point operator-(const Point &point)  const
	{
		return  Point(m_x - point.m_x, m_y - point.m_y);
	}
};

这样就没有问题了

在这里插入图片描述

所以到这里,我们要搞明白const返回值,const引用形参,const函数的特点。不是函数的最前面有一个const就是const函数,那只是const类型的返回值而已。

第一个const,是为了防止返回值被赋值。第二个const是为了扩大参数的范围。第三个const是为了让返回值为const类型的对象可以继续调用operator-函数。

const Point operator-(const Point &point)  const
{
	return  Point(m_x - point.m_x, m_y - point.m_y);
}

加等于 “+=” 运算符重载

加等于 “+=”目的就是想让两个对象相加之后,将结果赋值给前面的对象,即实现这种操作:

Point p1(10,20);
Point p2(20,30);
p1 += p2; // 即:p1 = p1 + p2  print:(30,50)

重载函数如下:

void operator+=(const Point &point)
{
	m_x += point.m_x;
	m_y += point.m_y;
}

执行+=操作,就可以函数内部更新p1。

Point p1(10, 20);
Point p2(20, 30);
p1 += p2; // p1.operator+=(p2)
p1.display(); //(30,50)

但是,别忘了,我们要保留运算符之前的一些特性

既然原本的+=有返回值,那么就是可以被赋值的。就像下面的操作一样:a+=b之后,a变成30,然后又把40赋值给a,所以最终的a就被变成了40。

int a = 10;
int b = 20;
a += b = 40;

所以,我们重载给对象的+=也要满足这种可以被赋值的操作,也就是重载函数不可以void类型了,尽量要有返回值。于是,重载函数可以被写成这样:

Point operator+=(const Point &point)
{
	m_x += point.m_x;
	m_y += point.m_y;
	return *this;
}

先解释一下,返回值是*this是怎么回事。

解释:既然传进这个函数的是p1对象,那么默认会把p1对象的地址赋值给this,那通过*this就可以读取p1对象那块内存的内容,进而返回对象的内容。

然后就可以在外面对表达式进行赋值了

Point p1(10, 20);
Point p2(20, 30);
(p1 += p2)  = Point(30, 30);
p1.display(); // (30,50)

而打印出来的p1确实(30,50),也就是说Point(30, 30)并没有赋值给p1。理想中的赋值过程应该是这样的:

 1. (p1 += p2); 
 2. p1 = p1 + p2; //p1 = (30,50)
 3. p1 = Point(30, 30);
 4. //最终p1是(30, 30)

既然我写出了理想中的赋值过程,那就是说我上面的赋值过程其实不是这样的。<重要的知识点来了>:非引用类型的返回值,只会返回一个原本对象的copy对象,而这个copy对象是临时的,也就是说上述真实的赋值过程是这样的:

 1. (p1 += p2); 
 2. p1 = p1 + p2; //p1 = (30,50)
 3. Ponit p1.copy = Point(30, 30); // 赋值给的是临时对象,并没有赋值给p1本身
 4. //最终p1还是第二步得到的(30,50)

引用返回和非引用返回的差异

这是因为,非引用类型的返回值,只会返回一个原本对象的拷贝对象(之前博文也有介绍,不仅会导致此次赋值的失败,而且还会调用拷贝构造函数,产生一些额外的中间变量,因此要避免值传递返回,影响程序效率)。但是引用类型的返回值就会避免这样的问题:

Point &operator+=(const Point &point)
{
	m_x += point.m_x;
	m_y += point.m_y;
	return *this;
}

(p1 += p2)  = Point(30, 30); //p1最后就是被新赋值的(30,30)

这是因为引用返回类型,直接是引用对象,而不是拷贝对象。所以返回类型是对象本身(虽然返回的是对象的别名变量,但是依然是对象本身,而不像非引用返回的是对象的拷贝)。我们通过汇编也可以看清楚这一点:

  • 引用返回

在这里插入图片描述

  • 非引用返回

在这里插入图片描述

可以看出,非引用返回比引用返回的汇编多了一行,这就是传递临时变量的地方。

总结:如果采用引用返回类型,则就是返回对象本身。若采用非引用返回对象,则会返回一个临时对象,但是也会完成p1+=p2操作,更新p1,只不过该表达式会返回一个临时对象,任何对这个表达式赋值的操作,都是对这个临时对象赋值。

等于 “==” 运算符重载

等于 “==”目的就是想判断两个对象是否相等,即实现这种操作:

Point p1(10,20);
Point p2(20,30);
if (p1 == p2)
{
	// do something;
}

重载函数如下:C++中用0/1表示假/真,而python用false/true表示。

bool operator==(const Point &point)
{
	if (m_x == point.m_x && m_y == point.m_y) // x等于并且y也要等于
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

判断两个对象相等或者不等,都要用bool类型,更加的直观。但是,这样写函数体是效率不高的,可以改写为这样:(以后写代码,能不用if else就不用)

bool operator==(const Point &point)
{
	return (m_x == point.m_x && m_y == point.m_y);
}

不等于 “!=” 运算符重载

不等于 “!=”目的就是想判断两个对象是否不相等,即实现这种操作:

Point p1(10,20);
Point p2(20,30);
if (p1 != p2)
{
	// do something;
}

重载函数如下:x不等于或者y不等于

bool operator==(const Point &point)
{
	return (m_x != point.m_x || m_y != point.m_y);
}

当我一个是const对象,一个是非const对象时,就会报错。

在这里插入图片描述
因此如果想要扩大函数调用范围,比如一个const对象想要判断相等或者不等,就需要将成员函数设置为const函数:

bool operator==(const Point &point) const
{
	return (m_x != point.m_x || m_y != point.m_y);
}

这样就不会出错,因此能使用const函数扩大调用者范围的就使用const。不能使用const函数的千万不要硬使用(比如内部修改成员变量时,就绝对不允许是const函数,具体内容之前的博文:const成员

在这里插入图片描述

赋值运算符“=” 的重载

重载赋值运算符,多数是自定义赋值操作。比如,将p1赋值给p2的时候,仅仅将p1的m_y值赋值给p2,而不将m_x赋值给p2。

Point p1(10, 20);
Point p2(20, 30);
p2 = p1; // p2 = (20,20)

重载函数:

void operator=(const Point &point)
{
	m_y = point.m_y;
}

这种就是根据自己需求,进行重载了,用的不多。

单目运算符“-”负号的重载

单目运算符“-”负号的重载,目的是将一个对象取反,即实现这种操作:

Point p1(10,20);
-p1; //(-10,-20)

重载函数如下:

const Point operator-() const
{
	return Point(-m_x, -m_y);
}

注意负号和减号的区别。虽然函数名一样,但是参数列表却不同。减号是双目运算符,需要传入另一个对象参数,而负号是单目运算符,不需要传入其他对象参数。因为调用者本身就是对象,只需要处理本身就行了。

解释一下,各处const的用处:

const返回类型是为了方式被赋值,因为原本的取反操作之后是不允许赋值的。第二个const成员函数的声明是为了多次调用该函数,比如:负负得正,两次调用该函数

(-(-p1))

为什么返回值是非引用类型呢?因为我们不希望-p1之后,改变p1的值。比如将-p1赋值给p4,我没有必要改变p1吧,所以-p1返回一个临时对象就可以了,只需要把临时变量值赋值给p4就行了,达到p4是(-10,-20)就可以了。

const Point p1(10, 20);
Point p4 = -p1;
p1.display(); //(10,20)
p4.display(); // (-10,-20)

所以,返回值选择引用或者非引用要看实际应用,并不是引用返回一定比非引用返回好。

单目运算符“++”自加的重载

单目运算符“++”自加的重载,目的是让对象自加,即实现这种操作:

Point p1(10,20);
p1++; //(11,21)

然后我们就可以写重载函数了:

void operator++()
{
	m_x++;
	m_y++;
}

执行操作:

Point p1(10, 20);
p1++;
p1.display();

但是编译器报错了,这是因为什么呢?

在这里插入图片描述

我们都知道,++有前置也有后置,即有p1++,也有++p1。我们那样定义重载函数,重载的是前置++。编译器为了区分后置++,要求重载函数必须写成下面这样:即参数列表里必须有一个int类型的参数传入

// 后置++
void operator++(int)
{
	m_x++;
	m_y++;
}

总结就是:重载++运算符,想要重载p1++,重载函数里必须有一个int类型的参数声明(只写一个int就行);想要重载前置++,即++p1,参数列表里可以什么都不写。

顺便唠叨一句a++和++a的区别

++a是先让a+=1,也就是先让a自加1,立马更新a。这一过程完成++a才结束

int a = 10;
int b = ++a + 5;

因此,上面的计算拆解就是这样:最终b=16,a=11

int a = 10;
a+=1; // a先加1,变成11,然后再参与计算
int b = a + 5;

a++ 是先让原本的a参与计算,计算完之后,自身再加1

int a = 10;
int b = a++ + 5;

所以,上面的过程拆解为:最终b=15,a=11

int a = 10;
int b = a + 5;
a+=1;

总结就是:前置++a,先自加后计算,因为++放在前面,所以先自加再说。后置a++,先计算再自加,因为++放在后面,所以先计算完了,再自加。

理解编译器为什么要那么区分两个++

前置++重载函数:

void operator++()
{
	m_x++;
	m_y++;
}

后置++重载函数:

void operator++(int)
{
	m_x++;
	m_y++;
}

我们知道,重载函数里有参数代表可能是双目运算。想想后置++要先计算,因此需要一个参数与其进行计算,所以后置++参数列表里才有了一个int变量。而前置++,不需要先计算,前置++就是很纯粹的自加,因此不需要和别的变量计算,参数列表里也就不需要参数了。

完善前置++可以被赋值的操作

我们了解了前置++和后置++的区别,很容易就知道++a是返回更新后的a,而a++则是先返回a之前的值。所以前置++是可以被赋值的

Point p1(10, 20);
++p1 = Point(20,30)
// 最后p1不是11,21,而是20,30

所以更改前置++的重载函数。返回引用类型不用多解释了,前面已经说过了,也就是返回对象本身,而不是返回对象的copy。

Point &operator++()
{
	m_x++;
	m_y++;
	return *this;
}

完善后置++可以作为表达式一份子的操作

如果后置++,还是void类型,那么p1++就不可以与其他变量构成表达式

void operator++(int)
{
	m_x++;
	m_y++;
}

如下所示:没有void+Point这种错误。因此p1++也要有返回值

在这里插入图片描述

我们知道这样写p1++,先返回p1之前的值,然后与Point(30,40)相加。所以重载函数得这么写:

const Point operator++(int)
{
	Point old(m_x, m_y);
	m_x++;
	m_y++;
	return old;
}

三个细节:第一个是返回的是之前的变量值;第二个是非引用返回,也就是说返回的是临时变量,让临时变量与Point(30,40)相加,而p1并不参与计算,这与后置++的计算过程也是一样的;第三个就是返回值是const类型,意味着p++,不可以被赋值,因为p++返回的是临时对象,临时对象赋值没有意义。

单目运算符“- -”自减的重载

单目运算符“- -”自减的重载,目的是让对象自减,即实现这种操作:

Point p1(10,20);
p1--; //(9,19)
--p1; //(9,19)

与++同理,重载函数也是有两个,一个是前置–,一个是后置–。

  • 后置- -
Point operator++(int)
{
	Point old(m_x, m_y);
	m_x--;
	m_y--;
	return old;
}
  • 前置- -
Point &operator++()
{
	m_x++;
	m_y++;
	return *this;
}

输出符号“<<”的重载(左移运算符)

输出符号?什么意思?想实现什么目的呢?想实现直接打印对象的目的!!

Point p1(10, 20);
cout << p1; // (10,20)或者打印其他别的也行

重载函数如下:

friend void operator<<(ostream & , const Point&); //类中友元函数

void operator<<(ostream &cout, const Point& point)  //全局函数,不写在类里
{
	cout << "(" << point.m_x << "," << point.m_y << ")" << endl;
}

因此我们就可以办到:直接打印对象。

Point p1(10, 20);
cout << p1;

看到上面的重载函数,我们恍然大悟,原来cout是一个对象,是ostream类下的一个对象而已。

重载函数为什么不写在类里,而要变成全局函数呢?

因为我们知道重载函数的规则。如果写在类里,一定是对象调用,那么运算符的前面一定是对象变量,运算符后面是啥,取决于函数的参数是啥。而写成全局函数,参数列表的第一个参数就是运算符左边变量,第二个参数就是运算符右边的变量。

所以,我们想要达到语义清楚,即cout在运算符左边,就不可以写在类里,因为写在类里,运算符左边一定是对象,打印语句的语义就不清晰了。

连续输出的运算符重载函数

我们一定有这样的需求:连续打印对象值

Point p1(10, 20);
Point p2(20, 30);
cout << p1 << p2;

通过上面的学习也知道,这样就相当于连续调用重载函数,且函数必须有返回值。那么拆解连续打印就是这样

cout << p1 << p2;
// 拆解
cout = cout << p1;
cout << p2;

即这样调用函数,因此operator<<(cout, p1)的返回值,必须得是cout

operator<<(operator<<(cout, p1), p2)

所以重载函数完善为:返回值是cout,所以是返回类型为:ostream &

ostream &operator<<(ostream &cout, const Point& point)
{
	cout << "(" << point.m_x << "," << point.m_y << ")" << endl;
	return cout;
}

然后就可以连续打印了:

Point p1(10, 20);
Point p2(20, 30);
cout << p1 << p2;

在这里插入图片描述

所以,我们之前连续打印东西时,每一步都会返回一个cout,然后继续打印。我们通过重载左移“<<”,了解了cout的本质。

输入符号“>>”的重载(右移运算符)

输入符号?什么意思?想实现什么目的呢?想实现可以键盘输入对象的目的!!

Point p1(10, 20);
cin >> p1; // 键盘输入p1的值,然后改变p1,或者初始化p1都可以

重载函数如下:

friend istream &operator>>(istream &cin, Point& point); //类中友元函数

istream &operator>>(istream &cin, Point& point)
{
	cin >> point.m_x;
	cin >> point.m_y;
	return cin;
}

有一个细节:operator>>函数的第二个参数,不能是const类型的,即Point& point前面不可以加const。因为我在函数体内部要输入(可以理解为更改)对象的值,所以输入不可以是const类型的。

仿函数

仿函数顾名思义,模仿函数,但其实跟函数还是有差别的。

仿函数定义:通过在类内重载"()"括号,达到仿函数的目的。

我们假如在类外定义了一个全局函数,长这样

int sum(int a, int b)
{
	return a + b;
}

那仿函数,长什么样的?可以看到,重载的是括号,参数跟普通函数是一样的,这就叫仿函数。

int operator()(int a, int b)
{
	return a + b;
}

仿函数的好处:因为定义在类内,可以访问成员变量,从而为类进行服务。这都属于语法糖,了解一下就好了。

运算符重载总结

  • 想要自由的定义运算符的左边是谁,右边是谁,那就需要定义为全局函数。如果该运算符仅仅跟某个类相关,那就优先定义为成员函数。如果实在不可以重载为成员函数,那就全局函数并且设置为友元函数。
  • 如果将运算符重载函数设置为private私有,那么外部就不可以实现运算符的重载。比如:ostream类下的cout对象即使返回的不是const类型,也不可以被赋值,就是因为C++内部将ostream的赋值运算符重载为private了。
  • 单例模式要求类外只能创建一个对象。即使我创建两个对象,这两个对象也是同一个对象。既然是同一个对象,就没必要存在赋值操作,这时候就可以在单例模式的类内,将赋值运算符重载为private成员。这样外部在两个相同对象进行赋值就是不被允许的。

在这里插入图片描述

在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值