用汇编的角度剖析c++的virtual
多态是c++的关键技术,背后的机制就是有一个虚函数表,那么这个虚函数表是如何存在的,又是如何工作的呢?
当然不用的编译器会有不同的实现机制,本文只剖析vs2015的实现。
单串继承
首先看一段简单的代码:
class A {
private:
int a_value;
public:
A() {};
virtual ~A() {};
virtual void my_echo() { std::cout << "A::my_echo" << std::endl; };
virtual void echo() { std::cout << "A::echo" << std::endl; };
virtual void print() { std::cout << "A::print" << std::endl; };
};
class B :public A {
public:
B() {};
~B() {};
int b_value;
virtual void echo()override { std::cout << "B::echo" << std::endl; };
virtual void print()override { std::cout << "B::print" << std::endl; };
void B_Fun() { std::cout << "B::B_Fun" << std::endl; };
};
class C :public B {
private:
int c_value;
public:
C() {};
~C() {};
virtual void echo()override { std::cout << "C::echo" << std::endl; };
virtual void my_print() { std::cout << "C::print" << std::endl; };
};
C继承B,B继承A。
先回顾下类的内存布局,看类C的内存布局。
class C size(16):
+---
0 | +--- (base class B)
0 | | +--- (base class A)
0 | | | {vfptr}
4 | | | a_value
| | +---
8 | | b_value
| +---
12 | c_value
+---
C::$vftable@:
| &C_meta
| 0
0 | &C::{dtor}
1 | &A::my_echo
2 | &C::echo
3 | &B::print
4 | &C::my_print
如上所示,虚函数表指针
在首地址,占用一个指针大小,值得注意的是虚函数表首位指针是D的析构函数
,那是因为基类有虚析构函数。
来个简单调用。
A* b = new C();
b->print();//输出为 B::print
print
这个虚函数到底是怎么执行的?又是如何达到多态效果的呢?
再看print
调用的汇编代码。
32位系统
b->print();
00EF6306 mov eax,dword ptr [b]
00EF6309 mov edx,dword ptr [eax]
00EF630B mov esi,esp
00EF630D mov ecx,dword ptr [b]
00EF6310 mov eax,dword ptr [edx+0Ch]
00EF6313 call eax
64位系统
b->print();
00007FF6336D2CBD mov rax,qword ptr [b]
00007FF6336D2CC1 mov rax,qword ptr [rax]
00007FF6336D2CC4 mov rcx,qword ptr [b]
00007FF6336D2CC8 call qword ptr [rax+18h]
32位解析
00EF6306 mov eax, dword ptr[b]
#将指针b变量指向内存地址的dword大小赋值给eax,实际就是获得b指向的地址,之后eax为b对象的实际地址,
00EF6309 mov edx, dword ptr[eax]
#将地址eax指向的内存地址,取出一个dword赋值给edx,之后edx就是虚函数表的首地址,
00EF630D mov ecx, dword ptr[b]
#同上,将b实际指向的地址赋值给ecx,这句话作用是为了成员函数活得所需的this指针,为call做参数
00EF6310 mov eax, dword ptr[edx+0Ch]
#这里是将edx +12,因为是32位,所以这里是在虚函数表头地址向后偏移了4个函数指针,此时的地址就是虚函数print的指针地址了
#然后将print指针赋值给eax
00EF6313 call eax
#调用print函数
64的汇编与32位相差不大,值得注意的是64位系统指针是8字节,所以是18h大小。可以看出在简单继承情况下,虚函数指针都是在首位的,而且虚函数表事一个共用表
。
比如上面的单继承C=>B=>A,在new出C后,转换为基类B或者A时,是共用的一个虚函数表,接下来用一段代码来证明。
#ifndef _WIN64
typedef unsigned int pointer;//32位指针
#else
typedef unsigned long long pointer;//64位指针
#endif
//------------------------------
B b;
B* b1 = new B();
A* a = new A();
A *a1 = dynamic_cast <A*>(b1);
B* b2 = dynamic_cast<B*>(a1);
pointer vfptr_b = *(pointer*)&b;
pointer vfptr_b1 = *(pointer*)b1;
pointer vfptr_a = *(pointer*)a;
pointer vfptr_a1 = *(pointer*)a1;
pointer vfptr_b2 = *(pointer*)b2;
std::cout
<< vfptr_b << "\n"
<< vfptr_b1 << "\n"
<< vfptr_a << "\n"
<< vfptr_a1 << "\n"
<< vfptr_b2 << std::endl;
上面的代码意思就是将各种类型强行转换为指针,得到虚函数表的地址,从结果我们看到,只有vfptr_a不一样,也就是new A的虚函数表不一样,其余的,特比是dynamic_cast <A*>
转换类型的虚函数表地址,还是为B的虚函数表地址。
我们就可以得到一个简单结论,当是单继承时,子类指针转换为父类指针,指针地址不变,虚函数表不变,父类指针只用虚函数表的前半段。
接下来从汇编结合虚函数表来分析单继承多态实现的原理。
B* b = new C();
b->print();
A *a = dynamic_cast <A*>(b);
a->echo();
B* b2 = new B();
b2->echo();
B* b = new C();
008B660D push 10h
008B660F call operator new (08B131Bh) //new 开辟内存
...........
008B6633 call C::C (08B14D3h) //调用构造
...........
00C06663 mov dword ptr [b],ecx //赋值给b
b->print();
008B6666 mov eax,dword ptr [b] //得到对象地址
008B6669 mov edx,dword ptr [eax] //得到虚函数表首地址
008B666D mov ecx,dword ptr [b] //this 指针
008B6670 mov eax,dword ptr [edx+0Ch] //虚函数表中的print函数实际地址
008B6673 call eax //调用print
A *a = dynamic_cast <A*>(b);
008B667C mov eax,dword ptr [b]
008B667F mov dword ptr [a],eax //编译器知道B是A的子类,所以,b指针转换为a指针,直接copy指针地址。
a->echo();
008B6682 mov eax,dword ptr [a]
008B6685 mov edx,dword ptr [eax]
008B6689 mov ecx,dword ptr [a] //this指针
008B668C mov eax,dword ptr [edx+8] //echo在表里的偏移
008B668F call eax //执行call
B* b2 = new B();
...........
00E5669A call operator new (0E5131Bh)
...........
00E566EE mov dword ptr [b2],ecx
b2->echo();
00E566F1 mov eax,dword ptr [b2]
00E566F4 mov edx,dword ptr [eax]
00E566F8 mov ecx,dword ptr [b2]
00E566FB mov eax,dword ptr [edx+8] //echo函数在表里的偏移,
00E566FE call eax
new的对象C转换基类B,再转换为基类A,a->echo
如何输出C::echo
?下面画个图,描述了单一继承时候的虚函数表运行机制。
多继承
多继承和单继承在虚函数表处理上有很大不同,先看代码。
class Base {
private:
int base_value;
public:
Base() {};
virtual ~Base() {};
virtual void base_echo() { std::cout << "Base::base_echo" << std::endl; };
};
class A {
private:
int a_value;
public:
A() {};
virtual ~A() {};
virtual void a_echo() { std::cout << "A::a_echo" << std::endl; };
virtual void a_print() { std::cout << "A::a_print" << std::endl; };
};
class B {
public:
B() {};
virtual ~B() {};
int b_value;
virtual void b_echo() { std::cout << "B::b_echo" << std::endl; };
virtual void b_print() { std::cout << "B::b_print" << std::endl; };
};
class C :public A,public B, public Base {
private:
int c_value;
public:
C() { };
~C() {};
virtual void a_echo()override { std::cout << "C::a_echo" << std::endl; };
virtual void b_print()override { std::cout << "C::b_print" << std::endl; };
virtual void c_echo() { std::cout << "C::c_echo" << std::endl; };
};
C类同时继承了A,B,Base三个类,此时内存和虚函数是怎样的呢?
A,B,Base原有固定各自虚函数表
:
Base::$vftable@:
| &Base_meta
| 0
0 | &Base::{dtor}
1 | &Base::base_echo
A::$vftable@:
| &A_meta
| 0
0 | &A::{dtor}
1 | &A::a_echo
2 | &A::a_print
B::$vftable@:
| &B_meta
| 0
0 | &B::{dtor}
1 | &B::b_echo
2 | &B::b_print
然后再看C类的内存布局和虚函数表:
class C size(28):
+---
0 | +--- (base class A)
0 | | {vfptr}
4 | | a_value
| +---
8 | +--- (base class B)
8 | | {vfptr}
12 | | b_value
| +---
16 | +--- (base class Base)
16 | | {vfptr}
20 | | base_value
| +---
24 | c_value
+---
C::$vftable@A@:
| &C_meta
| 0
0 | &C::{dtor}
1 | &C::a_echo
2 | &A::a_print
3 | &C::c_echo
C::$vftable@B@:
| -8
0 | &thunk: this-=8; goto C::{dtor}
1 | &B::b_echo
2 | &C::b_print
C::$vftable@Base@:
| -16
0 | &thunk: this-=16; goto C::{dtor}
1 | &Base::base_echo
非常惊讶的发现C类竟然有三个虚函数表,C::$vftable@A@:
, C::$vftable@B@:
, C::$vftable@Base@:
,在同时继承的了A,B,Base三个基类里,各自都有一个虚函数表。
而三个各自的虚函数表和源了A,B,Base三个基类虚函数表是不同的!!是单独属于C类的虚函数。
更值得注意的是,C::c_echo
是C的虚函数,但是却在C::$vftable@A@:
表里面。
从内存布局和虚函数布局可以得出简单结论,多继承时候,一般来说,对象会有同时继承类个数的虚函数表和表指针,子类的新虚函数会存在于第一个继承类的新虚函数
,
未完待续!!!!!!!
用汇编的角度剖析c++的virtual的更多相关文章
- 动态规划---等和的分隔子集(计蒜课)、从一个小白的角度剖析DP问题
自己还是太菜了,算法还是很难...这么简单的题目竟然花费了我很多时间...在这里我用一个小白的角度剖析一下这道题目. 晓萌希望将1到N的连续整数组成的集合划分成两个子集合,且保证每个集合的数字和是相等 ...
- 从汇编层面深度剖析C++虚函数
文章出处:http://blog.csdn.net/linyt/article/details/6336762 虚函数是C++语言实现运行时多态的唯一手段,因此掌握C++虚函数也成为C++程序员是否合 ...
- 从汇编的角度看待const与#define
先观察一下的代码: #include<stdio.h> int main(){ ; int y; int *pi=(int*)&i; *pi=; y=*pi; int tempi; ...
- 从汇编的角度看待变量类型与sizeof的机制
1.动机:前段时间,一直有个疑问,就是编译器是从哪里知道数据的类型的,数据的类型是存在内存里面的么,因为自己调试编译器,发现内存中并没有多余的数据,后来在群上发问,才知道数据在编译成汇编的过程就知道数 ...
- NLP︱LDA主题模型的应用难题、使用心得及从多元统计角度剖析
将LDA跟多元统计分析结合起来看,那么LDA中的主题就像词主成分,其把主成分-样本之间的关系说清楚了.多元学的时候聚类分为Q型聚类.R型聚类以及主成分分析.R型聚类.主成分分析针对变量,Q型聚类针对样 ...
- ArrayList 从源码角度剖析底层原理
本篇文章已放到 Github github.com/sh-blog 仓库中,里面对我写的所有文章都做了分类,更加方便阅读.同时也会发布一些职位信息,持续更新中,欢迎 Star 对于 ArrayList ...
- Swift 枚举-从汇编角度看枚举内存结构
一.基本使用 先看枚举的几种使用(暂不要问,看看是否都能看懂,待会会逐一讲解) 1.操作一 简单使用 //第一种方式 enum Direction { case east case west case ...
- JVM系列之:从汇编角度分析Volatile
目录 简介 重排序 写的内存屏障 非lock和LazySet 读的性能 总结 简介 Volatile关键字对熟悉java多线程的朋友来说,应该很熟悉了.Volatile是JMM(Java Memory ...
- SQLite剖析之内核研究
先从全局的角度把握SQLite内核各个模块的设计和功能.SQLite采用了层次化.模块化的设计,而这些使得它的可扩展性和可移植性非常强.而且SQLite的架构与通用DBMS的结构差别不是很大,所以它对 ...
随机推荐
- 【UVA11859】Division Game(SG函数,Nim游戏)
题意:给定一个n*m的矩阵,两个游戏者轮流操作. 每次可以选一行中的1个或多个大于1的整数,把它们中的每个数都变成它的某个真因子,不能操作的输. 问先手能否获胜 n,m<=50,2<=a[ ...
- *LOJ#2085. 「NOI2016」循环之美
$n \leq 1e9,m \leq 1e9,k \leq 2000$,求$k$进制下$\frac{x}{y}$有多少种不同的纯循环数取值,$1 \leq x \leq n,1 \leq y \leq ...
- 《Linux命令行与shell脚本编程大全 第3版》Linux命令行---2
以下为阅读<Linux命令行与shell脚本编程大全 第3版>的读书笔记,为了方便记录,特地与书的内容保持同步,特意做成一节一次随笔,特记录如下: <Linux命令行与shell脚本 ...
- usb 2.0 operation mode
一般來說 USB 的通訊結構有如 Server/Client,以 PC 上的情形為例,位於主機上的 USB 裝置稱為『USB Host』,我們可以在上面外接上數個裝置(與 USB Host 相連的裝置 ...
- Elasticsearch之pythonAPI简单使用
elasticsearch自动补全建议功能 数据入库操作 ESmapping要求 PUT music { "mappings": { "_doc" : { &q ...
- 设置div自适应高度滚动
<body> <div id="divc" style="overflow: auto;"> </div> <a id ...
- Syslog协议日志格式翻译
通用日志格式规范(参考 RFC5424 Syslog协议) 下面是RFC5424 Syslog协议关于信息格式的定义. Syslog信息的格式定义 # 一条信息的构成 SYSLOG-MSG = HEA ...
- Day 21 Object_oriented_programming_2
继承实现原理 python中的类可以同时继承多个父类,继承的顺序有两种:深度优先和广度优先. 一般来讲,经典类在多继承的情况下会按照深度优先的方式查找,新式类会按照广度优先的方式查找 示例解析: 没有 ...
- 树的直径新求法、codeforces 690C3 Brain Network (hard)
树的直径新求法 讲解题目 今天考了一道题目,下面的思路二是我在考场上原创,好像没人想到这种做法,最原始的题目,考场上的题目是这样的: 你现在有1 个节点,他的标号为1,每次加入一个节点,第i 次加入的 ...
- Xamarin XAML语言教程Xamarin.Forms中程序状态与进度(一)
Xamarin XAML语言教程Xamarin.Forms中程序状态与进度(一) 在Xamarin.Forms中,提供了两个控件用来指示程序的状态和进度.他们分别为活动指示器和进度条.其中,活动指示器 ...