当前位置:   article > 正文

C++ 多态

C++ 多态

目录

​编辑

0.前言

1.多态的定义及构成

1.1什么是多态?

1.2构成多态的条件

2.虚函数

2.1什么是虚函数?

2.2虚函数的重写

2.3虚函数重写的特例

2.3.1协变

2.3.2析构函数的重写

2.4 override和final关键字

2.5重载、重写(覆盖)和重定义(隐藏)的对比

2.5.1重载(Overloading)

2.5.2重写(覆盖)(Overriding)

2.5.3重定义(隐藏)(Hiding)

3.抽象类

3.1概念

3.2接口继承和实现继承

3.2.1接口继承

3.2.2实现继承

4.多态原理详析

4.1虚函数表

4.2多态的实现原理

4.3静态绑定和动态绑定

5.结语


(图像由AI生成) 

0.前言

多态是C++语言的三大特性之一,另两个特性是封装和继承。多态性使得对象可以根据运行时的实际类型来表现出不同的行为,从而实现灵活和可扩展的设计。在软件开发过程中,多态能够提高代码的复用性和可维护性,减少重复代码,并提供更加抽象和通用的接口。本文将详细探讨C++中的多态,包括其定义、构成、虚函数、抽象类以及实现原理,帮助读者全面理解这一重要概念。

1.多态的定义及构成

1.1什么是多态?

多态(Polymorphism)是指同一接口在不同场景下可以表现出不同的行为。在面向对象编程中,多态允许程序在不同的上下文中调用相同的接口,从而处理不同的数据类型或对象。具体来说,多态可以分为两类:编译时多态(静态多态)和运行时多态(动态多态)。在C++中,编译时多态通过函数重载和模板实现,而运行时多态则通过虚函数和继承实现。本文主要讨论运行时多态。

1.2构成多态的条件

在C++中,要实现多态,需要满足以下条件:

  1. 继承:必须有一个基类和至少一个派生类。
  2. 虚函数:基类中必须有至少一个函数被声明为虚函数(使用virtual关键字)。
  3. 基类指针或引用:通过基类指针或引用来调用虚函数。

示例代码

  1. #include <iostream>
  2. using namespace std;
  3. // 基类
  4. class Animal {
  5. public:
  6. // 声明虚函数
  7. virtual void makeSound() const {
  8. cout << "Animal makes a sound" << endl;
  9. }
  10. };
  11. // 派生类:Dog
  12. class Dog : public Animal {
  13. public:
  14. // 重写虚函数
  15. void makeSound() const override {
  16. cout << "Dog barks" << endl;
  17. }
  18. };
  19. // 派生类:Cat
  20. class Cat : public Animal {
  21. public:
  22. // 重写虚函数
  23. void makeSound() const override {
  24. cout << "Cat meows" << endl;
  25. }
  26. };
  27. int main() {
  28. // 创建对象
  29. Animal* animal1 = new Dog();
  30. Animal* animal2 = new Cat();
  31. // 通过基类指针调用虚函数
  32. animal1->makeSound(); // 输出:Dog barks
  33. animal2->makeSound(); // 输出:Cat meows
  34. // 释放内存
  35. delete animal1;
  36. delete animal2;
  37. return 0;
  38. }

在上面的代码中,Animal类是基类,DogCat类是派生类。基类中的makeSound函数被声明为虚函数,派生类对该函数进行了重写。在main函数中,通过基类指针调用虚函数,根据实际对象的类型,调用了不同的函数版本,实现了多态。这就是运行时多态的典型实现方式。

2.虚函数

2.1什么是虚函数?

虚函数是使用 virtual 关键字声明的函数,用于实现运行时多态。在基类中声明虚函数时,派生类可以重写该函数。当通过基类指针或引用调用虚函数时,会根据实际对象的类型调用对应的函数版本,而不是基类的版本。虚函数允许派生类提供自己特有的实现,使得代码更加灵活和可扩展。

2.2虚函数的重写

虚函数的重写是指在派生类中重新定义基类中的虚函数。重写虚函数时,派生类的函数签名必须与基类中的虚函数相同。通过这种方式,派生类可以提供其特有的行为。

示例代码

  1. #include <iostream>
  2. using namespace std;
  3. class Base {
  4. public:
  5. virtual void display() const {
  6. cout << "Display from Base class" << endl;
  7. }
  8. };
  9. class Derived : public Base {
  10. public:
  11. void display() const override {
  12. cout << "Display from Derived class" << endl;
  13. }
  14. };
  15. int main() {
  16. Base* basePtr = new Derived();
  17. basePtr->display(); // 输出:Display from Derived class
  18. delete basePtr;
  19. return 0;
  20. }

2.3虚函数重写的特例

2.3.1协变

协变是指在派生类中重写虚函数时,允许返回类型是基类返回类型的派生类。这种特性使得派生类可以返回更具体的对象,而不违反函数重写的规则。

示例代码

  1. #include <iostream>
  2. using namespace std;
  3. class Base {
  4. public:
  5. virtual Base* clone() const {
  6. return new Base(*this);
  7. }
  8. virtual void display() const {
  9. cout << "Base" << endl;
  10. }
  11. };
  12. class Derived : public Base {
  13. public:
  14. Derived* clone() const override { // 协变返回类型
  15. return new Derived(*this);
  16. }
  17. void display() const override {
  18. cout << "Derived" << endl;
  19. }
  20. };
  21. int main() {
  22. Base* basePtr = new Derived();
  23. Base* newBasePtr = basePtr->clone(); // 返回Derived类的对象
  24. newBasePtr->display(); // 输出:Derived
  25. delete basePtr;
  26. delete newBasePtr;
  27. return 0;
  28. }

在这个示例中,Derived类重写了clone函数,并返回类型是Derived*,而不是基类的Base*,这就是协变的应用。

2.3.2析构函数的重写

在使用继承和多态时,基类的析构函数应该声明为虚函数,以确保派生类对象被正确销毁。这是因为如果析构函数不是虚函数,通过基类指针删除派生类对象时,只会调用基类的析构函数,导致资源泄漏。

示例代码

  1. #include <iostream>
  2. using namespace std;
  3. class Base {
  4. public:
  5. virtual ~Base() { // 基类析构函数声明为虚函数
  6. cout << "Base destructor" << endl;
  7. }
  8. };
  9. class Derived : public Base {
  10. public:
  11. ~Derived() override {
  12. cout << "Derived destructor" << endl;
  13. }
  14. };
  15. int main() {
  16. Base* basePtr = new Derived();
  17. delete basePtr; // 正确调用Derived和Base的析构函数
  18. return 0;
  19. }

在这个示例中,基类Base的析构函数被声明为虚函数,因此通过基类指针删除派生类对象时,会正确调用派生类的析构函数,避免资源泄漏。

2.4 override和final关键字

override关键字用于显式声明派生类中的虚函数是重写基类中的虚函数。它有助于编译器进行检查,以确保派生类中的函数确实是在重写基类中的虚函数,而不是定义一个新的函数。这不仅提高了代码的可读性,还减少了因拼写错误或参数不匹配导致的隐藏错误。

  1. #include <iostream>
  2. using namespace std;
  3. class Base {
  4. public:
  5. virtual void display() const {
  6. cout << "Display from Base class" << endl;
  7. }
  8. };
  9. class Derived : public Base {
  10. public:
  11. void display() const override { // 使用override关键字
  12. cout << "Display from Derived class" << endl;
  13. }
  14. };
  15. int main() {
  16. Base* basePtr = new Derived();
  17. basePtr->display(); // 输出:Display from Derived class
  18. delete basePtr;
  19. return 0;
  20. }

在这个示例中,Derived类中的display函数使用了override关键字,明确指出这是对基类中display函数的重写。

final关键字用于防止虚函数在派生类中再次被重写。它可以用于虚函数的声明中,表示该函数不能在进一步派生的类中被重写。另外,final关键字还可以用于类声明,表示该类不能被继承。

  1. #include <iostream>
  2. using namespace std;
  3. class Base {
  4. public:
  5. virtual void display() const {
  6. cout << "Display from Base class" << endl;
  7. }
  8. };
  9. class Derived final : public Base { // 使用final关键字禁止进一步继承
  10. public:
  11. void display() const override {
  12. cout << "Display from Derived class" << endl;
  13. }
  14. };
  15. /*
  16. class FurtherDerived : public Derived { // 错误!Derived类被final修饰,不能被继承
  17. };
  18. */
  19. class AnotherBase {
  20. public:
  21. virtual void show() const final { // 使用final关键字禁止重写
  22. cout << "Show from AnotherBase class" << endl;
  23. }
  24. };
  25. class AnotherDerived : public AnotherBase {
  26. /*
  27. void show() const override { // 错误!show函数被final修饰,不能被重写
  28. cout << "Show from AnotherDerived class" << endl;
  29. }
  30. */
  31. };
  32. int main() {
  33. Base* basePtr = new Derived();
  34. basePtr->display(); // 输出:Display from Derived class
  35. delete basePtr;
  36. return 0;
  37. }

在这个示例中,Derived类被声明为final,表示不能再派生新的类。同时,AnotherBase类中的show函数被声明为final,表示不能在派生类中被重写。

2.5重载、重写(覆盖)和重定义(隐藏)的对比

2.5.1重载(Overloading)

重载是指在同一个作用域中,函数名相同但参数列表不同的多个函数。重载函数可以有不同的参数类型、数量或顺序,但不能仅靠返回类型区分。

  1. #include <iostream>
  2. using namespace std;
  3. class Example {
  4. public:
  5. void func(int x) {
  6. cout << "Function with int parameter: " << x << endl;
  7. }
  8. void func(double x) {
  9. cout << "Function with double parameter: " << x << endl;
  10. }
  11. void func(int x, double y) {
  12. cout << "Function with int and double parameters: " << x << ", " << y << endl;
  13. }
  14. };
  15. int main() {
  16. Example ex;
  17. ex.func(5); // 输出:Function with int parameter: 5
  18. ex.func(5.5); // 输出:Function with double parameter: 5.5
  19. ex.func(5, 5.5); // 输出:Function with int and double parameters: 5, 5.5
  20. return 0;
  21. }

在这个示例中,func函数被重载了三次,分别接受不同的参数列表。

2.5.2重写(覆盖)(Overriding)

重写是指在派生类中重新定义基类中的虚函数。重写函数的签名必须与基类中的虚函数一致。通过重写,派生类可以提供特定的实现。

  1. #include <iostream>
  2. using namespace std;
  3. class Base {
  4. public:
  5. virtual void display() const {
  6. cout << "Display from Base class" << endl;
  7. }
  8. };
  9. class Derived : public Base {
  10. public:
  11. void display() const override { // 重写基类的虚函数
  12. cout << "Display from Derived class" << endl;
  13. }
  14. };
  15. int main() {
  16. Base* basePtr = new Derived();
  17. basePtr->display(); // 输出:Display from Derived class
  18. delete basePtr;
  19. return 0;
  20. }

在这个示例中,Derived类重写了Base类中的虚函数display

2.5.3重定义(隐藏)(Hiding)

重定义(隐藏)是指在派生类中定义了与基类同名但参数列表不同的函数。这种情况下,基类的同名函数在派生类中会被隐藏,但它们并不是重写,因此不会发生多态。

 

  1. #include <iostream>
  2. using namespace std;
  3. class Base {
  4. public:
  5. void show() const {
  6. cout << "Show from Base class" << endl;
  7. }
  8. };
  9. class Derived : public Base {
  10. public:
  11. void show(int x) const { // 重定义基类的show函数
  12. cout << "Show from Derived class with parameter: " << x << endl;
  13. }
  14. };
  15. int main() {
  16. Derived derived;
  17. derived.show(5); // 输出:Show from Derived class with parameter: 5
  18. // derived.show(); // 错误!没有与之匹配的show函数
  19. Base* basePtr = &derived;
  20. basePtr->show(); // 输出:Show from Base class
  21. return 0;
  22. }

在这个示例中,Derived类中的show函数隐藏了Base类中的同名函数。通过派生类对象只能调用重定义后的函数,通过基类指针调用时则调用基类的函数。

3.抽象类

3.1概念

抽象类是不能实例化的类,通常作为其他类的基类使用。它包含至少一个纯虚函数。纯虚函数是没有具体实现的函数,只提供接口规范。纯虚函数使用=0语法声明,表示派生类必须重写该函数。抽象类用于定义一个统一的接口,让派生类去实现具体的行为,从而实现多态。

  1. #include <iostream>
  2. using namespace std;
  3. class AbstractClass {
  4. public:
  5. virtual void pureVirtualFunction() const = 0; // 纯虚函数
  6. void concreteFunction() const {
  7. cout << "Concrete function in AbstractClass" << endl;
  8. }
  9. };
  10. class ConcreteClass : public AbstractClass {
  11. public:
  12. void pureVirtualFunction() const override {
  13. cout << "Implementation of pureVirtualFunction in ConcreteClass" << endl;
  14. }
  15. };
  16. int main() {
  17. // AbstractClass obj; // 错误!不能实例化抽象类
  18. ConcreteClass obj;
  19. obj.pureVirtualFunction(); // 输出:Implementation of pureVirtualFunction in ConcreteClass
  20. obj.concreteFunction(); // 输出:Concrete function in AbstractClass
  21. return 0;
  22. }

在这个示例中,AbstractClass是一个抽象类,包含一个纯虚函数pureVirtualFunctionConcreteClassAbstractClass的派生类,提供了pureVirtualFunction的实现。

3.2接口继承和实现继承

3.2.1接口继承

接口继承是指派生类继承抽象类的函数声明,而不继承其具体实现。派生类必须提供所有纯虚函数的具体实现。接口继承使得派生类可以有不同的实现,但遵循相同的接口规范。

  1. #include <iostream>
  2. using namespace std;
  3. class Interface {
  4. public:
  5. virtual void doSomething() const = 0; // 纯虚函数,接口声明
  6. };
  7. class ImplementationA : public Interface {
  8. public:
  9. void doSomething() const override {
  10. cout << "Implementation A doing something" << endl;
  11. }
  12. };
  13. class ImplementationB : public Interface {
  14. public:
  15. void doSomething() const override {
  16. cout << "Implementation B doing something" << endl;
  17. }
  18. };
  19. int main() {
  20. Interface* a = new ImplementationA();
  21. Interface* b = new ImplementationB();
  22. a->doSomething(); // 输出:Implementation A doing something
  23. b->doSomething(); // 输出:Implementation B doing something
  24. delete a;
  25. delete b;
  26. return 0;
  27. }

在这个示例中,Interface类是一个抽象类,定义了一个纯虚函数doSomethingImplementationAImplementationB类分别提供了该函数的具体实现,实现了接口继承。

3.2.2实现继承

实现继承是指派生类不仅继承基类的接口,还继承其具体实现。基类中的非纯虚函数可以在派生类中直接使用,也可以在派生类中被重写。实现继承使得派生类可以复用基类的代码,减少重复实现。

  1. #include <iostream>
  2. using namespace std;
  3. class BaseClass {
  4. public:
  5. virtual void virtualFunction() const {
  6. cout << "BaseClass implementation of virtualFunction" << endl;
  7. }
  8. void anotherFunction() const {
  9. cout << "BaseClass implementation of anotherFunction" << endl;
  10. }
  11. };
  12. class DerivedClass : public BaseClass {
  13. public:
  14. void virtualFunction() const override {
  15. cout << "DerivedClass override of virtualFunction" << endl;
  16. }
  17. };
  18. int main() {
  19. DerivedClass obj;
  20. obj.virtualFunction(); // 输出:DerivedClass override of virtualFunction
  21. obj.anotherFunction(); // 输出:BaseClass implementation of anotherFunction
  22. return 0;
  23. }

在这个示例中,BaseClass包含一个虚函数virtualFunction和一个普通成员函数anotherFunctionDerivedClass重写了virtualFunction,但直接继承并使用了anotherFunction的实现。这就是实现继承的应用。

4.多态原理详析

4.1虚函数表

虚函数表(Virtual Table,简称vtable)是实现C++多态的核心机制。当一个类包含虚函数时,编译器会为该类生成一个虚函数表。虚函数表是一个指针数组,每个元素指向该类的一个虚函数。每个对象在创建时都会包含一个指向虚函数表的指针(vptr),通过这个指针,程序在运行时能够找到并调用对象实际类型的虚函数。

  1. #include <iostream>
  2. using namespace std;
  3. class Base {
  4. public:
  5. virtual void func1() { cout << "Base func1" << endl; }
  6. virtual void func2() { cout << "Base func2" << endl; }
  7. };
  8. class Derived : public Base {
  9. public:
  10. void func1() override { cout << "Derived func1" << endl; }
  11. void func2() override { cout << "Derived func2" << endl; }
  12. };
  13. int main() {
  14. Base* basePtr = new Derived();
  15. basePtr->func1(); // 输出:Derived func1
  16. basePtr->func2(); // 输出:Derived func2
  17. delete basePtr;
  18. return 0;
  19. }

在这个示例中,Base类和Derived类都有两个虚函数func1func2Base类的虚函数表会指向其虚函数实现,而Derived类的虚函数表会指向其重写的虚函数实现。通过基类指针调用虚函数时,会通过vptr找到实际对象的虚函数表,从而调用正确的函数版本。

4.2多态的实现原理

多态的实现原理依赖于虚函数表和虚函数表指针。以下是多态实现的几个关键步骤:

  1. 创建对象:当创建一个包含虚函数的类的对象时,编译器会为该对象分配内存,并初始化其vptr,使其指向该类的虚函数表。
  2. 调用虚函数:通过基类指针或引用调用虚函数时,程序会通过vptr找到实际对象的虚函数表,然后根据函数的偏移量找到正确的函数地址并进行调用。
  3. 运行时决策:由于vptr在运行时指向实际对象的虚函数表,程序可以在运行时根据对象的实际类型调用相应的函数,实现运行时多态。
  1. #include <iostream>
  2. using namespace std;
  3. class Animal {
  4. public:
  5. virtual void speak() { cout << "Animal speaks" << endl; }
  6. };
  7. class Dog : public Animal {
  8. public:
  9. void speak() override { cout << "Dog barks" << endl; }
  10. };
  11. class Cat : public Animal {
  12. public:
  13. void speak() override { cout << "Cat meows" << endl; }
  14. };
  15. void makeAnimalSpeak(Animal& animal) {
  16. animal.speak();
  17. }
  18. int main() {
  19. Dog dog;
  20. Cat cat;
  21. makeAnimalSpeak(dog); // 输出:Dog barks
  22. makeAnimalSpeak(cat); // 输出:Cat meows
  23. return 0;
  24. }

 在这个示例中,makeAnimalSpeak函数接受一个Animal类的引用参数,通过该引用调用虚函数speak。实际调用的是DogCat类重写的speak函数,从而实现多态。

4.3静态绑定和动态绑定

  • 静态绑定(Static Binding):静态绑定在编译时进行决策,函数调用在编译时被解析。普通成员函数和非虚函数使用静态绑定。静态绑定的优点是速度快,因为在编译时已经确定了调用地址。

  • 动态绑定(Dynamic Binding):动态绑定在运行时进行决策,函数调用在运行时通过虚函数表解析。虚函数使用动态绑定。动态绑定的优点是灵活,可以在运行时根据对象的实际类型调用相应的函数版本。

静态绑定示例代码

  1. #include <iostream>
  2. using namespace std;
  3. class Base {
  4. public:
  5. void staticFunc() { cout << "Base staticFunc" << endl; }
  6. };
  7. class Derived : public Base {
  8. public:
  9. void staticFunc() { cout << "Derived staticFunc" << endl; }
  10. };
  11. int main() {
  12. Base base;
  13. Derived derived;
  14. Base* basePtr = &derived;
  15. base.staticFunc(); // 输出:Base staticFunc
  16. derived.staticFunc(); // 输出:Derived staticFunc
  17. basePtr->staticFunc(); // 输出:Base staticFunc
  18. return 0;
  19. }

在这个示例中,staticFunc是普通成员函数,不是虚函数,因此使用静态绑定。即使通过基类指针调用staticFunc,也调用的是基类版本。

动态绑定示例代码

  1. #include <iostream>
  2. using namespace std;
  3. class Base {
  4. public:
  5. virtual void dynamicFunc() { cout << "Base dynamicFunc" << endl; }
  6. };
  7. class Derived : public Base {
  8. public:
  9. void dynamicFunc() override { cout << "Derived dynamicFunc" << endl; }
  10. };
  11. int main() {
  12. Base base;
  13. Derived derived;
  14. Base* basePtr = &derived;
  15. base.dynamicFunc(); // 输出:Base dynamicFunc
  16. derived.dynamicFunc(); // 输出:Derived dynamicFunc
  17. basePtr->dynamicFunc(); // 输出:Derived dynamicFunc
  18. return 0;
  19. }

在这个示例中,dynamicFunc是虚函数,因此使用动态绑定。通过基类指针调用dynamicFunc时,会根据实际对象的类型调用Derived类的版本。

5.结语

通过深入探讨C++中的多态特性及其实现原理,我们可以理解虚函数、虚函数表以及静态绑定和动态绑定的机制。多态作为C++的三大特性之一,不仅提升了代码的灵活性和可扩展性,还提高了程序设计的抽象能力。掌握多态的概念和应用,对于编写高质量的面向对象程序至关重要。希望本文能够帮助读者更好地理解和运用C++中的多态特性。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/812596
推荐阅读
相关标签
  

闽ICP备14008679号