c++_狄泰总结04继承/多态

C++的三大特性:封装、继承、多态

封装:为了隐藏机密的数据
继承:为了代码复用,函数重写,根据需求增加更多的功能
多态:为了在相同的调用根据实际情况得到不同的结果

1)隐式类型转换(bug的重要来源):

C语言支持 小类型->大类型(安全,不会发生数据截断/丢失)

char(1个字节) ->short->int(4个字节)->unsigned int-> long->unsigned long->float->double

==>编译器隐式转换类型带来的bug案例


```cpp

```cpp
short s = 'a';
    unsigned int ui = 1000;
    int i = -2000;

    if( (ui + i) > 0 )			//-2000+1000= -1000 按道理应该输出Negative,但是却输出了这个
    {
        cout << "Positive" << endl;	//理由是编译器做了隐式类型转换int ->unsigned int 型,无符号总是大于0
    }
    else
    {
        cout << "Negative" << endl;
    }
    //理想是输出2,但编译器优化(觉得int比较高效) short-> int ,char->int
     cout << "sizeof(s + 'b') = " << sizeof(s + 'b') << endl;	//

问题:
普通类型能否->类类型
类类型能否->普通类型

转换构造函数{构造函数只有一个参数,参数类型未非自身类型(class)}
来源:编译器会偷偷调用这个函数进行转换

    explicit Test(int i)	
    {
        mValue = i;
    }
   int main
  {	Test t ;
  		 t = 5;
  		 t= t+10;
}

explicit 拒绝编译器自行转换

2)显示转换方式:

**2.1)整形->类对象;**static_cast< className>(value)

static_cast< className>(value) //C++推荐写法
(ClassName) value => 不推荐,C语言写法
ClassName(value) ==>不推荐,调用构造函数的写法

class Test
{
    int mValue;
public:
    Test()
    {
        mValue = 0;
    }
    
    explicit Test(int i)	//这个又叫转换构造函数{只有一个参数,参数类型未非自身类型(class){
        mValue = i;
    }
    
    Test operator + (const Test& p)
    {
        Test ret(mValue + p.mValue);
        
        return ret;
    }
    
    int value()
    {
        return mValue;
    }
};

int main()
{   
    Test t;
    
    t = 5;	//当有转换构造函数 Test(int i)时,编译器会自动进行隐式转换=>t= Test(5)
    //当加上explicit后,拒绝编译器自行进行隐式转换,只能采用显示转换
    t = static_cast<Test>(5);    // t = Test(5);

    Test r
    r = t + static_cast<Test>(10);   // r = t + Test(10);	
    cout << r.value() << endl;
    
    return 0;
}

2.2)类类型转化为其他类型(包括其他): 类型转换函数 (operator type) (工程上很少使用这个函数(无法抑制编译器自行转换),通常使用成员函数 operator .tovalue)

1)当只存在 operator Value()类型转换函数时,编译器隐式转换 v=t-=> value v=t.operator value() ,
2)当只存在 Value(Test& t) 构造转换函数时,编译器隐式转换为 v=t- =>value =value(t);
3) 但当两个都存在时,由于同一等级,编译器会犯难
4)为了抑制编译器的隐式转换,会在转换函数里面加explicit Value(Test& t)
5)而类型转换函数由于无法抑制隐式转换,所以工程中基本不用类型转换函数,而是自定义一个类的成员函数并命名为 Value totype()函数替换
=>value v = t.value()
或 value = ??类类型转化无关系继承
****

class Value
{
public:
    Value()
    {
    }
    explicit Value(Test& t)		//在赋值符号左边边,转换类型有转换构造函数
    {	// v=t => value =value(t);
    }
};

class Test
{
    int mValue;
public:
    Test(int i = 0)
    {
        mValue = i;
    }
    int value()
    {
        return mValue;
    }
    operator Value()		//在赋值符号右边,被转换类型如果有类型转换函数 
    //v=t =>value v=t.operator value() 
    {
        Value ret;
        cout << "operator Value()" << endl;
        return ret;
    }
};

int main()
{   
    Test t(100);
    Value v = t;		
    /*当只存在 operator Value()类型转换函数时,编译器隐式转换 value v=t.operator value() ,
    //当只存在 Value(Test& t)	构造转换函数时,编译器隐式转换为 value =value(t);
    但当两个都存在时,由于同一等级,编译器会犯难
    *为了抑制编译器的隐式转换,会在转换函数里面加explicit Value(Test& t) 
    *而类型转换函数由于无法抑制隐式转换,所以工程中基本不用类型转换函数,而是自定义一个类的成员函数并命名为 Value totype()函数替换
    value v = t.value()
  或  value =	??类类型转化无关系继承
    */
    
    return 0;
}

3)继承 ==>(主要讨论父子间继承和冲突关系)

意义:代码复用的手段,子类继承父类的所有功能,并且可以在子类中重写已有功能或添加新功能)
1)工程项目一般只用public继承,其他private 跟protected的继承太过复杂
2)C++的派生语言优化了,只支持public继承(Java C# d语言),3种访问级别(public protected private)[protected: 继承的子类可以访问父类的成员,但外部无法访问protected成员]
3)继承间的构造函数与析构函数
子类构造函数—必须初始化继承而来的成员
初始化父类对象的两个方式:
通过初始化列表或者直接赋值()==》父类的private对象无法直接访问 ==》还是得用父类的构造函数
子类构造时调用父类的构造函数进行初始化
问:父类的构造函数在子类怎么调用?
隐式调用(只用于无参列表,或使用默认参数的构造函数)
显式调用(通过初始化列表进行调用),
4) {构造顺序:先父母,再成员,后自己},析构顺序刚好相反
5)父子间冲突
5.1)子类的成员可以隐藏父类的成员
5.2)子类的函数也可以隐藏父类的成员函数(由此无法重载,子类可以定义完全一样的父类函数)
5.3)当被隐藏后,想要调用父类同名的成员/成员函数,需要利用作用域分辨符
调用父类的成员(c(子).Parent(父)::mi)
调用父类的成员函数:c.Parent::add(2, 3);

#include <iostream>
#include <string>

using namespace std;

class Object
{
    string ms;
public:
    Object(string s)
    {
        cout << "Object(string s) : " << s << endl;
        ms = s;
    }
    ~Object()
    {
        cout << "~Object() : " << ms << endl;
    }
};

class Parent : public Object	//在类名这里 : 表示继承 publice 表示继承方式,工程上都用public继承
{
    string ms;
public:
    Parent() : Object("Default")	// 这里的:是初始化列表,这里是显式调用,当创建Parent对象时
    {								//会先创建父类,调用Object的构造函数 
        cout << "Parent()" << endl;
        ms = "Default";
    }
    Parent(string s) : Object(s)	//这里如果没有Object(s),会隐式调用父类Object()
    {
        cout << "Parent(string s) : " << s << endl;
        ms = s;
    }
    ~Parent()
    {
        cout << "~Parent() : " << ms << endl;
    }
};
//构造顺序,先父母,后客人,再自己
class Child : public Parent		//两种关系 ,继承关系:继承了Parent
{
    Object mO1;		//组合关系,Object类的成员对象,由于无'后客人的体现',所以要用初始化列表
    Object mO2;
    string ms;
public:
    Child() : mO1("Default 1"), mO2("Default 2")	//由于无'后客人的体现',所以要用初始化列表,这里有一个隐式调用父类构造函数, Parent()
    {
        cout << "Child()" << endl;
        ms = "Default";
    }
    Child(string s) : Parent(s), mO1(s + " 1"), mO2(s + " 2")  
    { //初始化列表,这里的初始化顺序 mO1 mO2   Parent(s)==>这个是利用初始化列表显示调用 Parent(string s),
  	//如果没有这个Parent(s),则会隐式调用 Parent()
        cout << "Child(string s) : " << s << endl;
        ms = s;
    }
    ~Child()
    {
        cout << "~Child() " << ms << endl;
    }
};

int main()
{       
    Child cc("cc");
    /*输出
	Object(string s) : cc
	Parent(string s) : cc
	Object(string s) : cc 1
	Object(string s) : cc 2
	Child(string s) : cc

	~Child() cc
	~Object() : cc 2
	~Object() : cc 1
	~Parent() : cc
	~Object() : cc

*/
    cout << endl;
    return 0;
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

同名覆盖:
在这里插入图片描述

4)父子间同名覆盖问题–》来源:赋值兼容原则

赋值兼容性的定义:
子类对象可以当做父类对象使用
==》子类对象可以直接赋值父类对象
==》子类对象可以直接初始化父类对象
==》父类指针可以直接指向子类对对象
==》父类引用可以直接引用子类对象

**问题1)编译器规则/赋值兼容原则(当采用父类指针指向/引用子类对对象时,子类对象退化为父类对象)
==》带来的后果:无法访问子类的成员

    Parent p1(c);		//子类对象c直接初始化父类对象
    
    4
    Parent& rp = c;	//父类指针引用子类对象,用rp做c的别名	==》子类对象已经退化为父类成员
    Parent* pp = &c;		//父类指针可以指向子类对象 ==》子类对象已经退化为父类成员

  rp.mi = 100;
    rp.add(5);             // 没有发生同名覆盖?	
    //==》虽然rp 已经代表了c对象, 但c子类对象已经退化为父类成员,add(5)正是父类的成员函数
    rp.add(10, 10);        // 没有发生同名覆盖?
    
    /* 为什么编译不过? */
    // pp->mv = 1000;
    // pp->add(1, 10, 100); //
    //==》虽然pp 已经获得了c对象的地址, 但子类对象c已经退化为父类成员,add(1, 10, 100)正是子类的成员函数,由此无法访问
    
    return 0;

问题2)函数重写遇上赋值兼容??
定义:子类对象重写父类对象的成员函数(同名覆盖的情况子类可以同名父类的成员和成员函数),且重定义发生在继承关系

为什么要函数重写?父类的功能不足以满足,所以才需要重写,在下面的案例中,重写了printf的函数,但输出确是父类的打印??
原因:编译器在编译过程中做了选择,how_to_print函数完整,在编译阶段,编译器并不知道指针p指向了哪个对象,根据兼容性原则(子类对象可以当做父类对象来使用),编译器选择最安全的策略,一律看做是父类对象 ==》由此也引出了virtual 多态的概念

void how_to_print(Parent* p)
{		//根据赋值兼容原则,作为付磊磊
    p->print();
}

int main()
{
    Parent p;
    Child c;
    
    how_to_print(&p);    // 期望输出 to print: I'm Parent.
    how_to_print(&c);    // 期望输出:I'm Child.  实际输出:I'm Parent
    
    return 0;
}

**

5.1)多态(特性):virtual (虚函数) =>相同的调用语句,产生不同的调用结果


意义:根据实际运行来判断如何调用重写函数(在程序运行中呈现动态特性)
需求:当触发兼容性原则时(当采用父类指针指向/引用子类对对象时,子类对象退化为父类对象),仍可以根据实际需求,选择采用父类/子类对象===》函数重写必须多态实现,否则没意义
关键字:virtual 在父类被重写的成员函数声明 >虚函数》编译器会知道

class Parent
{
    virtual void print()
    {
        cout << "I'm Parent." << endl;
 }
 }
 class Child : public Parent
{
public:
    void print()
}

5.2)多态的本质

父类对象virtual 定义的虚函数存放在虚函数表(存储成员函数地址的数据结构)中,编译器生成和维护,那存放在哪里?=》虚函数表是全局变量,且全局唯一 =》存放在全局数据区
怎么实现特性多态=》当该父类的成员函数定义为虚函数时,那么每个对象都就隐藏了指向虚函数表的指针(大小为4个字节)=》一个编译器知道该函数为虚函数时=》先去虚函数表里查询有无该函数的地址,如果无则直接确认add的地址
劣势=》多了查询,虚函数的效率 < 普通成员函数

链接地址:https://blog.csdn.net/jiary5201314/article/details/52627630

在c中定义(模拟c++虚函数表)

struct VTable	//全局变量,全局唯一,存放在数据区
{
		void * (pAddr)(void*int);   //void* 的理由 c++是面向对象,多态是既可以是子类的成员函数,也可以是指向父类的成员函数
}

//5.3)函数重载与函数重写

函数重载:在同一作用域,函数名一样,参数列表、个数不同,属于静态联编(在编译期间就能确定具体函数调用)
函数重写:函数名跟参数列表一致,但发生在继承关系,子类重写父类的成员函数,属于动态联编(在程序实际运行才能知道具体的函数调用)

void run(Parent* p)
{
p->func(1, 2); // 展现多态的特性
// 动态联编
}

class Parent	//这个类期望被继承
{
public:
    virtual void func()
    {
        cout << "void func()" << endl;
    }
    
    virtual void func(int i)
    {
        cout << "void func(int i) : " << i << endl;
    }
    
    virtual void func(int i, int j)
    {
        cout << "void func(int i, int j) : " << "(" << i << ", " << j << ")" << endl;
    }
};

class Child : public Parent
{
public:
    void func(int i, int j)		//这个重写了父类的 virtual void func(int i, int j)
    {
        cout << "void func(int i, int j) : " << i + j << endl;
    }
    
    void func(int i, int j, int k)	//同名覆盖(发生在父类和子类之间),会覆盖void func() void func(int i) void func(int i, int j)
    {	//	void func(int i, int j, int k)	与  void func(int i, int j)	的关系:函数重写
        cout << "void func(int i, int j, int k) : " << i + j + k << endl;
    }
};

int main()
{
    Parent p;
    
    p.func();         // 静态联编
    p.func(1);        // 静态联编
    p.func(1, 2);     // 静态联编
    
    cout << endl;
    
    Child c;
    
    c.func(1, 2);     // 静态联编	//辨认参数 func(int i, int j)	
    
    cout << endl;
    
    run(&p);
    run(&c);
    
    return 0;
}

6.1)抽象类(纯虚函数)与接口

抽象类的概念:为反映实际中的抽象概念,如图形
抽象类的实现: 1)C++语言上并没有提供抽象类,只能用纯虚函数实现抽象类,带有纯虚函数的类叫抽象类
抽象类的特点:
1)抽象类不能产生对象(没有必要产生对象,如图形),只能被继承;
2)纯虚函数(是指定义函数原型的成员函数)不需要写具体实现
3)某一些函数没有具体实现
4)子类须重写纯虚函数,重写完为虚函数,如果不重新纯虚函数,则为子类为抽象类,被下一个子类重新功能

class Shape
{
public:
    virtual double area() = 0;	//纯虚函数的写法  =0,告诉编译器是纯虚函数,没有具体实现
};
class Rect : public Shape
{
    int ma;
    int mb;
public:
    Rect(int a, int b)
    {
        ma = a;
        mb = b;
    }
    double area()		//类须重写纯虚函数,重写完为虚函数(多态的工能),如果不重新纯虚函数,则为子类为抽象类,被下一个子类重新功能		
    {
        return ma * mb;
    }
};

class Circle : public Shape
{
    int mr;
public:
    Circle(int r)
    {
        mr = r;
    }
    double area()
    {
        return 3.14 * mr * mr;
    }
};

void area(Shape* p)	//多态试验,根据实现运行呈现不同效果
{
    double r = p->area();
    
    cout << "r = " << r << endl;
}

int main()
{
    Rect rect(1, 2);
    Circle circle(10);
    area(&rect);
    area(&circle);
    return  0

6.2)接口(特殊抽象类模拟) =》一组行为的规范 //可以看作是一组函数原型

C++没有接口的概念,但后续的Java C#中存在接口的
定义:
1)没有成员函数
2)成员变量全部为publice属性
3)成员函数均为纯虚函数

例子:网络编程的收发 :以太网
串口编程(从串口收发)	:
蓝牙编程(通过蓝牙收发):
USB编程(通过USB口收发):
共性:都是收发:
不同点:介质不一样
class Channel	//抽象的概念,抽象类的存在    ,4个操作
{					//1)无成员函数
public:				2)
    virtual bool open() = 0;
    virtual void close() = 0;
    virtual bool send(char* buf, int len) = 0;
    virtual int receive(char* buf, int len) = 0;
};

实际作用? QT 项目合成

7)被遗弃的多重继承

问题1?子类可以拥有多个父类吗?
答c++ 支持编写多重继承的代码
多重继承的特性:
a)一个子类可以拥有多个父类
b)子类拥有所有父类成员
c)子类继承所有父类的成员函数
4)子类对象可以当作任意对象使用(赋值兼容性原则也适用)

语法:

class Derived : public BaseA, public BaseB
{
}

7.2)多重继承带来的问题:

a)通过多重继承来的对象可能拥有“不同的地址”==》引出问题:指向不同地址值的两个指针可能是指向同一个对象(以往指向同一个对象,两个指针的地址是一样的)
解决方案:无

   Derived d(1, 2, 3);
    BaseA* pa = &d;
    BaseB* pb = &d;
输出: 
pa = 0x7fff70575fa0
pb = 0x7fff70575fa4

b)多重继承可能产生冗杂的成员
解决方案:需继承,中间层父类不再关心顶层父类的初始化(不调用父类的构造函数),最终子类必须调用顶层父类的构造函数 =》增加了复杂性
但虚继承也会带来的问题:架构设计可能出现问题–>架构设计需要继承时,无法确定是直接继承还是需继承(当今类的层次 有 四 五 六层,花费了项目管理等时间问题)

public:
    People(string name, int age)
    {
        m_name = name;
        m_age = age;
    }
    void print()
    {
        cout << "Name = " << m_name << ", "
             << "Age = " << m_age << endl;
    }
};

class Teacher : virtual public People
{
public:
    Teacher(string name, int age) : People(name, age)
    {
    }
};

class Student : virtual public People
{
public:
    Student(string name, int age) : People(name, age)
    {
    }
};

class Doctor : public Teacher, public Student
{
public:
    Doctor(string name, int age) : Teacher(name+1, age+1), Student(name+2, age+2), People(name, age)
    {
    }
};
int main()
{
    Doctor d("Delphi", 33);
    
    d.print();	//这样写编译器会保错,说不知道应该使用哪个,除非加了虚继承class Teacher : virtual public People ,家里之后就只有一个printf
    d.Teacher::print();
    d.Studemt::print();
    return 0;
 输出:
 Name = Delphi1,Age =34
  Name = Delphi2,Age =35
}

c)多重继承可能产生多个虚函数表==>造成不可思议的问题,且难排除(原因:通过多重继承来的对象可能拥有“不同的地址”,funA跟funB为虚函数,要靠虚函数指针,在虚函数找对应地址)
解决方案:与多继承i相关的强制类型转换用dynamic_cast(进行强制类转换)

BaseA* pa = &d;
BaseB* pb = &d;
BaseB* pbb = (BaseB*)pa;  //出错,打印了"BaseA::funcA()",理想应该是BaseB::funcB(),由于pbb在pa
BaseB* pbe = (BaseB*)pa;    // oops!!
BaseB* pbc = dynamic_cast<BaseB*>(pa);
  cout << "pa = " << pa << endl;
    cout << "pb = " << pb << endl;
    cout << "pbe = " << pbe << endl;
    cout << "pbc = " << pbc << endl;

在这里插入图片描述

7.3)重点:工程开发中的多重继承方式:单继承某个类+实现(多个接口),巧妙 多看
特点:
解决了多余冗杂成员的问题
1)父类提供成员函数用于判断指针是否指向当前对象

7.3.1)使用多重继承的工程建议:

1)但用单继承多接口的方式(继承一个父类,然后实现多个接口)
2)父类中提供equal函数,判断当前指针地址是否指向该对象
3)与多重继承相关的强制转换用dynamic_cast完成

bool equal(Base* obj)	//判断参数指针是不是指向当前对象
    {
        return (this == obj);
    }

7.3.2)单继承多接口实例

class Base
{
protected:
    int mi;
public:
    Base(int i)
    {
        mi = i;
    }
    int getI()
    {
        return mi;
    }
    bool equal(Base* obj)	//判断参数指针是不是指向当前对象
    {
        return (this == obj);
    }
};

class Interface1
{
public:
    virtual void add(int i) = 0;
    virtual void minus(int i) = 0;
};

class Interface2
{
public:
    virtual void multiply(int i) = 0;
    virtual void divide(int i) = 0;
};

class Derived : public Base, public Interface1, public Interface2	
{
public:
    Derived(int i) : Base(i)
    {
    }
    void add(int i)
    {
        mi += i;
    }
    void minus(int i)
    {
        mi -= i;
    }
    void multiply(int i)
    {
        mi *= i;
    }
    void divide(int i)
    {
        if( i != 0 )
        {
            mi /= i;
        }
    }
};

int main()
{
    Derived d(100);
    Derived* p = &d;
    Interface1* pInt1 = &d;
    Interface2* pInt2 = &d;
    
    cout << "p->getI() = " << p->getI() << endl;    // 100
    
    pInt1->add(10);
    pInt2->divide(11);
    pInt1->minus(5);
    pInt2->multiply(8);
    
    cout << "p->getI() = " << p->getI() << endl;    // 40
    
    cout << endl;
   //错误示范:这样会编译出错
   //本意: p->equal((pInt1),判断pInt1是不是指向当前对象p
   //错误原因: bool equal(Base* obj),参数是一个指向Base的对象,所以直接调用会出错,所以需强制类型转换,
   //引入使用dynamic_cast<Base*>(pInt1)强制转换,解决了p1==p p2==p, 多重继承来的对象可能拥有“不同的地址的问题
	cout << "pInt1 == p : " << p->equal((pInt1)) << endl;		
    cout << "pInt2 == p : " << p->equal(pInt2) << endl;

    cout << "pInt1 == p : " << p->equal(dynamic_cast<Base*>(pInt1)) << endl;		//dynamic_Cast用于应对多重继承,地址可能不同的问题
    cout << "pInt2 == p : " << p->equal(dynamic_cast<Base*>(pInt2)) << endl;
    
    return 0;
}

8)C++ 对象模型分析

**1)C++中的类成员对象在内存分布时和结构体相同==》程序运行时对象退化为结构体
2)类的成员变量(在内存区依次排放)和成员函数(编译后存放在代码段)分开存放,子类对象的成员变量是父类对象的成员变量+子类对象
成员变量间可能存在内存空隙 ->跟结构体一样,遵循字节对齐

3)可以通过内存地址直接访问成员变量
4)访问权限关键字在运行时失效?,在编码时有效,程序执行时无效,可以被改变?(private的成员不能在类的外部被访问,被打破了)
答:编译后变成二进制可执行文件后,就没有了访问权限的限制,依旧可以用指针/访问内存的方式修改内存的值)
5)成员函数跟成员对象存放在两个区域,成员函数怎么访问成员对象?
答:通常是用 “.” 操作符访问成员函数=>成员函数通过对象地址访问成员变量
问:对象地址怎么来的?
答: c++语法规则隐藏了对象地址的传递过程==>编译器隐式传递(this指针)->隐藏的this指针包含了对象的地址>成员函数调用this指针访问成员对象**
#include
#include

using namespace std;

class A
{			//默认是private等级
    int i;
    int j;
    char c;
    double d;
public:
    void print()
    {
        cout << "i = " << i << ", "
             << "j = " << j << ", "
             << "c = " << c << ", "
             << "d = " << d << endl;
    }
};

struct B	//对象的内存分布跟结构体一样
{
    int i;
    int j;
    char c;
    double d;
};				

int main()
{
    A a;
    
    cout << "sizeof(A) = " << sizeof(A) << endl;    // 20 bytes
    cout << "sizeof(a) = " << sizeof(a) << endl;
    cout << "sizeof(B) = " << sizeof(B) << endl;    // 20 bytes
    
    a.print();
    
    B* p = reinterpret_cast<B*>(&a);	//reinterpret_cast解引用后 用结构体类型的指针p 指向对象的地址
    
    p->i = 1;		//可以通过内存地址(指针)直接访问成员变量
    p->j = 2;
    p->c = 'c';
    p->d = 3;
    
    a.print();
    
    p->i = 100;		//访问权限关键字在运行时失效,没有访问权限的限制=>依旧可以用指针/访问内存的方式修改内存的值)
    p->j = 200;
    p->c = 'C';
    p->d = 3.14;
    
    a.print();
    
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值