目录
一、为什么需要动态多态?
二、虚函数与 virtual 关键字
1. 什么是虚函数?
2. 使用 override 检查重写
三、函数重载、重写与隐藏
1. 函数重载
2. 函数重写
3. 函数隐藏
4. 三者的区别
四、基类指针与基类引用
1. 基类指针指向派生类对象
2. 基类引用绑定派生类对象
3. 基类指针指向不同的对象
五、静态绑定与动态绑定
1. 静态绑定
2. 动态绑定
3. 直接通过派生类对象调用
六、成员函数末尾的 const 与重写
1. const 的作用
2. 派生类必须匹配 const
3. 如果不写 override 呢?
4. 返回类型中的 const 不一样
七、协变返回类型
1. 什么是协变返回类型?
2. 协变返回类型的常见规则
3. 协变返回类型的意义
八、动态多态的实际应用
九、综合示例与常见错误
1. 虚函数与普通成员函数的区别
2. 常见易错点总结
十、本篇总结
一、为什么需要动态多态?
在学习 C++ 继承时,我们知道派生类可以继承基类的成员,也可以重新定义基类中的同名函数。但是,当我们使用基类指针指向派生类对象时,调用函数的结果可能和直接使用派生类对象不同。
先看下面的代码:
#include <iostream> using namespace std; class Animal { public: void speak() { cout << "Animal" << endl; } }; class Dog : public Animal { public: void speak() { cout << "Dog" << endl; } }; int main() { Dog d; Animal* p = &d; d.speak(); p->speak(); return 0; }运行结果:
Dog Animal为什么两次调用的结果不同?
d.speak():通过Dog对象直接调用,因此执行Dog::speak()。p->speak():虽然p指向的是Dog对象,但p的静态类型是Animal*,而speak()不是虚函数,所以调用Animal::speak()。体现了非虚函数的静态绑定(直接原因)
输出animal,也体现了函数隐藏这个知识点。
如果我们希望通过基类指针调用函数时,能够根据实际指向的对象类型选择对应的函数,就需要用到虚函数和动态绑定。
二、虚函数与 virtual 关键字
1. 什么是虚函数?
在 C++ 中,可以使用virtual关键字将基类中的成员函数声明为虚函数。
虚函数的一个重要作用是:当通过基类指针或基类引用调用该函数时,如果派生类正确重写了这个虚函数,程序就可以根据对象的实际类型调用相应的函数实现。
例如:
#include <iostream> using namespace std; class Animal { public: virtual void speak() { cout << "Animal" << endl; } }; class Dog : public Animal { public: void speak() { cout << "Dog" << endl; } }; int main() { Dog d; Animal* p = &d; d.speak(); p->speak(); return 0; }运行结果:
Dog Dog与前面的代码相比,我们只在基类的speak()前面增加了virtual,第二次调用的结果就发生了变化。
这是因为:
基类的
speak()是虚函数。派生类的
speak()正确重写了基类的虚函数。p实际指向一个Dog对象。通过基类指针调用虚函数时,程序根据对象的实际类型选择函数实现。
需要注意的是,派生类重写基类虚函数时,不需要再次写virtual,重写后的函数仍然是虚函数。不过,为了让编译器检查重写是否正确,建议使用override。
2. 使用 override 检查重写
class Animal { public: virtual void speak() { cout << "Animal" << endl; } }; class Dog : public Animal { public: void speak() override { cout << "Dog" << endl; } };override的作用是告诉编译器:我希望这个函数重写基类中的虚函数,请检查是否真的重写成功。
例如,如果把派生类函数改成:
void speak(int x) override { cout << "Dog" << endl; }由于参数列表发生变化,它没有重写基类的speak(),因此编译器会报错。
易错点:override不会让函数自动变成重写函数,它只是检查重写是否成功。
如果去掉override的话,相当于是函数隐藏。
三、函数重载、重写与隐藏
这三个概念容易混淆,需要分别理解。
1. 函数重载
函数重载是指在同一作用域内,函数名称相同,但参数列表不同。
class Animal { public: void speak() { cout << "Animal" << endl; } void speak(int x) { cout << "Animal " << x << endl; } };这里的两个speak()构成函数重载。
调用时,编译器根据传入的参数选择对应的函数:
Animal a; a.speak(); a.speak(10);2. 函数重写
函数重写发生在继承关系中。派生类重新实现基类的虚函数,并满足虚函数重写的要求。
class Animal { public: virtual void speak() { cout << "Animal" << endl; } }; class Dog : public Animal { public: void speak() override { cout << "Dog" << endl; } };这里Dog::speak()重写了Animal::speak()。
3. 函数隐藏
发生在基类与派生类之间
当派生类声明了与基类同名的成员函数时,派生类中的同名函数会影响基类同名函数的名字查找。
例如:
class Animal { public: void speak() { cout << "Animal" << endl; } }; class Dog : public Animal { public: void speak(int x) { cout << "Dog" << endl; } };此时:
Dog d; // d.speak(); // 编译错误:基类同名函数被隐藏 d.speak(10); // 调用 Dog::speak(int)虽然基类中存在speak(),但是派生类声明了同名函数speak(int),普通的名字查找不会自动把基类的同名函数重载集合合并进来。
如果希望基类的同名函数也参与名字查找,可以使用using:
class Dog : public Animal { public: using Animal::speak; void speak(int x) { cout << "Dog" << endl; } };这样:
Dog d; d.speak(); // 调用 Animal::speak() d.speak(10); // 调用 Dog::speak(int)需要注意,using Animal::speak;只是让基类同名函数重新参与名字查找,不会让speak(int)重写基类的speak(),也不会因此产生动态多态。
4. 三者的区别
| 对比项 | 函数重载 | 函数重写 | 函数隐藏 |
|---|---|---|---|
| 是否需要继承 | 不一定 | 需要 | 通常发生在继承关系中 |
| 函数名称 | 相同 | 相同 | 相同 |
| 参数列表 | 必须有区别 | 必须符合重写规则 | 可以不同 |
| 是否要求基类函数为虚函数 | 不要求 | 要求 | 不要求 |
| 主要作用 | 根据参数选择函数 | 实现动态多态 | 影响名字查找 |
四、基类指针与基类引用
1. 基类指针指向派生类对象
在继承关系中,基类指针可以指向派生类对象。
Dog d; Animal* p = &d;当基类中存在虚函数,并且派生类正确重写了该函数时,通过p调用虚函数,就可以执行派生类的实现。
p->speak();输出:
Dog2. 基类引用绑定派生类对象
除了基类指针,基类引用也可以绑定派生类对象。
Dog d; Animal& r = d; r.speak();如果speak()是虚函数,并且派生类正确重写了它,那么这次调用同样会执行Dog::speak()。
因此,基类指针和基类引用都可以用于实现动态多态。
3. 基类指针指向不同的对象
class Animal { public: virtual void speak() { cout << "Animal" << endl; } }; class Dog : public Animal { public: void speak() override { cout << "Dog" << endl; } }; class Cat : public Animal { public: void speak() override { cout << "Cat" << endl; } }; int main() { Dog d; Cat c; Animal a; Animal* p = &d; p->speak(); p = &c; p->speak(); p = &a; p->speak(); return 0; }运行结果:
Dog Cat Animal虽然指针p的类型始终是Animal*,但它可以指向不同类型的对象。每次通过它调用虚函数时,程序都会根据当前指向对象的实际类型选择对应的函数实现。
这说明:动态绑定关注的是对象的实际类型,而不只是指针的静态类型。
五、静态绑定与动态绑定
1. 静态绑定
对于普通非虚成员函数,通过基类指针调用时,调用哪个函数主要由指针的静态类型决定。
class Animal { public: void speak() { cout << "Animal" << endl; } }; class Dog : public Animal { public: void speak() { cout << "Dog" << endl; } }; int main() { Dog d; Animal* p = &d; p->speak(); return 0; }输出:
Animal因为speak()不是虚函数,调用p->speak()时使用的是Animal::speak()。
2. 动态绑定
将基类函数声明为虚函数,并让派生类正确重写:
class Animal { public: virtual void speak() { cout << "Animal" << endl; } }; class Dog : public Animal { public: void speak() override { cout << "Dog" << endl; } };此时:
Dog d; Animal* p = &d; p->speak();输出:
Dog通过基类指针调用虚函数时,程序会根据实际对象类型选择函数实现,这就是动态绑定。
3.直接通过派生类对象调用
Dog d; d.speak();无论speak()是否为虚函数,这里都会调用Dog::speak(),因为调用表达式直接使用Dog对象。
因此,不能简单地认为“只有虚函数才能调用派生类函数”。虚函数的关键作用,是支持通过基类指针或引用进行动态分派。
六、成员函数末尾的 const 与重写
1. const 的作用
成员函数末尾的const表示该成员函数不会通过当前对象修改普通的非mutable数据成员,并且可以通过常量对象调用。
例如:
class Animal { public: virtual void speak() const { cout << "Animal" << endl; } };这里的const是成员函数声明的一部分,会影响虚函数重写是否匹配。
2. 派生类必须匹配 const
下面的代码会导致override检查失败:
class Animal { public: virtual void speak() const { cout << "Animal" << endl; } }; class Dog : public Animal { public: void speak() override { cout << "Dog" << endl; } };原因是基类声明的是speak() const,而派生类声明的是speak(),两者不匹配。
即使派生类函数体中没有修改任何成员变量,也不能因此视为正确重写。函数是否修改数据,与函数声明中是否带有const,是两个不同的问题。
正确写法是:
class Dog : public Animal { public: void speak() const override { cout << "Dog" << endl; } };3. 如果不写 override 呢?
如果派生类仍然声明:
void speak() { cout << "Dog" << endl; }但没有写override,代码通常可以编译。不过,派生类的speak()并没有重写基类的speak() const。此时又相当于是函数的隐藏
Dog d; Animal* p = &d; d.speak(); // Dog p->speak(); // Animal输出:
Dog Animal因此,建议重写虚函数时尽量使用override,让编译器及时发现声明不匹配的问题。
4. 返回类型中的 const 不一样
下面的声明:
virtual const char* speak();其中const修饰的是字符,表示不能通过返回的指针修改相应字符。
它与下面的声明不同:
virtual void speak() const;后者的const修饰成员函数本身的对象限定,会影响重写匹配。
这两种const不要混淆。
七、协变返回类型
1. 什么是协变返回类型?
通常情况下,派生类重写虚函数时,返回类型需要满足相应的重写规则。
C++ 还允许一种特殊情况:基类虚函数返回某个类的指针或引用,派生类重写时,可以返回其派生类的指针或引用。这种机制称为协变返回类型。
例如:
class Animal { public: virtual Animal* clone() { return this; } }; class Dog : public Animal { public: Dog* clone() override { return this; } };基类返回Animal*,派生类返回Dog*。由于Dog继承自Animal,这里满足协变返回类型的要求。
2. 协变返回类型的常见规则
| 基类返回类型 | 派生类返回类型 | 是否允许 |
|---|---|---|
Animal* | Animal* | 允许 |
Animal* | Dog* | 满足条件时允许 |
Animal& | Dog& | 满足条件时允许 |
Animal* | Dog& | 不允许 |
Animal* | int | 不允许 |
Animal* | 无关类的Cat* | 不允许 |
这里的协变只适用于符合要求的类指针或类引用,并不是只要返回类型可以转换,就一定能够重写成功。
3. 协变返回类型的意义
协变返回类型可以让派生类在重写函数时返回更具体的类型。
例如,通过Dog对象直接调用Dog::clone(),返回值类型可以是Dog*,这样调用者就能够直接获得更具体的类型信息,而不必立即将结果转换回Animal*。
八、动态多态的实际应用
前面介绍了很多语法规则,下面来看动态多态在实际程序中的用途。
假设我们需要处理不同动物的叫声:
#include <iostream> using namespace std; class Animal { public: virtual void speak() { cout << "Animal" << endl; } }; class Dog : public Animal { public: void speak() override { cout << "汪汪" << endl; } }; class Cat : public Animal { public: void speak() override { cout << "喵喵" << endl; } }; void makeSound(Animal* p) { p->speak(); } int main() { Dog d; Cat c; makeSound(&d); makeSound(&c); return 0; }输出:
汪汪 喵喵这里的makeSound()只需要接收Animal*,不需要专门为狗和猫分别编写两个函数。
如果以后新增一个Bird类,只要它继承Animal并正确重写speak(),原来的makeSound()就不需要修改。
这体现了动态多态的优势:
统一接口:通过基类指针或引用调用不同派生类的实现。
减少重复代码:相同的调用逻辑不需要为每种派生类重复编写。
方便扩展:新增派生类时,可以复用已有的通用处理函数。
需要注意的是,使用动态多态时,基类指针或引用必须指向有效对象;如果传入空指针,直接调用成员函数会导致未定义行为。
九、综合示例与常见错误
1. 虚函数与普通成员函数的区别
#include <iostream> using namespace std; class Animal { public: virtual void speak() const { cout << "Animal" << endl; } void eat() { cout << "Animal eat" << endl; } }; class Dog : public Animal { public: void speak() const override { cout << "Dog" << endl; } void eat() { cout << "Dog eat" << endl; } }; int main() { Dog d; Animal* p = &d; p->speak(); p->eat(); d.eat(); return 0; }输出:
Dog Animal eat Dog eat分析:
p->speak():speak()是虚函数,Dog正确重写了它,因此调用Dog::speak()。p->eat():eat()是普通成员函数,通过Animal*调用时执行Animal::eat()。d.eat():直接通过Dog对象调用,因此执行Dog::eat()。
这道题体现了虚函数与普通成员函数在调用机制上的区别。
2. 常见易错点总结
错误一:认为派生类重写函数必须再次写 virtual。
实际上,基类声明为虚函数后,派生类正确重写时不需要重复写virtual。建议使用override。
错误二:认为函数体没有修改成员变量,就可以省略 const。
是否修改成员变量与成员函数声明中的const是两个不同的问题。重写时必须匹配相应的对象限定。
错误三:认为 override 会让函数自动重写成功。
override只负责检查。如果函数名、参数或其他重写条件不匹配,编译器会报错。
错误四:认为基类指针指向派生类对象,就一定调用派生类函数。
只有通过基类指针或引用调用符合条件的虚函数,才能根据对象实际类型进行动态分派。普通非虚函数不会因此自动进行动态绑定。
错误五:混淆重载、重写和隐藏。
三者虽然都可能涉及同名函数,但发生条件和作用不同。尤其要注意:派生类声明同名函数后,基类同名函数可能被隐藏。
错误六:认为协变返回类型允许任意不同的返回类型。
协变只允许符合要求的类指针或类引用之间的特定变化,不能把返回类型随意改成int、void*或无关类指针。
十、本篇总结
本篇主要学习了 C++ 动态多态的基础知识:
使用
virtual声明基类虚函数。使用
override检查派生类是否正确重写虚函数。理解函数重载、重写和隐藏的区别。
理解基类指针与基类引用如何参与动态多态。
区分静态绑定和动态绑定。
理解成员函数末尾的
const对重写的影响。了解协变返回类型的基本规则。
理解动态多态如何统一接口、减少重复代码并方便扩展。