• [杂记]C++中关于虚函数的一些理解


    最近在重学C++,做一些笔记。

    1. 为什么要使用虚函数

    我们现在考虑类的继承问题. 假如有一个父类 A A A, 有一个子类 B B B, 子类 B B B中重写了父类的一个方法(函数). 现在我们要额外写一个函数, 这个函数要调用父类的这个方法, 因此这个函数我们可以按值传递, 也可以按引用传递和指针传递. 但是在给这个函数传参的过程中, 我们有可能会传入的是子类对象, 即如下所示:

    void func(A& a){
    	a.method();
    }
    
    int main(){
    	B b;
    	func(b);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    那我们还有必要再写(重载)一个func, 把参数换成 B B B吗? 答案是否定的. 换言之, 既然函数里执行的是子类父类的相似行为, 那么我们可以期望利用父类类型来对可能传入的子类们进行统一操作, 提高代码的复用性.

    但是现在有什么问题? 我们传入的是子类对象, 而参数是父类引用, 在这个过程中会发生类型转换. 因此, 在调用method的时候, 我们还是调用的父类的method.

    解决这个问题的方式, 就是将父类的method声明为virtual的:

    class A{
    ...
    public:
    	virtual void method()...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这样编译器就会根据引用或指针指向的类型来确定调用哪个函数. 那么编译器是怎么做到的呢?

    2. 虚函数是如何工作的

    这里借用《C++ primer plus》中的讲述:

    通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table,vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含另一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl中。注意,无论类中包含的虚函数是1个还是10个,都只一需要在对象中添加1个地址成员,只是表的大小不同而已。

    在这里插入图片描述

    3. 其他细节

    1. 构造函数没有必要成为虚函数
      因为子类在执行构造函数时一定会执行父类的构造函数, 因此构造函数是不会被重写的, 因此也不应该成为虚函数
    2. 析构函数最好为虚函数
      这是因为如果子类申请了新的内存,而且按照上面说的方式定义了父类类型但是指向子类对象的引用或指针的话,则会执行父类的析构函数,而父类的析构函数不会释放子类申请的新内存,为此,应将父类的析构函数声明为virtual的,这样就先执行子类的析构,再执行父类的析构
    3. 友元函数不应为虚函数
      这是因为友元函数根本不是成员函数
    4. 子类重新定义函数并不是重载
      如果子类重新定义父类的方法,但是改变了参数类型等,这样会覆盖父类的定义,也即子类无法访问父类原本的方法。返回类型协变除外。
      此外,如果父类有重载,则子类如果想要修改其中的任一个,必须再一一声明重载,否则就覆盖了。

    4. 程序实例

    我简单写了一个测试程序,涵盖了以上几点:

    class BaseClass{
    private:
        int value;
    public: 
        BaseClass(const int v) { this->value = v;};
        virtual void showValue() const { std::cout << "BaseClass " << this->value << "\n"; };
        virtual ~BaseClass() { std::cout << "Deconstructor of BaseClass \n"; };
    }; 
    
    class Child : public BaseClass{
    private:
        char* name;
    public: 
        Child(const int v, const char* str) : BaseClass(v){
            this->name = new char[100];
            strcpy(this->name, str);
        }
        void showValue() const { std::cout << "ChildClass " << this->name << "\n"; };
        virtual ~Child(){
            delete[] this->name; 
            this->name = nullptr;
            std::cout << "Deconstructor of Child \n";
        }
    };
    
    void func(BaseClass& bc){
        bc.showValue();
    }
    
    void test(){
        BaseClass class0 (10);
    
        char str[] = "sadskda";
        Child class1 (10, str);
    
        func(class0);
        func(class1);
    }
    
    int main(){
        test();
    
        system("pause");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    其中func函数实现了只传递父类对象,但也可以执行子类方法,这是通过虚函数实现的。此外,子类的析构函数需要释放申请的内存,因此程序输出如下:

    BaseClass 10
    ChildClass sadskda
    Deconstructor of Child
    Deconstructor of BaseClass
    Deconstructor of BaseClass
    
    • 1
    • 2
    • 3
    • 4
    • 5
  • 相关阅读:
    第5章 uin-app本地主机数据跨域(Cors)数据交互实现
    (Part2)Python编写的计算跳过带有特定数字的车位数量小程序,并利用wxPython做成GUI界面打包成可执行文件
    MySQL学习Day26——事务基础知识
    大数据平台 Hadoop面临的三大安全问题及解决方案
    未来的编程语言「GitHub 热点速览」
    【教3妹学算法】特殊数组的特征值
    蓝桥杯单片机串口初始化加上之后,数码管就不能正常显示了,为什么
    异步FIFO实验小结
    网络编程之IO模型
    Spring IOC的应用
  • 原文地址:https://blog.csdn.net/wjpwjpwjp0831/article/details/126841423