美文网首页
一封虚函数的介绍信

一封虚函数的介绍信

作者: BUG源 | 来源:发表于2017-12-16 00:36 被阅读0次

    写在前边

    这篇文章准备讲讲什么是虚函数,我们知道C++语言的三个特性:抽象、继承和多态。其中谈到多态不得不说的就是虚函数了,那么虚函数究竟应该怎样去认识呢?

    一个简洁的例子

    在一个镇上,大家为了节约资源,采取了资源公有制制度,由镇上的政府负责监管这样制度的实施。制度是这样的:每一个家族(按照姓氏来划分),在镇上的仓库里有一个家族仓库,在里边放了各种资源,大到什么奔驰宝马呀,飞机游艇什么的,小到锅碗瓢盆的都有,仓库门口有安检,比如张家仓库,那么只能是姓张的人才能进去使用,姓王的人就不能进去,当然,姓王的改名字姓张了,那他就能进去了,但是他就进不去王家的仓库了。但是呢,大家又觉得一起共享这些资源吧,又不太好,比如手机电脑这些关系到隐私的,不能共享,因此呢,每个人在仓库里又会有一个专有的保险柜,只有自己拿着钥匙才能去保险柜里取到东西。这样的制度大家觉得很合理。

    再看虚函数

    为什么要讲这个故事呢?因为我觉得,家族仓库其实就可以理解为一个类,每一个人是一个对象,保险柜其实就是一个虚表,而钥匙就是虚指针。暂且这么理解,有bug的也懒得填了。

    类的内存布局

    在这里给介绍一下VS的一个功能,在编译的时候可以看到类的内存情况,这里具体演示一下:
    在VS的[项目]->[project属性]->[C/C++]->[命令行]下添加下边两条命令之一:

    /d1reportSingleClassLayoutAAA     查看类AAA的内存布局(AAA改成自己想看的类名即可)
    /d1reportAllClassLayout       查看所有类的内存布局
    
    image.png

    我在这里演示查看类AAA的内存情况。
    接下来点击确定后重新编译,在[输出]中选择[生成],可以看到如下情况:


    image.png

    这里把我写的类AAA代码先贴出来:

    class AAA
    {
    private:
        int a;
        char b;
        short c;
    
    public:
        AAA(int aa, char bb, short cc) :a(aa), b(bb), c(cc) {}
        ~AAA() {}
        void print()
        {
            std::cout << a << ',' << b << ',' << c << std::endl;
        }
    };
    

    对比[生成]中的类分布图,我们来具体分析一下。
    首先,类成员a是一个int型变量,它被放在类内存起始地址偏移为0的地方(前边那个数字是偏移地址,相对于类的起始地址),类成员b是一个char型的变量,它被放在偏移为4的地方,,然后类成员c是一个short型变量,它被放在偏移地址为6的地方。另外,我在64位操作系统下,char型变量占1字节,short型变量占2字节,int型占4字节。整个类AAA的大小为8字节,在AAA后边的那个size(8)中可以看到。这样我们画一个图出来看看:


    AAA的内存.png

    这就是类AAA的内存布局,我们可以分析出两个点:

    • 内存对齐
    • 只有成员变量,没有成员函数

    首先内存对齐就是蓝色的那一块,讲道理a是4字节,b是1字节,c是2字节,总共应该是7个字节,为什么会是8字节呢?为什么要空一块出来呢?这个是另一个知识点,我们先挖一个坑之后再填。
    然后是为什么没有成员函数的位置呢?联系之前我讲的故事,成员函数就像是家族仓库里的奔驰宝马一样,每个人都可以用一样的,没必要放在类的内存中去占地方,那么实际上C++的成员函数是怎样放置的呢?
    我们知道,虽然每一个类的对象使用的函数的代码都是一样的,但是实际上在使用函数时可能会调用到对象本身的成员变量,那么这是怎样实现的?
    这里就要提到this指针。其实在对象调用自己的成员函数时,编译器会把对象的this指针作为一个参数传到函数中去,这样函数在执行的时候就会知道访问哪个对象中的成员变量。所以有了这个机制以后,所有的成员共用一套函数代码是可行的,这样可以节省内存空间。

    虚函数

    首先我们看看虚函数的声明。
    虚函数是函数声明时带有virtual声明的函数,比如:

    virtual double area();
    

    另外还有一种叫纯虚函数,声明如下:

    virtual double area()=0;
    
    百度百科:虚函数
    百度百科把虚函数解释为上图,纯虚函数解释为将声明与实现分开。我对虚函数的解释是:
    纯虚函数是在基类中定义的接口,而虚函数是基类定义了接口并定义了接口的默认实现方式。
    来看一段代码:
    #include<iostream>
    
    class AAA
    {
    public:
        int a;
        AAA(int aa=0) :a(aa){}
        ~AAA() {}
        virtual void print()
        {
            std::cout <<"class AAA:"<< a << std::endl;
        }
    };
    
    class BBB:public AAA
    {   
    public:
        int b;
        BBB(int aa, int bb) :b(bb) { a=aa; }
        ~BBB() {}
        void print()
        {
            std::cout <<"class BBB:"<< AAA::a <<','<<b<< std::endl;
        }
    };
    
    class CCC :public AAA
    {
    public:
        CCC(int aa) { a=aa; }
        ~CCC() {}
    };
    
    int main()
    {
        BBB b(3, 5);
        CCC c(2);
    
        b.print();
        c.print();
    
        system("pause");
        return 0;
    }
    
    

    这是一段完整的代码,我们可以看到AAA作为基类,成员函数print为虚函数,并且有具体的实现,BBB类继承了AAA类,但是没有使用AAA类的print实现方式,而是重新实现了print,CCC类继承了AAA类,并且沿用了AAA类中print的实现,这样其实输出结果也能猜到了,b.print()会调用BBB类中print打印a和b的值,c.print()会调用AAA类的实现打印a的值:


    image.png

    这里插入一段新程序,这个是普通函数的继承:

    #include<iostream>
    
    class AAA
    {
    public:
        int a;
        AAA(int aa=0) :a(aa){}
        ~AAA() {}
        void print()
        {
            std::cout <<"class AAA:"<< a << std::endl;
        }
    };
    
    class BBB:public AAA
    {   
    public:
        int b;
        BBB(int aa, int bb) :b(bb) { a=aa; }
        ~BBB() {}
        void print()
        {
            std::cout <<"class BBB:"<< AAA::a <<','<<b<< std::endl;
        }
    };
    
    class CCC :public AAA
    {
    public:
        CCC(int aa) { a=aa; }
        ~CCC() {}
        void print()
        {
            std::cout << "class CCC:" << a << std::endl;
        }
    };
    
    int main()
    {
        BBB b(3, 5);
        CCC c(2);
    
        b.print();
        c.print();
        ((AAA*)&b)->print();
        ((AAA*)&c)->print();
        
    
        system("pause");
        return 0;
    }
    
    

    这里把print函数作为一个普通的函数,在BBB和CCC继承以后分别也重新重载了print函数,那么在最后b.print()和c.print()时都会调用BBB和CCC各自的print函数,接下来两条强制把b转换成AAA类型并调用时,会调用BBB中的print还是会调用AAA中的print呢?答案时AAA中的,我先把输出结果放出来:


    image.png

    可以看到最后两个都是调用了AAA的print实现,也就是说,普通函数的调用时随着类类型变化而调用对应的类中的函数实现的。这个其实就和前文的故事的人员改姓一样,姓王的人改姓了张,那么只能去张家仓库中去寻找共有资源,不能去王家仓库了。
    那么虚函数有什么特别的呢?我们对比一下相同的操作,接下来的代码是虚函数版本的print:


    image.png

    然后输出会是怎样呢?我们先看一下:

    image.png
    是不是很奇怪,强制转换以后,b和c依旧调用的自己的函数。怎么理解呢?就是我之前讲的保险柜,每个人有一把钥匙,虽然姓王改成了姓张,但是钥匙还在,还是能从保险柜里拿到自己的东西。
    什么意思呢?我们来看看类的内存布局:
    class AAA
    class BBB
    class CCC
    从AAA看,很容易发现AAA中多了一个{vfptr}的东西,还占了4个字节,在BBB和CCC中也有,这是个什么东西呢?这就是保险柜钥匙--虚表指针
    虚表是什么?
    虚表是存放虚函数入口的表。
    那么我们来解释一下刚才为什么((AAA)&b)->print()和((AAA)&c)->print()两条语句会调用BBB和CCC中的print,我们来看一张图:
    虚表指针
    b中的虚表指针在声明时就指向了类BBB的虚表,在把它强制转化成AAA以后,这个指针依旧指向的是BBB,那么在调用print()时还是会调用BBB中的实现。

    总结

    虚函数的基础杂谈就大概这些,当然虚函数肯定不止这一点内容,比如还有多继承时,虚表指针的指向,以及虚表中的实际内容等等,这些还有很多知识点。

    相关文章

      网友评论

          本文标题:一封虚函数的介绍信

          本文链接:https://www.haomeiwen.com/subject/jdodwxtx.html