多态(2)纯虚函数与重载、重写(覆盖)、重定义(隐藏)

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

一、函数重载,覆盖,隐藏,协变的概念和区别
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名的函数构成重载
    //因为作用域不同
};

下面这个图仅仅代表函数之间的关系,不代表内存布局!
威尼斯网址开户网站 1
那么上面的原则中提到:
virtual关键字在函数重载中可有可无
那么我们看一下加不加virtual对函数重载的影响。
(1).不加virtual

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

运行结果为:
威尼斯网址开户网站 2
(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;
};

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

运行结果依然是:
威尼斯网址开户网站 6
我们进行反汇编和在内存中查看可以得到:
威尼斯网址开户网站 7
我们可以看到,因为B名的函数均为虚函数,所以均在虚表中存储。
当编译器调用时,就在虚表中查找调用。
c.多个函数加virtual< 喎?”
target=”_blank”
class=”keylink”>vc3Ryb25nPjxiciAvPg0K0vLOqtTauq/K/dbY1NjW0KOs1Nqyu82swODT8tbQyseyu7m5s8m6r8r91tjU2LXEoaPL+dLUyc/D5s7Sw8e2vNa7t9bO9sHL1Nq7+cDg1tC1xNbY1NijrLKix9K2vNLRwb249tbY1Ni6r8r91/fOqsD919OjrLWryse24Lj2uq/K/bm5s8nW2NTY0rLKx7/J0tS1xKOstuC49rqvyv2803ZpcnR1YWy1xMfpv/a1yM2s09rBvbj2uq/K/ba8vNN2aXJ0dWFstcTH6b/2o6y2vLvhvavQ6bqvyv2808jr0Om6r8r9se3W0KOs1Nq199PDyrG9+Mjr0Om6r8r9se3W0L340NC199PDtcShozwvcD4NCjxwPs/W1Nq6r8r91tjU2NOmuMO+zcO709DOysziwcuwyX48YnIgLz4NCjxzdHJvbmc+tv6horqvyv24srjHPC9zdHJvbmc+PGJyIC8+DQrKssO0yse6r8r9uLK4x8TYo788YnIgLz4NCjxzdHJvbmc+uLK4x8rH1rjFycn6wOC6r8r9uLK4x7v5wOC6r8r9o6zM2NX3yscgPC9zdHJvbmc+PGJyIC8+DQqjqDGjqbK7zay1xLe2zqejqLfWsfDOu9PaxcnJ+sDg0+u7+cDgo6mjuzxiciAvPg0Ko6gyo6m6r8r9w/vX1s/gzayjuzxiciAvPg0Ko6gzo6myzsr9z+DNrKO7PGJyIC8+DQqjqDSjqbv5wOC6r8r9sdjQ69PQdmlydHVhbCC52Lz819ahozxiciAvPg0KtbHFycn6wOC21M/ztffTw9fTwODW0LjDzazD+7qvyv3Ksbvh19S2r7X308PX08Dg1tC1xLiyuMew5rG+o6y2+LK7yse4uMDg1tC1xLG7uLK4x7qvyv2w5rG+o6zV4tbWu/rWxr7NvdDX9riyuMehozxiciAvPg0Kuq/K/biyuMfT687Sw8fJz8Pmy7W1xLqvyv3W2NTY09DKssO0x/ix8MTYo788YnIgLz4NCsrXz8ijrLqvyv3W2NTY0qrH89TazazSu7j21/fTw9Pyo6y2+Lqvyv24srjH0OjSqtTasrvNrLe2zqfE2qGjPGJyIC8+DQrIu7rzvs3Kx7qvyv3W2NTY0qrH87LOyv2yu8/gzayjrLWryse6r8r9uLK4x9Kqx/Oyzsr9sdjQ68/gzayhozxiciAvPg0K1+6689K7teO+zcrHuq/K/dbY1NjW0LzTsru803ZpcnR1YWy2vL/J0tSjrLWrysfU2rqvyv24srjH1tC7+cDguq/K/dbQsdjQ69KqvNN2aXJ0dWFsudi8/NfWoaM8YnIgLz4NCr6tuf3Jz8PmtcS31s72ztLDx9aqtcDBy6OsztLDx9Tau/nA4LrNxcnJ+sDg1tC31rHwtqjS5cP719bP4M2so6yyzsr9srvNrLXEuq/K/aOs1Nq688PmtffTw7XEyrG68qOsseDS68b3zt63qL2ry/y0psDtzqq6r8r91tjU2KGjPGJyIC8+DQrEx8O0uq/K/biyuMfT1srHyrLDtMfpv/bE2KGjPGJyIC8+DQrG5Mq1uq/K/biyuMe31s6qwb3W1sfpv/ajujxiciAvPg0KPHN0cm9uZz4xLrbUz/O199PDuq/K/bXEx+m/9jwvc3Ryb25nPjxiciAvPg0KxcnJ+sDgttTP87X308O1xMrHxcnJ+sDgtcS4srjHuq/K/TxiciAvPg0Ku/nA4LXEttTP87X308O7+cDgtcS6r8r9PGJyIC8+DQrPwsPmv7S0+sLro7o8L3A+DQo8cHJlIGNsYXNzPQ==”brush:java;”>
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(),看一下运行结果,看会调用基类的函数还是派生类的函数:
威尼斯网址开户网站 8
因为我在基类和派生类的构造函数中都输出了语句,而且是打断点调试的,所以没有调用析构函数。
运行结果可以表明:
这里的Test()函数发生了函数覆盖。
那我们进入内存中看一下:
威尼斯网址开户网站 9
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()定义,那么我们现在看一下运行结果:
威尼斯网址开户网站 10
由结果可知,基类对象调用的是基类的函数。派生类对象调用的是什么呢?
我们进入内存中查看一下:
威尼斯网址开户网站 11
由上图我们可以看到,我们在派生类中定义了的函数,在派生类虚函数表中将基类函数覆盖了,即派生类虚函数表中绿色的部分,而派生类没有定义的函数,即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;
}

运行结果为:
威尼斯网址开户网站 12
在内存布局为:
威尼斯网址开户网站 13
由内存布局可以看出,指针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;
}

运行结果还是:
威尼斯网址开户网站 14
这就是发生了函数的隐藏
再看下第二种情况
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;
}

运行成功,结果为:
威尼斯网址开户网站 15
这就是发生了函数隐藏~
(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;
}

运行成功,结果为:
威尼斯网址开户网站 16
这也是发生了函数隐藏。
现在函数隐藏应该没有问题了吧~
总结一下前面的:
1.函数重载必须是在同一作用域的,在继承与多态这里,在基类与派生类之间是不能进行函数重载。
2.函数覆盖是多态的本质,在基类中的虚函数,在派生类定义一个同名同参数的函数,就可以用派生类新定义的函数对基类函数进行覆盖。
3.函数隐藏是发生在基类和派生类之间的,当函数同名但是不同参数的时候,不论是不是虚函数,都会发生函数隐藏。
这篇文章就暂且写到这里,本来是想对虚函数表进行深度剖析的,但是写那一篇的时候发现,会用到这里的知识,害怕初学者这里还不清楚,所以现将这些问题整理一下,再更新下一篇文章。
如有问题欢迎批评指正,人无完人,文无完文,希望大家共同进步!

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

直到今日,才发现自己对重载的认识长时间以来都是错误的。幸亏现在得以纠正,真的是恐怖万分,雷人至极。一直以来,我认为重载可以发生在基类和派生类之间,例如:

【C++】多态性(函数重载与虚函数),多态重载

  多态性就是同一符号或名字在不同情况下具有不同解释的现象。多态性有两种表现形式:

  • 编译时多态性:同一对象收到相同的消息却产生不同的函数调用,一般通过函数重载来实现,在编译时就实现了绑定,属于静态绑定。
  • 运行时多态性:不同对象在收到相同消息时产生不同的动作,一般通过虚函数来实现,只有在运行时才能实现绑定,属于动态绑定。

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

在上上一篇C++多态篇1一静态联编,动态联编、虚函数与虚函数表vtable中,我最后简单了剖析了一下虚函数表以及vptr。
而在上一篇文章C++多态篇2——虚函数表详解之从内存布局看函数重载,函数覆盖,函数隐藏中我详细介绍了虚函数的函数重载,函数覆盖以及函数隐藏的问题,其实在那一篇文章中,对单继承的虚函数已经做了十分详细的解答了,如果对前面有兴趣的人可以先看一下那篇文章。
在这一篇中,我会具体的分析一下在不同继承中(单继承,多继承)关于虚函数表在内存中的布局以及如何打印虚函数表。但是有关在虚继承也就是大名鼎鼎的带有虚函数的菱形继承,我会放到后面继续写一篇文章详细剖析它在内存中的布局。
一、虚函数表的打印
在前面的文章中我仅仅通过内存以及汇编代码中分析了虚函数表,现在我就再介绍一种查看虚函数表的方法,
即打印虚函数表
我们都知道如果类中一旦有了虚函数,那么编译器会自动给类中加四个字节的数据,这四个字节为指向虚函数表的指针,存放的是虚函数表在内存中的地址。
如果用代码表示即:

void* vftable_of_A[] = {A::v_fn, ...}; 
//A类的虚函数表
class A 
{
    const void* vftable = vftable_of_A;
    //指向虚函数表的指针vftable
    virtual void v_fn() 
    {}
};
void* vftable_of_B[] = {B::v_fn,...};
//B类的虚函数表
class B 
{ 
    const void *vftable = vftable_of_B; 
    //指向B类虚函数表的指针vftable
    vritual void v_fn() 
    {} 
};

其实上面的代码我们还需要注意两个问题:
1.为什么虚函数表指针的类型为void*?
答:上面vftable的类型之所以用void*表示,实际上一个类中所有虚函数的地址都被放到这个表中,不同虚函数对应的函数指针类型不尽相同,所以这个表的类型无法确定,但是在机器级里都是入口地址,即一个32位的数字(32位系统),等到调用时,因为编译器预先知道了函数的参数类型,返回值等,可以自动做好处理。
2.为什么虚函数表前要加const?
答:因为虚函数表是一个常量表,在编译时,编译器会自动生成,并且不会改变,所以如果有多个B类的实例,每个实例中都会有一个vftable指针,但是它们指向的是同一个虚函数表。
那么我们如何才能打印虚函数表呢?
首先我们用上一篇用过的例子。

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;
};

基类和派生类的定义如上:
那么我们再定义两个函数,分别用来打印基类虚函数表以及派生类虚函数表。

typedef void(*VTable)();//定义函数指针
void PrintBase(Base &b)
{
    VTable vtb = (VTable)(*((int *)*(int *)&b));
    //vtb就是函数的地址
    int i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*(int *)&b)) + i);
        //向后偏移四个字节
    }
    cout << "End" << endl;
}
void PrintDerive(Derive &b)
{
    VTable vtb = (VTable)(*((int *)*(int *)&b));
    //vtb就是函数的地址
    int i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*(int *)&b)) + i);
        //向后偏移四个字节
    }
    cout << "End" << endl;
}

在main函数中进行调用:

int main()
{
    Base b;
    Derive d;
    PrintBase(b);
    PrintDerive(d);
    return 0;
}

打断点一步一步运行得到:
威尼斯网址开户网站 17
现在我对上面的代码进行一下解释:
首先我们要定义一个函数指针用来调用函数。
即:

typedef void(*VTable)();//定义函数指针

接着我们将基类对象b的首四个字节给vtb变量。

VTable vtb = (VTable)(*((int *)*(int *)&b));

威尼斯网址开户网站 18vc/C0ru49rqvyv21xLXY1rfBy6GjPGJyIC8+DQrL+dLUo7o8L3A+DQo8cHJlIGNsYXNzPQ==”brush:java;”>
vtb();//先调用函数 vtb = (VTable)*(((int*)(*(int *)&b)) + i); //再偏移四个字节

这样知道vtb指向的是NULL,即到了虚函数的结束部分,就跳出循环。
这样就可以打印出虚函数表的内容了。
派生类的虚函数表打印同理,因为虚函数表在派生类中存储的机制相同。只不过形参类型变了而已。

二、单继承
因为上一篇文章中我已经讲了很多关于单继承时虚函数表的问题,在这里我不再赘述很多基础的问题,如果对这里不熟悉的人可以去看我上一篇文章,在这里我举一个简单的例子说明一下在单继承时,虚函数表的内存布局。

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()定义,那么我们现在看一下运行结果:
威尼斯网址开户网站 19
由结果可知,基类对象调用的是基类的函数。派生类对象调用的是什么呢?
我们进入内存中查看一下:
威尼斯网址开户网站 20
三、多继承
1.不带函数覆盖
先看一下下面的例子:

class Base1
{
public:
    Base1(int data = 1)
        :b1(data)
    {
        cout << "Base1()" << endl;
    }
    ~Base1()
    {
        cout << "~Base1()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base1::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base1::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base1::Test3()" << endl;
    }
    int b1;
};
class Base2
{
public:
    Base2(int data = 2)
        :b2(data)
    {
        cout << "Base2()" << endl;
    }
    ~Base2()
    {
        cout << "~Base2()" << endl;
    }
    virtual void Test4()
    {
        cout << "Base2::Test4()" << endl;
    }
    virtual void Test5()
    {
        cout << "Base2::Test5()" << endl;
    }
    virtual void Test6()
    {
        cout << "Base2::Test6()" << endl;
    }
    int b2;
};
class Derive :public Base1,public Base2
{
public:
    Derive(int data = 3)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    int d;
};

我们看一下这个多继承中派生类d的内存布局:
威尼斯网址开户网站 21
现在我们知道了内存布局,我们就可以打印出派生类的虚函数表了。
因为在多继承中有多个虚函数表,所以我们现在要改一下打印函数,下面的代码是我已经改过的。

void PrintDerive(Derive &b)
{
    VTable vtb = (VTable)(*((int *)*(int *)&b));
    //打印Derive中Base1的虚函数表
    //vtb就是函数的地址
    int i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*(int *)&b)) + i);
        //向后偏移四个字节
    }
    cout << "End" << endl;
    /*************************/
    //打印Derive中Base2的虚函数表
    vtb = (VTable)(*((int *)*((int *)&b+2)));
    //vtb就是函数的地址
     i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*((int *)&b+2))) + i);
        //向后偏移四个字节
    }
    cout << "End" << endl;
}

因为我们已经知道了派生类的内存布局,所以我们打印Base2的虚函数表的时候,改一下vtb的值即可,我现在以内存的角度剖析一下如何改变vtb的值。
威尼斯网址开户网站 22
我们将打印函数加入main函数中,打印出来的结果为:
威尼斯网址开户网站 23
2.带函数覆盖
下面我们再来看看带函数覆盖的多继承的情况:

class Base1
{
public:
    Base1(int data = 1)
        :b1(data)
    {
        cout << "Base1()" << endl;
    }
    ~Base1()
    {
        cout << "~Base1()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base1::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base1::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base1::Test3()" << endl;
    }
    int b1;
};
class Base2
{
public:
    Base2(int data = 2)
        :b2(data)
    {
        cout << "Base2()" << endl;
    }
    ~Base2()
    {
        cout << "~Base2()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base2::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base2::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base2::Test3()" << endl;
    }
    virtual void Test4()
    {
        cout << "Base2::Test4()" << endl;
    }
    virtual void Test5()
    {
        cout << "Base2::Test5()" << endl;
    }
    virtual void Test6()
    {
        cout << "Base2::Test6()" << endl;
    }
    int b2;
};
class Derive :public Base1,public Base2
{
public:
    Derive(int data = 3)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test1()
    {
        cout << "Derive::Test1()" << endl;
    }   
    void Test4()
    {
        cout << "Derive::Test4()" << endl;
    }
    int d;
};

typedef void(*VTable)();//定义函数指针
void PrintDerive(Derive &b)
{
    VTable vtb = (VTable)(*((int *)*(int *)&b));
    //打印Derive中Base1的虚函数表
    //vtb就是函数的地址
    int i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*(int *)&b)) + i);
        //向后偏移四个字节
    }
    cout << "End" << endl;
    /*************************/
    //打印Derive中Base2的虚函数表
    vtb = (VTable)(*((int *)*((int *)&b+2)));
    //vtb就是函数的地址
     i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*((int *)&b+2))) + i);
        //向后偏移四个字节
    }
    cout << "End" << endl;
}
int main()
{
    Derive d;
    PrintDerive(d);
    return 0;
}

运行结果为:
威尼斯网址开户网站 24
由结果可以得到,Test1()不仅覆盖了Base1中的Test1()函数,也覆盖了Base2类中的Test1()函数。
现在关于单继承和多继承中虚函数表的问题,应该都解释的差不多啦,如果还有问题,欢迎留言提出~

在上上一篇C++多态篇1一静态联编,动态联编、虚函数与虚函数表vtable中,我最后简…

纯虚函数

纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。纯虚函数的存在是为了更方便使用多态特性。它的一般格式如下:

class <类名>

{

virtual <类型><函数名>(<参数表>)=0;

};

 在成员函数的形参列表后面写上=0,
则成员函数为纯虚函数。纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义。凡是含有纯虚函数的类叫做抽象类。这种类不能声明对象,只是作为基类为派生类服务。除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。包含纯虚函数的类叫做抽象类(
也叫接口类) , 抽象类不能实例化出对象。
纯虚函数在派生类中重新定义以后, 派生类才能实例化出对 象。

 1 class A {
 2 public:
 3     A();
 4     virtual ~A();
 5     void f1();
 6     virtual void f2();
 7     virtual void f3()=0;
 8 };
 9 class B:public A{
10 public:
11     B();
12     virtual ~B();
13     void f1();
14     virtual void f2();
15     virtual void f3();
16 };
17 int main(int argc,char * argv[]) {
18     A *m_j = new B();
19     m_j -> f1();
20     m_j -> f2();
21     m_j -> f3();
22     delete m_j;
23     return 0;
24 }

f1()是一个隐藏,调用m_j->f1();会去调用A类中的f1(),它是在我们写好代码的时候就会定好的。也就是根据它是由A类定义的,这样就调用这个类的函数。f2()是重写(覆盖)调用m_j->f2();会调用m_j中到底保存的对象中,对应的这个函数。这是由于new的B对象(调用派生类的f2())f3()与f2()一样,只是在基类中不需要写函数实现。

1 class Person
2 {
3    vi rtual voi d Di splay () = 0; // 纯虚函数
4    protected :
5    stri ng _name ; // 姓名
6 } ;
7 class Student : publi c Person
8 { } ;
 1 class A {
 2 public:
 3          void test(int);    
 4 };
 5 class B : public A {
 6 public:
 7          void test(int, int);
 8 };
 9 
10 void main()
11 {
12        B b;
13        
14         b.test(5);  //错误,应该b.A::test(5);   
15 }

虚函数

  虚函数是在基类中用关键字virtual
标识的成员函数,可以在一个或多个派生类中被重新定义。如果一个函数被定义为虚函数,那么,即使是使用指向基类对象的指针来调用该成员函数,也能保证所调用的是正确的特定于实际对象的成员函数。这正是虚函数的优点所在。

  一旦基类的成员函数被定义成了虚函数,则该基类的派生类的同名成员函数(名字、返回值类型、参数个数及类型均相同)不管前面是否加关键字virtual
,同样也具有虚特性,同样是虚函数。定义虚函数语法格式如下:

    class <类名>
    {
      virtual <返回值类型><函数名>(<形参表>);
    };

  虚函数机制:如果通过引用或指针访问虚函数,编译时就不会确定具体的调用函数,只有在运行时才根据具体的对象类型,调用其相应的函数实现。注意:虚函数的“动态绑定虚特性”必须通过基类的指针挥着基类的引用才能够表现出来,通过对象调用虚函数不能进行动态绑定。

多态性就是同一符号或名字在不同情况下具有不同解释的现象。多态性有两种表现形式:…

总结:

1. 虚函数和纯虚函数可以定义在同一个类(class)中,含有纯虚函数的类被称为抽象类(abstract class),而只含有虚函数的类(class)不能被称为抽象类(abstract class)。 

2. 虚函数可以被直接使用,也可以被子类(sub class)重载以后以多态的形式调用,而纯虚函数必须在子类(sub class)中实现该函数才可以使用,因为纯虚函数在基类(base class)只有声明而没有定义。

3. 虚函数和纯虚函数都可以在子类(sub class)中被重载,以多态的形式被调用。

4. 虚函数和纯虚函数通常存在于抽象基类(abstract base class -ABC)之中,被继承的子类重载,目的是提供一个统一的接口。

5. 虚函数的定义形式:virtual {method body}
  纯虚函数的定义形式:virtual { } = 0; 在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时候要求前期bind,然而虚函数却是动态绑定(run-time bind),而且被两者修饰的函数生命周期(life recycle)也不一样。

6. 虚函数必须实现,如果不实现,编译器将报错,错误提示为:error LNK****: unresolved external symbol “public: virtual void __thiscall
ClassName::virtualFunctionName(void)”

7. 对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。

8. 实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。

9. 虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数

10. 多态性指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。
a.编译时多态性:通过重载函数实现
b 运行时多态性:通过虚函数实现。

11. 如果一个类中含有纯虚函数,那么任何试图对该类进行实例化的语句都将导致错误的产生,因为抽象基类(ABC)是不能被直接调用的。必须被子类继承重载以后,根据要求调用其子类的方法。

我一直认为当类B把类A中的test函数继承之后,在类B中,类A的test函数和类B自己定义的test函数是重载关系(因为我觉得这两个函数靠形参个数区分开来了),进而,我就认为第14行会调用类A的test函数。非常雷人。现在把重载和隐藏的注意事项总结出来,供理解有误的人们参考:

重载、重写、重定义

重载:

1.什么是函数重载

重载,简单说,就是函数或者方法有相同的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。一般是用于在一个类内实现若干重载的方法,这些方法的名称相同而参数形式不同。
重载的规则:
  
1、在使用重载时只能通过相同的方法名、不同的参数形式实现。不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不一样);
   2、不能通过访问权限、返回类型、抛出的异常进行重载;
   3、方法的异常类型和数目不会对重载造成影响;

成员函数被重载的特征:

(1)相同的范围(在同一个类中); 
(2)函数名字相同; 
(3)参数不同; 
(4)virtual 关键字可有可无 。

其中与其他另外两个概念最大的区别是:函数重载在同一个作用域内。

因为首先函数重载的第一个条件就没有满足,即:在相同的范围中(在同一个类中),派生类和基类是两个不同的类域,即不是同一个作用域,所以在继承中,基类和派生类之间永远不可能进行函数重载。

 1 class Base
 2 {
 3 public:
 4     Base(int data = 0)
 5         :b(data)
 6     {
 7         cout << "Base()" << endl;
 8     }
 9     ~Base()
10     {
11         cout << "~Base()" << endl;
12     }
13     void B()
14     {
15         cout << "Base::B()" << endl;
16     }
17     void B(int b)
18     {
19         cout << "Base::B(int)" << endl;
20     }
21     //B()与B(int b)构成了函数重载
22     //因为上面两个函数是在同一作用域中
23     int b;
24 };
25 class Derive :public Base
26 {
27 public:
28     Derive()
29     {
30         cout << "Derive()" << endl;
31     }
32     ~Derive()
33     {
34         cout << "~Derive()" << endl;
35     }
36     void B(int a, int b)
37     {
38         cout << "Derive::B(int,int)" << endl;
39     }
40     //不会与Base类中的两个B名的函数构成重载
41     //因为作用域不同
42 };

在一个类内,如果存在若干个同名函数,而且这些函数之间可以用形参个数或者形参类型区分开来的时候(注意不能靠函数返回类型区分),这几个函数就互为重载函数。这时,当你通过类对象调用这几个函数时,编译器就可以通过你传递的实参个数或者类型,去匹配相应的函数,而不会发生歧义。这也就是重载函数的作用所在(让你可以使用若干个同名函数)。需要注意的是:

2.什么是重写

子类可继承父类中的方法,而不需要重新编写相同的方法。但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。方法重写又称方法覆盖。重写即覆盖,是指派生类函数覆盖基类函数。例如,假设动物类存在”跑”的方法,从中派生出马和狗,马和狗的跑得形态是各不相同的,因此同样方法需要两种不同的实现,这就需要”重新编写”基类中的方法。”重写”基类方法就是修改它的实现或者说在派生类中重新编写。

重写(覆盖)的规则:
  
1、重写方法的参数列表必须完全与被重写的方法的相同,否则不能称其为重写。
  
2、重写方法的访问修饰符一定要大于被重写方法的访问修饰符(public>protected>default>private)。
  
3、重写的方法的返回值必须被重写的方法的返回值可能不同,(协变)可能是基类返回基类的指针或引用,子类返回子类的指针或引用;
  
4、重写的方法所抛出的异常必须和被重写方法的所抛出的异常一致,或者是其子类;
  
5、被重写的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行重写。

   6、静态方法不能被重写为非静态的方法(会编译出错)。

重写(覆盖)的主要特征:

(1)不同的范围(分别位于派生类与基类); 
(2)函数名字相同; 
(3)参数相同; 
(4)基类函数必须有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()
    {
        cout << "Derive::Test()" << endl;
    }
    int d;
};

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

在上面的代码中,分别在基类和派生类中定义了同名同参数的函数Test(),看一下运行结果,看会调用基类的函数还是派生类的函数:

Base()

Derive()

Derived::Test()

运行结果可以表明: 这里的Test()函数发生了函数覆盖。

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

 1 class Base
 2 {
 3 public:
 4     Base(int data = 1)
 5         :b(data)
 6     {
 7         cout << "Base()" << endl;
 8     }
 9     ~Base()
10     {
11         cout << "~Base()" << endl;
12     }
13     virtual void Test()
14     {
15         cout << "Base::Test()" << endl;
16     }
17     int b;
18 };
19 class Derive :public Base
20 {
21 public:
22     Derive(int data = 2)
23         :d(data)
24     {
25         cout << "Derive()" << endl;
26     }
27     ~Derive()
28     {
29         cout << "~Derive()" << endl;
30     }
31     void Test()
32     {
33         cout << "Derive::Test()" << endl;
34     }
35     int d;
36 };
37 
38 int main()
39 {
40     Base *pb;
41     Derive d;
42     pb = &d;
43     pb->Test();
44     return 0;
45 }

运行结果同上

多态的本质:不是重载声明而是覆盖。 虚函数调用方式:通过基类指针或引用,执行时会根据指针指向的对象的类,决定调用哪个函数。

1.重载绝对不会发生在基类和派生类之间,如上例所示。当基类和派生类中存在同名函数时,无论同名函数的形参个数或者类型是否相同,派生类中的同名函数都会将基类中的同名函数隐藏掉,因此它们是隐藏关系,而不是重载关系。关于隐藏,后边会提到。如此以来,上例的14行在编译时就会报错,提示类B中没有test(int)类型的函数。

重写总结:

1、必须是在继承体系下;

2、方法名、参数个数和参数类型 都必须相同;

3、返回值类型可以与父类相同,也可以与父类不同,但是要求返回值类型是父类的子类。如:父类的返回值类型是Object类,子类的返回值可以是Object类的子类

4、派生类重写的方法的访问权限不能低于基类的权限;

5、派生类抛出的异常应该和基类相等或比基类更小。

2.在同一个类中,重载函数之间必须依靠形参个数或者形参类型来进行区分,不能依靠返回类型。也就是说,如果同一个类中的两个同名函数形参个数和类型完全相同,但是返回值类型不同,这时候编译就会报错,因为当你通过类对象调用该同名函数时,编译器会出现二义性,不知道该选择哪个函数。记着,重载必须靠形参来区分。

3.什么是重定义

 重定义 (redefining)也叫做隐藏:
子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) 。
如果一个类,存在和父类相同的函数,那么,这个类将会覆盖其父类的方法,除非你在调用的时候,强制转换为父类类型,否则试图对子类和父类做类似重载的调用是不能成功的。 

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

 1 class Base
 2 {
 3 public:
 4     Base(int data = 1)
 5         :b(data)
 6     {
 7         cout << "Base()" << endl;
 8     }
 9     ~Base()
10     {
11         cout << "~Base()" << endl;
12     }
13     void Test()
14     {
15         cout << "Base::Test()" << endl;
16     }
17     int b;
18 };
19 class Derive :public Base
20 {
21 public:
22     Derive(int data = 2)
23         :d(data)
24     {
25         cout << "Derive()" << endl;
26     }
27     ~Derive()
28     {
29         cout << "~Derive()" << endl;
30     }
31     void Test()
32     {
33         cout << "Derive::Test()" << endl;
34     }
35     int d;
36 };
37 
38 int main()
39 {
40     Derive d;
41     d.Test();
42     return 0;
43 }

在我的另一篇文章C++中的继承(3)作用域与重定义,赋值兼容规则中对重定义也有解释。

3.在同一个类中,虚函数和虚函数,虚函数和普通函数之间也可以重载,规则完全同上。虚函数下边会提到。

总结:

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

2.函数覆盖是多态的本质在基类中的虚函数,在派生类定义一个同名同参数的函数,就可以用派生类新定义的函数对基类函数进行覆盖。

3.函数隐藏是发生在基类和派生类之间的,当函数同名但是不同参数的时候,不论是不是虚函数,都会发生函数隐藏。

隐藏:

隐藏只能出现在基类和派生类之间,而不能发生在同一个类内(比如上述2中,只会引起编译器出现二义性)。当基类和派生类中存在同名函数时,无论同名函数的形参个数或者类型是否相同,派生类中的同名函数都会将基类中的同名函数隐藏掉,而不会是重载关系。这时,当你通过派生类对象调用该同名函数时,只能访问派生类的该函数,如果硬要访问基类的该函数,则需要在函数名前加上类作用域,如上边代码所示。

覆盖:

覆盖也只能出现在基类和派生类之间,当派生类和基类中的存在同名函数,且参数个数和参数类型完全相同,并且基类中的该函数有virtual修饰(派生类中的该函数可有可无),则派生类的该函数覆盖掉基类的该函数。该性质用来实现多态。

虚函数:

在一个类中,用virtual关键字声明的函数都是虚函数。虚函数存在的唯一目的,就是为了实现多态(动态绑定/运行时绑定)。关于多态,后面会提到。虚函数只有在基类和派生类之间才能发挥虚特性(也就是说才能发挥虚函数的真正的目的)。在同一个类中,所有虚函数就和普通函数是一样,使用同样的重载规则(重载的第3点中提到过)。因此在同一个类中可以把虚函数看作普通函数来使用(因为其虚特性发挥不出来),使用方法和注意事项与普通函数一模一样。

多态:

多态是面向对象思想的精髓所在。说白了,就是通过基类指针或引用调用一个成员函数时,直到运行阶段在才能决定该成员函数是哪个派生类中定义的成员函数。有点抽象吧?没事,先看一段代码吧。

 1 class A {
 2 public:
 3         virtual void test(int);  
 4 };
 5 
 6 class B : public A {
 7 public:
 8         void test(int);
 9 };
10 
11 class C : public A {
12 public:
13         void test(int);
14 };
15 
16 void main()
17 {
18          A *a0;
19          A &a1 = b;
20          A &a2 = c;
21          B  b;
22          C  c;
23 
24          a0 = &b;
25          a0.test(2);   //调用类B的test函数
26          
27          a0 = &c;
28          a0.test(3);   //调用类C的test函数
29 
30          a1.test(4);   //调用类B的test函数
31          a2.test(5);   //调用类C的test函数
32 }

我们先说什么是多态吧,随后再讲产生多态的条件。第18行,在main函数中定义了一个指向类A类型的指针变量a0,第21和22行分别定义了派生类B和C的对象b,c。第24行,将对象b的指针赋给a0,第25行a0.test将调用类B的test函数;第27行,将对象c的指针赋给a0,第28行a0.test将调用类C的test函数。这就是多态,有感觉了吗?说白了,就是当基类指针变量指向了哪个派生类对象,就可以调用哪个派生类对象的方法。类似的,引用也可以实现多态,第19-20,30-31行所展示的。

下面总结下实现多态的条件:

哪些成员函数想要以多态的形式来执行,那么这些函数必须:

1.在基类中将这些成员函数声明为虚函数,并实现(必须要实现)。

2.在派生类中也声明这些成员函数并实现(必须实现),基类和派生类的这些函数必须同名,而且其形参个数和类型,返回值类型必须与基类中的这些函数完全相同。此时,派生类中这些函数无论是否用virtual来声明,都会被自动虚化。

3.将派生类对象赋给基类的指针变量或者引用。至此,多态实现,可用基类指针或引用调用派生类的方法(符合多态条件的方法,而不是普通方法)。

实现多态的这三个条件必须完全满足,虚函数的虚特性才能发挥出来,也才能实现多态。缺少任何一个条件,虚函数的虚特性都会被打破,无法实现多态。虚特性被打破的虚函数和普通函数是一样的,因此说虚函数的唯一用途就是实现多态。

下面举一些不是多态的例子:

a.基类中声明为虚函数,派生类中也声明为虚函数,并且也同名。但是派生类中该函数的形参类型或者形参个数和基类中的不相同。此时,多态不满足,派生类和基类的这两个虚函数仅仅是隐藏关系,没有虚特性。

b.基类中声明为虚函数,派生类中也声明了一个同名函数,但没有使用virtual,并且形参类型或者形参个数和基类不相同。这时候基类中的虚函数也丢失虚特性,派生类的该函数不会被虚化,当然也就够不成多态,这两个函数也仅仅是隐藏关系。

c.基类中的函数不是虚函数,派生类中声明为虚函数,它们同名,这时也够不成多态,派生类的虚函数没有虚特性,它们也是隐藏关系。

d.基类和派生类的两个函数同名,都是虚函数,形参的个数和类型也都相同,但是返回值类型不同,这时编译会报错,因为两个虚函数在隐藏时,返回值类型发生了冲突,因此隐藏发生错误。注意,如果这两个函数不是虚函数,这不会报错,隐藏会成功;同时,如果派生类中是虚函数,基类中不是虚函数,也不过报错,隐藏也是成功的。这也说明,虚化并隐藏时,返回值类型一定要保持相同。

 

相关文章