• C++ 多态和虚函数详解


    本文章内容来源于C++课堂上的听课笔记

    多态基础

    多态(Polymorphism)是面向对象编程中的一个重要概念,它允许使用统一的接口来表示不同的对象和操作。多态性有两种主要形式:静态多态性(编译时多态性)和动态多态性(运行时多态性)

    多态性分为两类: 静态多态性和动态多态性

    静态多态性(编译时多态性):
    定义: 在编译时确定方法的调用,通常与函数重载(overloading)相关。
    例子: 方法重载是一种静态多态性的体现,编译器在编译时能够根据方法的参数类型或个数来选择正确的方法

    1. class StaticPolymorphism {
    2. public:
    3. void display(int value) {
    4. // ...
    5. }
    6. void display(double value) {
    7. // ...
    8. }
    9. };

    注意:重载(overload)和重写(override)的区别,下面是个重写的例子:

    1. #include
    2. using namespace std;
    3. class CA
    4. { public:
    5. void f(int)
    6. { cout << "CA::f(int)"<< endl; }};
    7. class CB : public CA
    8. { public:
    9. void f(int) {cout << "CB::f(int) "
    10. << endl;}
    11. void f(int,int) {cout << "CB::f(int,int)" << endl; }
    12. int f(int,int,int) {
    13. cout << "CB::f(int,int,int)"
    14. << endl; }
    15. void test() {
    16. f(1); f(1,1); f(1,1,1); }};
    17. int main()
    18. { CB B;
    19. B.test();}

    如果把上面的void f(int) {cout << "CB::f(int) "<< endl;} 注释掉,会发生什么错误?

    原因:在调用一个类的成员函数时,编译器会沿着类的继承链逐级的向上查找函数的定义,如果找到了则停止查找 如果派生类CB和基类CA都有同一个同名函数f(不论参数是否相同),编译器最终将选择派生类中的函数,即派生类的成员函数“隐藏”了基类的成员函数, 它阻止了编译器继续向上查找函数的定义

    所以如果修改成下面这样就可以运行了:

    1. #include
    2. using namespace std;
    3. class CA
    4. { public:
    5. void f(int)
    6. { cout << "CA::f(int)"<< endl; }};
    7. class CB : public CA
    8. { public:
    9. void test() {
    10. f(1); }};
    11. int main()
    12. { CB B;
    13. B.test();}

    动态多态性(运行时多态性):
    定义: 在运行时确定方法的调用,通常与虚函数(virtual function)和继承相关。
    例子: 通过虚函数和基类指针实现动态多态性,可以在运行时选择调用合适的函数。 

    1. #include
    2. using namespace std;
    3. class Base {
    4. public:
    5. virtual void display() {
    6. // ...
    7. cout<<1<
    8. }
    9. };
    10. class Derived : public Base {
    11. public:
    12. void display() override{
    13. // ...
    14. cout<<2<
    15. }
    16. };
    17. int main() {
    18. Base* ptr = new Derived();
    19. ptr->display(); // 在运行时调用 Derived 类的 display 方法
    20. delete ptr;
    21. return 0;
    22. }

    静态多态性和动态多态性的区别:
    时机不同: 静态多态性在编译时确定,动态多态性在运行时确定。

    实现机制不同: 静态多态性通常与函数重载等相关,而动态多态性通常与虚函数和继承相关。

    使用场景不同: 静态多态性适用于编译时能够确定的情况,而动态多态性适用于在运行时确定的情况。

    总的来说,多态性是面向对象编程的一个强大特性,它允许代码更加灵活、可扩展和易维护。

    虚函数

    在面向对象编程中,我们有时候会有一系列的类,它们可能会有一些相同的函数名,但是具体的实现可能会因为类的不同而有所不同。虚函数就是为了解决这个问题而设计的。

    想象一下,你有一群动物,比如猫、狗、鸟等,它们都能发出声音。你可能会定义一个名为 makeSound 的函数来表示这个动作。但是,猫“喵喵”叫,狗“汪汪”叫,鸟“啾啾”叫,它们的叫声不同。这时候,你就可以使用虚函数了

    1. #include
    2. using namespace std;
    3. class Animal {
    4. public:
    5. virtual void makeSound() {
    6. // 这里可以是一个默认的实现,也可以是空的
    7. }
    8. };
    9. class Cat : public Animal {
    10. public:
    11. void makeSound() override {
    12. std::cout << "喵喵" << std::endl;
    13. }
    14. };
    15. class Dog : public Animal {
    16. public:
    17. void makeSound() override {
    18. std::cout << "汪汪" << std::endl;
    19. }
    20. };
    21. class Bird : public Animal {
    22. public:
    23. void makeSound() override {
    24. std::cout << "啾啾" << std::endl;
    25. }
    26. };
    27. int main()
    28. {
    29. Animal* myPet = new Dog();
    30. myPet->makeSound(); // 输出:汪汪
    31. Cat cat;
    32. Bird bird;
    33. myPet = &cat;
    34. myPet->makeSound();
    35. myPet = &bird;
    36. myPet->makeSound();
    37. return 0;
    38. }

     如果基类的函数不加关键字virtual会发生什么?

    1.所有派生类的相关函数不能添加override关键字(以为实际上没有重写)

    2.函数最终调用的是基类的函数,但可能使用对应派生类对象的数据

    举个例子展示不用virtual时的情况

    1. #include
    2. #include
    3. using namespace std;
    4. class Student
    5. {public:
    6. Student(int, string,float);
    7. void display( );
    8. protected:
    9. int num;
    10. string name;
    11. float score;
    12. };
    13. Student::Student(int n, string nam,float s)
    14. {num=n;name=nam;score=s;}
    15. void Student::display( )
    16. {cout<<"num:"<"\nname:"<<
    17. name<<"\nscore:"<"\n\n";}
    18. class Graduate: public Student
    19. {public:
    20. Graduate(int, string, float, float);
    21. void display( );
    22. private:
    23. float pay;
    24. };
    25. void Graduate::display( ) {cout<<"num:"<"\nname:"<"\nscore:"<"\npay="<
    26. Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){ }
    27. int main()
    28. {
    29. Student stud1(1001,"Li",87.5);
    30. Graduate grad1(2001,"Wang",98.5,563.5);
    31. Student *pt=&stud1;
    32. pt->display( );
    33. pt=&grad1;
    34. pt->display( );
    35. return 0;
    36. }

    如果修改成虚函数,即Student类中函数声明变为:

    virtual void display( );  

    运行结果

    在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。派生类重新声明该虚函数时,可以加virtual,也可以不加

    下面我们通过修改上述代码来证明这一点 

    添加类Test

    1. class Test: public Graduate
    2. {
    3. public:
    4. Test(int ,string,float,float,int);
    5. void display();
    6. private:
    7. int others;
    8. };
    9. void Test::display( ) {cout<<"\n\nnum:"<"\nname:"<"\nscore:"<"\nothers="<
    10. Test::Test(int n, string nam,float s,float p,int o):Graduate(n,nam,s,p),others(o){ }

    main中添加:

    1. Test t(9999,"s",11,22,33);
    2. pt=&t;
    3. pt->display();

    结果:

    说明Graduate派生类Test中的同名函数display已经成为虚函数,如果不是虚函数,应该输出下面的结果:
    num:1001
    name:Li
    score:87.5

    num:2001
    name:Wang
    score:98.5
    pay=563.5


    num:9999
    name:s
    score:11
    pay=22

    静态关联和动态关联

    在C++中,静态关联和动态关联是面向对象编程中两个关键的概念,通常与继承和多态性相关

    静态关联(Static Binding):
    概念: 静态关联发生在编译时,编译器在编译阶段就能够确定程序中各个函数或方法的调用关系。
    实现: 在静态关联中,编译器根据函数或方法的声明类型来确定调用哪个函数或方法,这种绑定在编译时期就已经确定,因此称为静态关联。
    优点: 效率高,因为在编译时已经确定了函数调用关系,不需要在运行时进行额外的查找。
    例子: C++中的函数重载是一种静态关联的例子,编译器在编译时根据参数的类型和数量确定调用哪个重载版本。

    动态关联(Dynamic Binding):
    概念: 动态关联发生在运行时,程序在执行过程中才能够确定调用哪个函数或方法。
    实现: 在动态关联中,通常通过使用虚函数和指针(或引用)来实现。这种绑定在运行时根据实际对象的类型确定,因此称为动态关联。
    优点: 提供了更高的灵活性和可扩展性,允许在运行时根据实际情况改变调用关系。
    例子: C++中的虚函数和纯虚函数是动态关联的例子。当基类指针或引用指向派生类对象,并调用虚函数时,根据实际对象的类型来确定调用哪个版本的函数

    什么情况下适合声明虚函数?
    1.基类预期被继承: 如果你设计一个基类,并且希望它能够作为其他类的基础,支持多态性,那么你应该在基类中声明虚函数。这样,派生类就有机会覆盖这些虚函数,实现自己的版本
    2.需要运行时动态绑定: 如果你希望在运行时根据对象的实际类型来调用函数,而不是根据指针或引用的静态类型,那么你应该使用虚函数。这种动态绑定提供了多态性的特性
    3.需要覆盖基类函数: 如果你希望派生类能够覆盖基类中的同名函数,以提供特定于派生类的实现,那么这个函数应该声明为虚函数
    4.实现抽象类和接口: 如果你想要创建一个抽象类(包含至少一个纯虚函数)或者接口,那么你应该声明虚函数。这样的类不能被实例化,但可以作为基类供其他类继承并实现虚函数

    虚析构函数

    在C++中,析构函数是用来在对象生命周期结束时进行清理工作的函数。而当我们在面向对象的程序设计中使用继承时,有时候我们会遇到这样的情况:基类指针指向派生类对象。

    如果你使用了继承,并且在基类和派生类中都定义了析构函数,那么就有可能发生一个问题。当你使用基类指针指向派生类对象时,如果你删除这个指针,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致一些资源(比如内存)没有得到正确释放,从而产生问题。

    这时候,虚析构函数就发挥作用了。在基类的析构函数前面加上virtual关键字,就可以将它声明为虚析构函数。这样,当你通过基类指针删除派生类对象时,会正确地调用派生类的析构函数。

    1. #include
    2. using namespace std;
    3. class Base {
    4. public:
    5. virtual ~Base() {
    6. // 基类的清理工作
    7. cout<<"Base Destruction"<
    8. }
    9. };
    10. class Derived : public Base {
    11. public:
    12. ~Derived() {
    13. // 派生类的清理工作
    14. cout<<"Derived Destruction"<
    15. }
    16. };
    17. int main()
    18. {
    19. Base *pt=new Derived;
    20. delete pt;
    21. return 0;
    22. }

    这里,~Base()是虚析构函数,而~Derived()覆盖了基类的虚析构函数。当你使用基类指针指向派生类对象时,通过这个基类指针删除对象时,会正确地调用~Derived()来完成清理工作,确保所有相关资源都被正确释放。

    总的来说,虚析构函数是为了在继承层次结构中正确地释放资源而引入的。通过使用虚析构函数,可以确保在删除基类指针时正确调用派生类的析构函数,从而实现正确的资源清理。

    纯虚函数和抽象类

    抽象类:
    首先,想象一下你要画一只动物的图,但是你不知道具体是什么动物,只知道它是动物。你可能会画一些共性的特征,比如四条腿、有尾巴等。这个画得很模糊的图就好比一个抽象类。
    在编程中,抽象类也是一种类,但是它是一种不完整的类,不能被实例化(也就是不能创建对象)。抽象类里面可能包含了一些方法(函数),但是这些方法没有具体的实现,只是一个声明。抽象类的存在是为了给其他类提供一种共同的接口,让这些类去继承它并实现这些方法。
    纯虚函数:
    再想象一下,你画的那只动物,有一些特征是必须由具体的动物来定义的,比如各种动物的叫声。这时,你可以把叫声这个特征标记为一个“待定”项,告诉其他人,这个特征必须由实际的动物类来具体实现。
    在编程中,这个“待定”项就是纯虚函数。一个纯虚函数是在抽象类中声明的虚函数,但是没有具体的实现。它的目的是让派生类强制实现这个方法,以使抽象类变得更加具体。
    综合起来,抽象类是一种不完整的类,包含一些没有具体实现的方法,其中可能包含了纯虚函数。而纯虚函数是为了强制派生类实现某些方法,使得抽象类可以更具体、更有实际意义。在C++中,含有纯虚函数的类就被称为抽象类

    注意!
    1.凡是包含纯虚函数的类都是抽象类,包含纯虚函数的类是无法建立对象的
    2.如果在抽象类所派生出的新类中对基类的所有纯虚函数进行了定义,那么这些函数就被赋予了功能,可以被调用。这个派生类就不是抽象类,而是可以用来定义对象的具体类。如果在派生类中没有对所有纯虚函数进行定义,则此派生类仍然是抽象类,不能用来定义对象
    3.虽然抽象类不能定义对象(或者说抽象类不能实例化),但是可以定义指向抽象类数据的指针变量
    下面举一个综合性例子体现上面所说的一切:

    1. #include
    2. // 抽象类 Animal
    3. class Animal {
    4. public:
    5. // 纯虚函数,表示动物的叫声
    6. virtual void makeSound() const = 0;
    7. // 普通方法,表示动物的一般特征
    8. void sleep() const {
    9. std::cout << "Zzz..." << std::endl;
    10. }
    11. };
    12. // 具体的动物类 Lion(狮子)
    13. class Lion : public Animal {
    14. public:
    15. // 实现了纯虚函数,给出了狮子的叫声
    16. void makeSound() const override {
    17. std::cout << "Roar!" << std::endl;
    18. }
    19. };
    20. // 具体的动物类 Elephant(大象)
    21. class Elephant : public Animal {
    22. public:
    23. // 实现了纯虚函数,给出了大象的叫声
    24. void makeSound() const override {
    25. std::cout << "Trumpet!" << std::endl;
    26. }
    27. };
    28. // 具体的动物类 Monkey(猴子)
    29. class Monkey : public Animal {
    30. public:
    31. // 猴子并没有实现纯虚函数 makeSound
    32. // 因此 Monkey 仍然是抽象类
    33. };
    34. int main() {
    35. // 1. 尝试创建抽象类的对象,编译错误
    36. // Animal animal; // 编译错误,抽象类不能实例化
    37. // 2. 使用抽象类指针,指向派生类对象
    38. Animal* lion = new Lion();
    39. Animal* elephant = new Elephant();
    40. // Animal* monkey = new Monkey(); // 编译错误,抽象类不能实例化
    41. // 调用纯虚函数,实际上会调用相应派生类的实现
    42. lion->makeSound(); // 输出:Roar!
    43. elephant->makeSound(); // 输出:Trumpet!
    44. // 调用抽象类的普通方法
    45. lion->sleep();
    46. elephant->sleep();
    47. // 3. 定义指向抽象类的指针变量
    48. Animal* abstractAnimal = nullptr; // 可以定义指针变量
    49. // 4. 尝试使用派生类 Monkey,编译错误
    50. // Monkey monkeyObj; // 编译错误,抽象类不能实例化
    51. delete lion;
    52. delete elephant;
    53. return 0;
    54. }

  • 相关阅读:
    【Python】python使用docxtpl生成word模板
    计算机网络——物理层(​ 物理层下面的传输媒体)
    PTE-写作 学习(一)
    Cloud-Sleuth分布式链路追踪(服务跟踪)
    Linux crontabs定时执行任务
    平台系统老板驾驶舱的重要性,我选云表
    【目标检测】SAHI: 切片辅助推理和微调小目标检测
    js原生获取URL 地址中 ? 后面的参数
    BOM系列之sessionStorage
    中兴面试-Java开发
  • 原文地址:https://blog.csdn.net/m0_63222058/article/details/134528773