• 虚函数、纯虚函数、多态


    一.虚函数

            在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数。

    注意:如果子类没有重写虚函数,那么子类对象仍然有虚指针,虚指针指向的是基类的虚函数表!

    • 虚函数的限制
      • 1.只有类的成员函数才能说明为虚函数
      • 2.静态成员函数不能写为虚函数
        • 原因:静态成员函数是属于类的,虚函数是属于对象的,因此将静态成员函数声明为虚函数是没有意义的也是不被允许的。
      • 3.内联函数不能为虚函数
        • ​​​​​​​原因:机制冲突。内联函数需要在编译时就确定函数的实现,而虚函数是在运行时才能确定。
      • 4.构造函数不能为虚函数
        • ​​​​​​​原因:虚函数的机制是对象构造过程中建立的,因此对象构造期间虚函数机制无法正常工作;并且构造函数的目的是初始化对象的状态,而不是实现多态性。
      • 5.析构函数可以写成虚函数
        • ​​​​​​​原因:为了确保在删除派生类对象时正确地调用析构函数;如果析构函数不是虚函数,基类指针只会调用基类的析构函数,无法析构派生类对象,可能会造成资源泄露或未能正确释放资源。

    (一)虚表和虚基表指针

    虚函数表(Virtual Function Table)
    • 在C++中,每个类(包括基类和派生类)包含一个虚函数表,也称为虚表(vtable)。虚表是一个包含虚函数指针的数组,每个虚函数指针指向相应虚函数的地址虚表是在编译时创建的,并且对于每个类只有一个
    • 虚表是类的元数据,它记录了该类的虚函数及其位置。当类的对象被创建时,一个指向虚表的指针会添加到对象的内存布局中。

    虚表指针
    • 在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚表的指针,它是在构造函数中被初始化的。

    (二)纯虚函数

    纯虚函数(Pure Virtual Function)是C++中的一个特殊类型的虚拟函数,它在基类中声明但没有定义。纯虚函数的声明使用virtual关键字,并在函数声明的末尾添加= 0来表示它是一个纯虚函数。子类(派生类)必须提供纯虚函数的实际实现,否则子类也会被标记为抽象类,无法创建对象。

    1. class Shape {
    2. public:
    3. // 声明纯虚函数
    4. virtual void draw() = 0;
    5. // 普通成员函数
    6. void displayInfo() {
    7. // 这里可以包含一些通用的代码
    8. std::cout << "This is a shape." << std::endl;
    9. }
    10. };
    11. class Circle : public Shape {
    12. public:
    13. // 子类必须提供纯虚函数的实现
    14. void draw() override {
    15. std::cout << "Drawing a circle." << std::endl;
    16. }
    17. };
    18. class Square : public Shape {
    19. public:
    20. // 子类必须提供纯虚函数的实现
    21. void draw() override {
    22. std::cout << "Drawing a square." << std::endl;
    23. }
    24. };
    25. int main() {
    26. Circle circle;
    27. Square square;
    28. circle.displayInfo(); // 调用基类函数
    29. circle.draw(); // 调用派生类函数
    30. square.displayInfo(); // 调用基类函数
    31. square.draw(); // 调用派生类函数
    32. return 0;
    33. }

    在上述示例中,Shape类包含一个纯虚函数draw(),因此Shape类本身是一个抽象类,不能创建它的对象。然后,CircleSquare类都继承自Shape类,并必须提供对draw()的实际实现。这种机制允许多态性(Polymorphism)的实现,允许不同的派生类以不同的方式实现相同的虚拟函数。

    二.多态的实现

    根据上图举例分析:

    1. #include
    2. #include
    3. using namespace std;
    4. class A {
    5. public:
    6. virtual void prints() {
    7. cout << "A::prints" << endl;
    8. }
    9. A() {
    10. cout << "A:构造函数" << endl;
    11. }
    12. };
    13. class B:public A {
    14. public:
    15. virtual void prints() {
    16. cout << "B::prints" << endl;
    17. }
    18. B() {
    19. cout << "B:构造函数" << endl;
    20. }
    21. };
    22. class C :public A {
    23. public:
    24. };
    25. int main() {
    26. A *b = new B();
    27. b->prints();
    28. b = new C();
    29. b->prints();
    30. return 0;
    31. }

    多态的原理:

    1.虚函数表和虚指针(上面)
    2.虚函数声明和覆盖
    • 在基类中声明虚函数时,使用virtual关键字。这告诉编译器将该函数视为虚函数,它可以在派生类中被覆盖(重写)。

    • 派生类中的虚函数覆盖基类中的虚函数时,必须使用override关键字,以确保正确的函数被覆盖。这也使得代码更容易阅读和维护

    3.动态绑定的过程
    • 当您使用基类指针或引用调用虚函数时,编译器不会在编译时决定要调用哪个函数版本。相反,它会在运行时根据对象的实际类型来选择正确的函数版本。

    • 这个过程大致如下:

      1. 在基类指针或引用上调用虚函数。
      2. 运行时系统会查找对象的虚表指针。
      3. 使用虚表指针找到虚表。
      4. 从虚表中获取正确的函数指针。
      5. 调用相应的函数。
    4.多态性的优势
    • 多态性无需关心对象的具体类型,从而提高了代码的可复用性和可维护性。
    • 它允许轻松扩展代码,通过添加新的派生类来增加功能,而不必修改现有的代码。
    • 多态性还支持抽象编程,允许定义基于接口的类,然后通过派生类来实现具体的功能。

    总结一下,多态的原理基于虚函数和虚表,它允许在运行时根据对象的实际类型来选择要调用的函数版本,从而实现了面向对象编程中的灵活性和可扩展性。多态性是C++和其他面向对象编程语言的核心概念之一,有助于构建可维护和可扩展的软件系统。

    三.为什么析构函数一般写成虚函数?

            由于类的多态性,通常通过父类指针或引用来操作子类对象。因为多态允许我们以统一的方式处理不同的派生类对象,并且在运行时确定要调用的方法。

            如果析构函数不被声明为虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样会造成派生类析构不完全,造成内存泄漏。

            这种行为是为了确保资源的正确释放。由于我们只知道父类的类型,编译器无法确定指针指向的是哪个子类对象,因此只能调用父类的析构函数来释放资源。

    没有虚析构:

    1. #include<iostream>
    2. #include<vector>
    3. using namespace std;
    4. class A {
    5. public:
    6. virtual void prints() {
    7. cout << "A::prints" << endl;
    8. }
    9. A() {
    10. cout << "A:构造函数" << endl;
    11. }
    12. virtual ~A() {
    13. cout << "A:析构函数 " << endl;
    14. }
    15. };
    16. class B:public A {
    17. public:
    18. virtual void prints() {
    19. cout << "B::prints" << endl;
    20. }
    21. B() {
    22. cout << "B:构造函数" << endl;
    23. }
    24. ~B() {
    25. cout << "B:析构函数 " << endl;
    26. }
    27. };
    28. int main() {
    29. A *b = new B();
    30. b->prints();
    31. delete b;
    32. b = NULL;
    33. return 0;
    34. }

    虚析构:

    1. #include<iostream>
    2. #include<vector>
    3. using namespace std;
    4. class A {
    5. public:
    6. virtual void prints() {
    7. cout << "A::prints" << endl;
    8. }
    9. A() {
    10. cout << "A:构造函数" << endl;
    11. }
    12. virtual ~A() {
    13. cout << "A:析构函数 " << endl;
    14. }
    15. };
    16. class B:public A {
    17. public:
    18. virtual void prints() {
    19. cout << "B::prints" << endl;
    20. }
    21. B() {
    22. cout << "B:构造函数" << endl;
    23. }
    24. ~B() {
    25. cout << "B:析构函数 " << endl;
    26. }
    27. };
    28. int main() {
    29. A *b = new B();
    30. b->prints();
    31. delete b;
    32. b = NULL;
    33. return 0;
    34. }

     

    分析:可以看到析构函数是,先从子类析构,再到父类析构 

  • 相关阅读:
    什么时候用 C 而不用 C++?
    RISC-V内核中科蓝讯BT8922开发
    pyechart练习三:黑色星期五用户画像
    MySQL中的高级查询
    SpringBoot 01: JavaConfig + @ImportResource + @PropertyResource
    Java模拟西宝高速公路
    【高频笔试题】513.找树左下角的值
    linux C++ vscode连接mysql
    计算机操作系统:实验3 【虚拟存储器管理】
    Redis笔记
  • 原文地址:https://blog.csdn.net/Ricardo_XIAOHAO/article/details/132741040