深度剖析C++类的多态特性(非常详细)
类作为 C++ 与面向对象程序设计相结合的产物,是面向对象程序设计在 C++ 中的具体体现。类的设计从成员构成到类之间的继承关系,再到虚函数,都体现了面向对象程序设计的封装、继承和多态这三大特征。
那么,多态的具体含义到底是什么呢?
既然“学生”是“人”类的一种,那么在使用“人”这个概念时,这个“人”可以指代“学生”,而“学生”也可以应用在“人”的场合。例如,可以问“教室里有多少人”,实际上问的是“教室里有多少学生”。
这种用基类指代派生类的关系在C++中,就反映为基类指针可以指向派生类的对象,而派生类的对象也可以当成基类对象使用。
这样的解释对大家来说是否有些抽象?没关系,可以回想生活中经常遇到的一个场景:上车的人请买票。在这句话中,涉及一个类—人,以及它的一个动作—买票。但上车的人可能是老师、学生,也可能是工人、农民或者程序员,他们买票的方式也各不相同,有的投币,有的刷卡,但为什么售票员不说“上车的老师请刷卡买票”或者说“上车的工人请投币买票”,而仅仅说“上车的人请买票”就足够了呢?
这是因为虽然上车的人可能是老师、学生、公司职员等,但他们都是“人”这个基类的派生类,所以这里可以用基类“人”来指代所有派生类对象,通过基类的接口“买票”来调用派生类对这个接口的具体实现,完成买票的具体动作,如下图所示。

图 1 上车的人请买票
学习了前面的封装和继承,我们可以用 C++ 来描述这个场景:
在主函数中,我们模拟了“上车买票”这一场景:首先分别创建了 Teacher 类和 Student 类的对象,并用基类 Human 的两个指针分别来指代这两个对象。然后,通过 Human 类的指针调用接口函数 BuyTicket(),模拟“上车的人请买票”的过程,从而完成 Teacher 和 Student 类对象的买票动作。
最后,程序的输出结果是:
然而,在使用基类的指针调用这个函数时,得到的动作却是相同的,都是来自基类的动作。这显然是不合适的。虽然都是“人买票”,但是不同的人应该有不同的买票方式,比如老师可以投币买票,而学生则刷卡买票。因此,根据“人”所指代的具体对象不同,买票的动作也应有所不同。
为了解决这个问题,C++ 提供了虚函数(virtual function)机制。在基类的函数声明前加上 virtual 关键字,函数就成为虚函数。无论派生类中是否显式地使用 virtual 关键字重新定义该函数,这个函数仍然是虚函数。
使用虚函数的机制时,如果通过基类指针调用虚函数,实际会调用该指针所指向的具体对象(无论是基类对象还是派生类对象)的虚函数,而不是基类的函数。这就解决了上述问题。实现了根据实际对象来决定调用哪一个函数的机制,该机制被称为函数重写或覆盖(override) 。
现在,我们可以用虚函数来解决上面例子中的问题,使得通过 Human 基类指针调用的 BuyTicket() 函数,可以根据指针所指向的实际对象来选择不同的买票动作:
此时,Human 基类的指针 p1 和 p2 对 BuyTicket() 函数的调用,不再执行基类的这个函数,而是根据这些指针在运行时所指向的实际类对象来动态选择,指针指向哪个类的对象就执行哪个类的 BuyTicket() 函数。
例如,在执行 p1->BuyTicket() 语句时,p1 指向的是一个 Teacher 类的对象,那么执行的就是 Teacher 类的 BuyTicket() 函数,于是输出“老师投币买票。”。
经过虚函数机制的改写,这个程序最终能够输出符合实际情况的结果:
在这个例子中,Human 基类中的 BuyTicket() 虚函数就从未被调用过,所以我们也可以把它声明为一个纯虚函数,这相当于只提供了一个“买票”动作的接口,而具体的买票方式则留给它的派生类去实现。例如:
与普通类相比,抽象类有一些特殊之处。
1) 首先,因为抽象类中包含尚未实现的纯虚函数,所以不能创建抽象类的实例(即对象)。如果尝试创建一个抽象类的对象,将会导致编译错误。例如:
2) 其次,如果某个类从抽象类派生而来,那么它必须实现其中的纯虚函数才能成为一个实体类(concrete class,或称为具体类)。如果派生类没有实现所有的纯虚函数,它仍然是一个抽象类,无法创建实例对象。
例如:
如果虚函数出现在基类的构造函数或者析构函数中,在创建或者销毁派生类对象时,虚函数不会如我们所预期的那样,执行派生类重写后的虚函数,取而代之的是,它会直接执行这个基类自身的虚函数。换句话说,在基类构造或析构期间,虚函数是被禁止的。
为什么会出现这种奇怪的行为?这是因为在创建派生类的对象时,基类的构造函数会先于派生类的构造函数执行。如果在基类的构造函数中调用派生类重写的虚函数,此时派生类对象尚未创建完成,其数据成员尚未被初始化。由于派生类虚函数的执行可能涉及它的数据成员,而对未初始化的数据成员进行访问,无疑是一场噩梦的开始。
类似的问题也存在于基类的析构函数中。
基类的析构函数会在派生类的析构函数之后执行。如果在基类的析构函数中调用派生类的虚函数,此时派生类的数据成员已经被释放。如果在虚函数中尝试访问这些派生类已释放的数据成员,会导致访问未定义成员的错误。
为了避免这些行为可能带来的危害,C++ 禁止在构造函数和析构函数中进行虚函数的向下匹配。为避免这种不一致的匹配规则带来的歧义(你可能以为虚函数会像在普通函数中一样调用派生类的虚函数,但实际上调用的是基类自身的虚函数),最好的方法是,不要在基类的构造函数和析构函数中调用虚函数。
例如,如果 Teacher 类重写了 Human 类中的 BuyTicket() 虚函数,即使在 Teacher 类中省略了 virtual 关键字,重写仍然有效。
然而,尽管省略 virtual 关键字在语法上是可行的,但这种做法可能会引起混淆,影响代码的可读性和可维护性。如果派生类中的重写函数缺少 virtual 关键字,读者可能会疑惑这个函数是基类中虚函数的重写,还是派生类中新添加的普通成员函数。
为了清晰地表达代码的意图,提高代码的可读性和可维护性,建议在派生类中重写虚函数时仍然使用 virtual 关键字。这样可以明确地告诉其他开发者和未来的代码维护者,这个函数是对基类中虚函数的重写,而不是一个全新的函数声明。
例如:
上面例子中的 DoHomework() 函数并没有基类的同名虚函数可供重写,因此在其声明后的 override 关键字会引发编译错误。
如果希望某个函数是虚函数的重写,应在其函数声明后加上 override 关键字,这样可以在很大程度上提高代码的可读性,同时确保代码严格符合程序员的意图。
例如,如果程序员希望派生类中的某个函数重写基类中的虚函数,并为其加上 override 修饰,编译器会帮助检查是否真正形成了虚函数的重写。如果基类没有同名虚函数,或者虚函数的函数形式不同,无法形成重写,编译器就会给出相应的错误提示信息,程序员可以根据这些信息进行修正。
凡事都有其两面性,C++ 的虚函数重写也不例外。实际上,我们有很多正当的理由来阻止一个虚函数在它的派生类中被重写。
首先,这样做可以提高程序的性能。因为虚函数的调用需要查找类的虚函数表,如果程序中大量使用虚函数,可能会在虚函数的调用上浪费很多不必要的时间,从而影响程序的性能。阻止不必要的虚函数重写,可以减小虚函数表的大小,自然就减少了虚函数调用时的查表时间,从而提高了程序的性能。
其次,代码安全性也是一个考虑因素。某些函数库出于扩展的需要,提供了一些虚函数作为接口,供专业程序员对其进行重写,从而扩展函数库的功能。但是,对于函数库的普通使用者而言,重写这些函数是非常危险的,因为缺乏相关知识或经验而容易出错。所以有必要使用 final 关键字来阻止这类重写。
此外,虚函数的重写可以实现面向对象程序设计的多态机制,但过多的虚函数重写会影响程序的性能,并使得程序变得复杂。这时,我们需要使用 final 关键字来阻止这些虚函数被无意义地重写,以平衡灵活性与性能。
那么,何时应该使用 final,何时不应该使用呢?这里有一条简单的原则:如果重新定义了一个派生类并重写了基类的某个虚函数会产生语义上的错误,则需要使用 final 关键字来阻止虚函数被重写。
例如,前文例子中的 Student 类有一个来自其基类 Human 的虚函数 BuyTicket(),而当定义 Student 类的派生类 Pupil 时,不应该再重写这个虚函数,因为无论是 Student 还是 Pupil,BuyTicket() 函数的行为应该是一样的,不需要重新定义。在这种情况下,可以使用 final 关键字来阻止虚函数的重写。出于性能的需求,或者只是简单地不希望虚函数被重写,最好的做法是从一开始就不要将这个函数定义为虚函数。
这样可以实现“以不变应万变”,即在应对需求的不断变化,只需要修改派生类的具体实现,而不需要改变函数的调用方式,从而大大提高程序的可复用性(针对接口的复用)。
例如,在前文的例子中,如果想要增加一种新的乘客类,只需要添加一个 Human 的派生类,并实现这个派生类自己的 BuyTicket() 函数即可。在使用这个新创建的类时,无须修改程序代码中的调用形式。
那么,多态的具体含义到底是什么呢?
用虚函数实现多态
我们知道,在大多数情况下,派生类是基类的“一种”,就像“学生”是“人”类的一种一样。既然“学生”是“人”类的一种,那么在使用“人”这个概念时,这个“人”可以指代“学生”,而“学生”也可以应用在“人”的场合。例如,可以问“教室里有多少人”,实际上问的是“教室里有多少学生”。
这种用基类指代派生类的关系在C++中,就反映为基类指针可以指向派生类的对象,而派生类的对象也可以当成基类对象使用。
这样的解释对大家来说是否有些抽象?没关系,可以回想生活中经常遇到的一个场景:上车的人请买票。在这句话中,涉及一个类—人,以及它的一个动作—买票。但上车的人可能是老师、学生,也可能是工人、农民或者程序员,他们买票的方式也各不相同,有的投币,有的刷卡,但为什么售票员不说“上车的老师请刷卡买票”或者说“上车的工人请投币买票”,而仅仅说“上车的人请买票”就足够了呢?
这是因为虽然上车的人可能是老师、学生、公司职员等,但他们都是“人”这个基类的派生类,所以这里可以用基类“人”来指代所有派生类对象,通过基类的接口“买票”来调用派生类对这个接口的具体实现,完成买票的具体动作,如下图所示。

图 1 上车的人请买票
学习了前面的封装和继承,我们可以用 C++ 来描述这个场景:
// “上车买票”演示程序 // 定义 Human 类,这个类有一个接口函数 BuyTicket() 表示买票的动作 class Human { // Human 类的行为 public: // 买票接口函数 void BuyTicket() { cout << "人买票。" << endl; } }; // 从“人”派生出两个类,分别表示老师和学生 class Teacher : public Human { public: // 重新定义基类提供的接口函数,以适应派生类的具体情况 void BuyTicket() { cout << "老师投币买票。" << endl; } }; class Student : public Human { public: void BuyTicket() { cout << "学生刷卡买票。" << endl; } }; // 在主函数中模拟上车买票的场景 int main() { // 两个人上车了,一个是老师,另一个是学生 // 基类指针指向派生类对象 Human* p1 = new Teacher(); Human* p2 = new Student(); // 上车的人请买票 p1->BuyTicket(); // 第一个人是老师,投币买票 p2->BuyTicket(); // 第二个人是学生,刷卡买票 // 销毁对象 delete p1; delete p2; p1 = p2 = nullptr; return 0; }在这段代码中,我们定义了一个基类 Human,它有一个接口函数 BuyTicket(),表示“人”买票的动作。接着,我们定义了它的两个派生类 Teacher 和 Student。虽然这两个派生类通过继承已经直接拥有了 BuyTicket() 函数,但由于“老师”和“学生”买票的行为各有特殊性,因此我们在这两个派生类中重新定义了 BuyTicket() 函数,以表达他们的特殊买票动作。
在主函数中,我们模拟了“上车买票”这一场景:首先分别创建了 Teacher 类和 Student 类的对象,并用基类 Human 的两个指针分别来指代这两个对象。然后,通过 Human 类的指针调用接口函数 BuyTicket(),模拟“上车的人请买票”的过程,从而完成 Teacher 和 Student 类对象的买票动作。
最后,程序的输出结果是:
人买票。
人买票。
然而,在使用基类的指针调用这个函数时,得到的动作却是相同的,都是来自基类的动作。这显然是不合适的。虽然都是“人买票”,但是不同的人应该有不同的买票方式,比如老师可以投币买票,而学生则刷卡买票。因此,根据“人”所指代的具体对象不同,买票的动作也应有所不同。
为了解决这个问题,C++ 提供了虚函数(virtual function)机制。在基类的函数声明前加上 virtual 关键字,函数就成为虚函数。无论派生类中是否显式地使用 virtual 关键字重新定义该函数,这个函数仍然是虚函数。
使用虚函数的机制时,如果通过基类指针调用虚函数,实际会调用该指针所指向的具体对象(无论是基类对象还是派生类对象)的虚函数,而不是基类的函数。这就解决了上述问题。实现了根据实际对象来决定调用哪一个函数的机制,该机制被称为函数重写或覆盖(override) 。
现在,我们可以用虚函数来解决上面例子中的问题,使得通过 Human 基类指针调用的 BuyTicket() 函数,可以根据指针所指向的实际对象来选择不同的买票动作:
// 经过虚函数机制改写后的“上车买票”演示程序 // 定义 Human 类,提供公有接口 class Human { // Human 类的行为 public: // 在函数前添加 virtual 关键字,将 BuyTicket() 函数声明为虚函数 // 表示其派生类可能重新定义这个虚函数以满足其特殊的需要 virtual void BuyTicket() { cout << "人买票。" << endl; } }; // 在派生类中重新定义虚函数 class Teacher : public Human { public: // 根据实际情况重新定义基类的虚函数以满足自己的特殊需要 // 不同的买票方式 virtual void BuyTicket() { cout << "老师投币买票。" << endl; } }; class Student : public Human { public: // 不同的买票方式 virtual void BuyTicket() { cout << "学生刷卡买票。" << endl; } };虚函数机制的改写只是在基类的 BuyTicket() 函数前加上了 virtual 关键字(派生类中的 virtual 关键字可以省略),使其成为一个虚函数。其他代码没做任何修改,但代码所执行的动作却发生了变化。
此时,Human 基类的指针 p1 和 p2 对 BuyTicket() 函数的调用,不再执行基类的这个函数,而是根据这些指针在运行时所指向的实际类对象来动态选择,指针指向哪个类的对象就执行哪个类的 BuyTicket() 函数。
例如,在执行 p1->BuyTicket() 语句时,p1 指向的是一个 Teacher 类的对象,那么执行的就是 Teacher 类的 BuyTicket() 函数,于是输出“老师投币买票。”。
经过虚函数机制的改写,这个程序最终能够输出符合实际情况的结果:
老师投币买票。
学生刷卡买票。
C++纯虚函数
我们还可以在虚函数声明后加上“= 0”的标记,将该虚函数声明为纯虚函数。纯虚函数意味着基类不会实现这个函数,它的所有实现都留给其派生类完成。在这个例子中,Human 基类中的 BuyTicket() 虚函数就从未被调用过,所以我们也可以把它声明为一个纯虚函数,这相当于只提供了一个“买票”动作的接口,而具体的买票方式则留给它的派生类去实现。例如:
// 使用纯虚函数 BuyTicket() 作为接口的 Human 类 class Human { // Human 类的行为 public: // 声明 BuyTicket() 函数为纯虚函数 // 在代码中,我们在函数声明后加上 “= 0” 来表示它是一个纯虚函数 virtual void BuyTicket() = 0; };当类中有纯虚函数时,该类就成为一个抽象类(abstract class)。抽象类仅用于被继承,提供一致的公有接口。
与普通类相比,抽象类有一些特殊之处。
1) 首先,因为抽象类中包含尚未实现的纯虚函数,所以不能创建抽象类的实例(即对象)。如果尝试创建一个抽象类的对象,将会导致编译错误。例如:
// 编译错误,不能创建抽象类的对象 Human aHuman;
2) 其次,如果某个类从抽象类派生而来,那么它必须实现其中的纯虚函数才能成为一个实体类(concrete class,或称为具体类)。如果派生类没有实现所有的纯虚函数,它仍然是一个抽象类,无法创建实例对象。
例如:
class Student : public Human { public: // 实现基类中的纯虚函数,让 Student 类成为一个实体类 virtual void BuyTicket() { cout << "学生刷卡买票。" << endl; } };
C++虚函数的实际应用
使用 virtual 关键字将普通函数修饰成虚函数,可以实现多态。一个重要的应用是将基类的析构函数声明为虚函数,以确保通过基类指针释放派生类对象时,派生类的析构函数能够正确执行。例如:class Human { public: // 用 virtual 修饰的析构函数 virtual ~Human() { cout << "销毁 Human 对象" << endl; } }; class Student : public Human { public: // 重写析构函数,完成特殊的销毁工作 virtual ~Student() { cout << "销毁 Student 对象" << endl; } }; // 将一个 Human 类的指针指向一个 Student 类的对象 Human* pHuman = new Student(); // ... // 利用 Human 类的指针,释放它指向的 Student 类的对象 // 因为析构函数是虚函数,所以这个指针所指向的 Student 类的对象的析构函数会被调用 // 否则,会错误地调用 Human 类的析构函数 delete pHuman; pHuman = nullptr;
不要在构造函数或析构函数中调用虚函数
我们知道,在基类的普通函数中,调用虚函数时,C++ 的多态机制会根据具体调用该函数的对象,动态决定执行哪个派生类重写后的虚函数。这是 C++ 多态机制的基本规则。然而,这一规则并不是放之四海皆准的。如果虚函数出现在基类的构造函数或者析构函数中,在创建或者销毁派生类对象时,虚函数不会如我们所预期的那样,执行派生类重写后的虚函数,取而代之的是,它会直接执行这个基类自身的虚函数。换句话说,在基类构造或析构期间,虚函数是被禁止的。
为什么会出现这种奇怪的行为?这是因为在创建派生类的对象时,基类的构造函数会先于派生类的构造函数执行。如果在基类的构造函数中调用派生类重写的虚函数,此时派生类对象尚未创建完成,其数据成员尚未被初始化。由于派生类虚函数的执行可能涉及它的数据成员,而对未初始化的数据成员进行访问,无疑是一场噩梦的开始。
类似的问题也存在于基类的析构函数中。
基类的析构函数会在派生类的析构函数之后执行。如果在基类的析构函数中调用派生类的虚函数,此时派生类的数据成员已经被释放。如果在虚函数中尝试访问这些派生类已释放的数据成员,会导致访问未定义成员的错误。
为了避免这些行为可能带来的危害,C++ 禁止在构造函数和析构函数中进行虚函数的向下匹配。为避免这种不一致的匹配规则带来的歧义(你可能以为虚函数会像在普通函数中一样调用派生类的虚函数,但实际上调用的是基类自身的虚函数),最好的方法是,不要在基类的构造函数和析构函数中调用虚函数。
建议为所有虚函数声明virtual关键字
在派生类中重写基类的虚函数时,并不强制要求在派生类的函数声明中使用 virtual 关键字。只要派生类的函数声明与基类中的虚函数声明匹配,编译器就会识别这是一个重写。例如,如果 Teacher 类重写了 Human 类中的 BuyTicket() 虚函数,即使在 Teacher 类中省略了 virtual 关键字,重写仍然有效。
然而,尽管省略 virtual 关键字在语法上是可行的,但这种做法可能会引起混淆,影响代码的可读性和可维护性。如果派生类中的重写函数缺少 virtual 关键字,读者可能会疑惑这个函数是基类中虚函数的重写,还是派生类中新添加的普通成员函数。
为了清晰地表达代码的意图,提高代码的可读性和可维护性,建议在派生类中重写虚函数时仍然使用 virtual 关键字。这样可以明确地告诉其他开发者和未来的代码维护者,这个函数是对基类中虚函数的重写,而不是一个全新的函数声明。
C++ override修饰虚函数
此外,为了让代码的意图更为明确,C++ 中提供了 override 关键字,用于标注一个重写的虚函数。使用这个关键字可以让程序员在编写代码时更清楚地表达出对虚函数的重写意图。例如:
class Student : public Human { public: // 虽然没有 virtual 关键字 // 但是 override 关键字表明,这是一个重写的虚函数 void BuyTicket() override { cout << "学生刷卡买票。" << endl; } // 错误:基类中没有 DoHomework() 这个虚函数,不能重写虚函数 void DoHomework() override { cout << "完成家庭作业。" << endl; } };从这里可以看到,override 关键字仅能用于修饰派生类重写的虚函数,以表达程序员的实现意图,而不能用于普通成员函数。
上面例子中的 DoHomework() 函数并没有基类的同名虚函数可供重写,因此在其声明后的 override 关键字会引发编译错误。
如果希望某个函数是虚函数的重写,应在其函数声明后加上 override 关键字,这样可以在很大程度上提高代码的可读性,同时确保代码严格符合程序员的意图。
例如,如果程序员希望派生类中的某个函数重写基类中的虚函数,并为其加上 override 修饰,编译器会帮助检查是否真正形成了虚函数的重写。如果基类没有同名虚函数,或者虚函数的函数形式不同,无法形成重写,编译器就会给出相应的错误提示信息,程序员可以根据这些信息进行修正。
C++ final修饰虚函数
与 override 相对,有时我们希望某个虚函数不被派生类继承。这时,可以使用 final 关键字来阻止该虚函数被进一步重写。例如:// 学生类 class Student : public Human { public: // final 关键字表示这就是这个虚函数的最终(final)实现 // 阻止被派生类重写,即重新定义 virtual void BuyTicket() final { cout << "学生刷卡买票。" << endl; } // 新增加的一个虚函数 // 没有 final 关键字修饰的虚函数,派生类可以对其进行重写,重新定义 virtual void DoHomework() { cout << "完成家庭作业。" << endl; } }; // 小学生类 class Pupil : public Student { public: // 错误:不能对基类中使用 final 修饰的虚函数进行重写 // 这里表达的意义是,无论是 Student 类还是派生的 Pupil 类,买票的方式都是一样的 // 无须也不能通过虚函数重写对其行为进行重新定义 virtual void BuyTicket() { cout << "学生刷卡买票。" << endl; } // 派生类对基类中没有 final 关键字修饰的虚函数进行重写 virtual void DoHomework() override { cout << "小学生完成家庭作业。" << endl; } };既然虚函数的意义是为了实现面向对象程序设计的多态机制,使得虚函数可以被重写,那么为什么我们还要使用 final 关键字来阻止虚函数的重写呢?
凡事都有其两面性,C++ 的虚函数重写也不例外。实际上,我们有很多正当的理由来阻止一个虚函数在它的派生类中被重写。
首先,这样做可以提高程序的性能。因为虚函数的调用需要查找类的虚函数表,如果程序中大量使用虚函数,可能会在虚函数的调用上浪费很多不必要的时间,从而影响程序的性能。阻止不必要的虚函数重写,可以减小虚函数表的大小,自然就减少了虚函数调用时的查表时间,从而提高了程序的性能。
其次,代码安全性也是一个考虑因素。某些函数库出于扩展的需要,提供了一些虚函数作为接口,供专业程序员对其进行重写,从而扩展函数库的功能。但是,对于函数库的普通使用者而言,重写这些函数是非常危险的,因为缺乏相关知识或经验而容易出错。所以有必要使用 final 关键字来阻止这类重写。
此外,虚函数的重写可以实现面向对象程序设计的多态机制,但过多的虚函数重写会影响程序的性能,并使得程序变得复杂。这时,我们需要使用 final 关键字来阻止这些虚函数被无意义地重写,以平衡灵活性与性能。
那么,何时应该使用 final,何时不应该使用呢?这里有一条简单的原则:如果重新定义了一个派生类并重写了基类的某个虚函数会产生语义上的错误,则需要使用 final 关键字来阻止虚函数被重写。
例如,前文例子中的 Student 类有一个来自其基类 Human 的虚函数 BuyTicket(),而当定义 Student 类的派生类 Pupil 时,不应该再重写这个虚函数,因为无论是 Student 还是 Pupil,BuyTicket() 函数的行为应该是一样的,不需要重新定义。在这种情况下,可以使用 final 关键字来阻止虚函数的重写。出于性能的需求,或者只是简单地不希望虚函数被重写,最好的做法是从一开始就不要将这个函数定义为虚函数。
合理使用C++多态特性
面向对象程序设计的多态机制为派生类修改基类的行为,并以一致的调用形式满足不同的需求提供了一种可能。合理利用多态机制可以为程序开发带来更大的灵活性。1) 接口统一,高度复用
程序不必为每个派生类编写具体的函数调用,只需要在基类中定义好接口,然后针对接口编写函数调用,而具体的实现则留给派生类自己去处理。这样可以实现“以不变应万变”,即在应对需求的不断变化,只需要修改派生类的具体实现,而不需要改变函数的调用方式,从而大大提高程序的可复用性(针对接口的复用)。
2) 向后兼容,灵活扩展
派生类的行为可以通过基类的指针进行访问,可以在很大程度上提高程序的可扩展性。一个基类可以有多个派生类,并且可以不断扩充。例如,在前文的例子中,如果想要增加一种新的乘客类,只需要添加一个 Human 的派生类,并实现这个派生类自己的 BuyTicket() 函数即可。在使用这个新创建的类时,无须修改程序代码中的调用形式。