美文网首页C++
虚函数的讲解

虚函数的讲解

作者: 王王王王王景 | 来源:发表于2019-07-24 10:19 被阅读0次

    1、前言

    简单地说,每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。例:



    其中:

    B的虚函数表中存放着B::foo和B::bar两个函数指针。
    D的虚函数表中存放的既有继承自B的虚函数B::foo,又有重写(override)了基类虚函数B::bar的D::bar,还有新增的虚函数D::quz。

    2、虚函数表构造过程

    从编译器的角度来说,B的虚函数表很好构造,D的虚函数表构造过程相对复杂。下面给出了构造D的虚函数表的一种方式(仅供参考):


    3. 虚函数调用过程

    以下面的程序为例:



    编译器只知道pb是B*类型的指针,并不知道它指向的具体对象类型 :pb可能指向的是B的对象,也可能指向的是D的对象。

    但对于“pb->bar()”,编译时能够确定的是:此处operator->的另一个参数是B::bar(因为pb是B*类型的,编译器认为bar是B::bar),而B::bar和D::bar在各自虚函数表中的偏移位置是相等的。

    无论pb指向哪种类型的对象,只要能够确定被调函数在虚函数中的偏移值,待运行时,能够确定具体类型,并能找到相应vptr了,就能找出真正应该调用的函数。

    3、虚函数的使用

    人们提出这样的设想,能否用同一个调用形式,既能调用派生类又能调用基类的同名函数。在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针调用它们。例如,用同一个语句“pt->display( );”可以调用不同派生层次中的display函数,只需在调用前给指针变量 pt 赋以不同的值(使之指向不同的类对象)即可。

    打个比方,你要去某一地方办事,如果乘坐公交车,必须事先确定目的地,然后乘坐能够到达目的地的公交车线路。如果改为乘出租车,就简单多了,不必查行车路线,因为出租车什么地方都能去,只要在上车后临时告诉司机要到哪里即可。如果想访问多个目的地,只要在到达一个目的地后再告诉司机下一个目的地即可,显然,“打的”要比乘公交车 方便。无论到什么地方去都可以乘同—辆出租车。这就是通过同一种形式能达到不同目的的例子。

    C++中的虚函数就是用来解决这个问题的。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

    #include <iostream>
    #include <string>
    using namespace std;
    //声明基类Student
    class Student
    {
    public:
       Student(int, string,float);  //声明构造函数
       void display( );//声明输出函数
    protected:  //受保护成员,派生类可以访问
       int num;
       string name;
       float score;
    };
    //Student类成员函数的实现
    Student::Student(int n, string nam,float s)//定义构造函数
    {
       num=n;
       name=nam;
       score=s;
    }
    void Student::display( )//定义输出函数
    {
       cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\n\n";
    }
    //声明公用派生类Graduate
    class Graduate:public Student
    {
    public:
       Graduate(int, string, float, float);//声明构造函数
       void display( );//声明输出函数
    private:float pay;
    };
    // Graduate类成员函数的实现
    void Graduate::display( )//定义输出函数
    {
      // 该输出相比于父类中的输出多了一个pay
       cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\npay="<<pay<<endl;
    }
    Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){}
    //主函数
    int main()
    {
       Student stud1(1001,"Li",87.5);//定义Student类对象stud1
       Graduate grad1(2001,"Wang",98.5,563.5);//定义Graduate类对象grad1
       Student *pt=&stud1;//定义指向基类对象的指针变量pt
       pt->display( );
       pt=&grad1;
       pt->display( );
       return 0;
    }
    

    运行结果如下:

    num:1001(stud1的数据)
    name:Li
    score:87.5
    
    num:2001 (grad1中基类部分的数据)
    name:wang
    score:98.5
    

    假如想输出grad1的全部数据成员,当然也可以采用这样的方法:通过对象名调用display函数,如grad1.display(),或者定义一个指向Graduate类对象的指针变量ptr,然后使ptr指向gradl,再用ptr->display()调用。这当然是可以的,但是如果该基类有多个派生类,每个派生类又产生新的派生类,形成了同一基类的类族。每个派生类都有同名函数display,在程序中要调用同一类族中不同类的同名函数,就要定义多个指向各派生类的指针变量。这两种办法都不方便,它要求在调用不同派生类的同名函数时采用不同的调用方式,正如同前面所说的那样,到不同的目的地要乘坐指定的不同的公交车,一一 对应,不能搞错。如果能够用同一种方式去调用同一类族中不同类的所有的同名函数,那就好了。

    用虚函数就能顺利地解决这个问题。下面对程序作一点修改,在Student类中声明display函数时,在最左面加一个关键字virtual,即
    virtual void display( );
    这样就把Student类的display函数声明为虚函数。程序其他部分都不改动。再编译和运行程序,请注意分析运行结果:

    num:1001(stud1的数据)
    name:Li
    score:87.5
    
    num:2001 (grad1中基类部分的数据)
    name:wang
    score:98.5
    pay=1200 (这一项以前是没有的)
    

    看!这就是虚函数的奇妙作用。现在用同一个指针变量(指向基类对象的指针变量),不但输出了学生stud1的全部数据,而且还输出了研究生grad1的全部数据,说明已调用了grad1的display函数。用同一种调用形式“pt->display()”,而且pt是同一个基类指针,可以调用同一类族中不同类的虚函数。这就是多态性,对同一消息,不同对象有 不同的响应方式。

    1、在基类用virtual声明成员函数为虚函数。

    这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。在类外定义虚函数时,不必再加virtual。

    2、在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。

    C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。

    3、定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
    通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。

    4、通过虚函数与指向基类对象的指针变量的配合使用,就能方便地调用同一类族中不同类的同名函数.

    只要先用基类指针指向即可。如果指针不断地指向同一类族中不同类的对象,就能不断地调用这些对象中的同名函数。这就如同前面说的,不断地告诉出租车司机要去的目的地,然后司机把你送到你要去的地方。

    需要说明;有时在基类中定义的非虚函数会在派生类中被重新定义(如例12.1中的area函数),如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。

    虚函数表的布局

    原文链接:https://blog.csdn.net/castle_kao/article/details/71024411

      1. 一个对象实例只有一个虚函数表,只有一个虚基类表。
      1. 对象的每个基类都有一个属于自己的虚函数表指针(vfptr)指向虚函数表(vftbl)的某一项,都有一个属于自己的虚基类表指针(vbptr)指向虚基类表(vbtbl)的某一项。
      1. 虚函数表中按照对象继承的顺序排列对象的虚函数地址,虚基类表中按照对象继承的顺序排列对象的直接虚继承类到虚基类的偏移。
      1. 当基类无虚函数,且派生类有独立虚函数时,派生类对象起始位置为自己的虚函数表指针。否则派生类的虚函数会归到第一个带虚函数表指针的基类的虚函数表指向范围,这样就节省了一个vfptr的空间。
    class B
    {
    public:
        int ib;
        char cb;
    public:
        B() :ib(0), cb('B') {}
    
        virtual void f() { cout << "B::f()" << endl; }
        virtual void Bf() { cout << "B::Bf()" << endl; }
    };
    class B1 : virtual public B
    {
    public:
        int ib1;
        char cb1;
    public:
        B1() :ib1(11), cb1('1') {}
    
        virtual void f() { cout << "B1::f()" << endl; }
        virtual void f1() { cout << "B1::f1()" << endl; }
        virtual void Bf1() { cout << "B1::Bf1()" << endl; }
    
    
    };
    class B2 : virtual public B
    {
    public:
        int ib2;
        char cb2;
    public:
        B2() :ib2(12), cb2('2') {}
    
        virtual void f() { cout << "B2::f()" << endl; }
        virtual void f2() { cout << "B2::f2()" << endl; }
        virtual void Bf2() { cout << "B2::Bf2()" << endl; }
    
    };
    
    class D : public B1, public B2
    {
    public:
        int id;
        char cd;
    public:
        D() :id(100), cd('D') {}
    
        virtual void f() { cout << "D::f()" << endl; }
        virtual void f1() { cout << "D::f1()" << endl; }
        virtual void f2() { cout << "D::f2()" << endl; }
        virtual void Df() { cout << "D::Df()" << endl; }
    
    };
    

    相关文章

      网友评论

        本文标题:虚函数的讲解

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