C++总结(四)——继承与派生

C++总结(四)——继承与派生

继承

简述

C++是一种面向对象的编程语言,继承是面向对象编程中一个非常重要的概念。继承可以让子类继承父类的属性和方法,从而减少代码的重复性,提高代码的复用性。本篇博客将详细介绍C++中的继承概念、继承方式以及继承的用法。

概念

继承是一种面向对象编程的特性,它允许程序员创建一个新类,该类从现有的类中继承属性和方法。在C++中,继承被定义为一个派生类从一个或多个基类继承属性和方法的过程。基类是一个包含要继承的属性和方法的类,派生类是从基类继承属性和方法的类。C++中的继承有三种方式:公有继承、保护继承和私有继承。

继承的方式

公有继承

公有继承是最常用的继承方式,它允许派生类访问基类中的公有成员。派生类可以继承基类的公有成员函数和变量。公有继承的语法如下:

#include <iostream>
using namespace std;

class Shape
{

   public:
       void setWidth(int w)
       
{
           width = w;
       }
       void setHeight(int h)
       
{
           height = h;
       }
   protected:
       int width;
       int height;
};

class Rectangle: public Shape
{
   public:
       int getArea()
       
{
          return (width * height);
       }
};

int main()
{
   Rectangle rect;
   rect.setWidth(5);
   rect.setHeight(7);
   cout << "Area of the rectangle is: " << rect.getArea() << endl;
   return 0;
}

结果输出: Area of the rectangle is: 35

保护继承

protected继承是一种继承方式,它将父类的protected和public成员以protected成员的方式继承到子类中,而父类的private成员不会被继承。

class Parent 
{
 public:
   void publicFunc() 
   {
     std::cout << "This is a public function in Parent class." << std::endl;
   }
 protected:
   void protectedFunc() 
  {
     std::cout << "This is a protected function in Parent class." << std::endl;
  }
private:
   void privateFunc() 
   {
     std::cout << "This is a private function in Parent class." << std::endl;
   }
};

class Child : protected Parent 
{
 public:
   void accessParentFunc() 
   {
     publicFunc(); // 可以访问父类的public成员函数
     protectedFunc(); // 可以访问父类的protected成员函数
     // privateFunc(); // 编译错误,不能访问父类的private成员函数
   }
};

int main() 
{
  Child c;
  c.accessParentFunc();
  return 0;
}

在上面的代码中,我们定义了一个Parent类,其中有一个public成员函数publicFunc,一个protected成员函数protectedFunc和一个private成员函数privateFunc。

然后我们定义了一个Child类,使用protected继承自Parent类。在Child类中,我们定义了一个accessParentFunc函数,它可以访问Parent类中的public和protected成员函数,但不能访问private成员函数。

在main函数中,我们创建了一个Child对象c,并调用了它的accessParentFunc函数。该函数成功地访问了Parent类的public和protected成员函数。

总之,protected继承使得父类的protected和public成员在子类中变为protected成员,可以被子类自身和子类的派生类访问,但不能被外部程序访问。

私有继承

私有继承是一种继承方式,它将基类的公共和保护成员以私有成员的方式继承到派生类中,不允许派生类对象对基类的公共和保护成员进行访问。私有继承的例子如下:

#include <iostream>

class Base {
public:
   void public_func() {
       std::cout << "Base public function." << std::endl;
   }
protected:
   void protected_func() {
       std::cout << "Base protected function." << std::endl;
   }
private:
   void private_func() {
       std::cout << "Base private function." << std::endl;
   }
};

class Derived : private Base {
public:
   void call_base_funcs() {
       // 私有继承将基类的所有成员都变成了私有成员,无法直接访问
       // public_func(); // 编译错误
       // protected_func(); // 编译错误
       // private_func(); // 编译错误

       // 可以通过派生类的成员函数间接访问基类的成员
       Base::public_func(); // 调用基类的公共成员函数
       Base::protected_func(); // 调用基类的保护成员函数
       // Base::private_func(); // 编译错误
   }
};

int main() {
   Derived d;
   d.call_base_funcs();
   return 0;
}

在上面的示例代码中,我们定义了一个基类 Base,其中包含了一个公共成员函数、一个保护成员函数和一个私有成员函数。然后我们定义了一个派生类 Derived,使用私有继承方式继承了基类 Base。在派生类中,我们定义了一个成员函数 call_base_funcs(),该函数可以间接访问基类的成员函数。

在主函数中,我们创建了一个派生类对象 d,并调用了 call_base_funcs() 函数。该函数调用了 Base::public_func()Base::protected_func() 函数,这两个函数都是基类的成员函数,但在派生类中无法直接访问。我们可以通过 Base:: 后跟函数名的方式来间接访问这两个函数。而 Base::private_func() 函数是私有成员函数,即使通过间接访问的方式也无法调用。

改变访问权限

使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。

注意:using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。

1,将基类的保护成员变成公共成员

class Animal {
protected:
   std::string m_name;
};

class Cat : public Animal {
public:
   using Animal::m_name; // 将 m_name 变成公共成员

   void printName() {
       std::cout << "My name is " << m_name << std::endl;
   }
};

int main() {
   Cat cat;
   cat.m_name = "Tom"// 直接访问 m_name 变量
   cat.printName();
   return 0;
}

上面的代码中,我们使用 using Animal::m_name; 将 m_name 变成了公共成员,然后在 Cat 类中直接访问了这个成员变量。这样就可以在 Cat 中访问 Animal 的保护成员了。

2,将基类的公共成员变成保护成员

class Person {
public:
   void sayHello() {
       std::cout << "Hello!" << std::endl;
   }
};

class Student : public Person {
protected:
   using Person::sayHello; // 将 sayHello 函数变成保护成员

public:
   void sayHello(int grade) {
       std::cout << "Hello! My grade is " << grade << std::endl;
   }
};

int main() {
   Student student;
   student.sayHello(); // 编译错误,无法直接访问 sayHello 函数
   student.sayHello(90);
   return 0;
}

上面的代码中,我们使用 using Person::sayHello; 将 sayHello 函数变成了保护成员,然后在 Student 类中重载了这个函数。这样就可以在 Student 中访问 Person 的公共成员函数了。

继承中的遮蔽问题

如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。

class Base {
public:
   int var;
   void func();
};

class Derived : public Base {
public:
   int var;
   void func();
};

在上面的例子中,派生类Derived继承自基类Base,同时也定义了同名的成员变量var和成员函数func,这就会导致基类的同名成员被遮蔽。

为了解决这个问题,C++提供了以下两种方法:

1. 通过作用域解析运算符(::)访问基类的同名成员。

class Derived : public Base {
public:
   int var;
   void func();
   void test() {
       // 访问基类中的var
       Base::var = 10;
       // 访问基类中的func
       Base::func();
   }
};

2,使用using声明,在派生类中引入基类的同名成员。

class Derived : public Base {
public:
   using Base::var;
   using Base::func;
   int var;
   void func();
   void test() {
       // 访问基类中的var
       Base::var = 10;
       // 访问基类中的func
       Base::func();
   }
};

在上面的例子中,使用using声明引入了基类的同名成员,在派生类中就可以直接访问基类的同名成员,而不需要使用作用域解析运算符。

借助指针突破访问权限的限制,访问private、protected属性的成员变量

C++中的private和protected成员变量是不能直接访问的,这是C++类的封装特性之一,如果直接访问,编译器会报错。

然而,通过指针可以实现对private和protected成员变量的访问,这是因为指针可以绕过编译器对成员访问权限的限制,直接访问内存中的数据。具体实现方式如下:

1. 使用指针来访问private成员变量:

#include<iostream>
using namespace std;

class A {
private:
   int num;
public:
   void setNum(int n) {
       num = n;
   }
   int getNum() {
       return num;
   }
};

int main() {
   A a;
   int *p = (int *)&a; //将对象地址强制转换为int指针
   *p = 10; //通过指针访问private成员变量
   cout << a.getNum() << endl; //输出结果为10
   return 0;
}

2. 使用指针来访问protected成员变量:

#include<iostream>
using namespace std;

class A {
protected:
   int num;
public:
   void setNum(int n) {
       num = n;
   }
   int getNum() {
       return num;
   }
};

class B : public A {
public:
   void test() {
       int *p = (int *)&num; //将protected成员变量地址强制转换为int指针
       *p = 20; //通过指针访问protected成员变量
   }
};

int main() {
   B b;
   b.setNum(10);
   b.test();
   cout << b.getNum() << endl; //输出结果为20
   return 0;
}

需要注意的是,这种方法不建议在实际开发中使用,因为它会破坏类的封装特性,增加代码的不可维护性和不稳定性。

总结

(1) public继承方式 基类中所有 public 成员在派生类中为 public 属性; 基类中所有 protected 成员在派生类中为 protected 属性; 基类中所有 private 成员在派生类中不能使用。

(2) protected继承方式 基类中的所有 public 成员在派生类中为 protected 属性; 基类中的所有 protected 成员在派生类中为 protected 属性; 基类中的所有 private 成员在派生类中不能使用。

(3) private继承方式 基类中的所有 public 成员在派生类中均为 private 属性; 基类中的所有 protected 成员在派生类中均为 private 属性; 基类中的所有 private 成员在派生类中不能使用。

构造与析构函数

构造函数

问题

在C++中,派生类对象的构造过程中,首先调用基类的构造函数,然后调用派生类的构造函数。这是因为派生类中包含了基类的成员,需要先初始化基类成员,然后再初始化派生类成员。

解决

这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。 派生类调用基类构造函数的方式有两种:一种是显式调用基类构造函数,另一种是使用默认构造函数进行隐式调用。

显式调用基类构造函数的方式如下:

class Base {
public:
   Base(int a, int b) {
       // ...
   }
};

class Derived : public Base {
public:
   Derived(int a, int b, int c) : Base(a, b) {
       // ...
   }
};

在派生类的构造函数中,使用冒号初始化列表来调用基类的构造函数。这里使用了显式调用基类构造函数的方式,即在冒号后面调用Base(a, b),将ab作为参数传递给基类的构造函数进行初始化。

隐式调用

class Base {
public:
   Base() {
       // ...
    }
};

class Derived : public Base {
public:
    Derived(int a) {
        // ...
    }
};

在派生类的构造函数中没有使用冒号初始化列表进行显式调用基类构造函数,因此会自动调用基类的默认构造函数进行隐式调用。

构造函数的调用顺序

从上面的分析中可以看出,基类构造函数总是被优先调用,这说明创建派生类对象时,会先调用基类构造函数,再调用派生类构造函数,如果继承关系有好几层的话,例如:

A --> B --> C

那么创建 C 类对象时构造函数的执行顺序为:

A类构造函数 --> B类构造函数 --> C类构造函数

还有一点要注意,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。以上面的 A、B、C 类为例,C 是最终的派生类,B 就是 C 的直接基类,A 就是 C 的间接基类。

C++ 这样规定是有道理的,因为我们在 C 中调用了 B 的构造函数,B 又调用了 A 的构造函数,相当于 C 间接地(或者说隐式地)调用了 A 的构造函数,如果再在 C 中显式地调用 A 的构造函数,那么 A 的构造函数就被调用了两次,相应地,初始化工作也做了两次,这不仅是多余的,还会浪费CPU时间以及内存,毫无益处,所以 C++ 禁止在 C 中显式地调用 A 的构造函数。

析构函数

问题

同理,析构函数也是不能被派生类继承的。

另外析构函数的执行顺序和构造函数的执行顺序也刚好相反:

-创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。

-而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。

#include <iostream>
using namespace std;
class Beae{
public:
   Base(){cout<<"Base constructor"<<endl;}
   ~Base(){cout<<"Base destructor"<<endl;}
};
class Persion1: public Base{
public:
   Persion1(){cout<<"Persion1 constructor"<<endl;}
   ~Persion1(){cout<<"Persion1 destructor"<<endl;}
};
class Persion2: public Persion1{
public:
   Persion2(){cout<<"Persion2 constructor"<<endl;}
   ~Persion2(){cout<<"Persion2 destructor"<<endl;}
};
int main(){
   Persion2 test;
   return 0;
}
 

输出:

Base constructor

Persion1 constructor

Persion2 constructor

Persion2 destructor

Persion1 destructor

Base destructor

多继承

概念

多继承是指一个类可以从多个父类中继承属性和方法。在C++中,可以通过以下方式实现多继承

class A {
public:
   void funcA() {}
};

class B {
public:
   void funcB() {}
};

class C : public A, public B {
public:
   void funcC() {}
};

在上述代码中,类C继承了类A和类B的所有属性和方法。可以通过以下方式调用:

C c;
c.funcA();
c.funcB();
c.funcC();

问题

1. 二义性问题:如果多个父类中有同名的属性或方法,子类在调用时不知道应该继承哪个父类的属性或方法。

class A {
public:
   void foo() {
       cout << "A's foo" << endl;
   }
};

class B {
public:
   void foo() {
       cout << "B's foo" << endl;
   }
};

class C : public A, public B {
public:
};

int main() {
   C c;
   c.foo();
   return 0;
}
 

在这个例子中,类A和类B都有一个名为foo的函数。当C派生自A和B时,就会出现二义性,因为C不知道应该使用哪个foo函数。为了解决这个问题,C++中提供了一些技术。一个常用的方法是使用作用域解析符(::)来指定要使用的基类的函数或变量。例如,我们可以修改上面的代码:

class A {
public:
   void foo() {
       cout << "A's foo" << endl;
   }
};

class B {
public:
   void foo() {
       cout << "B's foo" << endl;
   }
};

class C : public A, public B {
public:
   void foo() {
       A::foo(); // 使用A的foo函数
   }
};

int main() {
   C c;
   c.foo();
   return 0;
}

总之,在使用多继承时,需要注意避免出现二义性问题。可以使用作用域解析符来指定要使用的基类的函数或变量,也可以使用虚拟继承来避免出现多个基类的实例。

2. 菱形继承问题:如果存在一个类A,两个类B和C都继承了A,又有一个类D继承了B和C,那么D就会继承两份A的属性和方法,导致代码冗余和浪费。

class Grandfather {
public:
   int age;
};

class Father1 : public Grandfather {
public:
   int height;
};

class Father2 : public Grandfather {
public:
   int weight;
};

class Child : public Father1, public Father2 {
public:
   string name;
};

在这个例子中,Child 类继承自 Father1 和 Father2,而 Father1 和 Father2 都继承自 Grandfather。这就形成了一个菱形继承的结构:

  Grandfather
    /     \\
Father1   Father2
    \\     /
     Child

这种继承结构会引发一些问题,主要是以下几个方面:

1. 数据冗余:Grandfather 类的成员变量 age 会被 Father1 和 Father2 类各自继承一次,而 Child 类又同时继承自 Father1 和 Father2,因此 Child 类中其实有两份 age 的副本,这就造成了数据冗余。

2. 命名冲突:如果 Father1 和 Father2 中有相同的成员变量或成员函数,那么在 Child 类中就会产生命名冲突,需要使用作用域解析运算符(::)来区分。

3. 虚函数二义性:如果 Grandfather 类中有一个虚函数 func(),而 Father1 和 Father2 分别重写了这个虚函数,那么在 Child 类中就会产生虚函数二义性的问题。因为 Child 类同时继承自 Father1 和 Father2,而这两个类又都重写了 func(),所以 Child 类无法确定应该调用哪一个版本的 func()。

虚函数与虚继承

问题:

前面提及的菱形继承的问题。C++提出了虚函数与虚继承来解决。

虚函数

概念

在C++中,通过使用虚函数可以实现多态。多态是面向对象编程的核心概念之一,它允许我们使用同一个接口来处理不同类型的对象,从而提高了程序的可扩展性和可维护性。虚函数的定义格式如下:

class Base {
public:
   virtual void func() {
       // ...
   }
   // ...
};

class Derived : public Base {
public:
   void func() override {
       // ...
   }
   // ...
};

其中,关键字virtual表示该函数是虚函数,而override则表示该函数是覆盖了基类的虚函数。使用虚函数的好处在于,当我们对一个指向派生类对象的基类指针或引用调用虚函数时,会根据实际的对象类型来决定调用哪个版本

Derived d;
Base& b = d;  // 派生类对象的引用绑定到基类引用上
b.func();     // 调用 Derived 类的 func() 函数

虚函数的实现依赖于虚函数表(vtable)和虚函数指针(vptr)。每个对象都有一个指向虚函数表的指针,而虚函数表中存储着虚函数的地址。当我们调用一个虚函数时,实际上是通过对象的虚函数指针来定位虚函数表,并根据虚函数表中的地址调用相应的虚函数。因此,虚函数的调用是相对较慢的,但是可以带来更好的灵活性和可维护性。

虚继承

C++中的多重继承会引发菱形继承问题,即一个派生类同时继承了两个基类,而这两个基类又分别继承自同一个基类,从而导致派生类中包含了两份相同的基类成员。这样会带来许多问题,比如代码冗余、内存浪费、数据不一致等等。为了解决这个问题,C++中提供了虚继承的机制。虚继承可以让派生类共享相同的基类子对象,从而避免了菱形继承问题。虚继承的语法如下:

class Base {
public:
   // ...
};

class Derived1 : virtual public Base {
public:
   // ...
};

class Derived2 : virtual public Base {
public:
   // ...
};

class Derived3 : public Derived1, public Derived2 {
public:
   // ...
};

在上面的代码中,Derived1Derived2都使用了虚继承,从而共享了Base的子对象。而Derived3则同时继承了Derived1Derived2,从而实现了多重继承。

虚继承的实现依赖于虚基类表(vtable)和虚基类指针(vptr)。每个虚继承的子对象都有一个虚基类指针,指向虚基类表中的虚基类信息。而虚基类表中存储着虚基类的地址和偏移量,用于计算虚基类成员在派生类中的地址。这样,当一个派生类同时继承了多个虚基类时,就可以共享这些虚基类的子对象,从而避免了菱形继承问题。

下面是一个简单的代码示例,演示了虚函数和虚继承的用法。在这个示例中,我们定义了一个基类Animal,以及两个派生类CatDogCatDog类都覆盖了Animal类的makeSound虚函数,并调用了基类的makeSound函数。此外,CatDog类都使用了虚继承,以共享Animal类的子对象。

#include <iostream>

class Animal {
public:
   virtual void makeSound() {
       std::cout << "Animal makes a sound" << std::endl;
   }
};

class Cat : virtual public Animal {
public:
   void makeSound() override {
       Animal::makeSound();
       std::cout << "Cat meows" << std::endl;
   }
};

class Dog : virtual public Animal {
public:
   void makeSound() override {
       Animal::makeSound();
       std::cout << "Dog barks" << std::endl;
   }
};

class CatDog : public Cat, public Dog {
public:
   // ...
};

int main() {
   Cat cat;
   cat.makeSound();

   Dog dog;
   dog.makeSound();

   CatDog catDog;
   catDog.makeSound();

   return 0;
}

输出结果如下:

Animal makes a sound
Cat meows
Animal makes a sound
Dog barks
Animal makes a sound
Cat meows
Dog barks

从输出结果可以看出,当我们调用CatDog类的makeSound函数时,会先调用基类的makeSound函数,然后再打印出相应的动物叫声。而当我们调用CatDog类的makeSound函数时,则会依次调用Cat类和Dog类的makeSound函数,从而实现了多重继承的效果。

虚继承时的构造函数

虚继承的构造函数有一些特殊的要求和实现方式。首先,虚基类的构造函数必须由最底层的派生类负责调用,以确保虚基类子对象只被构造一次。其次,在虚继承中,构造函数的调用顺序也有所不同,需要先调用虚基类的构造函数,再调用非虚基类的构造函数。下面我们通过一个简单的例子来说明虚继承的构造函数实现方式:

#include <iostream>

using namespace std;

class Base {
public:
   Base() { cout << "Base constructor." << endl; }
};

class VirtualBase {
public:
   VirtualBase() { cout << "VirtualBase constructor." << endl; }
};

class Derived : public Base, virtual public VirtualBase {
public:
   Derived() : VirtualBase(), Base() {
       cout << "Derived constructor." << endl;
   }
};

int main() {
   Derived d;
   return 0;
}

在上面的例子中,我们定义了一个基类Base和一个虚基类VirtualBase,然后我们定义了一个派生类Derived,它同时继承了BaseVirtualBase。在Derived的构造函数中,我们先调用了VirtualBase的构造函数,再调用了Base的构造函数。

运行上面的程序,输出如下:

VirtualBase constructor.
Base constructor.
Derived constructor.

需要注意的是,如果派生类的构造函数中没有显式调用虚基类的构造函数,C++编译器会自动生成一个默认的调用。但是,如果虚基类没有默认构造函数,就必须在派生类的构造函数中显式调用带参数的虚基类构造函数。例如:

class VirtualBase {
public:
   VirtualBase(int x) { cout << "VirtualBase constructor." << endl; }
};

class Derived : public Base, virtual public VirtualBase {
public:
   Derived() : VirtualBase(10), Base() {
       cout << "Derived constructor." << endl;
   }
};

在上面的例子中,虚基类VirtualBase的构造函数带有一个参数x,我们在派生类Derived的构造函数中显式调用了带参数的虚基类构造函数,并传入了一个参数10

向上转型

类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型(Upcasting)。相应地,将基类赋值给派生类称为向下转型(Downcasting)。

C++中的向上转型(upcasting)是将派生类对象转换为基类对象的过程,它是一种安全的转换,不会丢失任何信息。向上转型可以通过将派生类对象赋值给基类指针或引用来实现向上转型非常安全,可以由编译器自动完成;向下转型有风险,需要程序员手动干预。

例如,假设有一个基类Animal和一个派生类Dog,代码如下:

class Animal {
public:
   virtual void makeSound() {
       cout << "Animal makes sound." << endl;
   }
};

class Dog : public Animal {
public:
   void makeSound() override {
       cout << "Dog barks." << endl;
   }
};

我们可以创建一个Dog对象并将其转换为Animal类型:

Dog myDog;
Animal* myAnimal = &myDog;

在这里,我们将myDog对象的地址赋给一个Animal类型的指针myAnimal,并且向上转型已经完成。我们可以通过myAnimal指针调用makeSound函数,它会调用Dog类中的makeSound函数。

myAnimal->makeSound(); // 输出 "Dog barks."

这是因为Dog类重写了Animal类的makeSound函数,所以调用makeSound函数时实际上是调用Dog类中的函数。

总的来说,向上转型是一种非常常见的操作,它可以使代码更加简洁和灵活。但需要注意的是,在向上转型后,我们只能访问基类中定义的成员函数和变量,而不能访问派生类中定义的成员函数和变量。

总结

虚函数和虚继承是C++中非常重要的概念,它们都是面向对象编程中实现多态和解决菱形继承问题的关键。虚函数通过虚函数表和虚函数指针实现了多态,而虚继承通过虚基类表和虚基类指针实现了共享基类子对象,从而避免了菱形继承问题。在实际编程中,我们可以根据需要灵活使用虚函数和虚继承,以提高代码的可扩展性和可维护性。



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值