C++多态篇2——虚函数表详解之从内存布局看函数重载,函数覆盖,函数隐藏

  上一篇C++多态篇1一静态联编,动态联编、虚函数与虚函数表vtable中,我在最后分析了虚函数与虚函数表的内存布局,在下一篇详细剖析虚函数及虚函数表的过程中,我发现有关函数重载,函数覆盖,函数重写和函数协变的知识也要理解清楚才能对虚函数表在内存中的布局,对派生类的对象模型以及对多态的实现有更深的理解。
  所以这一篇我作为一篇过渡篇,也同时对我以前写过的一篇博文进行一个收尾。在C++继承详解之二——派生类成员函数详解(函数隐藏、构造函数与兼容覆盖规则)文章中,我对函数覆盖,重载,重写提了一下,但是没有深入内存中查看内存布局,所以这一篇对前面剩下的问题做一个总结和详细解答。
  注意:
  因为都是我自己画的图,因为图很多,截图也挺多,写一篇文章不容易,所以有的图我画的挺大但是上传出来可能就有点小,或者颜色搭配不合理导致看不清,大家谅解一下。。ctrl+滑轮向上或者ctrl+向上键将网页放大一下看吧,感谢感谢

一、函数重载,覆盖,隐藏,协变的概念和区别

1.函数重载

  首先,什么是函数重载?

成员函数被重载的特征

(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无
相信对C++有一定了解的朋友都知道函数重载的条件是:
  在同一个作用域内

  在C++继承详解之二——派生类成员函数详解(函数隐藏、构造函数与兼容覆盖规则)的开头我也提到了,在派生类中定义一个函数名相同,参数名不同的函数,不是与基类中同名函数进行了函数重载,而是发生了函数隐藏。大家可以去我那篇文章开头看一下那个例子。
  因为首先函数重载的第一个条件就没有满足,即:在相同的范围中(在同一个类中),派生类和基类是两个不同的类域,即不是同一个作用域,所以在继承中,基类和派生类之间永远不可能进行函数重载。

class Base
{
public:
    Base(int data = 0)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    void B()
    {
        cout << "Base::B()" << endl;
    }
    void B(int b)
    {
        cout << "Base::B(int)" << endl;
    }
    //B()与B(int b)构成了函数重载
    //因为上面两个函数是在同一作用域中
    int b;
};
class Derive :public Base
{
public:
    Derive()
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void B(int a, int b)
    {
        cout << "Derive::B(int,int)" << endl;
    }
    //不会与Base类中的两个B名的函数构成重载
    //因为作用域不同
};

下面这个图仅仅代表函数之间的关系,不代表内存布局!
这里写图片描述
那么上面的原则中提到:
virtual关键字在函数重载中可有可无

  那么我们看一下加不加virtual对函数重载的影响。

(1).不加virtual

//定义一个测试函数
void Test()
{
    Base b;
    b.B();
    b.B(1);
}
//main函数调用测试函数

运行结果为:
这里写图片描述

(2).加virtual

a.一个函数加virtual

class Base
{
public:
    Base(int data = 0)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    void B()
    {
        cout << "Base::B()" << endl;
    }
    virtual void B(int b)
    {
        cout << "Base::B(int)" << endl;
    }
    //B()与B(int b)构成了函数重载
    //因为上面两个函数是在同一作用域中
    int b;
};

  运行结果为:
这里写图片描述
  我们对代码进行一下反汇编查看,
这里写图片描述
  可以看到,我们Base b中b一共有八个字节,前四个字节为指向虚表的指针,保存的是虚表的地址,后四个字节是Base类中int b的值,关于虚表的问题可以去我的上一篇博文学习查看 C++多态篇1一静态联编,动态联编、虚函数与虚函数表vtable
  看过我上一篇博文后,或者对虚表有一定了解后,我们可以参照汇编代码看,我们可以看到在汇编代码中,调用重载函数是根据地址不同调用的,调用B(1)时,是进入虚表中调用的,但是不影响函数重载。
  有的人可能要问,那么不加virtual的函数编译器在哪寻找呢?
  实际上,编译器将类的对象存储时是按下图这样存储的
这里写图片描述
  成员函数是单独存储的,所以编译器在存储成员函数那寻找函数即可

b.两个函数都加virtual

class Base
{
public:
    Base(int data = 0)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void B()
    {
        cout << "Base::B()" << endl;
    }
    virtual void B(int b)
    {
        cout << "Base::B(int)" << endl;
    }
    //B()与B(int b)构成了函数重载
    //因为上面两个函数是在同一作用域中
    int b;
};

  运行结果依然是:
这里写图片描述
  我们进行反汇编和在内存中查看可以得到:
这里写图片描述
  我们可以看到,因为B名的函数均为虚函数,所以均在虚表中存储。
当编译器调用时,就在虚表中查找调用。

c.多个函数加virtual

  因为在函数重载中,在不同类域中是不构成函数重载的。所以上面我们都只分析了在基类中的重载,并且都已两个重载函数作为例子,但是多个函数构成重载也是可以的,多个函数加virtual的情况等同于两个函数都加virtual的情况,都会将虚函数加入虚函数表中,在调用时进入虚函数表中进行调用的。

  现在函数重载应该就没有问题了吧~

二、函数覆盖

什么是函数覆盖呢?
  覆盖是指派生类函数覆盖基类函数,特征是

(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。

  当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖函数版本,这种机制就叫做覆盖。
  函数覆盖与我们上面说的函数重载有什么区别呢?
  首先,函数重载要求在同一个作用域,而函数覆盖需要在不同范围内。
  然后就是函数重载要求参数不相同,但是函数覆盖要求参数必须相同。
  最后一点就是函数重载中加不加virtual都可以,但是在函数覆盖中基类函数中必须要加virtual关键字。

  经过上面的分析我们知道了,我们在基类和派生类中分别定义名字相同,参数不同的函数,在后面调用的时候,编译器无法将它处理为函数重载。
  那么函数覆盖又是什么情况呢。
  其实函数覆盖分为两种情况:

1.对象调用函数的情况

  派生类对象调用的是派生类的覆盖函数
  基类的对象调用基类的函数
  下面看代码:

class Base
{
public:
    Base(int data = 1)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void Test()
    {
        cout << "Base::Test()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = 2)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test()
    {
        cout << "Derive::Test()" << endl;
    }
    int d;
};

int main()
{
    Derive d;
    d.Test();
    return 0;
}

  我们在上面的代码中,分别在基类和派生类中定义了同名同参数的函数Test(),看一下运行结果,看会调用基类的函数还是派生类的函数:
这里写图片描述
  因为我在基类和派生类的构造函数中都输出了语句,而且是打断点调试的,所以没有调用析构函数。
  运行结果可以表明:
  这里的Test()函数发生了函数覆盖。
  那我们进入内存中看一下:
这里写图片描述
  PS:因为是我自己截图画图的,不知道为什么传上来就压缩了,如果大家看不清,可以ctrl+向上键放大看一下。
  这张图能够更清楚地看到,在派生类的虚表中,只有一个函数,就是Derive::Test(),没有从Base类继承下来的Test(),所以能够更清楚的看到发生了函数的覆盖。
  如果这样你还没太理解,那么我就再多加几个函数。
  看下面的代码:

class Base
{
public:
    Base(int data = 1)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base::Test3()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = 2)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test1()
    {
        cout << "Derive::Test1()" << endl;
    }   
    void Test2()
    {
        cout << "Derive::Test2()" << endl;
    }
    int d;
};

int main()
{
    Base b;
    b.Test1();
    b.Test2();
    b.Test3();
    Derive d;
    d.Test1();
    d.Test2();
    d.Test3();
    return 0;
}

  从代码可以看出在基类定义了三个虚函数,根据我们以前所说的知识,我们可以猜测基类会生成一个虚函数表,那么派生类中我们定义了两个同名同参数的函数,为了让函数覆盖的现象更加明显,我特意没有将Test3()定义,那么我们现在看一下运行结果:
这里写图片描述
  由结果可知,基类对象调用的是基类的函数。派生类对象调用的是什么呢?
  我们进入内存中查看一下:
这里写图片描述
  由上图我们可以看到,我们在派生类中定义了的函数,在派生类虚函数表中将基类函数覆盖了,即派生类虚函数表中绿色的部分,而派生类没有定义的函数,即Test3(),基类和派生类的函数地址完全相同。
  这就更清楚的看出了,派生类中定义了同名同参数的函数后,发生了函数覆盖。

2.指针或引用调用函数的情况

  指向派生类的基类指针调用的也是派生类的覆盖函数
  还是上面的例子,我们将调用者换一下:

class Base
{
public:
    Base(int data = 1)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void Test()
    {
        cout << "Base::Test()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = 2)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test()
    {
        cout << "Derive::Test()" << endl;
    }
    int d;
};

int main()
{
    Base *pb;
    Derive d;
    pb = &d;
    pb->Test();
    return 0;
}

运行结果为:
这里写图片描述
在内存布局为:
这里写图片描述
  由内存布局可以看出,指针pb指向的虚表就是派生类对象d所拥有的虚表,所以当然调用的是派生类已经覆盖了的函数。
  所以说:
  多态的本质:不是重载声明而是覆盖。
  虚函数调用方式:通过基类指针或引用,执行时会根据指针指向的对象的类,决定调用哪个函数。

三、函数隐藏

  经过上面的分析我们知道,在不同的类域定义不同参数的同名函数,是无法构成函数重载的。
  那么当我们这么做的时候,会发生什么呢。
  实际上,这种情况叫做函数隐藏。
“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下

(1)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
(2)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

首先来看第一种情况。

1.同名同参数

  那么在上面的例子中我们试一下不加virtual关键字看看。
  即将基类改为:

class Base
{
public:
    Base(int data = 1)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    void Test()
    {
        cout << "Base::Test()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = 2)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test()
    {
        cout << "Derive::Test()" << endl;
    }
    int d;
};

int main()
{
    Derive d;
    d.Test();
    return 0;
}

运行结果还是:
这里写图片描述
这就是发生了函数的隐藏
再看下第二种情况

2.同名不同参数

(1)基类函数不加virtual

class Base
{
public:
    Base(int data = 1)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    void Test()
    {
        cout << "Base::Test()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = 2)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test(int a)
    {
        cout << "Derive::Test()" << endl;
    }
    int d;
};

int main()
{
    Derive d;
    d.Test();
    return 0;
}

  我们在基类中定义了Test()函数,在派生类中定义了Test(int a)函数,这就是同名不同参数情况。
编译运行一下:
编译器报错:

Error   1   error C2660: 'Derive::Test' : function does not take 0 arguments    e:\demo\blog\project1\project1\source.cpp   105 1   Project1

  我们可以看出,编译器报错:Test函数不能为0参数。
  如果我们将main函数改变一下:

int main()
{
    Derive d;
    d.Test(1);
    return 0;
}

运行成功,结果为:
这里写图片描述
  这就是发生了函数隐藏~

(2)基类函数加virtual

class Base
{
public:
    Base(int data = 1)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void Test()
    {
        cout << "Base::Test()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = 2)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test(int a)
    {
        cout << "Derive::Test()" << endl;
    }
    int d;
};

int main()
{
    Derive d;
    d.Test();
    return 0;
}

编译运行依然报错:

Error   1   error C2660: 'Derive::Test' : function does not take 0 arguments    e:\demo\blog\project1\project1\source.cpp   105 1   Project1

那么将main函数改变一下:

int main()
{
    Derive d;
    d.Test(1);
    return 0;
}

运行成功,结果为:
这里写图片描述
这也是发生了函数隐藏。
  现在函数隐藏应该没有问题了吧~
  总结一下前面的:

  1.函数重载必须是在同一作用域的,在继承与多态这里,在基类与派生类之间是不能进行函数重载。
  2.函数覆盖是多态的本质

  在基类中的虚函数,在派生类定义一个同名同参数的函数,就可以用派生类新定义的函数对基类函数进行覆盖。
  3.函数隐藏是发生在基类和派生类之间的,当函数同名但是不同参数的时候,不论是不是虚函数,都会发生函数隐藏。
  如有问题欢迎批评指正,人无完人,文无完文,希望大家共同进步!

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页