virtual之虚函数,虚继承
- 当类中包含虚函数时,则该类每个对象中在内存分配中除去数据外还包含了一个虚函数表指针(vfptr),指向虚函数表(vftable),虚函数表中存放了该类包含的虚函数的地址。
- 当子类通过虚继承的方式从父类中派生出来,此时称父类为子类的虚基类。子类中将包含虚基表指针(vbptr),指向虚基类表(vbtable)
- 在单继承形式下,子类将完全获得父类的虚函数表和数据(假入父类中有虚函数的话)。如果子类中重写了父类的虚函数,就会在虚函数表中原本记录父类中虚函数的地址覆盖为子类中对应的重定义后的该函数地址,否则不做变动。如果在子类中定义了新的虚函数,则虚函数表中会追加一条记录,记录该函数的地址(虚函数表中是顺序存放虚函数地址的,记住,虽然虚函数表中添加了内容,但是此时对于该类的大小来说并未发生改变,因为始终只有一个指向虚函数表的指针vfptr)。
- 如果在派生类对象直接访问自身中的重定义的虚函数是不会触发多态机制的,因为这个函数调用在编译时期是可以确定的,编译器只需要直接调用即可;
- 当对父类指针赋予不同的子类指针时,在调用子类中重定义的虚函数时才会触发多态机制,即动态的在运行期间调用属于子类的该函数。(可适度地了解下覆盖override和重载overload的区别)
- 在多重继承形式下,派生类会把所有的父类按继承时的顺序包含在自身内部。每个父类对应一个单独的虚函数表。多重继承下,子类不再具有自身的虚函数表,它的虚函数表与第一个父类的虚函数表合并了。同样的,如果子类重写了任意父类的虚函数,都会覆盖对应的函数地址记录。如果如果两个虚基类都有该函数那么两个虚函数表的记录都需要被覆盖!
- 此时由于多重继承下将存在对公共基类的多份拷贝问题,为了节省内存空间,故而出现了虚拟继承,那么将只生成一个共享的基类。
- 虚继承的引入把对象的模型变得复杂,除了每个基类和公共基类的虚函数表指针vfptr需要记录外,每个虚拟继承了的父类还需要记录一个虚基类表vbtable的指针vbptr。
- 虚基类表每项记录了被继承的虚基类子对象相对于虚基类表指针的偏
来自:https://blog.csdn.net/u012209626/article/details/48682555
虚函数开销
- 每一个包含虚函数的类都需要专门的空间存放这个类的虚函数表。
- 需要在包含虚函数的类的每个对象中放置一个额外的指针。
- 必须放弃内联
特性 | 增加对象大小 | 增加类的大小 | 减少内联 |
虚函数 | 是 | 是 | 是 |
多继承 | 是 | 是 | 否 |
虚基类 | 经常 | 有时 | 否 |
RTTI | 否 | 是 | 否 |
虚函数与虚继承的具体实现
一、基本对象模型
class MyClass
{
int var;
public:
virtual void fun()
{}
};
编译输出的MyClass对象结构如下:
> class MyClass size():
> +---
> | {vfptr}
> | var
> +---
>
> MyClass::$vftable@:
> | &MyClass_meta
> |
> | &MyClass::fun
>
> MyClass::fun this adjustor:
从这段信息中我们看出,MyClass对象大小是8个字节。前四个字节存储的是虚函数表的指针vfptr,后四个字节存储对象成员var的值。虚函数表的大小为4字节,就一条函数地址,即虚函数fun的地址,它在虚函数表vftable的偏移是0。
因此,MyClass对象模型的结果如图1所示
图1 MyClass对象模型
MyClass的虚函数表虽然只有一条函数记录,但是它的结尾处是由4字节的0作为结束标记的。
adjust表示虚函数机制执行时,this指针的调整量,假如fun被多态调用的话,那么它的形式如下:
*(this+0)[0]()
总结虚函数调用形式,应该是:
*(this指针+调整量)[虚函数在vftable内的偏移]()
二、单重继承对象模型
我们定义一个继承于MyClass类的子类MyClassA,它重写了fun函数,并且提供了一个新的虚函数funA。
class MyClassA:public MyClass
{
int varA;
public:
virtual void fun()
{}
virtual void funA()
{}
};
它的对象模型为:
> class MyClassA size():
> +---
> | +--- (base class MyClass)
> | | {vfptr}
> | | var
> | +---
> | varA
> +---
>
> MyClassA::$vftable@:
> | &MyClassA_meta
> |
> | &MyClassA::fun
> | &MyClassA::funA
>
> MyClassA::fun this adjustor:
> MyClassA::funA this adjustor:
可以看出,MyClassA将基类MyClass完全包含在自己内部,包括vfptr和var。并且虚函数表内的记录多了一条——MyClassA自己定义的虚函数funA。它的对象模型如图2所示。
图2 MyClassA对象模型
我们可以得出结论:在单继承形式下,子类的完全获得父类的虚函数表和数据。子类如果重写了父类的虚函数(如fun),就会把虚函数表原本fun对应的记录(内容MyClass::fun)覆盖为新的函数地址(内容MyClassA::fun),否则继续保持原本的函数地址记录。如果子类定义了新的虚函数,虚函数表内会追加一条记录,记录该函数的地址(如MyClassA::funA)。
使用这种方式,就可以实现多态的特性。假设我们使用如下语句:
MyClass*pc=new MyClassA;
pc->fun();
编译器在处理第二条语句时,发现这是一个多态的调用,那么就会按照上边我们对虚函数的多态访问机制调用函数fun。
*(pc+0)[0]()
因为虚函数表内的函数地址已经被子类重写的fun函数地址覆盖了,因此该处调用的函数正是MyClassA::fun,而不是基类的MyClass::fun。
如果使用MyClassA对象直接访问fun,则不会出发多态机制,因为这个函数调用在编译时期是可以确定的,编译器只需要直接调用MyClassA::fun即可。
三、多重继承对象模型
和前边MyClassA类似,我们也定义一个类MyClassB
class MyClassB:public MyClass
{
int varB;
public:
virtual void fun()
{}
virtual void funB()
{}
};
它的对象模型和MyClassA完全类似,这里就不再赘述了。
为了实现多重继承,我们再定义一个类MyClassC。
class MyClassC:public MyClassA,public MyClassB
{
int varC;
public:
virtual void funB()
{}
virtual void funC()
{}
};
为了简化,我们让MyClassC只重写父类MyClassB的虚函数funB,它的对象模型如下:
> class MyClassC size():
> +---
> | +--- (base class MyClassA)
> | | +--- (base class MyClass)
> | | | {vfptr}
> | | | var
> | | +---
> | | varA
> | +---
> | +--- (base class MyClassB)
> | | +--- (base class MyClass)
> | | | {vfptr}
> | | | var
> | | +---
> | | varB
> | +---
> | varC
> +---
>
> MyClassC::$vftable@MyClassA@:
> | &MyClassC_meta
> |
> | &MyClassA::fun
> | &MyClassA::funA
> | &MyClassC::funC
>
> MyClassC::$vftable@MyClassB@:
> | -
> | &MyClassB::fun
> | &MyClassC::funB
>
> MyClassC::funB this adjustor:
> MyClassC::funC this adjustor:
上述红色数据表示重复数据,这也是钻石继承造成的问题---代码冗余和二义性
和单重继承类似,多重继承时MyClassC会把所有的父类全部按序包含在自身内部。而且每一个父类都对应一个单独的虚函数表。MyClassC的对象模型如图3所示。
图3 MyClassC对象模型
多重继承下,子类不再具有自身的虚函数表,它的虚函数表与第一个父类的虚函数表合并了。同样的,如果子类重写了任意父类的虚函数,都会覆盖对应的函数地址记录。如果MyClassC重写了fun函数(两个父类都有该函数),那么两个虚函数表的记录都需要被覆盖!在这里我们发现MyClassC::funB的函数对应的adjust值是12,按照我们前边的规则,可以发现该函数的多态调用形式为:
*(this+12)[1]()
此处的调整量12正好是MyClassB的vfptr在MyClassC对象内的偏移量。
虚函数表,虚函数继承实现:https://blog.csdn.net/best_fiends_zxh/article/details/59111761
虚拟继承是为了解决多重继承下公共基类的多份拷贝问题。比如上边的例子中MyClassC的对象内包含MyClassA和MyClassB子对象,但是MyClassA和MyClassB内含有共同的基类MyClass。为了消除MyClass子对象的多份存在,我们需要让MyClassA和MyClassB都虚拟继承于MyClass,然后再让MyClassC多重继承于这两个父类。相对于上边的例子,类内的设计不做任何改动,先修改MyClassA和MyClassB的继承方式:
class MyClassA:virtual public MyClass
class MyClassB:virtual public MyClass
class MyClassC:public MyClassA,public MyClassB
由于虚继承的本身语义,MyClassC内必须重写fun函数,因此我们需要再重写fun函数。这种情况下,MyClassC的对象模型如下:
> class MyClassC size():
> +---
> | +--- (base class MyClassA)
> | | {vfptr}
> | | {vbptr}
> | | varA
> | +---
> | +--- (base class MyClassB)
> | | {vfptr}
> | | {vbptr}
> | | varB
> | +---
> | varC
> +---
> +--- (virtual base MyClass)
> | {vfptr}
> | var
> +---
>
> MyClassC::$vftable@MyClassA@:
> | &MyClassC_meta
> |
> | &MyClassA::funA
> | &MyClassC::funC
>
> MyClassC::$vftable@MyClassB@:
> | -
> | &MyClassC::funB
>
> MyClassC::$vbtable@MyClassA@:
> | -
> | (MyClassCd(MyClassA+)MyClass)
>
> MyClassC::$vbtable@MyClassB@:
> | -
> | (MyClassCd(MyClassB+)MyClass)
>
> MyClassC::$vftable@MyClass@:
> | -
> | &MyClassC::fun
>
> MyClassC::fun this adjustor:
> MyClassC::funB this adjustor:
> MyClassC::funC this adjustor:
>
> vbi: class offset o.vbptr o.vbte fVtorDisp
> MyClass
虚继承的引入把对象的模型变得十分复杂,除了每个基类(MyClassA和MyClassB)和公共基类(MyClass)的虚函数表指针需要记录外,每个虚拟继承了MyClass的父类还需要记录一个虚基类表vbtable的指针vbptr。MyClassC的对象模型如图4所示。
图4 MyClassC对象模型
注意:对于在对象中存取虚基类的问题,虚基类表仅是Microsoft编译器的解决办法。在其他编译器中,一般采用在虚函数表中放置虚基类的偏移量的方式。
一般编译器实现动态多态方法:
1、通过vbptr找到对象的vtbl(this指针的调整量);
2、找到vfptr中对应函数的指针(虚函数表中记录的个数);
3、调用对象中指针指向的函数
*(this指针+调整量)[虚函数在vftable内的偏移]()
MyClassC中的fun()函数直接继承与基类!!!
虚基类表每项记录了被继承的虚基类子对象相对于虚基类表指针的偏移量。比如MyClassA的虚基类表第二项记录值为24,正是MyClass::vfptr相对于MyClassA::vbptr的偏移量,同理MyClassB的虚基类表第二项记录值12也正是MyClass::vfptr相对于MyClassA::vbptr的偏移量。
和虚函数表不同的是,虚基类表的第一项记录着当前子对象相对与虚基类表指针的偏移。MyClassA和MyClassB子对象内的虚表指针都是存储在相对于自身的4字节偏移处,因此该值是-4。假定MyClassA和MyClassC或者MyClassB内没有定义新的虚函数,即不会产生虚函数表,那么虚基类表第一项字段的值应该是0。
通过以上的对象组织形式,编译器解决了公共虚基类的多份拷贝的问题。通过每个父类的虚基类表指针,都能找到被公共使用的虚基类的子对象的位置,并依次访问虚基类子对象的数据。至于虚基类定义的虚函数,它和其他的虚函数的访问形式相同,本例中,如果使用虚基类指针MyClass*pc访问MyClassC对象的fun,将会被转化为如下形式:
*(pc+28)[0]()
通过以上的描述,我们基本认清了C++的对象模型。尤其是在多重、虚拟继承下的复杂结构。通过这些真实的例子,使得我们认清C++内class的本质,以此指导我们更好的书写我们的程序。本文从对象结构的角度结合图例为大家阐述对象的基本模型,和一般描述C++虚拟机制的文章有所不同。
来自http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/14/2859064.html
注意;两个函数的返回类型,参数类型,参数个数都得相同,不然就起不到多态的作用
#include<iostream>
#include<cmath>
using namespace std; class A
{
public :
virtual void fun(int x)
{
cout << "A: " << x << endl;
}
};
class B :public A
{
public :
virtual void fun(float x)
{
cout << "B: " << x << endl;
}
}; void test(A & x)
{
int i = ;
x.fun(i);
float a = 2.0;
x.fun(a);
} int main()
{
A a;
B b;
test(a);
test(b);
return ;
}
但是有一种特殊的情况,那就是如果基类中虚函数返回一个基类指针或引用,派生类中返回一个派生类的指针或引用,则c++将其视为同名虚函数而进行迟后联编。
#include<iostream>
#include<cmath>
using namespace std; class A
{
public :
virtual A * fun()
{
cout << "A: " << endl;
return this;
}
};
class B :public A
{
public :
virtual B * fun()
{
cout << "B: " << endl;
return this;
}
}; void test(A & x)
{
x.fun();
} int main()
{
A a;
B b;
test(a);
test(b);
return ;
}
虚函数在g++中的实现
因为vptr明确的属于一个实例,所以vptr的赋值理应放在类的构造函数中。 g++为每个有虚函数的类在构造函数末尾中隐式的添加了为vptr赋值的操作,vtbl的生成并不是运行时的,而是在编译期就已经确定了存放在这两个地址上的,这个地址属于.rodata(只读数据段)。所以g++在编译期就为每个类确定了vtbl的内容,并且在构造函数中添加相应代码使vptr能够指向已经填好的vtbl的地址。
来自:https://www.tuicool.com/articles/iUB3Ebi
继承是如何实现的:https://blog.csdn.net/dream_1996/article/details/68931347
virtual之虚函数,虚继承的更多相关文章
- 【整理】C++虚函数及其继承、虚继承类大小
参考文章: http://blog.chinaunix.net/uid-25132162-id-1564955.html http://blog.csdn.net/haoel/article/deta ...
- C++ - 类的虚函数\虚继承所占的空间
类的虚函数\虚继承所占的空间 本文地址: http://blog.csdn.net/caroline_wendy/article/details/24236469 char占用一个字节, 但不满足4的 ...
- C++学习 - 虚表,虚函数,虚函数表指针学习笔记
http://blog.csdn.net/alps1992/article/details/45052403 虚函数 虚函数就是用virtual来修饰的函数.虚函数是实现C++多态的基础. 虚表 每个 ...
- C++解析(25):关于动态内存分配、虚函数和继承中强制类型转换的疑问
0.目录 1.动态内存分配 1.1 new和malloc的区别 1.2 delete和free的区别 2.虚函数 2.1 构造函数与析构函数是否可以成为虚函数? 2.2 构造函数与析构函数是否可以发生 ...
- 虚函数&&虚继承
如果说没有虚函数的虚继承只是一个噩梦的话,那么这里就是真正的炼狱.这个C++中最复杂的继承层次在VS上的实现其实我没有完全理解,摸爬滚打了一番也算得出了微软的实现方法吧,至于一些刁钻的实现方式我也想不 ...
- C++基础 (6) 第六天 继承 虚函数 虚继承 多态 虚函数
继承是一种耦合度很强的关系 和父类代码很多都重复的 2 继承的概念 3 继承的概念和推演 语法: class 派生类:访问修饰符 基类 代码: … … 4 继承方式与访问控制权限 相对的说法: 爹派生 ...
- C#中的虚函数及继承关系
转载:http://blog.csdn.net/suncherrydream/article/details/8423991 若一个实例方法声明前带有virtual关键字,那么这个方法就是虚方法. 虚 ...
- C++继承-重载-多态-虚函数
C++ 继承 基类 & 派生类 一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数.定义一个派生类,我们使用一个类派生列表来指定基类.类派生列表以一个或多个基类命名,形式如下: ...
- 虚函数&纯虚函数&抽象类&虚继承
C++ 虚函数&纯虚函数&抽象类&接口&虚基类 1. 多态 在面向对象语言中,接口的多种不同实现方式即为多态.多态是指,用父类的指针指向子类的实例(对象),然后通过 ...
随机推荐
- 全--教程API, gem 'rest-client'(用于发简单请求); 请求测试;
安装:rest-client4400✨ gem install rest-client 一个简单的HTTP和REST client for Ruby. 可以用它来发HTTP请求 基本用法: requi ...
- iometer测试磁盘IO性能
of Outstanding I/Os per target – 被选中worker的每个磁盘一次所允许的未处理的异步I/O的数量.模拟测试多个应用向 IO 请求读写,默认是 1.通常不用这个参数,除 ...
- Friendly Date Ranges
让日期区间更友好! 把常见的日期格式如:YYYY-MM-DD 转换成一种更易读的格式. 易读格式应该是用月份名称代替月份数字,用序数词代替数字来表示天 (1st 代替 1). 记住不要显示那些可以被推 ...
- Qt:表格 tableWidget
1.设置行数和列数 //设置行数 tableWidget->setRowCount(); //设置列数 tableWidget->setColumnCount(); 2.隐藏表头 tabl ...
- 由浅入深了解EventBus:(一)
概述 由greenrobot织贡献(该组织还贡献了greenDAO),一个Android事件发布/订阅轻量级框架; EventBus是一个消息总线,以观察者模式实现,用于简化程序的组件.线程通信,可以 ...
- 微信小程序单个倒计时效果
var end_time = grouponList.expire_time.replace(/-/g, '/') grouponcountdown(that, end_time) //适用于商品列表 ...
- 2016年度,这40项IT技能年薪轻松超过10万美元
众所周知,科技行业聚集了大批高薪职位,但这同样也是一个快速变化的市场.今天的热门技能明天就有可能惨遭淘汰. 求职网站Dice.com最近发布了<2016薪酬调查>, 列举了年薪最高的各种科 ...
- uname命令行
常用命令uname -v # uname -i #uname -a dream361@master:~$ uname -n #主机名称 master dream361@master:~$ uname ...
- C语言动态库和静态库的使用及实践
转自:https://www.cnblogs.com/CoderTian/p/5902154.html 1.C语言中的链接器 (1)每个 C 语言源文件被编译后生成目标文件,这些目标文件最终要被链接 ...
- 【Shell脚本】逐行处理文本文件
经常会对文体文件进行逐行处理,在Shell里面如何获取每行数据,然后处理该行数据,最后读取下一行数据,循环处理.有多种解决方法如下: 1.通过read命令完成. read命令接收标准输入,或其他文件描 ...