C++学习(3)——类和对象(中)六大默认成员函数

系列文章目录

C++学习传送门



一、默认成员函数

一个没有成员的类,被称之为空类。

class classname
{
};

但空类中并非一无所有。因为不自行规定任何成员时,编译器会默认生成六个成员函数:
1.构造函数
2.析构函数
3.拷贝构造函数
4.赋值重载函数
5.对普通对象取地址函数
6.对const修饰的对象取地址函数

二、构造函数

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

构造函数其实“名不符实”,它虽然叫做构造函数但起到的作用却是为对象赋初始值,而真正开辟空间创建对象的是编译器。

特性

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

在下述代码中,我们没有显示调用构造函数,且在构造函数内部写入打印语句,以此来验证编译器在对象实例化时是否自动调用对应构造函数。

class classname
{
public:
	classname()
	{
		cout << "构造函数" << endl;
	}
};
int main()
{
	classname test;
	return 0;
}

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

  1. 构造函数可以重载。

常用的函数重载有以下几种类型:
无参类型构造函数
带参类型构造函数:注意全缺省的带参函数与无参函数,在不传参调用时的二义性。
拷贝构造函数(下文讲述)

class Date
{
public:
	//无参构造
	Date()
	{

	}
	//带参构造
	//可以设置缺省值
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

注意在调用无参构造函数时,不要在对象后面加圆括号,防止编译器将此行为看作函数声明。

  void TestDate()
 {
      Date d1; // 调用无参构造函数
      Date d2(2015, 1, 1); // 调用带参的构造函数
  
      // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
      //下述代码就是错误使用
      // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
      Date d3();
 }
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

注意编译器自动生成的无参默认函数,对与自定义类型会自动调用自定义类型的构造函数,对于内置类型不做处理。下述代码可以观察出:

class Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "调用Stack构造函数" << endl;
		_arr = new int[capacity];
		_capacity = capacity;
		_top = 0;
	}
private:
	int* _arr;
	int _capacity;
	int _top;
};

class Test
{
public:
	int Get1()
	{
		return num1;
	}
	int Get2()
	{
		return num2;
	}
private:
	//内置类型
	int num1;
	int num2;
	//自定义类型
	Stack st;
};
int main()
{
	Test test;
	cout << "num1:" << test.Get1() <<" num2:"<< test.Get2() << endl;
	return 0;
}

为了弥补编译器自动生成的默认构造函数对内置类型不做处理,我们可以在类内声明内置类型时给予一个缺省值,注意该值是缺省值而不是初始化。

class Test
{
public:
	int Get1()
	{
		return num1;
	}
	int Get2()
	{
		return num2;
	}
private:
	//内置类型
	int num1=10;
	int num2=20;
	//自定义类型
	Stack st;
};

6.默认构造函数只能有一个。而无参构造函数全缺省构造函数编译器自动生成的构造函数都是默认构造函数。

当我们显示定义构造函数时,编译器就不会再自动生成构造函数。所以自动生成的构造函数始终满足构造函数只有一个的要求。重点就在于无参构造函数和全缺省构造函数,我们应保证二者不同时存在,以避免对象实例化时产生二义性。

三、析构函数

析构函数与构造函数功能相反,且同样名不副实。析构函数不是完成局部对象的销毁,而是在编译器销毁局部对象时自动调用来完成对对象资源的清理。
注意:多个对象在程序结束需要析构时,遵循先构造后析构的顺序。

特性:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
class Stack
{
public:
	//构造函数
	Stack(int capacity = 4)
	{
		cout << "调用Stack构造函数" << endl;
		_arr = new int[capacity];
		_capacity = capacity;
		_top = 0;
	}
	//析构函数
	~Stack()
	{
		cout << "调用Stack析构函数" << endl;
		free(_arr);
		_arr = nullptr;
		_capacity = _top = 0;
	}
private:
	int* _arr;
	int _capacity;
	int _top;
};
  1. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
class Stack
{
public:
	//构造函数
	Stack(int capacity = 4)
	{
		cout << "调用Stack构造函数" << endl;
		_arr = new int[capacity];
		_capacity = capacity;
		_top = 0;
	}
	//析构函数
	~Stack()
	{
		cout << "调用Stack析构函数" << endl;
		free(_arr);
		_arr = nullptr;
		_capacity = _top = 0;
	}
private:
	int* _arr;
	int _capacity;
	int _top;
};
int main()
{
	Test test;
	return 0;
}

运行结果,证明析构函数会被自动调用:
在这里插入图片描述

  1. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。

与构造函数类似,自动生成的析构函数,对内置类型不做处理。因为内置类型直接由编译器销毁即可,无需进行资源清理。而对于自定义类型,自动生成的析构函数会去调用该自定义类型的析构函数。

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的析构函数。
因为我们虽然只定义了Date类的对象,但是该对象中的四个成员变量包括Time类的对象。而我们没有显示定义Date类的析构函数,所以在程序结束时编译器会自动调用默认生成的析构函数。该析构函数对三个内置类型成员变量(_year _month _day)不做处理。而对自定义类型成员变量 _t 自动调用Time类的析构函数。所以最终打印了~Time()。

四、拷贝构造函数

拷贝构造函数的功能是用已存在的对象去拷贝创建出一个同类型的对象。
拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

特性:

  1. 拷贝构造函数是构造函数的一个重载形式。
class Date
{
public:
	//全缺省构造函数
	 Date(int year = 1900, int month = 1, int day = 1)
	 {
	 _year = year;
	 _month = month;
	 _day = day;
	 }
	 //拷贝构造函数
	 Date(const Date& d)
	 {
	 _year = d._year;
	 _month = d._month;
	 _day = d._day;
	 }
private:
	 int _year;
	 int _month;
	 int _day;
};
  1. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

因为如果我们使用传值调用,根据之前对临时变量的学习,我们知道:传值调用时,实参会拷贝创建一个临时变量,再将该临时变量传给形参,最终造成形参是实参的一份拷贝
而实参与形参都是类类型的对象,其传参过程中使用的拷贝功能就是我们要定义实现的拷贝构造函数,所以传值调用会导致无穷递归调用。
因此必须使用传引用调用

class Date
{
public:
	//全缺省构造函数
	 Date(int year = 1900, int month = 1, int day = 1)
	 {
	 _year = year;
	 _month = month;
	 _day = day;
	 }
	 //正确的拷贝构造函数(传引用)
	 Date(const Date& d)
	 {
	 _year = d._year;
	 _month = d._month;
	 _day = d._day;
	 }
	 //错误的拷贝构造函数(传值)
	 Date(const Date d)
	 {
	 _year = d._year;
	 _month = d._month;
	 _day = d._day;
	 }
private:
	 int _year;
	 int _month;
	 int _day;
};
  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝。这种拷贝叫做浅拷贝,或者值拷贝。

编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用该类型自己的拷贝构造函数完成拷贝的。这一点类似构造函数和析构函数对自定义类型的处理方法。

class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time(const Time& t)
	{
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
		cout << "使用Time类的拷贝构造函数" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	void Show()
	{
		cout << _year << ' ' << _month << ' ' << _day << endl;
	}
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
	Date d2(d1);
	//展示d1、d2
	d1.Show();
	d2.Show();
	return 0;
}

运行结果:
在这里插入图片描述
4.默认生成的拷贝构造函数只能完成浅拷贝,而涉及内存管理的对象必须使用深拷贝。这也是显示定义拷贝构造函数的价值所在。

如果对涉及内存管理的对象使用浅拷贝(默认生成的拷贝构造函数):
该程序最终会崩溃报错,因为在使用s1拷贝构造s2时按照字节顺序拷贝,会导致s2中的_array与s1的_array完全一样,也就是两个指针指向同一块空间。而在程序结束时,编译器会自动调用两个对象的析构函数完成对指针资源的清理。对同一块空间的两次销毁(free)最终导致了崩溃。

typedef int DataType;
class Stack
{
public:
	//构造函数
	Stack(size_t capacity = 10)
	{
		_array = new DataType[capacity];
		_size = 0;
		_capacity = capacity;
	}
	//入栈(不深究扩容)
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	//析构函数
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	//内置类型
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	//用s1拷贝构造s2
	Stack s2(s1);
	return 0;
}

显示定义拷贝构造函数,实现深拷贝:
为了节省篇幅,这里只展示显式定义的拷贝构造函数

Stack(const Stack& st)
	{
		//先开辟同样大小的空间
		_array = new DataType[st._capacity];
		//完成对数组的拷贝
		memcpy(this->_array, st._array, sizeof(DataType) * st._capacity);
		//更新其他内置类型
		_capacity = st._capacity;
		_size = st._size;
	}
  1. 拷贝构造函数典型调用场景:

(1)使用已存在对象创建新对象
(2)函数参数类型为类类型对象(传值调用生成临时变量进行拷贝)
(3)函数返回值类型为类类型对象(返回值生成临时变量进行拷贝)

为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

五、运算符重载

C++为了增强代码的可读性引入了运算符重载
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)

特性:

1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数
3. 用于内置类型的运算符,其含义不能改变。 例如:内置的整型+,不能改变其含义。因为运算符重载的价值就在于可读性;
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针
5. .* :: sizeof ?: .这五个运算符不可重载。点星、作用域限定符、sizeof、条件运算符、点

1、赋值运算符重载

(1)赋值运算符重载的形式

参数类型:const T&,传递引用可以提高传参效率;
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
注意检测是否自己给自己赋值,如果自己给自己赋值可以直接返回;
返回*this :要满足连续赋值的要求;

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

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

	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;
}; 

(2)赋值运算符只能重载成类的成员函数不能重载成全局函数

赋值运算符如果不在类内显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

下述代码会因为冲突而报错

class Date
{
public:
 Date(int year = 1900, int month = 1, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 int _year;
 int _month;
 int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
 if (&left != &right)
 {
 left._year = right._year;
 left._month = right._month;
 left._day = right._day;
 }
 return left;
}

(3)编译器会自动生成赋值重载函数

该默认生成的赋值重载函数功能类似默认生成的拷贝构造函数,只能实现浅拷贝,以字节顺序赋值。

而且对于内置类型和自定义类型的处理类似之前的函数,即内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。该特性的验证参照上述代码验证。

因此在涉及内存分配的对象时,我们必须显示定义赋值重载函数,实现深层拷贝

2、前置与后置的重载

以前置++和后置++为例:

前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载(即按照函数名修饰规则产生差异)。
C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。

//前置
Date& operator++()
//后置
Date operator++(int)

实现前置++和后置++

对于前置++,我们使用引用返回。因为前置++要返回的仍是对象本身,而该对象在++后不会销毁,所以可以使用引用返回提高效率。
对于后置++,我们使用传值返回。因为后置++要返回改变前的对象,因此我们必须创建局部变量存储未改变的对象,而该局部变量在++后会被销毁,所以我们只能使用传值返回。

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 前置++:返回+1之后的结果
	// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
	Date& operator++()
	{
		_day += 1;
		return *this;
	}
	// 后置++:
	// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
	// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
	Date operator++(int)
	{
		Date temp(*this);
		_day += 1;
		return temp;
	}
private:
	int _year;
	int _month;
	int _day;
};

3、流插入与流提取的重载

c++中常用的cout和cin其实分别是ostream和istream类的对象。为了方便实现自定义类的输入输出我们可以对流插入(<<)和流提取(>>)这两个运算符进行重载。
进行重载时,我们要注意满足连续使用的要求,所以分别返回值分别是ostream、istream。又因为cout、cin在函数调用后仍然存在,所以我们可以使用引用返回。
但重载仍有以下两种形式:

(1)在类内进行重载

以日期类为例:
如果我们在类内定义流提取和流插入重载,编译器会在函数中隐式调用this指针,且this指针始终是第一个参数。而又因为这两个运算符是双目运算符,这使得我们使用流提取和流插入时,需要写成d.operator<<(cout)或者简写为d<<cout
这显然和我们的常规使用形式相违背,也不符合运算符重载的初衷:增强可读性。
所以我们常将流提取和流插入的重载写成全局函数,以此来避免指向对象的指针是左参数。

 #include<iostream>
using namespace std;
class Date
{
public:

	Date(int year = 0, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//类内流插入重载
	ostream& operator<<(ostream& out)
	{
		out << this->_year << ' ' << this->_month << ' ' << this->_day << endl;
		return out;
	}

	//类外流提取重载
	istream& operator>>(istream& in)
	{
		in >> this->_year >> this->_month >> this->_day;
		return in;
	}

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

int main()
{
	Date d(2003, 6, 18);
	d.operator<<(cout);
	d << cout;

	d.operator>>(cin);
	d << cout;

	d >> cin;
	d << cout;
	return 0;
}

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

(2)在类外进行重载

为了迎合正常的cout、cin使用形式,我们采用在类外的全局函数定义。
注意:进行类外定义时,参数列表中的第一项必须放ostream或者istream对象(最好使用引用),这样才能符合正常使用习惯。
难点:如何让类外的流提取和流插入重载函数访问到private限制的成员 ?

方法一:曲线救国

对于流插入函数(<<)我们可以在类内的public中设置三个Get函数,分别返回_year 、_month 、_day。因为类外也可以访问到public限制的成员,所以我们可以在类外的函数定义中访问这三个Get函数。
对于流提取函数(>>),我们可以先创建三个与year、month、day同类型的变量,然后为这三个变量赋值,再利用这三个值构造对象,最后使用赋值重载完成对原先对象中三个成员变量的数值更新。

#include<iostream>
using namespace std;
class Date
{
public:

	//构造函数
	Date(int year = 0, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//赋值重载
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this;
	}

	//三个Get函数
	int GetYear()
	{
		return _year;
	}

	int GetMonth()
	{
		return _month;
	}

	int GetDay()
	{
		return _day;
	}
	

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

//流插入重载
ostream& operator<<(ostream& out, Date& d)
{
	out << d.GetYear() << ' ' << d.GetMonth() << ' ' << d.GetDay() << endl;
	return out;
}

//流提取重载
istream& operator>>(istream& in, Date& d)
{
	int num1, num2, num3;
	in >> num1 >> num2 >> num3;
	Date tem(num1, num2, num3);
	d = tem;
	return in;
}
int main()
{
	Date d(2003, 6, 18);
	operator<<(cout, d);
	cout << d;

	operator>>(cin, d);
	cout << d;

	cin >> d;
	cout << d;
	return 0;
}

运行截图:

在这里插入图片描述

方法二:乔装打扮

我们可以使用友元函数,达成在类外访问类内所有成员的目的。友元函数的具体使用方法和效果将在下一篇博客中详讲。

#include<iostream>
using namespace std;
class Date
{
public:

	//声明友元函数(不受三大限定符限制,可在类内任何位置声明)
	friend ostream& operator<<(ostream& out, Date& d);
	friend istream& operator>>(istream& out, Date& d);
		
	//构造函数
	Date(int year = 0, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//赋值重载
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};

//流插入重载
ostream& operator<<(ostream& out, Date& d)
{
	out << d._year << ' ' << d._month << ' ' << d._day << endl;
	return out;
}

//流提取重载
istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}
int main()
{
	Date d(2003, 6, 18);
	operator<<(cout, d);
	cout << d;

	operator>>(cin, d);
	cout << d;

	cin >> d;
	cout << d;
	return 0;

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

六、const成员函数

当我们使用未加const修饰的成员函数处理const 修饰的对象,会发现编译器报错。原因在于指针(引用)的权限问题

错误案例:

class Date
{
public:		
	//构造函数
	Date(int year = 0, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//三个Get函数
	int GetYear()
	{
		return _year;
	}
	int GetMonth()
	{
		return _month;
	}
	int GetDay()
	{
		return _day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	const Date test;
	cout << test.GetYear() << endl;
	return 0;
}

我们知道:对于指针和引用有权限上的要求,即权限可以缩小和不变,但不可以放大
而当我们将const修饰的对象传参给成员函数时,因为原有的成员函数使用this指针接收参数,而this指针的类型是:类类型指针加constthis指针类型中的const修饰的是this指针本身而不是this指针指向的对象,而我们所传参数的类型是const类类型指针,这意味着我们将对象的权限放大了,所以编译器会报错。
在明白原因后,解决方法就很明了了。我们只需要给this指针在最前面加上const修饰, 即将其类型改为:const 类名*const;但是我们知道this指针是由编译器进行隐式调用的,这意味着我们没有机会加const。
此时我们的唯一解决方案是:在函数原型的末尾加上const,该const只修饰隐藏的this指针,不修饰函数的其他参数。
因此这种const修饰的函数被称之为const成员函数!const成员函数无法改变类内的任何成员!
注意:对于要修改类内成员的函数,无法进行const修饰,如构造和析构

class Date
{
public:		
	//构造函数
	Date(int year = 0, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//三个Get函数
	//加上const修饰,保证权限的正确
	int GetYear() const
	{
		return _year;
	}
	int GetMonth() const
	{
		return _month;
	}
	int GetDay() const 
	{
		return _day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	const Date test;
	//此时编译通过
	cout << test.GetYear() << endl;
	return 0;
}

七、取地址重载和const取地址重载

这两个默认函数大多情况下无需自行定义,除非我们想要使取地址获得特殊值。
注意:根据上述对const函数的学习,我们知道对const修饰的对象取地址时,重载函数也要用const修饰,返回值同样使用const修饰,以此确保权限的正确!

class Date
{
public:
	Date* operator&()
	{
		return this;
	}
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

如果我们想要使取地址始终返回nullptr,此时就可以自行定义(基本无用):

class Date
{
public:
	Date* operator&()
	{
		return nullptr;
	}
	const Date* operator&()const
	{
		return nullptr;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

八、利用上述知识实现日期类:

C++学习——日期类的实现

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值