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.函数隐藏是发生在基类和派生类之间的,当函数同名但是不同参数的时候,不论是不是虚函数,都会发生函数隐藏。

这篇文章就暂且写到这里,本来是想对虚函数表进行深度剖析的,但是写那一篇的时候发现,会用到这里的知识,害怕初学者这里还不清楚,所以现将这些问题整理一下,再更新下一篇文章。

如有问题欢迎批评指正,人无完人,文无完文,希望大家共同进步!

时间: 2024-12-24 22:54:00

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

C++多态篇3——虚函数表详解之多继承、虚函数表的打印

在上上一篇C++多态篇1一静态联编,动态联编.虚函数与虚函数表vtable中,我最后简单了剖析了一下虚函数表以及vptr. 而在上一篇文章C++多态篇2--虚函数表详解之从内存布局看函数重载,函数覆盖,函数隐藏中我详细介绍了虚函数的函数重载,函数覆盖以及函数隐藏的问题,其实在那一篇文章中,对单继承的虚函数已经做了十分详细的解答了,如果对前面有兴趣的人可以先看一下那篇文章. 在这一篇中,我会具体的分析一下在不同继承中(单继承,多继承)关于虚函数表在内存中的布局以及如何打印虚函数表.但是有关在虚继承

虚函数表详解

虚函数表 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的.简称为V-Table.在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承.覆盖的问题,保证其容真实反应实际的函数.这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数. 这里我们着重看一下这张虚函数表.C++的编译器应该是

详解一道C++笔试题,考察重载、覆盖、多态

C++版看到的,说是面试宝典里的题目,考察重载.覆盖.多态等概念,比较有代表性.今天早上远程辅导 Yan Wang 同学学习 Qt 时还想到了这个题目,如果你能够正确理解这个题目,说明对于 C++ 中的函数重载.覆盖.虚函数.多态等有了正确的认识.然后呢,再来学习 Qt 就顺风顺水了. 博文决赛,请给我投票:Qt Quick 图像处理实例之美图秀秀(附源码下载),谢谢. 原文地址: Qt Quick 图像处理实例之美图秀秀(附源码下载) 题目是酱紫的: #include <iostream>

C++虚函数表解析(基础篇)

原文:http://blog.csdn.net/haoel/article/details/1948051 一.简介 C++中的虚函数的作用主要是实现了多态的机制.虚函数(Virtual Function)其实是通过一张虚函数表(Virtual Table)来实现的.简称为V-Table.在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承.覆盖的问题,保证其容真实反应实际的函数.这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这

深入剖析C++多态、VPTR指针、虚函数表

在讲多态之前,我们先来说说关于多态的一个基石------类型兼容性原则. 一.背景知识 1.类型兼容性原则 类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代.通过公有继承,派生类得到了基类中除构造函数.析构函数之外的所有成员.这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决.类型兼容规则中所指的替代包括以下情况: 子类对象可以当作父类对象使用 子类对象可以直接赋值给父类对象 子类对象可以直接初始化父类对象 父类指针可以直接指向子类对

虚函数表与多态内存布局

参考博客:https://blog.csdn.net/songguangfan/article/details/87898915 C++中 的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术 可以让父类的指针有“多种形态”,这是一种泛型技术. 虚函数表每个含有虚函数的类都有一个虚函数表(Virtual Table)来实现的.简称为V-Table. C++的编译器应该是保证虚函数表的指针存在于对象实例中最

C++中的虚函数表

学习了虚基类,立马就会想到虚函数,虚基类有个虚基类表与之对应,才使其发挥了不一般的作用,当然虚函数也有一个不函数表,其原理如下所示: 1.如果虚函数在基类与派生类中出现,仅仅是名字相同,而形式参数不同,或者是返回类型不同,那么即使加上了virtual关键字,也是不会进行滞后联编的. 2.只有类的成员函数才能说明为虚函数,因为虚函数仅适合用与有继承关系的类对象,所以普通函数不能说明为虚函数. 3.静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象. 4.内联(inline)函数不

C++ 虚函数表解析

转自陈浩的博客 前言 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态",这是一种泛型技术.所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法.比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议. 关于虚函数的使用方法,我在这里不做过多的阐述.大家可以看看相关的C++的书籍.在这篇文章中,我只想从虚函数的实现

C++ 虚函数表解析(转)

转自:http://blog.csdn.net/haoel 前言 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态",这是一种泛型技术.所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法.比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议. 关于虚函数的使用方法,我在这里不做过多的阐述.大家可以看看相关的C