C++类与对象(中)

书接上回说 类与对象(上)

目录

1.类的六个默认成员函数

2.构造函数

构造函数概念与特性

构造函数重载

 默认构造函数

3.析构函数

4.拷贝构造函数

默认拷贝构造函数

5.运算符重载operator

 GetMonthDay()

1.operator==

2.operator>

3.operator+=

4.operator+

5.+=和+的另一种写法

operator+

operator+=

 6.operator=

7.operator!=

8.operator>=

9.operator<

10.operator<=

11.operator-=

12.operator-(日期减去天数)

13.前置++ 与 后置++

14.operator-(日期减日期)

15.opeartor<<

 16.operator>>

 17.前置operator -- 

18.后置operator--(int)

 6.const成员函数


有这样一道面试题: 给出下面这一段代码请问这段代码的运行结果是什么?

A 编译错误      B运行崩溃     C 正常运行

class A
{
public:
	void Print()
	{
		cout << _a << endl;
		cout << "Print()" << endl;
	}
// private:
	int _a;
};
int main()
{
	A* p = nullptr;
	// 成员函数的地址不在对象中
	// 成员变量是存在对象中
	p->Print();
	return 0;
}

 许多同学乍一看p是nullptr,然后p->Print()对p这个空指针进行了解引用,所以这个程序的运行结果是运行崩溃。这种想法其实是错误的。那么究竟为什么呢?

我们来转到反汇编看一看

 p->Print call了一下A::Print这个成员函数,但是我们知道的是成员函数的地址并不存放在对象中,而是存放在了公共代码段,相当于没有对p进行解引用,直接在公共代码段找到了Print函数,所以这段代码会正常运行。

这里还有一道类似的题目:那么下面这个程序的运行结果又是什么呢?

class A
{
public:
	void Print()
	{
		cout << _a << endl;
	}
// private:
	int _a;
};

int main()
{
	A* p = nullptr;

	// 成员函数的地址不在对象中
	// 成员变量是存在对象中
	p->_a=1;

	return 0;
}

 同上题,我们先转到反汇编看一看

 与成员函数不同的是,成员变量是存储在对象中的。

在上面cout时相当于cout<<this->_a<<<endl; 这里的this是一个空指针,那么此时就会运行崩溃

1.类的六个默认成员函数

2.构造函数

在之前数据结构的学习栈的过程中,我们极有可能未对栈进行初始化就对栈进行 删除插入等操作,此时编译器会崩溃,我们也十分的崩溃,那么有没有什么好的方法,来避免这种情况,使得我们不需要初始化,在调用某个函数时自动进行初始化呢?C++中的构造函数很好的解决了这个问题

构造函数概念与特性

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务不是开空间创建对象,而是初始化对象

下面是构造函数的特征:

1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。

5.如果类中没有显示定义构造函数,则c++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义后编译器将不再自动生成。

 下面讲讲几个在使用构造函数时容易出错的情况

1.不传参的情况

class Date
{
public:
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

 d1直接不传参数直接全缺省调用了构造函数,那为什么这里Date d1后面不加括号呢?

原因是Date d1(); 与函数声明冲突了,混淆了,编译器不知道这是构造函数的调用还是某个函数的调用

构造函数重载

class Date
{
public:
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

这个时候两个构造函数 构成了函数重载,但是不能够同时存在,因为无参调用的时候会发生歧义,编译器此时也会报错

 默认构造函数

#include<iostream>
using namespace std;
class Date
{
public:
	//Date()
	//{
	//	_year = 1;
	//	_month = 1;
	//	_day = 1;
	//}
	//Date(int year = 1, int month = 1, int day = 1)
	//{
		//_year = year;
		//_month = month;
		//_day = day;
	//}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

class Stack
{
public:
	/*Stack(size_t capacity = 3)
	{
		cout << "Stack(size_t capacity = 3)" << endl;

		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
		}

		_capacity = capacity;
		_top = 0;
	}*/

private:
	int* _a;
	int _capacity;
	int _top;
};

// 两个栈实现一个队列
class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;
	int _size = 1;
};

int main()
{
	Date d1;
	d1.Print();

	Stack st1;

	MyQueue mq;

	return 0;
}

首先我们来观察一下Date,Stack和MyQueue的成员的类型。

Date的三个私有成员的类型都是int类型的,Stack的三个私有成员有两个是int类型的,一个是int*类型的,而MyQueue的三个私有成员函数一个是int类型的另外两个是Stack类型的。

上面这三个类,只有MyQueue的成员有自定义类型,其余都是内置类型的。

此时我们将构造函数注释掉,观察一下默认构造函数对哪些类型的成员起作用。

我们可以观察到下图,在这里d1和st1的成员都没有进行初始化,全部都是随机值,而mq中的两个自定义类型的成员进行了初始化需要注意的是这里的mq中的_size的值被编译器编译成了1,但是默认构造函数是没有对这里的_size起初始化作用的,这里显示他为1也只是编译器的一个不太合理的优化罢了

所以我们得到了默认构造函数的一个特点:内置成员不做处理(其实是不同编译器的处理方式不同,我们将他当作不处理),是自定义类型的话会去调用它的默认构造函数

what‘s more,Date *也是一个内置类型,这是一个指针类型

关于构造函数,我们一般都需要自己去写,成员全都是自定义类型或者声明时给了缺省值的,可以考虑让编译器自己调用它的构造函数。

3.析构函数

就像上面构造函数中说的,我们经常会忘记对类进行初始化,同样的,我们也有可能忘记释放类的成员所开辟的空间,C++的编译器会帮助我们处理很多东西,其中也包括释放类的成员开辟的空间,而实现这个功能的函数叫做析构函数

析构函数的特性:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	~Date()
	{
		// Date严格来说,不需要写析构函数
		cout << "~Date()" << endl;
	}
private:
	// C++11支持,声明时给缺省值
	int _year = 1;
	int _month = 1;
	int _day = 1;
};

class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		cout << "Stack(size_t capacity = 3)" << endl;

		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
		}

		_capacity = capacity;
		_top = 0;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;

		free(_a);
		_capacity = _top = 0;
		_a = nullptr;
	}

private:
	int* _a;
	int _capacity;
	int _top;
};

class MyQueue
{
	// 默认生成析构函数,行为跟构造类似
	// 内置类型成员不做处理
	// 自定义类型成员会去调用他的析构
private:
	Stack _pushst;
	Stack _popst;
	int _size = 1;
};

int main()
{
	//Date d1;
	//Stack st1;
	MyQueue mq;

	return 0;
}

 与构造函数类似,析构函数也有以下的特点:

默认生成的析构函数,行为跟构造类似

内置类型成员不做处理

自定义类型成员会去调用它的默认析构函数

 那关于默认生成的构造函数会去进行一些什么操作呢?

class Time
{
public:
 ~Time()
 {
 cout << "~Time()" << endl;
 }
private:
 int _hour;
 int _minute;
 int _second;
};
class Date
{
private:
 // 基本类型(内置类型)
 int _year = 1970;
 int _month = 1;
 int _day = 1;
 // 自定义类型
 Time _t;
};
int main()
{
 Date d;
 return 0;
}

程序运行结束后输出:~Time()
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,
_day三个是
内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在
d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数
中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函
数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time
类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁
main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

4.拷贝构造函数

拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错
因为会引发无穷递归调用。

C++规定,自定义类型对象传参拷贝,必须调用拷贝构造

void func(Date d1)
{
    d1.printf;
}

上面这个方式叫做浅拷贝也叫做值拷贝。这种方法对于成员函数都是内置类型的对象的影响不是很大,但是如果一个对象的成员中有自定义类型的话那么就会报错,那么这是什么原因呢?

void func2(Stack st)
{
	//...
}


int main()
{
	Stack st1;
	func2(st1);
	return 0;
}

我们用st1拷贝了st的内容,其中st的_a和st1的_a都指向同一片空间在出了作用域之后st会去调用析构函数,会把_a这片空间释放掉,当st1出作用域之后,st1也会调用析构函数,_a这片空间会再一次被释放,_a指向的空间被释放了两次,第一次释放之后_a就变成了野指针,再对他进行释放操作,编译器就会报错。那我们如何解决问题:

自定义类型对象拷贝的时候,调用一个函数,这个函数就叫拷贝构造。

 那么我们就开始写Stack类的拷贝构造函数了:

     Date d2(d1);

     Date(Date d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

但是,编译器这个时候又开始发力了,没错,编译器又开始报错了,那这究竟是为什么呢?

原因是这样的写法引发了无限递归!!!

我们陷入了这样的一个循环:

调用拷贝构造->传参->传值传参->在形成新的拷贝构造->调用拷贝构造......

 下面是传值时,编译器发生无限递归报错的图示:

承接拷贝构造函数的特性中的第二条, 拷贝构造函数的参数只有一个且必须是类类型对象的引用,否则就会引发无限递归。最后拷贝构造函数的写法是这样的

Date d2(const &d1);

Date (Date& dd)
{
_year =dd.year;
_month=dd.month;
_day=dd.day;
}

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(const Date& dd)
	{
		_year = dd._year;
		_month = dd._month;
		_day = dd._day;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{

	Date d1(2023, 10, 22);
	func1(d1);
	Date d2(d1);

	Stack st1;
	func2(st1);

	Stack st2(st1);
	return 0;
}

Date的成员调用拷贝构造函数的过程如下图:

 dd是d1的别名   this指针指向了d。但是这种调用方式有一些麻烦,我们可以这样写

这里dd是d1的别名,this指针指向d2。

Stack类的拷贝构造函数这样写:

Stack(const Stack& stt)
	{
		cout << "	Stack(Stack& stt)" << endl;
		// 深拷贝
		_a = (int*)malloc(sizeof(int) * stt._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_a, stt._a, sizeof(int) * stt._top);
		_top = stt._top;
		_capacity = stt._capacity;
	}

默认拷贝构造函数

编译器自动生成的默认拷贝构造函数会自动对内置类型进行值拷贝,对自定义类型会去调用它的成员的默认拷贝构造函数

我们将主函数改成如下,对应的Stack的拷贝构造函数不变

class MyQueue
{
Stack _pushst;
Stack _popst;
int _size = 0;
};//使用两个栈实现一个队列
int main()
{

	MyQueue q1;
    MyQueue q2(q1);
	return 0;
}

我们没有写自定义类型MyQueue的拷贝构造函数但是我们发现还是可以拷贝构造成功,原因是编译器帮我们调用了MyQueue的成员的拷贝构造函数。

所以我们得到了一下的一些结论:

    1、内置类型成员编译器会完成值拷贝
    2、自定义类型成员调用这个成员的拷贝构造
    stack需要自己写拷贝构造,完成深拷贝。
    顺序表、链表、二叉树等等的类,都需要深拷贝

    3.需要直接开空间的对象一般都需要深拷贝,但是MyQueue类是通过Stack类开辟的空间,所以MyQueue类不需要自己去写拷贝构造函数。

 至于传参数时,前面为什么要用const修饰,我们来观察下面这段代码对比

Date(Date& dd)
{
_year=dd.year;
_month=dd.month;
_day=dd.day;
}


Date(Date& dd)
{
dd.year=_year;
dd.month=_month;
dd.day=_day;
}//错误的


我们发现如果是拷贝的时候,我们把赋值的顺序搞反了,这个时候编译器不会报错,但是拷贝的结果会都是随机数,拷贝出错了,所以我们在传的参数前加上const修饰,防止这样的情况发生。

5.运算符重载operator

对于一个自定义类型,我们如果想比较他们的大小该怎么比较呢?编译器中的库函数是不具有这样的能力的,但是我们可以使用opereator运算符重载,需要注意的是:

不能通过连接其他符号来创建新的操作符,比如:operator@

重载操作符必须有一个类型参数

用于内置类型的运算符,其含义不能改变,例如operator+的含义应该与+的含义相同

作为类成员函数时,其形参看起来比操作数目少1,因为成员函数的第一个参数为隐藏的this

.*       ::   sizeof     ?:(三目运算符)   .    这五个运算符不能重载

 预备知识:

我们将运算符重载写在了类外面,但是写在了类外面又要面临一个问题:在类外我们无法访问到类中的私有成员,友元函数也许是一个解决方法,但是c++中一般会选择将运算符重载写在类中,放在类中的时候我们发现operator会报错:

原因是,类中的成员都会有一个隐藏的this指针,实际的参数会比传的参数多一个

那么我们可以这样写:

某些重载符传参数时 传的是const修饰的 引用,原因是:Date x会去调用拷贝构造,如果数据比较多的话空间的开销还是比较大的,所以我们在这里选择传引用,而且不对传过来的日期修改,所以传const

为了方便这里日期类的各种重载符的实现我们在这里写了一个GetMonthDay()函数,用于获取某一年中,某一个月份的天数

 GetMonthDay()

	int Date::GetMonthDay(int year, int month)
	{
		assert(year >= 1 && month >= 1 && month <= 12);
		int monthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
		{
			/*monthArray[2] = 29;*/
			return 29;
		}
		return monthArray[month];
	}

1.operator==

bool operator==(const Date& x, const Date& y)
{
	return x._year == y._year
		&& x._month == y._month
		&& x._day == y._day;
}

2.operator>

bool operator>(const Date& y)
{
	if (_year > y._year)
	{
		return true;
	}
	else if (_year == y._year && _month > y._month)
	{
		return true;
	}
	else if (_year == y._year &&_month == y._month && _day > y._day)
	{
		return true;
	}

	return false;
}

这个时候我们的调用d1>d2就应该是d1.operator(d2);

3.operator+=

在这里传引用的开销比较小,注意的是这里对d1做了修改,返回的结果是加了day之后的,所以此操作为+=而不是+

Date& operator+=(int day)
{
    if (day < 0)
    {
	    return *this -= (-day);
    }
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}

4.operator+

+和+=有什么区别呢? +=操作符会对自身进行修改,和+操作符不会对自身进行修改。所以我们可以在+操作符重载中 先将d1拷贝一份,然后对tmp进行修改,最后返回tmp的值,但是对d1本身没有做任何修改,而且这里没有用传值返回,原因是返回值为tmp,是一个局部变量,出了作用域之后就被销毁了。

	Date operator+(int day)
	{
		Date tmp(*this);;

		tmp._day += day;
		while (tmp._day > GetMonthDay(tmp._year, tmp._month))
		{
			tmp._day -= GetMonthDay(tmp._year, tmp._month);
			tmp._month++;
			if (tmp._month == 13)
			{
				tmp._year++;
				tmp._month = 1;
			}
		}
		return tmp;
	}

 同时,我们也可以使用上面的+=来对这里的+操作符进行复用

	Date operator+(int day)
	{
		Date tmp(*this);

		tmp += day;

		return tmp;
	}

5.+=和+的另一种写法

上面的+和+= 是+操作符对复用了+=操作符。下面这种写法是+=复用+操作符。

但是这种方法不是很推荐,相比于上面一种方法的+和+=对象拷贝次数一共只有2次,而下面这种方法的对象拷贝次数多达5次

operator+

Date Date::operator+(int day)
{
	Date tmp(*this);
	tmp._day += day;
	while (tmp._day > GetMonthDay(tmp._year, tmp._month))
	{
		tmp._day -= GetMonthDay(tmp._year, tmp._month);
		++tmp._month;
		if (tmp._month == 13)
		{
			tmp._year++;
			tmp._month = 1;
		}
	}
	return tmp;
}

operator+=

Date& Date::operator+=(int day)
{
	*this = *this + day;
	return *this;
}

 6.operator=

operator=我们不写的话,编译器会生成默认的operator=

跟拷贝构造的行为类型,内置类型值拷贝,自定义类型调用它的赋值

同样的Date类MyQueue类可以不写,但是Stack类必须自己实现,实现深拷贝

	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

赋值运算符写一个返回值,方便多组连续赋值。 加一个if判断,如果自己给自己赋值那么就直接返回自己

缺省参数不能在声明和定义时同时给缺省值,规定在声明的时候给。

7.operator!=

	bool Date::operator!=(const Date& y)
	{
		return !(*this == y);
	}

8.operator>=

bool Date::operator>=(const Date& y)
{
	return (*this > y) || (*this == y);
}

9.operator<

bool Date::operator<(const Date& y)
{
	return !(*this >= y);
}

10.operator<=

bool Date::operator<=(const Date& y)
{
	return !(*this > y);
}

11.operator-=

Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += (-day);
	}

	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			--_year;
			_month = 12;
		}

		_day += GetMonthDay(_year, _month);
	}

	return *this;
}

12.operator-(日期减去天数)

Date Date::operator-(int day)
{
	Date tmp(*this);
	tmp -= day;

	return tmp;
}

13.前置++ 与 后置++

运算符重载基本上都是opereator运算符,但是我们这里的++有些许的问题,operator++究竟是前置++还是后置++呢?我们是无法分清楚的,所以c++语法规定,在使用时传int整型的operator++为后置++,不传参的operator为前置++

Date& operator++();//前置++不需要传参数
//这里传不传int用于区分前置++和后置++
Date operator++(int);//后置++传一个int参数

//前置++
Date& Date::operator++()//前置++ 不传参数
{
	*this += 1;
	return *this;
}
//后置++
Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}

14.operator-(日期减日期)

将较小的日期一直++,并定义一个计数器count,小的日期每次++,count也跟着++,直到min=max的时候,count就是相差的日期。

int Date::operator-(const Date& d)
{
	int flag = 1;
	Date max = *this;
	Date min = d;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int count = 0;
	while(min!=max)
	{
		++min;
		++count;
	}
	return count * flag;
}

15.opeartor<<

在此之前我们需要知道双操作数的运算符,第一个参数是左操作数,第二个参数是右操作数。

void Date::operator<<(ostream& out)
{
	out << _year << "年" << _month << "月" << _day << "日" << endl;
}

但是我们又发现写好之后,不能用正常使用,原因是,operator在类中,类中有一个隐藏的参数this,为该函数的左操作数,所以我们需要把operator<<操作符写在类外面,以保证左操作数能为cout。

ostream& operator<<(ostream& out,const Date& d)
{
out<<d._year<<"年"<<d.month<<"月"<<d._day<<"日"<<endl;
return out;
}

 16.operator>>

istream& operator>>(istream& in,Date& d)
{
in>>d._year>>d._month>>d._day;
return in;
}

 17.前置operator -- 

Date& Date::operator--()
{
*this-=-1;
return *this;
}

18.后置operator--(int)

Date Date::opeartor--(int)
{
Date tmp(*this);
*this-=1;
return tmp;
}

 6.const成员函数

 我们可以用下面这段代码来验证一下;

class Date
{
public:
 Date(int year, int month, int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 void Print()
 {
 cout << "Print()" << endl;
 cout << "year:" << _year << endl;
 cout << "month:" << _month << endl;
 cout << "day:" << _day << endl << endl;
 }
 void Print() const
 {
 cout << "Print()const" << endl;
 cout << "year:" << _year << endl;
 cout << "month:" << _month << endl;
 cout << "day:" << _day << endl << endl;
 }
private:
 int _year; // 年
 int _month; // 月
 int _day; // 日
};
void Test()
{
 Date d1(2022,1,13);
 d1.Print();
 const Date d2(2022,1,13);
 d2.Print();
}

const调用的结果是Print()const。

成员函数定义的规则:

1.能定义成const的成员函数都应该定义成const,这样const对象和非const对象都可以调用,非const对象调用const成员函数是权限的缩小。权限可以缩小但是不可以放大,const对象不能调用非const对象

2.要修改成员变量的成员函数,不能定义成const

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值