一万字深度解析C++类和对象(下篇):再谈构造函数、Static成员、友元、内部类、匿名对象

目录

1. 再谈构造函数

1.1 构造函数体赋值

1.1.1 const对象

1.1.2 const成员变量

1.1.3 const成员函数

1.1.4 隐式类型转换

1.2 初始化列表

1.3 explicit关键字

1.3.1日期类减号运算符重载问题 

2. static成员

2.1静态成员变量

2.2静态成员函数

3. 友元

3.1友元函数

3.2友元类

4. 内部类

5. 匿名对象

5.1.匿名对象无参构造

5.2.匿名对象的生命周期

5.3.匿名对象具有常性

5.4多次构造会优化

5.5匿名对象的使用场景

5.5.1调用类中某个函数时

5.5.2传参优化


1. 再谈构造函数

1.1 构造函数体赋值

在构造一个对象时编译器会自动调用构造函数,我前面都说构造函数是在给对象初始化,这其实是不太准确的,实际上构造函数是在给一个已经开好空间的对象的成员变量赋值构造函数体赋值就是说的构造函数的‘{}’内的赋值操作。

class A
{
public:
	A(int a = 1)
	{
		_a = a;
	}
private:
	int _a;
};
int main()
{
	A a;

	return 0;
}

也就是说在构造一个对象时,会先给对象开辟一块空间,此时对象的成员变量是随机值,然后再调用构造函数,给对象赋值,此时对象的‘初始化’就完成了。注意这里强调赋值这个操作,因为如果构造的是一个具有const成员变量的对象,那构造函数中给const成员变量赋值就会报错!但是如果是一个const对象,对象没有const成员变量,此时构造函数又不会报错。详细在下面讲解。

1.1.1 const对象

const对象是指在构造时加了个const的对象,这种对象又叫常对象,形式如下:

class A
{
public:
	A(int a = 1)
	{
		_a = a;
	}
private:
	int _a;
};
int main()
{
	const A a(2);//a就是一个常对象
	return 0;
}

这种对象的成员在对象创建之后就不能更改了,整个对象的生命周期中唯一一次能更改成员变量的时候就是在构造对象时调用的构造函数。 它在调用成员函数时也会隐式传参它的地址,它的指针是一个const date* const 类型,所以隐式接收的形参也必须是const date* const this。但是它在构造时调用的构造函数是个例外,这个构造函数可以不给*this加const(因为加了也就不能给对象的成员变量赋值了),这个例外除了构造函数(这里构造函数包括拷贝构造)外,还有析构函数也不用加const。

这里复习一下默认生成的拷贝构造和赋值运算符重载是什么形式的:

	//这里拷贝构造也属于构造函数的一种,所以*this也不需要加const
    A(const A& a)
	{
		//函数体
	}
	A& operator=(const A& a)
	{
		//函数体
		return *this;
	}

1.1.2 const成员变量

const成员变量则是指类中的被const修饰的成员变量。

class A
{
public:
	A(int a = 1)
	{
		_a = a;
	}
private:
	const int _a;
};

这里语法是错误的,因为const成员变量不能像const对象一样在构造函数体被赋值 ,那既然const成员变量不能在构造函数体被赋值,我该如何初始化它呢?这就要引入一个新的知识叫初始化列表初始化列表就可以给const成员变量初始化,在接下来会讲到。

注意:

  • 在同一个对象中const成员变量必须初始化,非const成员变量可以不初始化。
  • 而const对象中的const成员变量也必须初始化,但是注意:如果const对象中没有const成员变量,他也必须要写个构造函数,哪怕写个空构造函数,也必须得写一个(但是不建议这么做,因为const对象只有在构造时才能初始化,之后成员原变量的值都不能改变了),如果不写构造函数,用编译器默认生成的构造函数会报错,也就是说可以认为const对象的非const成员变量也必须初始化,不过只要你写了构造函数,它就认为你初始化了,哪怕你构造函数是空内容。

1.1.3 const成员函数

const成员函数我在“C++类和对象(中篇)”中讲过,这里带大家复习一下,const成员函数实际上是给函数参数中的*this修饰,修饰后的*this不能改变。形式如下:

class A
{
public:

	void print()const
	{
		cout << _a << endl;
	}
private:
	const int _a;
};

1.1.4 隐式类型转换

大家看下面这段代码。

class A
{
public:
	A(int a = 1,int b = 7)
	{
		_a = a;
		_b = b;
	}
private:
	int _a;
	int _b;
};
int main()
{
	A a(2);
	A b = 2;
	a = 3;
	return 0;
}

 先说明这段代码没有错误,大家可能会有疑问:这个A类怎么能直接用一个整形给赋值呢?赋值后这个b._a,b._b都等于多少?

这里其实发生了隐式类型转换,A b = 2;这句其实是先拿一个2构造一个临时对象,然后再拿这个临时对象拷贝构造b对象,假设这个临时对象叫x,这句代码实际上就是A x(2);A b = x;然后x销毁。a = 3;这句实际上就是A x(3);a = x;然后x销毁。

注意:

  • 临时变量具有常性。

大家可以通过下面这个例子观察到临时变量具有常性。

class A
{
public:
	A(int a = 1,int b = 7)
	{
		_a = a;
		_b = b;
	}
private:
	int _a;
	int _b;
};
int main()
{
	A& a = 2;
	return 0;
}

 这段代码会报错,为什么这里变成引用就不行了?我们还是从隐式类型转换这个角度来分析,这里会先用2构造一个常量临时对象:const A x(2);然后A& a = x;看到这里大家应该都会发现为什么报错了,引用一个const对象,这不限权放大了嘛!当然报错。所以大家可以试试把A& a = 2;改成const A& a = 2;就不会报错了。

class A
{
public:
	A(int a = 1, int b = 7)
	{
		cout << this << endl;
		_a = a;
		_b = b;
	}
private:
	int _a;
	int _b;
};

int main()
{
	const A& a = 2;
	return 0;
}

1.2 初始化列表

 前面说过const成员变量不能用构造函数体赋值初始化,初始化列表就是用来给const成员变量初始化的,

初始化列表:以一个 冒号开始 ,接着是一个以 逗号分隔的数据成员列表 ,每个 " 成员变量 " 后面跟一个 放在括 号中的初始值或表达式。
class Date
{
public:
	Date(int year = 2020, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	const int _year;
	const int _month;
	const int _day;
};
注意:
1. 每个成员变量在初始化列表中 只能出现一次 ( 初始化只能初始化一次 )
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时)
class A
{
public:
	A(int a,int b = 2)
		:_a(a)
		,_b(b)
	{}
private:
	int _a;
	int _b;
};
class B
{
public:
	B(int a, int ref)
		:_aobj(a,ref)
		, _ref(ref)
		, _n(10)
	{
		cout << &ref << endl;
	}
private:
	A _aobj; // 没有默认构造函数
	int& _ref; // 引用
	const int _n; // const 
};

int main()
{
	B a(7,5);
	return 0;
}

 这里自定义类型成员的初始化列表实际上就是调用自定义类型成员的构造函数。

注意:

  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}

	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main() {
	A aa(1);
	aa.Print();
}

//A.输出1 1
//B.程序崩溃
//C.编译不通过
//D.输出1 随机值

这里选D,输出1和随机值。因为初始化列表的顺序是按照成员变量的顺序来初始化的,所以这里先初始化_a2,再初始化_a1。而初始化_a2时_a1是随机值,所以_a2是随机值,_a1是a,也就是1。

1.3 explicit关键

 构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用(就是我上面说过的隐式类型转换)

explicit关键字是禁止函数类型转换。格式为在函数名前面加上explicit。

class Date
{
public:
	// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
	explicit Date(int year)
		:_year(year)
	{}
	
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};
void Test()
{
	Date d1(2022);//这一句通过编译
	
    //这一句会产生隐式类型转换,用2023构造一个无名的临时对象,
    //但是构造函数被explicit修饰,禁止类型转换,所以这句话编不过去。
	d1 = 2023;//这一句编译报错
}

1.3.1日期类减号运算符重载问题 

我以前在写日期类的时候写过这两个减号运算符重载函数,一个是日期类减整形,一个是日期类减日期类

加const修饰是因为这两个函数内部并没有改变*this,所以加个const修饰避免限权放大导致报错。 但是我发现如果把第二个日期减日期的函数的const去掉,在main函数中用一个非const对象减一个整形,竟然会报错!

理由是有多个减运算符函数与这两个操作数匹配,我的减运算符函数一共就这两个,他说有多个那一定就是指的这两个了,第一个函数能匹配肯定没问题,我们来分析一下为什么第二个能匹配,我猜测是对象d的地址传给this指针,100隐式类型转换传给了const date& a。所以如果我给构造函数用explicit修饰,那就不允许隐式类型转换了,应该就不会报错了。

 发现果然没有报错了,猜测应该是对的,那又引来一个问题,我之前没有用explicit修饰构造函数,只不过给第二个减运算符函数用const修饰,为啥也不会报错呢??按理来说const修饰的是*this,不影响100隐式类型转换构造一个date对象啊?

这里就需要引出一个函数匹配的问题,大家看看下面两个函数构成重载,哪一个会被调用

void func(int a, char b)
{
	cout << "int char" << endl;
}
void func(int a, int b)
{
	cout << "int int" << endl;
}
int main()
{
	int a = 0;
	char b = '0';
	func(b, a);
	return 0;
}

 

 结果是第二个,分析一下:

 理由就是第一个个函数两个参数都不匹配,第二个函数有一个参数是匹配的,所以理所应当调用第二个函数,在看看下面这段代码:

void func(int a, char b)
{
	cout << "int char" << endl;
}
void func(char a, int b)
{
	cout << "int int" << endl;
}
int main()
{
	int a = 0;
	func(a, a);
	return 0;
}

 像上面一样分析,第一个函数有一个参数是匹配的,第二个函数也有一个参数是匹配的,那这是编译器会调用那个函数?答案是编译器会报错!有多个函数与之匹配。相信到这大家应该知道函数匹配是怎么一回事了。知道这个后再来看看上面的日期类的问题。

 这里构造函数没加explicit,为什么会报错?来分析一下。为了方便分析,我把this指针显示写出来(程序里显现写出是会报错的)

 再来分析分析为什么第二个函数加了const就不报错了,

到这这个问题就完全解决了。 

2. static成员

声明为 static 的类成员 称为 类的静态成员 ,类的静态成员分为静态成员变量静态成员函数
  • static修饰的成员变量,称之为静态成员变量
  • static修饰成员函数,称之为静态成员函数

2.1静态成员变量

静态成员变量即在成员变量之前加static修饰。格式如下:

class A
{
private:
	static int _i;
};

int A::_i = 1;

 注意:类的静态成员变量在类实例化出对象前就已经存在,位置在静态区。类的静态成员变量为所有类实例化出的对象共有的。不属于某个单独的对象。静态成员变量一定要在类外进行初始化可以理解为类里面只是声明,类外面才是定义。

类的静态成员变量在其他方面和普通成员变量一样,私有成员只能在类域中访问,共有成员可以在类域外访问。

class A
{
public:
	static int _i;
	int _a;
};

int A::_i = 1;

int main()
{
	A::_i = 9;//可以访问

	A::_a = 9;//报错,不能访问
	return 0;
}

大家看一下上面为什么A::_i = 9;不报错A::_a = 9;报错?不是说共有的成员变量可以在类域外访问吗?

答案是A::_i能访问是因为A::_i已经存在,而A::_a并不存在,_a需要实例化出一个对象才能有。而且不同对象的_a不同,但是_i是相同的。

class A
{
public:
	static int _i;
	int _a;
};

int A::_i = 1;

int main()
{
	A::_i = 9;
	A a;
	a._a = 9;
	return 0;
}

这样才是对的。 

2.2静态成员函数

静态成员函数即在函数前用static修饰:

class A
{
public:
	static int Add()
	{
		++_i;
	}
private:
	static int _i;
	int _a;
};

int A::_i = 1;

其中Add就是静态成员函数。

注意:静态成员函数没有this指针,共有静态成员函数可在类域外调用,私有静态成员函数只能在类域内调用。

class A
{
public:
	static int Add()
	{
		++_i;
		return _i;
	}

private:
	static int _i;
	int _a;
};

int A::_i = 1;

int main()
{
	cout << A::Add() << endl;
	return 0;
}

静态成员特性
  1. 静态成员所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受publicprotectedprivate 访问限定符的限制

看题:

求1+2+3+...+n,要求不能使用逻辑运算符、乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

这道题就可以利用静态成员的特性来写.


class A {
  public:
    A() {
        _sum += _i;
        ++_i;
    }
    static int getSum() {
        return _sum;
    }
  private:
    static int _i;
    static int _sum;
};
int A::_i = 1;
int A::_sum = 0;
class Solution {
  public:
    int Sum_Solution(int n) {
        A arr[n];
        return A::getSum();
    }
};
问题:
  1. 静态成员函数可以调用非静态成员函数吗?
  2. 非静态成员函数可以调用类的静态成员函数吗?

答:

  1. 可以,但是前提是得用一个对象调用,因为静态成员函数是没有this指针的,非静态成员函数要接收this指针,所以得用一个对象调用。
    class A
    {
    public:
    	
    	static int Add()
    	{
    		A a;
            a.Add2();//得创建个对象来调用非静态成员函数
    		return _i;
    	}
    	void Add2()
    	{
    		_i += 2;
    	}
    
    private:
    	static int _i;
    	int _a;
    	int _b;
    	int _c;
    };
    
    int A::_i = 1;
    
    int main()
    {
    	A::Add();//A::_i变成3
    	return 0;
    }
  2. 可以,他们两都在同一个类域,可以直接调用,并且由于静态成员函数没有this指针,所以不会传递this指针。
    class A
    {
    public:
    	
    	static int Add()
    	{
    		++_i;
    		return _i;
    	}
    	void Add2()
    	{
    		Add();
    		Add();
    	}
    
    private:
    	static int _i;
    	int _a;
    	int _b;
    	int _c;
    };
    
    int A::_i = 1;
    
    int main()
    {
        A a;
    	a.Add2();//调用完后A::_i变成3;
    	return 0;
    }

3. 友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为: 友元函数 友元类 

3.1友元函数

我在类和对象中篇中讲过:重载 operator<< ,发现没办法将 operator<< 重载成成员函数。 因为 cout 的输出流对 象和隐含的 this 指针在抢占第一个参数的位置 this 指针默认是第一个参数也就是左操作数了。但是实际使用中cout 需要是第一个形参对象,才能正常使用。所以要将 operator<< 重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>> 同理。
class Date
{
public:
 Date(int year, int month, int day)
 : _year(year)
 , _month(month)
 , _day(day)
 {}
 // d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
 // 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
 ostream& operator<<(ostream& _cout)
 {
 _cout << _year << "-" << _month << "-" << _day << endl;
 return _cout;
 }
private:
 int _year;
 int _month;
 int _day;
};
友元函数 可以 直接访问 类的 私有 成员,它是 定义在类外部 普通函数 ,不属于任何类,但需要在类的内部声明,声明时需要加friend 关键字。
class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>>(istream& _cin, Date& d);
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month; 
	int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year;
	_cin >> d._month;
	_cin >> d._day;
	return _cin;
}
int main()
{
	Date d;
	cin >> d;
	cout << d << endl;
	return 0;
}
说明 :
  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同

3.2友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
class Time
{
	friend class Date;
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		:_hour(hour), _minute(minute), _second(second) {}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	Date(int year = 2020, int month = 1, int day = 1)
		:_year(year),_month(month),_day(day){}
	void print()
	{
		cout << _year << endl;
		cout << _month << endl;
		cout << _day << endl;
		cout << _t._hour << endl;
		cout << _t._minute << endl;
		cout << _t._second << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

int main()
{
	Date a;
	a.print();
	return 0;
}

 注意:

  • 友元关系是单向的,不具有交换性。
  • 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time
  • 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递
  • 如果BA的友元,CB的友元,则不能说明CA的友元。
  • 友元关系不能继承,在继承位置再给大家详细介绍。

4. 内部类

概念 如果一个类定义在另一个类的内部,这个内部类就叫做内部类 。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
class A
{
public:
	class B
	{
	private:
		int _c;
		int _d;
	};
private:
	int _a;
	int _b;
};

注意

  • 内部类默认是外部类的友元类。也就是说内部类可以访问外部类的私有成员。
  • 内部类的成员变量不能是外部类的自定义类型。
    class A
    {
    public:
    	class B
    	{
    		B(int b1 = 2, int b2 = 2)
    			:_b1(b1), _b2(b2) {}
    	private:
    		int _b1;
    		int _b2;
    		A _a;
    	};
    
    public:
    	A(int a1 = 1, int a2 = 1)
    		:_a1(a1),_a2(a2){}
    private:
    	int _a1;
    	int _a2;
    };

    报错:A::B::_a使用未定义的class A。

通过内部类是外部类的友元类这个特性,上面的题目:

求1+2+3+...+n,要求不能使用逻辑运算符、乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

 可以改进为

class Solution {
public:
    class A
    {
    public:
        A()
        {
            _sum += _i;
            ++_i;
        }
    };

    int Sum_Solution(int n) {
        A arr[n];
        return _sum;
    }
private:
    static int _i;
    static int _sum;
};

int Solution::_i = 1;
int Solution::_sum = 0;

5. 匿名对象

匿名对象就是上面“1.1.4隐式类型转换”提到的临时对象,匿名对象顾名思义就是没有名字的对象, 格式和普通定义一个对象的格式一样,只不过匿名对象没有名。

class A
{
public:
	A(int a = 0, int b = 1)
		:_a(a)
		, _b(b)
	{}
private:
	int _a;
	int _b;
};

int main()
{
	A a(1,2);//有名对象
	A(1,2);//匿名对象
	return 0;
}

5.1.匿名对象无参构造

如果构造有名对象如果不传参,则不用括号,匿名对象如果不传参必须加个括号

class A
{
public:
	A(int a = 0, int b = 1)
		:_a(a)
		, _b(b)
	{}
private:
	int _a;
	int _b;
};

int main()
{
	A a;
	A();
	return 0;
}

5.2.匿名对象的生命周期

匿名对象的声明周期只有创建匿名对象的这一句。过了这一句匿名对象就自动销毁了。

class A
{
public:
	A(int a = 0, int b = 1)
		:_a(a)
		, _b(b) 
	{
		cout << "A(int,int):::" << this << endl;
	}
	A(const A& a)
		:_a(a._a) ,_b(a._b)
	{
		cout << "A(A&):::"<< this << endl;
	}
	A& operator=(const A& a)
	{
		cout << "operator=(A&):::" << this << endl;
		_a = a._a;
		_b = a._b;
		return *this;
	}
	~A()
	{
		cout << "~A():::" << this << endl;
	}
private:
	int _a;
	int _b;
};

int main()
{
	A a;
	A();
	A b;
	return 0;
}

 大家可以根据调用析构函数的时间来判断对象的声明周期。

5.3.匿名对象具有常性

匿名对象具有常性,如果const引用匿名对象,会延长匿名对象的生命周期。

5.4多次构造会优化

看这段代码:

class A
{
public:
	A(int a = 0, int b = 1)
		:_a(a)
		, _b(b) 
	{
		cout << "A(int,int):::" << this << endl;
	}
	A(const A& a)
		:_a(a._a) ,_b(a._b)
	{
		cout << "A(A&):::"<< this << endl;
	}
	A& operator=(const A& a)
	{
		cout << "operator=(A&):::" << this << endl;
		_a = a._a;
		_b = a._b;
		return *this;
	}
	~A()
	{
		cout << "~A():::" << this << endl;
	}
private:
	int _a;
	int _b;
};

int main()
{
	A a = 100;
	cout << "------" << endl;
	return 0;
}

 经过分析可知道:100构造了一个匿名对象,然后匿名对象拷贝构造了对象a,之后匿名对象销毁。然后下一句cout打印“--------”,接着对象a销毁,函数结束;

所以输出应该是:

A(int,int)

A(A&)

~A()

--------

~A()

但是看vs运行结果:

发现就构造了一个对象,然后函数结束后销毁了这个对象。 这是为什么呢??

原因是vs编译器如果发生多次连续构造时,会优化成一次构造,这里本来是100构造了个匿名对象,匿名对象构造(拷贝构造也是构造)了个a,被编译器优化成100直接构造了a,相当于A a(100)。

5.5匿名对象的使用场景

5.5.1调用类中某个函数时

class A
{
public:
	int get_c()
	{
		return _c;
	}
private:
	int _a;
	int _b;
	static int _c;
};

int A::_c = 1;

int main()
{
	int a = A().get_c();//A()是个匿名对象
	return 0;
}

5.5.2传参优化

直接用匿名对象传参

class A
{
public:
	explicit A(int a = 1, int b = 2)
		:_a(a), _b(b) {}

	void print()
	{
		cout << _a << ' ' << _b << endl;
	}
private:
	int _a;
	int _b;
};
void test(A a)
{
	a.print();
}
int main()
{
	test(A(10, 20));
	return 0;
}

以上就是全部内容,如有错误请一定指出! 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值