【C++ Primer | 15】C++虚函数表剖析①
概述
为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是如何实现动态绑定的。
C++多态实现的原理:
• 当类中声明虚函数时,编译器会在类中生成一个虚函数表 • 虚函数表是一个存储成员函数地址的数据结构 • 虚函数表是由编译器自动生成与维护的 • virtual成员函数会被编译器放入虚函数表中 • 存在虚函数表时,每个对象中都有一个指向虚函数表的指针 |
类的虚表
每个包含了虚函数的类都包含一个虚表。
我们知道,当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。
我们来看以下的代码。类A包含虚函数vfunc1,vfunc2,由于类A包含虚函数,故类A拥有一个虚表。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
图1:类A的虚表示意图
虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。
虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。
虚表指针
虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。
为了指定对象的虚表,每个对象的内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
图2:对象与它的虚表
上面指出,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。
动态绑定
说到这里,大家一定会好奇C++是如何利用虚表和虚表指针来实现动态绑定的。我们先看下面的代码。
class A
{
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
}; class B : public A
{
public:
virtual void vfunc1();
void func1();
private:
int m_data3;
}; class C: public B
{
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};
类A是基类,类B继承类A,类C又继承类B。类A,类B,类C,其对象模型如下图3所示。
图3:类A,类B,类C的对象模型
由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类A的虚表(A vtbl),类B的虚表(B vtbl),类C的虚表(C vtbl)。类A,类B,类C的对象都拥有一个虚表指针,*__vptr,用来指向自己所属类的虚表。
- 类A包括两个虚函数,故A vtbl包含两个指针,分别指向A::vfunc1()和A::vfunc2()。
- 类B继承于类A,故类B可以调用类A的函数,但由于类B重写了B::vfunc1()函数,故B vtbl的两个指针分别指向B::vfunc1()和A::vfunc2()。
- 类C继承于类B,故类C可以调用类B的函数,但由于类C重写了C::vfunc2()函数,故C vtbl的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()。
虽然图3看起来有点复杂,但是只要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。[非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。
【注意】非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。
下面以代码说明,代码如下:
#include <iostream>
using namespace std; class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
}; typedef void(*Fun)(void); //函数指针
int main()
{
Base b;
// 这里指针操作比较混乱,在此稍微解析下: // *****printf("虚表地址:%p\n", *(int *)&b); 解析*****:
// 1.&b代表对象b的起始地址
// 2.(int *)&b 强转成int *类型,为了后面取b对象的前四个字节,前四个字节是虚表指针
// 3.*(int *)&b 取前四个字节,即vptr虚表地址
// // *****printf("第一个虚函数地址:%p\n", *(int *)*(int *)&b);*****:
// 根据上面的解析我们知道*(int *)&b是vptr,即虚表指针.并且虚表是存放虚函数指针的
// 所以虚表中每个元素(虚函数指针)在32位编译器下是4个字节,因此(int *)*(int *)&b
// 这样强转后为了后面的取四个字节.所以*(int *)*(int *)&b就是虚表的第一个元素.
// 即f()的地址.
// 那么接下来的取第二个虚函数地址也就依次类推. 始终记着vptr指向的是一块内存,
// 这块内存存放着虚函数地址,这块内存就是我们所说的虚表.
//
printf("虚表地址:%p\n", *(int *)&b);
printf("第一个虚函数地址:%p\n", *(int *)*(int *)&b);
printf("第二个虚函数地址:%p\n", *((int *)*(int *)(&b) + )); Fun pfun = (Fun)*((int *)*(int *)(&b)); //vitural f();
printf("f():%p\n", pfun);
pfun(); pfun = (Fun)(*((int *)*(int *)(&b) + )); //vitural g();
printf("g():%p\n", pfun);
pfun();
}
输出结果:
通过这个示例,我们可以看到,我们可以通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:
(Fun)*((int*)*(int*)(&b)+); // Base::f()
(Fun)*((int*)*(int*)(&b)+); // Base::g()
(Fun)*((int*)*(int*)(&b)+); // Base::h()
注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
一般继承(无虚函数覆盖)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:
class Base
{
public:
virtual void f() { cout << "Base::f()" << endl; }
virtual void g() { cout << "Base::g()" << endl; }
virtual void h() { cout << "Base::h()" << endl; }
}; class Derive : public Base
{
public:
virtual void f1() { cout << "Base::f1()" << endl; }
virtual void g1() { cout << "Base::g1()" << endl; }
virtual void h1() { cout << "Base::h1()" << endl; }
};
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
对于实例:Derive d; 的虚函数表如下:
我们可以看到下面几点:
- 虚函数按照其声明顺序放于表中。
- 父类的虚函数在子类的虚函数前面。
一般继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
class Base
{
public:
virtual void f() { cout << "Base::f()" << endl; }
virtual void g() { cout << "Base::g()" << endl; }
virtual void h() { cout << "Base::h()" << endl; }
}; class Derive : public Base
{
public:
virtual void f() { cout << "Base::f1()" << endl; }
virtual void g1() { cout << "Base::g1()" << endl; }
virtual void h1() { cout << "Base::h1()" << endl; }
};
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
我们从表中可以看到下面几点,
- 覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
- 没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序:
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
class Base1
{
public:
virtual void f() { cout << "Base1::f" << endl; } //虚函数定义
virtual void g() { cout << "Base1::g" << endl; }
virtual void h() { cout << "Base1::h" << endl; }
}; class Base2
{
public:
virtual void f() { cout << "Base2::f" << endl; } //虚函数定义
virtual void g() { cout << "Base2::g" << endl; }
virtual void h() { cout << "Base2::h" << endl; }
}; class Base3
{
public:
virtual void f() { cout << "Base3::f" << endl; }
virtual void g() { cout << "Base3::g" << endl; }
virtual void h() { cout << "Base3::h" << endl; }
}; class Derive :public Base1, public Base2, public Base3 //多继承的情况——无虚继承覆盖
{
public:
virtual void f1() { cout << "Derive::f1" << endl; } //虚函数定义
virtual void g1() { cout << "Derive::g1" << endl; }
};
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
测试代码:
#include <iostream>
using namespace std; class Base1
{
public:
//虚函数定义
virtual void f() { cout << "Base1::f" << endl; }
virtual void g() { cout << "Base1::g" << endl; }
virtual void h() { cout << "Base1::h" << endl; }
}; class Base2
{
public:
//虚函数定义
virtual void f() { cout << "Base2::f" << endl; }
virtual void g() { cout << "Base2::g" << endl; }
virtual void h() { cout << "Base2::h" << endl; }
}; class Base3
{
public:
virtual void f() { cout << "Base3::f" << endl; }
virtual void g() { cout << "Base3::g" << endl; }
virtual void h() { cout << "Base3::h" << endl; }
}; //多继承的情况——无虚继承覆盖
class Derive :public Base1, public Base2, public Base3
{
public:
//虚函数定义
virtual void f1() { cout << "Derive::f1" << endl; }
virtual void g1() { cout << "Derive::g1" << endl; }
}; int main()
{
typedef void(*fun)(void); cout << "多重继承时的情况(无虚函数覆盖):" << endl;
Derive dobj;
cout << "Derive类的第一个虚函数表地址:" << (int *)(&dobj) << endl;
cout << "依次调用三个虚函数表中的虚函数:" << endl;
cout << "第一个虚函数表中的虚函数:" << endl;
fun pfun = NULL;
pfun = (fun)*((int *)*((int *)(&dobj)));
pfun();
pfun = (fun)*((int *)*((int *)(&dobj)) + );
pfun();
pfun = (fun)*((int *)*((int *)(&dobj)) + );
pfun();
pfun = (fun)*((int *)*((int*)(&dobj)) + );
pfun();
pfun = (fun)*((int *)*((int*)(&dobj)) + );
pfun();
//
cout << "第二个虚函数表中的虚函数:" << endl;
pfun = (fun)*((int *)*((int*)(&dobj) + ));
pfun();
pfun = (fun)*((int *)*((int*)(&dobj) + ) + );
pfun();
pfun = (fun)*((int *)*((int*)(&dobj) + ) + );
pfun();
//
cout << "第三个虚函数表中的虚函数:" << endl;
pfun = (fun)*((int *)*((int*)(&dobj) + ));
pfun();
pfun = (fun)*((int *)*((int*)(&dobj) + ) + );
pfun();
pfun = (fun)*((int *)*((int*)(&dobj) + ) + );
pfun(); return ;
}
输出结果:
多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
class Base1
{
public:
virtual void f() { cout << "Base1::f" << endl; }
virtual void g() { cout << "Base1::g" << endl; }
virtual void h() { cout << "Base1::h" << endl; }
}; class Base2
{
public:
virtual void f() { cout << "Base2::f" << endl; }
virtual void g() { cout << "Base2::g" << endl; }
virtual void h() { cout << "Base2::h" << endl; }
}; class Base3
{
public:
virtual void f() { cout << "Base3::f" << endl; }
virtual void g() { cout << "Base3::g" << endl; }
virtual void h() { cout << "Base3::h" << endl; }
}; class Derive : public Base1, public Base2, public Base3
{
public:
virtual void f() { cout << "Derive::f" << endl; } //唯一一个覆盖的子类函数
virtual void g1() { cout << "Derive::g1" << endl; }
};
下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f() b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
安全性
每次写C++的文章,总免不了要批判一下C++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。
一、通过父类型的指针访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:
Base1 *b1 = new Derive();
b1->f1(); //编译出错
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)
二、访问non-public的虚函数
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。如:
#include<iostream>
using namespace std; class Base {
private:
virtual void f() { cout << "Base::f" << endl; } }; class Derive : public Base { }; typedef void(*Fun)(void); int main()
{
Derive d;
Fun pFun = (Fun)*((int*)*(int*)(&d) + );
pFun();
}
输出结果:
单一的一般继承
下面,我们假设有如下所示的一个继承关系:
注意,在这个继承关系中,父类,子类,孙子类都有自己的一个成员变量。而了类覆盖了父类的f()方法,孙子类覆盖了子类的g_child()及其超类的f()。
我们的源程序如下所示:
#include<iostream>
using namespace std; class Parent {
public:
int iparent;
Parent(): iparent() {}
virtual void f() { cout << " Parent::f()" << endl; }
virtual void g() { cout << " Parent::g()" << endl; }
virtual void h() { cout << " Parent::h()" << endl; } }; class Child : public Parent {
public:
int ichild;
Child(): ichild() {}
virtual void f() { cout << "Child::f()" << endl; }
virtual void g_child() { cout << "Child::g_child()" << endl; }
virtual void h_child() { cout << "Child::h_child()" << endl; }
}; class GrandChild : public Child {
public:
int igrandchild;
GrandChild(): igrandchild() {}
virtual void f() { cout << "GrandChild::f()" << endl; }
virtual void g_child() { cout << "GrandChild::g_child()" << endl; }
virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }
}; int main()
{
typedef void(*Fun)(void); GrandChild gc; int** pVtab = (int**)&gc; cout << "[0] GrandChild::_vptr->" << endl;
for (int i = ; (Fun)pVtab[][i] != NULL; i++)
{
Fun pFun = (Fun)pVtab[][i];
cout << " [" << i << "] ";
pFun();
}
cout << "[1] Parent.iparent = " << (int)pVtab[] << endl;
cout << "[2] Child.ichild = " << (int)pVtab[] << endl;
cout << "[3] GrandChild.igrandchild = " << (int)pVtab[] << endl;
}
输出结果:
使用图片表示如下:
可见以下几个方面:
- 虚函数表在最前面的位置。
- 成员变量根据其继承和声明顺序依次放在后面。
- 在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。
参考资料
第二部分
【C++ Primer | 15】C++虚函数表剖析①的更多相关文章
- 【C++ Primer | 15】C++虚函数表剖析②
多重继承 下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系. 注意:子类只overwrite了父类的f()函数,而还有一个是自己的函数(我们这样做的目的是为了用g1()作为一个标记 ...
- C++虚函数表剖析
关键词:虚函数.虚表,虚表指针,动态绑定,多态 一.概述 为了实现C++的多态,C++使用了一种动态绑定的技术. 这个技术的核心是虚函数表(下文简称虚表).本文介绍虚函数表是怎样实现动态绑定的. 二. ...
- 深入剖析C++多态、VPTR指针、虚函数表
在讲多态之前,我们先来说说关于多态的一个基石------类型兼容性原则. 一.背景知识 1.类型兼容性原则 类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代.通过公有继承,派 ...
- 从零开始学C++之虚函数与多态(一):虚函数表指针、虚析构函数、object slicing与虚函数
一.多态 多态性是面向对象程序设计的重要特征之一. 多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为. 多态的实现: 函数重载 运算符重载 模板 虚函数 (1).静态绑定与动态绑 ...
- C++ 虚函数表解析
转载:陈皓 http://blog.csdn.net/haoel 前言 C++中 的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实 ...
- C++虚函数与虚函数表
多态性可分为两类:静态多态和动态多态.函数重载和运算符重载实现的多态属于静态多态,动态多态性是通过虚函数实现的. 每个含有虚函数的类有一张虚函数表(vtbl),表中每一项是一个虚函数的地址, 也就是说 ...
- C++的虚函数表
这里的例子全部来自陈皓的C++ 虚函数表解析,经过修改的. 编译器:g++ (Ubuntu 4.9.2-10ubuntu13) 4.9.2 环境:ubuntu 15.04 64位系统(地址占8字节) ...
- C++ 虚函数表解析(转载)
转载自:陈皓 http://blog.csdn.net/haoel/article/details/1948051/ 前言 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型 ...
- 转载:C++ 虚函数表解析
目录(?)[+] 转载:http://blog.csdn.net/haoel/article/details/1948051# 前言 C++中 的虚函数的作用主要是实现了多态的机制.关于多态,简而 ...
随机推荐
- UESTC - 1324 卿学姐与公主
题目链接 某日,百无聊赖的卿学姐打开了某11区的某魔幻游戏 在这个魔幻的游戏里,生活着一个美丽的公主,但现在公主被关押在了魔王的城堡中. 英勇的卿学姐拔出利刃冲向了拯救公主的道路. 走过了荒野,翻越了 ...
- hashmap和hashtable异同
(一)继承的历史不同 Hashtable是继承自Dictionary类的,而HashMap则是Java 1.2引进的Map接口的一个实现. public class Hashtable extends ...
- logback异步输出日志(生产者消费者模型),并非批量写入日志。
一直对logback异步输出日志误解为异步批量写入日志. 今天看了源代码. 首先logback的异步日志是如何配置的: <!-- 管理端用户行为日志异步输出,异步的log片段必须在同步段后面,否 ...
- 通用Excel文件导出工具类
1:Excel格式 2:ExcelUtil.java import java.io.ByteArrayOutputStream; import java.io.IOException; import ...
- C# 使用ffmpeg视频截图
<appSettings> <add key="ffmpeg" value="E:\ffmpeg\ffmpeg-20141012-git-20df026 ...
- yolov3实践(二)
这次给大家带来一个有趣的项目,项目主要是Python写的,基于Keras,backend是tf. 首先,当我们回顾视觉目标检测这个任务时,我们可能纠结于如何使这个项目变得更加work,我理解的更加wo ...
- 在浏览器中使用Javascript
在浏览器中点击鼠标右键——检查,选择console,可以在里面写Javascript代码,并可以在页面实时看到结果: 关于JavaScript的几个注意事项: JavaScript 语句和 JavaS ...
- idea的起步配置
工欲善其事,必先利其器 1.安装 https://www.jetbrains.com/idea/download/#section=windows 可以选择不同平台的安装包,版本一般Ultimate, ...
- 【windows核心编程】远程线程DLL注入
15.1 DLL注入 目前公开的DLL注入技巧共有以下几种: 1.注入表注入 2.ComRes注入 3.APC注入 4.消息钩子注入 5.远线程注入 6.依赖可信进程注入 7.劫持进程创建注入 8.输 ...
- script & scriptreplay
script是什么 scirpt就是一个命令,可以制作一份记录输出到终端的记录.对于那些想要真实记录终端会话的人来说,这很有用.该记录可以保存并在以后再打印出来. 怎么用 默认情况下,我们可以通过在终 ...