用汇编的角度剖析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的结构差别不是很大,所以它对 ... 
随机推荐
- Cover
			[题目描述] 有 N 个时间段,某个时间段可能包含其它时间段. 请找出能包含其它时间段最多的那个段,并计算出它包括的其它时间段有多少? [数据范围] 1 <= N <= 25,000 1 ... 
- derby数据库的一些总结
			本文主要是针对在osgi开发过程中的一些问题进行总结,其中dbcp数据源的配置是在SpringDM下配置的.一,derby数据源的内嵌模式 该模式的主要应用是嵌入式程序,因为其小巧,且不 ... 
- FileInputStream/FileOutputStream的应用
			这是一对继承于InputStream和OutputStream的类,用于本地文件读写(二进制格式读写并且是顺序读写,读和写要分别创建出不同的文件流对象): 本地文件读写编程的基本过程为: ① 生成文 ... 
- linux 共享内存 信号量 同步
			这篇文章将讲述别一种进程间通信的机制——信号量.注意请不要把它与之前所说的信号混淆起来,信号与信号量是不同的两种事物.有关信号的更多内容,可以阅读我的另一篇文章:Linux进程间通信——使用信号.下面 ... 
- Linux 一个sysv 脚本参考模板
			说明: 1.很多时候我们的服务都是通过源码包编译安装,但是有的源码包编译完成后并不提供该服务的sysv风格脚本,我们只能手动执其二进制程序+配置文件 2.如果服务器宕机或重启,就不能自动完 ... 
- Chrome 75 将原生支持图片懒加载
			4 月 6 日,Google 的 Chrome & Web 平台工程经理 Addy Osmani 在个人博客发文,介绍到 <img> 和 <iframe> 的 load ... 
- Android布局实现阴影效果
			最近某个模块的UI,设计想要卡片式阴影效果.之前查阅过资料,用传统的xml方式作为布局的background <?xml version="1.0" encoding=&qu ... 
- Java开发笔记(一百零四)普通线程池的运用
			前面介绍了线程的基本用法,以及多线程并发的问题处理,但实际开发中往往存在许多性质相似的任务,比如批量发送消息.批量下载文件.批量进行交易等等.这些同类任务的处理流程一致,不存在资源共享问题,相互之间也 ... 
- 【Kafka】《Kafka权威指南》——写数据
			不管是把 Kafka 作为消息队列.消息.总线还是数据存储平台来使用 ,总是需要有一个可以往 Kafka 写入数据的生产者和一个可以从 Kafka读取数据的消费者,或者一个兼具两种角 色的应用程序. ... 
- python 集合互相转换
			#-*-coding:utf-8-*- #1.字典 dict = {'name': 'Zara', 'age': 7, 'class': 'First'} #字典转为字符串,返回:<type ' ... 
