【C++】虚函数指针和虚函数列表

本篇文章主要来讲述,C++多态的实现原理,也就是虚函数和虚函数列表是怎么回事?它们是如何实现多态的?

  • 虚函数概述:

首先,C++多态的实现是通过关键字virtual,有了这个关键字之后,通过继承的关系就可以在程序运行时,使用子类的函数替换掉父类的函数,达到多态的作用。

C++实现虚函数的方法:为每个类对象添加一个隐藏成员,隐藏成员保存了一个指针,这个指针叫虚表指针(vptr),它指向一个虚函数表(virtual function table, vtbl)(备注:一个类对象一个虚指针,一个类对应一个虚函数列表)。

虚函数表就像一个数组,表中有许多的槽(slot),每个槽中存放的是一个虚函数的地址(可以理解为数组里存放着指向每个虚函数的指针)。如下所示:


说明:

1.虚函数列表中的最后一个.表示的是虚函数列表的结束符,类似于字符串的/0。

2.虚函数指针往往是在类对象的第一个元素。

3.对于派生类而言,如果派生类实现了基类中的虚函数,在派生类的虚函数列表中,对应的虚函数会被替换成派生类的这个函数地址。

  • 单继承的多态实例分析

例子:

#include <iostream>
using namespace std;


class Base  {
public:
    virtual void f();
    virtual void print();
};
void Base::f() {
    cout<<"Base f() ."<<endl;
}
void Base::print() {
    cout<< "Base print() ."<<endl;
}
class Derive :public Base{
public:
    void f1();
    virtual void print();
};
void Derive::f1() {
    cout<<"Derive f1() ."<<endl;
}
void Derive::print() {
    cout<< "Derive print() ."<<endl;
}
typedef void(*Fun)(void);
int main() {
  Base b;
  cout << "b:虚函数表的地址:" << (int*)(&b) << endl;
    for(int i = 0; i < 2; i++){
        unsigned long* vtbl = (unsigned long*)(*(unsigned long*)&b) + i;
        cout << "b:虚函数表的第"<<i<<"个函数地址:" << vtbl << endl;
        Fun pFun = (Fun) *(vtbl);
        pFun();
    }
    
    Base b1;
  cout << "b1:虚函数表的地址:" << (int*)(&b1) << endl;
    for(int i = 0; i < 2; i++){
        unsigned long* vtbl = (unsigned long*)(*(unsigned long*)&b1) + i;
        cout << "b1:虚函数表的第"<<i<<"个函数地址:" << vtbl << endl;
        Fun pFun = (Fun) *(vtbl);
        pFun();
    }
    
    Derive d;
    cout << "d:虚函数表的地址:" << (int*)(&d) << endl;
    for(int i = 0; i < 2; i++){
        unsigned long* vtbl = (unsigned long*)(*(unsigned long*)&d) + i;
        cout << "d:虚函数表的第"<<i<<"个函数地址:" << vtbl << endl;
        Fun pFun = (Fun) *(vtbl);
        pFun();
    }
  return 0;
}

输出结果:

b:虚函数表的地址:0x7fffd27d5e70 // 1. 对象b对应的虚指针
b:虚函数表的第0个函数地址:0x400f10 // 这里是对象b对应的虚函数列表首地址
Base f() .
b:虚函数表的第1个函数地址:0x400f18
Base print() .
b1:虚函数表的地址:0x7fffd27d5e80 // 2. 对象b1对应的虚指针
b1:虚函数表的第0个函数地址:0x400f10 // 这里是对象b1对应的虚函数列表首地址
Base f() .
b1:虚函数表的第1个函数地址:0x400f18
Base print() .
d:虚函数表的地址:0x7fffd27d5e90 // 3. 对象d对应的虚指针
d:虚函数表的第0个函数地址:0x400ef0 // 这里是对象d对应的虚函数列表首地址
Base f() .
d:虚函数表的第1个函数地址:0x400ef8
Derive print() .


运行结果分析:

1. 虚指针是跟对象绑定的,每一个类对象会对应一个虚指针,这个原因应该是虚指针是作为类的一个数据存储的导致的。例子参考 Base b和b1两个对象的虚指针地址,明显是不相同的。

2. 虚函数列表跟类是绑定的,每一个类会生成一个虚函数列表的地址,应该是存储在全局数据区。

3. 基类的虚函数列表和继承类的虚函数列表是两个,是不相同的,继承类的虚函数列表中存储的是继承类的虚函数实现,如果继承类没有实现基类的虚函数的话,会存储基类的虚函数地址。例子参见继承类的执行结果。

  • 多继承的多态实例分析

例子:

#include <iostream>
using namespace std;


class Base  {
public:
    virtual void f();
    virtual void print();
};
void Base::f() {
    cout<<"Base f() ."<<endl;
}
void Base::print() {
    cout<< "Base print() ."<<endl;
}
class Base1  {
public:
    virtual void f1();
    virtual void print();
};
void Base1::f1() {
    cout<<"Base1 f() ."<<endl;
}
void Base1::print() {
    cout<< "Base1 print() ."<<endl;
}
class Derive :public Base,public Base1{
public:
    virtual void f();
    virtual void f1();
};
void Derive::f() {
    cout<<"Derive f() ."<<endl;
}
void Derive::f1() {
    cout<<"Derive f1() ."<<endl;
}


typedef void(*Fun)(void);
int main() {
    void *p;
    Derive d;
    cout<<"d中虚指针数量:"<<sizeof(d)/sizeof(p)<<endl;
    unsigned long** vtbl = (unsigned long**)(&d);
    cout << "d:虚函数表的地址:" << (int*)(&d) << endl;
    for(int i = 0; i < 2; i++){
        for (int j = 0; j< 2; j++) {
            cout << "d:虚函数表的第["<<i<<"]["<<j<<"]个函数地址:" << &vtbl[i][j] << endl;
            Fun pFun = (Fun) (vtbl[i][j]);
            pFun();
        }
    }
  return 0;
}

输出结果:

d中虚指针数量:2 // 1. 这里可以看出是2个虚函数指针,对应于基类数量
d:虚函数表的地址:0x7fff5934cc80
d:虚函数表的第[0][0]个函数地址:0x400e90 // 2.第一个基类的虚函数表首地址
Derive f() .
d:虚函数表的第[0][1]个函数地址:0x400e98
Base print() .
d:虚函数表的第[1][0]个函数地址:0x400eb8 // 3.第二个基类的虚函数首地址
Derive f1() .
d:虚函数表的第[1][1]个函数地址:0x400ec0
Base1 print() .


执行结果分析:

通过上面执行结果,我们可以看出多继承的情况下,继承类对象中的虚函数指针个数是虚基类的数量。同样,如果继承类实现了虚基类中的虚函数的话,会被替换成继承类中实现的函数。

  • C++多态的副作用

C++采用虚函数和虚函数列表的方式来实现多态,确实给我们带来了很大的好处,让我们可以在不改变代码的时候,就能直接替换成运行的继承类的函数。

     同样这种实现策略,却也带来了隐患,我们可以通过上面例子的方式来访问基类所有的虚函数,就算这个人虚函数被设置成了private也不行,所以让C++的封装行遭到了破坏。


(友情说明:Go语言系列一周会出1到2篇文章,并没有停止更新;C++最近有些囤货,尽量一天一篇文章。)


公众号 “灰子学技术”:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值