多态是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的更多相关文章

  1. 动态规划---等和的分隔子集(计蒜课)、从一个小白的角度剖析DP问题

    自己还是太菜了,算法还是很难...这么简单的题目竟然花费了我很多时间...在这里我用一个小白的角度剖析一下这道题目. 晓萌希望将1到N的连续整数组成的集合划分成两个子集合,且保证每个集合的数字和是相等 ...

  2. 从汇编层面深度剖析C++虚函数

    文章出处:http://blog.csdn.net/linyt/article/details/6336762 虚函数是C++语言实现运行时多态的唯一手段,因此掌握C++虚函数也成为C++程序员是否合 ...

  3. 从汇编的角度看待const与#define

    先观察一下的代码: #include<stdio.h> int main(){ ; int y; int *pi=(int*)&i; *pi=; y=*pi; int tempi; ...

  4. 从汇编的角度看待变量类型与sizeof的机制

    1.动机:前段时间,一直有个疑问,就是编译器是从哪里知道数据的类型的,数据的类型是存在内存里面的么,因为自己调试编译器,发现内存中并没有多余的数据,后来在群上发问,才知道数据在编译成汇编的过程就知道数 ...

  5. NLP︱LDA主题模型的应用难题、使用心得及从多元统计角度剖析

    将LDA跟多元统计分析结合起来看,那么LDA中的主题就像词主成分,其把主成分-样本之间的关系说清楚了.多元学的时候聚类分为Q型聚类.R型聚类以及主成分分析.R型聚类.主成分分析针对变量,Q型聚类针对样 ...

  6. ArrayList 从源码角度剖析底层原理

    本篇文章已放到 Github github.com/sh-blog 仓库中,里面对我写的所有文章都做了分类,更加方便阅读.同时也会发布一些职位信息,持续更新中,欢迎 Star 对于 ArrayList ...

  7. Swift 枚举-从汇编角度看枚举内存结构

    一.基本使用 先看枚举的几种使用(暂不要问,看看是否都能看懂,待会会逐一讲解) 1.操作一 简单使用 //第一种方式 enum Direction { case east case west case ...

  8. JVM系列之:从汇编角度分析Volatile

    目录 简介 重排序 写的内存屏障 非lock和LazySet 读的性能 总结 简介 Volatile关键字对熟悉java多线程的朋友来说,应该很熟悉了.Volatile是JMM(Java Memory ...

  9. SQLite剖析之内核研究

    先从全局的角度把握SQLite内核各个模块的设计和功能.SQLite采用了层次化.模块化的设计,而这些使得它的可扩展性和可移植性非常强.而且SQLite的架构与通用DBMS的结构差别不是很大,所以它对 ...

随机推荐

  1. .NET泛型编程 性能提升工具 List<T>

    原文发布时间为:2009-10-27 -- 来源于本人的百度文章 [由搬家工具导入] 结论  .NET 2.0中的泛型是强有力的,你写的代码不必限定于一特定类型,然而你的代码却能具有类型安全性。泛型的 ...

  2. Codeforces 920E Connected Components? 补图连通块个数

    题目链接 题意 对给定的一张图,求其补图的联通块个数及大小. 思路 参考 ww140142. 维护一个链表,里面存放未归入到任何一个连通块中的点,即有必要从其开始进行拓展的点. 对于每个这样的点,从它 ...

  3. Push pull, open drain circuit, pull up, pull down resistor

    Push pull 就以下面這個 電路來說, 因為沒有 pull up resistor, 所以 output voltage 由 low 往 high 的速度會較快. 有兩個電晶體,一個on,一個 ...

  4. Python 文本(txt) 转换成 EXCEL(xls)

    #!/bin/env python # -*- encoding: utf-8 -*- #------------------------------------------------------- ...

  5. EBImage - - 给图片增加字符

    EBImage中文文档 英文版出处:http://www.bioconductor.org/packages/release/bioc/vignettes/EBImage/inst/doc/EBIma ...

  6. 在 CentOS 7 中以命令行方式安装 MySQL 5.7.11 for Linux Generic 二进制版本

    MySQL 目前的最新版本是 5.7.11,在 Linux 下提供特定发行版安装包(如 .rpm)以及二进制通用版安装包(.tar.gz).一般情况下,很多项目都倾向于采用二进制通用安装包形式来进行安 ...

  7. javascript 省市二级联动

    通过遍历二维数组 获取到 二级列表的 每个option 然后onchange事件 获取到省,然后循环遍历该省具有的市并将遍历到的市添加到id为city的选择器中. 获取完需要清空二级列表的内容,不然不 ...

  8. Python Challenge 第十五关

    第15关,题目是 whom? 有一张图片,是个日历.日历的年份是 1XX6,中间是被挖去的洞.然后图中1月26日被画了个圈,当天是星期一.右下角的二月小图中有29号,可以得知这是闰年.然后查看源代码. ...

  9. webapi 初识 net

    1.新建一个webapi 项目. 2.新建筛选器文件,用户在接口执行前后进行特性操作. public class MyActionWebApiAttribute : ActionFilterAttri ...

  10. 51nod 1050 循环数组最大子段和【环形DP/最大子段和/正难则反】

    1050 循环数组最大子段和 基准时间限制:1 秒 空间限制:131072 KB 分值: 10 难度:2级算法题  收藏  关注 N个整数组成的循环序列a[1],a[2],a[3],…,a[n],求该 ...