• C++多态、虚函数、纯虚函数、抽象类



    多态的概念
            通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
            举个简单的例子:抢红包,我们每个人都只需要点击一下红包,就会抢到金额。有些人能抢到几十元,而有些人只能抢到几元甚至几毛。也正说明了不同的人做相同的事,结果却不同,这就是多态。

            在C++中有两种多态性,一种是静态的多态、一种是动态的多态;

    静态的多态:函数重载,看起来调用同一个函数却有不同的行为。静态:原理是编译时实现。

    动态的多态:一个父类的引用或指针去调用同一个函数,传递不同的对象,会调用不同的函数。动态:原理是运行时实现。


    一、前言

            多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。下面的实例中,基类 Shape 被派生为两个类,如下所示:

    1. #include
    2. using namespace std;
    3. class Shape {
    4. public:
    5. void area()
    6. {
    7. cout << "Parent class area :" << endl;
    8. }
    9. };
    10. class Rectangle : public Shape {
    11. public:
    12. void area()
    13. {
    14. cout << "Rectangle class area :" << endl;
    15. }
    16. };
    17. class Triangle : public Shape {
    18. public:
    19. void area()
    20. {
    21. cout << "Triangle class area :" << endl;
    22. }
    23. };
    24. void func(Shape& p) {
    25. p.area();
    26. }
    27. // 程序的主函数
    28. int main()
    29. {
    30. Rectangle Rec;
    31. // 调用矩形的求面积函数 area
    32. func(Rec);
    33. Triangle Tri;
    34. // 调用三角形的求面积函数 area
    35. func(Tri);
    36. return 0;
    37. }

    当上面的代码被编译和执行时,它会产生下列结果:

    1. Parent class area :
    2. Parent class area :

            导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。

            但现在,让我们对程序稍作修改,在 Shape 类中,area() 的声明前放置关键字 virtual,其余不变,如下所示:

    1. #include
    2. using namespace std;
    3. class Shape {
    4. public:
    5. virtual void area()
    6. {
    7. cout << "Parent class area :" << endl;
    8. }
    9. };
    10. class Rectangle : public Shape {
    11. public:
    12. void area()
    13. {
    14. cout << "Rectangle class area :" << endl;
    15. }
    16. };
    17. class Triangle : public Shape {
    18. public:
    19. void area()
    20. {
    21. cout << "Triangle class area :" << endl;
    22. }
    23. };
    24. void func(Shape& p) {
    25. p.area();
    26. }
    27. // 程序的主函数
    28. int main()
    29. {
    30. Rectangle Rec;
    31. // 调用矩形的求面积函数 area
    32. func(Rec);
    33. Triangle Tri;
    34. // 调用三角形的求面积函数 area
    35. func(Tri);
    36. return 0;
    37. }

    修改后,当编译和执行前面的实例代码时,它会产生以下结果:

    1. Rectangle class area :
    2. Triangle class area :

            此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。

            正如您所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。

    二、多态的定义及实现

    1.多态的构成条件 

            在继承中要构成多态还有两个条件:

            (1)必须通过基类的指针或者引用调用虚函数。

            (2)被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

    2.虚函数

            虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
            我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定

            一旦定义了虚函数,该基类的派生类中同名函数也自动成为了虚函数。也就是说在派生类中有一个和基类同名的函数,只要基类加了virtual修饰,派生类不加virtual修饰也是虚函数。虚函数只能是类中的一个成员函数,不能是静态成员或普通函数。

            注意:我们在继承中为了解决数据冗余和二义性的问题,需要用到虚拟继承,关键字也是virtual,和多态中的virtual是没有关系的。

    3.虚函数的重写

            虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

            通过对虚函数的重写,就能够实现多态:

    1. #include
    2. using namespace std;
    3. //买票
    4. class Person
    5. {
    6. public:
    7. virtual void BuyTicket() { cout << "买票-全价" << endl; }
    8. };
    9. //学生买票
    10. class Student : public Person
    11. {
    12. public:
    13. virtual void BuyTicket() { cout << "买票-半价" << endl; }
    14. };
    15. //军人买票
    16. class Soldier : public Person
    17. {
    18. public:
    19. void BuyTicket() { cout << "优先-买票-半价" << endl; }
    20. };
    21. //构成多态,传的哪个类型的对象,调用的就是这个类型的虚函数 --- 跟对象有关
    22. //不构成多态,调用就是P的类型 --- 跟类型有关
    23. void Func(Person& p) //或void Func(Person* p)
    24. {
    25. p.BuyTicket(); //p->BuyTicket();
    26. }
    27. int main()
    28. {
    29. Person ps;
    30. Func(ps); //没有任何身份去买票,一定是全价
    31. Student st;
    32. Func(st); //以学生的身份去买票,是半价
    33. Soldier so;
    34. Func(so); //以军人的身份去买票,是优先并且半价
    35. return 0;
    36. }

    4.虚函数重写的两个例外

    (1).协变(基类与派生类虚函数返回值类型不同)

            派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。 

    另一种解释:

            C++中的协变(Covariance)指的是派生类的返回类型可以是基类函数的返回类型的子类型。当一个派生类继承了一个基类,并且覆盖(override)了基类中的虚函数时,可以使用协变来改变返回类型。

            具体而言,如果基类函数的返回类型是指针或引用,那么派生类中覆盖该函数时,返回类型可以是基类返回类型所指向或引用的类型的派生类型。

    实现协变需满足以下条件:

    • 基类中的函数必须是虚函数(使用 virtual 关键字声明)。
    • 派生类中重写的函数必须具有相同的函数签名(函数名、参数列表和常量性)。
    • 派生类中重写的函数的返回类型必须是基类函数返回类型的子类型。

    示例:

    引用自:C++协变(covariant)-CSDN博客

            假设有一个基类 Animal 和两个派生类 Dog 和 Cat。Animal 类中有一个虚函数 makeSound(),它返回一个指向 Animal 对象的指针。在派生类 Dog 中,可以重写 makeSound() 函数并返回一个指向 Dog 对象的指针。同样,在派生类 Cat 中也可以重写 makeSound() 函数并返回一个指向 Cat 对象的指针。

    1. #include
    2. class Animal {
    3. public:
    4. virtual Animal* makeSound() {
    5. std::cout << "Animal makes a sound." << std::endl;
    6. return this;
    7. }
    8. };
    9. class Dog : public Animal {
    10. public:
    11. virtual Dog* makeSound() {
    12. std::cout << "Dog barks." << std::endl;
    13. return this;
    14. }
    15. };
    16. class Cat : public Animal {
    17. public:
    18. virtual Cat* makeSound() {
    19. std::cout << "Cat meows." << std::endl;
    20. return this;
    21. }
    22. };
    23. int main() {
    24. Animal* animal;
    25. Dog dog;
    26. Cat cat;
    27. animal = &dog;
    28. animal->makeSound(); // Output: "Dog barks."
    29. animal = &cat;
    30. animal->makeSound(); // Output: "Cat meows."
    31. return 0;
    32. }

    协变与多态的区别: 

            C++中协变和多态是密切相关的。多态(Polymorphism)指的是同一个函数在不同的对象上被调用时,可以表现出不同的行为方式。

            在C++中,通过使用虚函数(virtual function),实现了运行时多态的语法机制。基类中的虚函数可以在派生类中被重写(覆盖),当派生类对象调用该虚函数时,会根据对象的实际类型来确定调用哪个虚函数。

            而协变则指的是派生类可以改变继承自基类函数的返回类型,使得返回类型成为基类返回类型所指向或引用的类型的派生类型。

            通过将协变和多态结合起来,我们可以在派生类中覆盖基类的虚函数,并且返回派生类特有的类型。这就允许我们在多态的情况下,在派生类中使用更具体的返回类型。

     (2).析构函数的重写(基类与派生类析构函数的名字不同) 

            如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

            在C++中,析构函数是一种特殊的成员函数,用于在对象销毁时执行清理工作。通常情况下,析构函数会自动由编译器生成,默认执行对象的成员变量和基类的析构函数。

            当需要对派生类进行额外的清理工作或资源释放时,可以通过重写(override)基类的析构函数来实现。

            在派生类中重写析构函数需要遵循以下规则:

    1. 函数名与基类的析构函数完全相同。
    2. 参数列表为空。
    3. 返回类型为空(void)。
    4. 可以添加override关键字(可选),以显式地说明正在重写基类的析构函数。

            以下是一个示例代码:

    1. class Base {
    2. public:
    3. virtual ~Base() {
    4. // 基类的析构函数
    5. }
    6. };
    7. class Derived : public Base {
    8. public:
    9. ~Derived() override {
    10. // 派生类的析构函数,重写了基类的析构函数
    11. }
    12. };

            在上述代码中,基类Base定义了一个虚析构函数,派生类Derived通过重写基类的析构函数,实现了自己的清理逻辑。

            需要注意的是,在继承关系中,如果基类的析构函数是一个虚函数,则派生类中的析构函数也应该声明为虚函数。这样,在使用基类指针或引用指向派生类对象,并通过该指针或引用调用析构函数时,能够正确地调用到派生类的析构函数。

            总之,通过在派生类中重写基类的析构函数,可以实现额外的清理工作或资源释放。重写析构函数需要遵循特定的规则,并且建议将基类的析构函数声明为虚函数。

     

    另一种解释:

    (引用自:C++ 多态(一) : 多态的构成条件、final、override、协变、析构函数的重写、抽象类_c++ 多态 override-CSDN博客)

            析构函数虽然函数名不同,但是也能构成重写,因为站在编译器的视角,他所调用的析构函数名称都叫做destructor。

    为什么编译器要通过这种方式让析构函数也能构成重写呢?

    假设我用一个基类指针或者引用指向派生类对象,如果不构成多态会怎样?

    1. class Human
    2. {
    3. public:
    4. ~Human()
    5. {
    6. cout << "~Human()" << endl;
    7. }
    8. };
    9. class Student : public Human
    10. {
    11. public:
    12. ~Student()
    13. {
    14. cout << "~Student()" << endl;
    15. }
    16. };
    17. int main()
    18. {
    19. Human* h = new Student;
    20. delete h;
    21. return 0;
    22. }

    输出结果: 

    ~Human()

    分析:

            上述代码只会调用类Human的析构函数,即如果不构成多态,那么指针是什么类型的,就会调用什么类型的析构函数,这也就导致了一种情况,如果派生类的析构函数中有资源释放,而这里却没有释放掉那些资源,就会导致内存泄漏的问题。

            所以为了防止这种情况,必须要将析构函数定义为虚函数。这也就是编译器将析构函数重命名为destructor的原因。

    1. class Human
    2. {
    3. public:
    4. virtual ~Human()
    5. {
    6. cout << "~Human()" << endl;
    7. }
    8. };
    9. class Student : public Human
    10. {
    11. public:
    12. virtual ~Student() // 该virtual关键字可省略
    13. {
    14. cout << "~Student()" << endl;
    15. }
    16. };
    17. int main()
    18. {
    19. Human* h = new Student;
    20. delete h;
    21. return 0;
    22. }

    输出结果: 

    ~Student()
    ~Human() 

    5.C++11 override和final

            从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了 override 和 final 两个关键字,可以帮助用户检测是否重写。

    (1) final

            在C++11标准中,final是一个关键字,用于禁止继承和覆盖类的虚函数。当一个类或者一个类的成员函数被声明为final时,意味着它不能再被其他类继承或者它的虚函数不能被派生类覆盖。

    使用final关键字的好处是:

    1. 可以增强代码的安全性:使用final关键字可以防止不恰当的继承和覆盖。
    2. 可读性:使用final关键字可以增强代码的可读性和可维护性,明确了类或函数的意图。

    final:修饰虚函数,表示该虚函数不能再被重写。

    1. #include
    2. class Car
    3. {
    4. public:
    5. virtual void Drive() final
    6. {}
    7. };
    8. class Benz :public Car
    9. {
    10. public:
    11. virtual void Drive() override //检查是否完成重写
    12. {
    13. std::cout << "Benz-舒适" << std::endl;
    14. }
    15. };
    16. int main() {
    17. Benz benz;
    18. benz.Drive();
    19. }

     上述程序因为final关键字的存在会报错,报错原因是:

    final:修饰类,表示该类不能再被继承。

    示例:

    1. #include
    2. class Car final
    3. {
    4. public:
    5. virtual void Drive()
    6. {}
    7. };
    8. class Benz :public Car
    9. {
    10. public:
    11. virtual void Drive() //检查是否完成重写
    12. {
    13. std::cout << "Benz-舒适" << std::endl;
    14. }
    15. };
    16. int main() {
    17. Benz benz;
    18. benz.Drive();
    19. }

    上述程序报错: 

    不能将final用于基类,否则程序报错! 

    (2) override

            在C++中,override是一个特殊的关键字,用于显式地标识派生类中的函数是覆盖(override)基类中的虚函数。

            当派生类中的函数与基类中的虚函数具有相同的名称、参数列表和返回类型时,可以使用override关键字来明确指示该函数是对基类函数的覆盖。

    使用override关键字的好处是:

    1. 错误检查:编译器会在编译时检查是否存在函数覆盖错误。如果派生类中使用了override关键字,但没有正确地覆盖基类中的虚函数,编译器将报错。
    2. 可读性:使用override关键字可以增强代码的可读性和可维护性,明确了派生类函数的意图。

    下面是一个示例代码:

    1. #include
    2. class Car
    3. {
    4. public:
    5. virtual void Drive()
    6. {}
    7. };
    8. class Benz :public Car
    9. {
    10. public:
    11. virtual void Drive() override //检查是否完成重写
    12. {
    13. std::cout << "Benz-舒适" << std::endl;
    14. }
    15. };
    16. int main() {
    17. Benz benz;
    18. benz.Drive(); // Benz-舒适
    19. }

    三、抽象类

    纯虚函数

            您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

            在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

    1. class Shape {
    2. public:
    3. // pure virtual function
    4. virtual int area() = 0;
    5. };

       = 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数

    包括纯虚函数的类叫做抽象类,也叫接口类,抽象类不能实例化出对象。

    示例:

    1. #include
    2. //抽象类
    3. class Car
    4. {
    5. public:
    6. virtual void Drive() = 0;//纯虚函数
    7. };
    8. int main()
    9. {
    10. Car c;//抽象类不能实例化出对象
    11. return 0;
    12. }

    上述程序运行报错: 

     派生类继承后也不能实例化出对象。

    示例:

    1. #include
    2. class Car
    3. {
    4. public:
    5. virtual void Drive() = 0; // 纯虚函数
    6. };
    7. class Benz :public Car{};
    8. int main()
    9. {
    10. Benz b1;
    11. }

    上述程序运行出错: 

            派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

    示例:

    1. #include
    2. class Car
    3. {
    4. public:
    5. //纯虚函数一般只声明,不实现(可以实现,但没有价值,因为不能实例化出对象,可以定义指针或引用)
    6. virtual void Drive() = 0;
    7. };
    8. class Benz :public Car
    9. {
    10. public:
    11. virtual void Drive()
    12. {
    13. std::cout << "Benz-舒适" << std::endl;
    14. }
    15. };
    16. class BMW :public Car
    17. {
    18. public:
    19. virtual void Drive()
    20. {
    21. std::cout << "BMW-操控" << std::endl;
    22. }
    23. };
    24. int main()
    25. {
    26. //派生类只有重写了纯虚函数才能实例化出对象
    27. Benz b1;
    28. BMW b2;
    29. //通过基类的指针去调用不同对象的函数
    30. Car* pBenz = new Benz;
    31. pBenz->Drive();
    32. Car* pBMW = new BMW;
    33. pBMW->Drive();
    34. }

    输出结果: 

    Benz-舒适
    BMW-操控

    接口继承和实现继承

    • 普通函数的继承是一种实现继承,派生类继承了基类的普通成员函数,可以使用函数,继承的是函数的实现。
    • 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
    • 所以如果不实现多态,不要把函数定义成虚函数。

    参考自(很值得学习):

    【精选】【C++】—— 多态_c++多态_霄沫凡的博客-CSDN博客

    注:参考内容只是为了自身学习,并无其他想法!!!

  • 相关阅读:
    循环神经网络不常应用于,循环神经网络应用举例
    SSM - Springboot - MyBatis-Plus 全栈体系(十四)
    精分合并抑郁康复经历分享:如何从死亡边缘回到生的海洋?
    应急响应-日志分析
    Unity 动画系统基本概念
    Code Complate - 什么是软件构建
    七夕节赚取徽章啦
    跟我学c++中级篇——c++11中的模板变参化
    安卓开发-基础知识补习12
    透过现象看本质,如何针对用户做好需求分析
  • 原文地址:https://blog.csdn.net/m0_48241022/article/details/133958647