多态概念
多态 面向对象三大基本特性的最后一个,多态可以实现**"一个接口,多种方法"** ,比如父类和子类中的同名方法,在增加了多态后,调用同名函数时候,可以根据不同的对象调用属于自己的函数,实现不同的方法,因为 多态 的实现依赖于继承
多态分为编译时多态(静态多态) 和运行时多态(动态多态) ,编译时多态(静态多态) 主要就是我们前⾯讲的函数重载和函数模板 ,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在 编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态
运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种 形态。⽐如买票这个⾏为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军 ⼈买票时是优先买票。再⽐如,同样是动物叫的⼀个⾏为(函数),传猫对象过去,就是"(>^ω^<)


多态应用
买票场景
cpp
class Person {
public:
// [virtual] + [返回值] + [函数名] + [参数] 相同 = 构成多态
👇 👇 👇 👇
virtual void BuyTicket () {
cout << "Person: 买票-全价 100¥" << endl;
}
};
class Student : public Person {
public:
// 这里也都相同
virtual void BuyTicket() {
cout << "Student: 买票-半价 50¥" << endl;
}
};
class Soldier : public Person {
public:
// 这里也都相同
virtual void BuyTicket() {
cout << "Soldier: 优先买预留票-全价 100¥" << endl;
}
};
宠物
cpp
class Pet {
public:
virtual void makeSound() {
cout << "Some generic pet sound" << endl;
}
};
class Cat : public Pet {
public:
void makeSound() override {
cout << "Meow" << endl;
}
};
class Dog : public Pet {
public:
void makeSound() override {
cout << "Woof" << endl;
}
};
class Bird : public Pet {
public:
void makeSound() override {
cout << "Tweet" << endl;
}
};
就宠物场景进行多态讲解,我们现在有pet类型的指针,它可以指向 任何一种具体的宠物 对象:
cpp
Pet* myPet = new Dog();
myPet->makeSound(); // 输出 "Woof"
尽管 myPet 是 Pet 类型的指针,但它指向的是一个**Dog** 对象。因此,当你调用 makeSound() 方法时,会调用 Dog 类的 makeSound() 方法,而不是 Pet 类的 。(这就是多态)
当你将mypet指向一个Cat对象:
cpp
myPet = new Cat();
myPet->makeSound(); // 输出 "Meow"

多态形成条件
多态是 在不同继承关系的类对象中去调用同一个函数,产生了不同的行为。
比如我们刚才的 Student 继承了 Person,Person 买票是全价,但 Student 买票却是半价:

**📌 注意:**继承中想要构成多态,必须满足以下两个条件:
-
① 必须是子类的虚函数 重写成父类函数(重写:三同 + 虚函数)
-
② 必须是父类的指针 或者引用去调用虚函数。
-
* 三同指的是:同函数名、同参数、同返回值。
* 虚函数:即被 virtual 修饰的类成员函数。
cpp
// 基类
class Base {
public:
virtual void display() // 虚函数
{
cout << "Display from Base" << endl;
}
};
// 派生类
class Derived : public Base {
public:
void display() override {
cout << "Display from Derived" << endl;
}
};
虚函数重写
在面向对象编程中,虚函数重写(覆盖) 是指派生类重新定义基类中的虚函数**(返回值、函数名、参数列表,均相同)** 。当通过基类指针或引用调用虚函数时,程序会根据实际对象类型调用对应的重写函数,而不是基类中的函数。
具体实现步骤
- 定义基类和虚函数
- 定义派生类并重写虚函数
- 通过基类指针或引用调用虚函数
虚函数重写的例外
💬 观察下面的代码,并没有达到 "三同" 的标准,它的返回值是不同的,但依旧构成多态:
cpp
class A {};
class B : public A {};
class Person {
public:
virtual A* f() {
cout << "virtual A* Person::f()" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* f() {
cout << "virtual B* Student:::f()" << endl;
return nullptr;
};
};
int main(void)
{
Person p;
Student s;
Person* ptr = &p;
ptr->f();
ptr = &s;
ptr->f();
return 0;
}
结果如下:

💡 因为虚函数的重写要求有一个例外 ------ 协变(Covariant)。
但是协变也是有条件的,协变的类型必须是父子关系。
cpp
class A{};
class B{}; //我们取消 A B 的父子关系
理所当然多态不复存在,代码报错
🚩 运行结果:(报错)
error C2555: "Student::f": 重写虚函数返回类型有差异,且不是来自"Person::f"的协 message : 参见"Person::f"的声明
💬 父类的虚函数没了无法构成多态:
cpp
class Person {
public:
A* f() {
cout << "virtual A* Person::f()" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* f() {
cout << "virtual B* Student:::f()" << endl;
return nullptr;
};
};
int main(void)
{
Person p;
Student s;
Person* ptr = &p;
ptr->f();
ptr = &s;
ptr->f();
return 0;
}
结果如下:

💬 但是,子类的虚函数没了却能构成多态:
cpp
class A {};
class B : public A {};
class Person {
public:
virtual A* f() {
cout << "virtual A* Person::f()" << endl;
return nullptr;
}
};
class Student : public Person {
public:
B* f() {
cout << "virtual B* Student:::f()" << endl;
return nullptr;
};
};
int main(void)
{
Person p;
Student s;
Person* ptr = &p;
ptr->f();
ptr = &s;
ptr->f();
return 0;
}
结果如下:

那么这时候就有人问了------这都不是虚函数了怎么还能构成多态!
**💡 解答:**子类虚函数没有写 virtual,但 f 依旧是虚函数,是因为先继承了父类的函数接口声明。
子类继承父类的虚函数是一种接口继承,所以即使子类的 virtual 没写,它也是虚函数,符合多态条件。
这是重写父类虚函数的实现,也就是说父类有 virtual 的属性,子类也就有了。
最后,虽然子类虚函数可以不加 virtual,但是我们自己写的时候 子类虚函数建议加上 virtual。
协变返回类型
协变返回类型 :指的是在 派生类中重写基类虚函数时,返回类型可以是基类返回类型的派生类 。例如,如果是基类的虚函数返回一个基类指针或引用,派生类可以重写这个函数并返回派生类的指针或引用。
首先,我们定义一个基类 Base 和一个从 Base 继承的派生类 Derived。
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual Base* clone() const {
return new Base(*this);
}
virtual void print() const {
cout << "This is Base" << endl;
}
};
class Derived : public Base {
public:
Derived* clone() const override {
return new Derived(*this);
}
void print() const override {
cout << "This is Derived" << endl;
}
};
Base类定义了一个虚函数clone,返回一个Base*类型。Derived类重写了clone函数,但返回类型是Derived*。
使用协变返回类型
在 main 函数中,我们可以使用协变返回类型来创建对象的副本。
cpp
int main() {
Base* b = new Derived();
Base* b_clone = b->clone(); // 调用的是 Derived::clone(),返回 Derived*,但可以赋值给 Base*
b->print(); // 输出 "This is Derived"
b_clone->print(); // 输出 "This is Derived"
delete b;
delete b_clone;
return 0;
}
解释:
基类定义 :基类 Base 中的虚函数 clone 返回一个Base* 类型的对象。
派生类重写 :派生类Derived 重写了 clone 函数,并返回一个Derived* 类型的对象。这是合法的,因为 Derived* 是Base* 的派生类指针。
多态性 :通过基类指针调用clone 函数时,会实际调用Derived 类的 clone 函数,并返回一个 Derived* 。尽管返回的是 Derived* ,但它可以被赋值给**Base***类型的指针,这是C++的类型兼容性特性。
析构函数的重写
cpp
class Person {
public:
~Person() {
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
~Student() {
cout << "~Student()" << endl;
}
};
int main(void)
{
Person p;
Student s;
return 0;
}
**❓ 思考:**这三行分别是谁的?
💡 解读:第一行和第二行是 Student s 的,第三行是Person p 的。我们来看看析构顺序,Student s 是后定义的 ,析构顺序是后定义先析构 。根据子类对象析构先子后父 ,调用子类的析构函数结束后自动调用父类的析构函数 ,所以第一行的 ~Student() 和第二行的 ~Person() 都是 Student 的,随后第三行的 ~Person() 是 Person p 自己调的。
现在这两个析构函数默认是隐藏关系
因为它们的函数名会被同一处理修改成 destructor:

但是如果我用 virtual 修饰 ~Person,我们知道,如果这加了不管 ~Student 加不加 virtual,
子类都会跟着父类变身成 virtual,那么现在这两个析构函数还是隐藏关系吗?
如果 Person 的析构函数加了 virtual,它们的关系就变了:

干脆直接用一个 ptr 去演示好了:
cpp
Person* ptr = new Person;
delete ptr; // ptr->destructor() + operator delete(ptr)
ptr = new Student;
delete ptr; // ptr->destructor() + operator delete(ptr)
刚才我们看到了,如果这里不加 virtual,~Student 是没有调用析构的。
你可能会想这有啥,那是因为这里没场景,这其实是非常致命的,是不经意间会发生的内存泄露。
所以 析构函数为什么要重写的结果就是:
当使用基类指针或引用指向派生类对象 时,如果基类的析构函数不是虚函数,那么在删除对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能会导致资源泄漏,因为派生类的清理工作未被执行。
**🔺 结论:**如果设计的类可能会作为父类,析构函数最好设计成虚函数,即加上 virtual。
------------------------------------------------------------------------------------------------------------------------------
C++11 override和final关键字
override 是C++11引入的一种功能,用来**明确表示派生类中的函数是覆盖基类中的虚函数。**它有以下几个好处:
- 增加代码可读性:明确表明这个函数是用来覆盖基类的虚函数的,方便代码阅读。
- 编译器检查:编译器会检查这个函数是否确实覆盖了基类中的虚函数。如果没有(例如函数签名不匹配),编译器会报错。这可以帮助我们捕捉错误
cpp
class Pet {
public:
virtual void makeSound() {
cout << "Some generic pet sound" << endl;
}
};
class Cat : public Pet {
public:
void makeSound() override {
cout << "Meow" << endl;
}
};
在 Cat 类中,makeSound() 函数后面的 override 关键字告诉编译器这是对基类 Pet 中 makeSound() 函数的覆盖。如果我们不小心拼写错误或参数列表不同,编译器会报错。
如果我有个虚函数,但我不想让它被人重写:
这种情况,就可以将 C++11 的 final 关键字置于函数尾部:
cpp
class Car {
public:
virtual void Drive() final {}
};
class Benz : public Car {
public:
virtual void Drive() { ❌
cout << "Benz-舒适" << endl;
}
};
final 不仅能让虚函数不能被重写,还能让直接把类的继承功能一刀砍了。
将 final 放在类名后面,该类就不能被继承了,因此你不用创建对象他就可以报错给你检查出来。
final 的意思是 "最终的",可以理解为是最终的类了,不能再继承了。
以后如果你想让某个类不能被继承,就可以在类名后面加上 final 关键字。
- 防止类被继承 :将
final关键字放在类声明之后,表示该类不能被继承。 - 防止虚函数被重写 :将 **
final**关键字放在虚函数声明之后,表示该虚函数不能在派生类中被重写。
重载 重写(覆盖) 重定义(隐藏)区别
- 重载(Overloading):同一个作用域内同名函数的参数列表不同,构成重载。
- 覆盖(重写)(Overriding):派生类重新定义基类中的虚函数,函数签名必须相同,构成覆盖。
- 重定义(隐藏)(Hiding):派生类中定义一个与基类中同名但参数列表不同的函数,构成重定义或隐藏。

抽象类
纯虚函数和抽象类
在虚函数的后面写上 =0,则我们称这个函数为 "纯虚函数"。

包含纯虚函数的类,就是抽象类(abstract class),也叫接口类。
cpp
/* 抽象类 */
class Car {
public:
virtual void Drive() = 0; // 纯虚函数
};
抽象类不能实例化出对象,子类即使在继承后也不能实例化出对象:

只有重写纯虚函数,子类才能实例化出对象:
cpp
/* 抽象类 */
class Car {
public:
virtual void Drive() = 0;
};
// 如果父类是抽象类,子类必须重写才能实例化
class BMW : public Car {
public:
virtual void Drive() { // 重写
cout << "BMW-操控性" << endl;
}
};
int main(void)
{
BMW b;
b.Drive();
return 0;
}

**🔺 总结:**抽象类不能实例化出对象,子类即使在继承后也不能实例化出对象,除非子类重写。
抽象类指针
虽然父类是抽象类不能定义对象,但是可以定义指针。
定义指针时如果 new 父类对象因为是纯虚函数,自然是 new 不出来的,但是可以 new 子类对象:
cpp
/* 抽象类 */
class Car {
public:
virtual void Drive() = 0;
};
class Benz : public Car {
public:
virtual void Drive() {
cout << "Benz-舒适" << endl;
}
};
int main(void)
{
Car* pBenz1 = new Benz;
pBenz1->Drive();
Benz* pBenz2 = new Benz;
pBenz2->Drive();
return 0;
}

关于接口继承和实现继承
普通函数的继承是一种实现继承 ,子类继承了父类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承 ,子类继承的是父类虚函数的接口 ,目的是为了重写,
达成多态,继承的是接口 。所以如果不实现多态,不要把函数定义成虚函数。
出现虚函数就是为了提醒你重写的,以实现多态。如果虚函数不重写,那写成虚函数就没价值了。
