前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【C++】虚函数指针和虚函数列表

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

作者头像
灰子学技术
发布2020-04-02 21:11:25
1.3K0
发布2020-04-02 21:11:25
举报
文章被收录于专栏:灰子学技术灰子学技术

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

  • 虚函数概述:

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

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

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

说明:

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

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

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

  • 单继承的多态实例分析

例子:

代码语言:javascript
复制
#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;}

输出结果:

代码语言:javascript
复制
b:虚函数表的地址:0x7fffd27d5e70 // 1. 对象b对应的虚指针b:虚函数表的第0个函数地址:0x400f10 // 这里是对象b对应的虚函数列表首地址Base f() .b:虚函数表的第1个函数地址:0x400f18Base print() .b1:虚函数表的地址:0x7fffd27d5e80 // 2. 对象b1对应的虚指针b1:虚函数表的第0个函数地址:0x400f10 // 这里是对象b1对应的虚函数列表首地址Base f() .b1:虚函数表的第1个函数地址:0x400f18Base print() .d:虚函数表的地址:0x7fffd27d5e90 // 3. 对象d对应的虚指针d:虚函数表的第0个函数地址:0x400ef0 // 这里是对象d对应的虚函数列表首地址Base f() .d:虚函数表的第1个函数地址:0x400ef8Derive print() .

运行结果分析:

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

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

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

  • 多继承的多态实例分析

例子:

代码语言:javascript
复制
#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;}

输出结果:

代码语言:javascript
复制
d中虚指针数量:2 // 1. 这里可以看出是2个虚函数指针,对应于基类数量d:虚函数表的地址:0x7fff5934cc80d:虚函数表的第[0][0]个函数地址:0x400e90 // 2.第一个基类的虚函数表首地址Derive f() .d:虚函数表的第[0][1]个函数地址:0x400e98Base print() .d:虚函数表的第[1][0]个函数地址:0x400eb8 // 3.第二个基类的虚函数首地址Derive f1() .d:虚函数表的第[1][1]个函数地址:0x400ec0Base1 print() .

执行结果分析:

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

  • C++多态的副作用

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

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


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


本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-02-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 灰子学技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档