图解C++虚函数机制

是不是面试官一问虚函数机制,就支支吾吾说不上所以然来。读懂这篇文章让面试官对你刮目相看。从汇编角度,深入解析虚函数的原理,同时理解纯虚函数的机制。大量使用图示的方式,力求解释清楚虚函数的来龙去脉。

在本场 Chat 中,会讲到如下内容:

  1. 虚函数指针和虚表在哪里?
  2. 我们如何手动调用虚函数?
  3. 为什么只有在子类以父类的引用或者指针的形式才能出现多态?
  4. 虚函数的调用为什么效率相比普通的成员函数较低?又具体低了多少?
  5. 为什么构造函数和析构函数尽量不要调用虚函数?
  6. 纯虚函数到底是什么?为什么禁止我调用?有什么办法可以绕编译器?

适合人群:想对 C++ 背后的机制有更深入的理解

看码说话

class MemsetObj {public:    MemsetObj(){    memset(this, 0, sizeof(MemsetObj));        cout << "memsetobj ctor" << endl;    }    void test() { cout << "memset obj test" << endl; }    virtual void virtual_func() {  cout << "virtual test" << endl; }};     // 1.    MemsetObj obj;    obj.virtual_func();     // 2.    MemsetObj *pobj = new MemsetObj();    pobj->virtual_func();// 这两种方式调用有问题?什么问题?

0.基础知识说明

1.VS 调试

1.打开内存窗口

  1. 先下断点
  2. 在 VS 中启动程序
  3. VS 就会你下的断点处停下
  4. 在菜单栏->调试->窗口->内存,就会发现 4 个内存选项随便选择一个就可以

2.打开反汇编窗口

  1. 同样在调试的状态下,鼠标右键菜单->转到反汇编点

2.汇编指令

这里我解释下常用的和常见的一些指令,更多的需要大家自己课后学习。

  1. 汇编中有些通用的寄存器分别为 eax,ebx,ecx,edx,esi,edi,esp,ebp,es,ds,ss,sp 等等,类似高级语言中的变量,但是这些变量的数量和名称都是固定的。
  2. mov ax,bx ; 将 bx 的内容移动 ax 中
  3. lea ax,[obj] ; load effective address 将 obj 对象的地址移动 ax 中
  4. pop,push 就是栈中的压栈和出栈的操作,函数中的参数就是这么传递的。
  5. 在汇编的角度看 C++的成员函数调用的方式叫做__thiscall,就是 C++中的 this 指针,是通过 ecx 指针传递的,所以会经常看到 lea ecx,[base] call FunctionName,套路都是固定的。

1.虚函数的含义

只有用 virtual 声明类的成员函数,称之为虚函数。

2.虚函数的作用

就是一句话:实现多态的基石实现多态的三大步:

  1. 存在继承关系
  2. 重写父类的 virtual function
  3. 子类以父类的指针或者是引用的身份出现

3.虚函数的实现原理

相信很多人都能说出来其中实现关键原理,就是两点:虚函数表指针(vptr),虚函数表(vftable)

3.1 虚函数表指针

1. 什么是虚函数表指针,他在哪里,有什么用?

我们把对象从首地址开始的 4 个字节,这个位置我们称之为虚函数表指针(vptr),它里面包含一个地址指向的就是虚函数表(vftable)的地址。

3.2 虚函数表

1. 什么是虚函数表,他又在哪里,有什么用?

虚函数表说白了就是里面是一组地址的数组(就是函数指针数组),他所在的位置就是虚函数表指针里面所存储的地址,它里面所包含的地址就是我们重写了父类的虚函数的地址(没有重写父类的虚函数那么默认的就是父类的函数地址)。

3.3 探索虚表位置

1. 手动探索虚表位置

class Base {public:    int m_a = 0;    virtual void f() { cout << "Base::f()" << endl; }    virtual void g() { cout << "Base::g()" << endl; }virtual void h() { cout << "Base::h()" << endl; }    // virtual ~Base() { cout << "~Base" << endl; } };class BaseOne{public:    int m_b = 0;    virtual void i() { cout << "BaseOne::i()" << endl; }    virtual void j() { cout << "BaseOne::j()" << endl; }virtual void k() { cout << "BaseOne::k()" << endl; }// virtual ~ BaseOne () { cout << "~ BaseOne " << endl; }};class Derive : public Base{public:    int m_d = 0;virtual void g() { cout << "Derive::g()" << endl; }// virtual ~ Derive () { cout << "~ Derive " << endl; }};class MultiDerive: public BaseOne, public Base{public:    int m_e = 0;    virtual void h() { cout << "MultiDerive::h()" << endl; }    virtual void k() { cout << "MultiDerive::k()" << endl; }virtual void m() { cout << "MultiDerive::m()" << endl; }// virtual ~ MultiDerive () { cout << "~ MultiDerive " << endl; }};

如果代码看的累,我们直接看图(这里我没有用 UML 图表示):

image

既然我们知道 vptr 的位置,我们开始尝试手动调用虚函数

typedef void (*Fun)();void test_multi_inhert_virtual_fun(){    MultiDerive multiderive;    // 从对象的首地址 4 个字节,获取 vptr 的地址,x86 平台下指针都是 4 个字节    long *pbaseone_tablepoint      = (long *)(&multiderive);    // 对 vptr 指针解引用操作获取 vftable 的地址    long *baseone_table_address    = (long *)*(pbaseone_tablepoint);    for(int i = 0; i < 4; ++i) {        cout << std::hex << "0x" << baseone_table_address[i] << endl;    }    ((Fun)(baseone_table_address[0]))();    ((Fun)(baseone_table_address[1]))();    ((Fun)(baseone_table_address[2]))();    /* 打印的结果    BaseOne::i()    BaseOne::j()    MultiDerive::k()    */    // 获取第二个虚函数表指针的位置,从基址开始的地址+第一个继承对象的大小    long *pbase_tablepoint =         (long *)(reinterpret_cast<char *>(pbaseone_tablepoint)+sizeof(BaseOne));    long *base_table_address = (long *)(*pbase_tablepoint);    for(int i = 0; i < 4; ++i) {        cout << std::hex << "0x" << base_table_address[i] << endl;    }    ((Fun)(base_table_address[0]))();    ((Fun)(base_table_address[1]))();    ((Fun)(base_table_address[2]))();    /*打印的结果    Base::f()    Base::g()    MultiDerive::h()    */}

对于不想看代码的同学,我画了一张图

image

对于这张图里面的一些强转我做些解释:

  1. x86 平台下,指针类型都是 4 个字节,所用你可以把 long *替换为 int *没有问题,只要这个指针的步长是 4 字节的都可以。
  2. 获取第二个 vftable 时,强转为 char*类型,因为指针类型做+/-运算有个步长概念,有的称之为比例因子。
函数 打印结果
((Fun)(baseonetableaddress[0]))(); BaseOne::i()
((Fun)(baseonetableaddress[1]))(); BaseOne::j()
((Fun)(baseonetableaddress[2]))(); MultiDerive::k()
((Fun)(basetableaddress[0]))(); Base::f()
((Fun)(basetableaddress[1]))(); Base::g()
((Fun)(basetableaddress[2]))(); MultiDerive::h()

从表中我们可以看出来,其实这个 vptr 和 vftable 也没啥神秘的地方,就是一个指针指向装有函数指针的数组。数组里面的内容如果子类没有 override,那么默认值就是父类的虚函数地址,否则就是子类自己的函数(表中红色部分)

2.cl.exe 验证虚表的位置

我们对类的声明代码稍作修改,做成独立的 cpp 文件

/******************************************************************************** Copyright (C) 2019 [email protected]** All rights reserved.******************************************************************************/#include <iostream>using std::cout;using std::endl;class BaseOne{public:    int m_b = 0;    virtual void i() { cout << "BaseOne::i()" << endl; }    virtual void j() { cout << "BaseOne::j()" << endl; }    virtual void k() { cout << "BaseOne::k()" << endl; } };class Base{public:    int m_a = 0;    virtual void f() { cout << "Base::f()" << endl; }    virtual void g() { cout << "Base::g()" << endl; }    virtual void h() { cout << "Base::h()" << endl; }};class MultiDerive :public BaseOne, public Base{public:    int m_e = 0;    virtual void h() { cout << "MultiDerive::h()" << endl; }    virtual void k() { cout << "MultiDerive::k()" << endl; }    virtual void m() { cout << "MultiDerive::m()" << endl; }};int main(){       return 1;}

在 windows 菜单中找到 vs2013 命令行工具

image

cd 到你所在 cpp 文件的目录中比如我:cd /d j:\code\polymorphismvirtual\source\执行命令 cl /d1 reportSingleClassLayoutMultiDerive analysisvirtualbytools.cpp注意 MultiDerive 表示你想导出来的类布局

image

从导出来的数据中可以看出:

1.MultiDerive 大小、内存布局、以及成员变量的偏移、vftable 的寻址

2.大家可能看到红色标注的-8 有点奇怪,这个就是计算第二个 vptr 的偏移地址,计算方式是首地址-第二个 vptr 的所在的地址=-8,他们之间的距离就是一个 BaseOne 的大小。

3.VS IDE 查看虚表位置

这种方式比较简答,就是启动 VS,下个断点,将鼠标移动到 MultiDerive 实例上,可以看到详细的信息。

image

image

从这里你可能发现一个问题,子类自己的虚函数这里没有体现出来,所以建议 cl.exe 工具验证虚表。

4.内存布局图

到这里我们可以画图上述代码中类的内存布局图:

image

image

image

从 Derive 和 MultiDerive 内存图我们发现一些规律:

1.继承的体系越复杂,子类的体积越大。
2.子类中普通成员顺序按照继承的先后顺序来的。
3.多重继承,子类中含有多个 vptr,分别指向不同的 vftable。
4.vftable 中的虚函数地址和在类中声明的顺序一致。
5.如果子类 override 父类中虚函数,那么子类 vftable 中就会替换原来父类的虚函数。
6.如果子类自己含有额外的虚函数,则会附加到第一个 vftable 中。
7.vftable 中的最后一个值可能为 0x0,有时候并不是为 0,上图中红色字部分。
8.子类有 vftable,同时父类也有一份 vftable,两个 vftable 没有关联。每个实例化子类都共享一个 vftable,同样父类所有实例化对象也共享一份 vftable,非常类似类的静态变量

3.4 虚析构函数用函数指针间接调用会崩溃

眼尖的同学可能注意到探索虚表位置的代码,我注释掉了父类中所有的虚析构函数。我本想拿到虚表地址,就拿到了虚析构函数的地址,我用函数指针强制转换就可以调用,但是我一调用就崩溃。好,废话不说,反汇编走起来。

1.手动调用析构函数的反汇编代码

;  base.~Base();; 传递参数 0002D99EF  push        0  ; 传递 this 到 ecx002D99F1  lea         ecx,[base]  ; 调用一个类似析构函数的函数002D99F4  call        virtual_fun_table::Base::`scalar deleting destructor' (02D16C2h) 

2.动手验证猜想

看了反汇编代码,会惊讶的发现,居然 push 0 了,说明编译器帮我们传递一个参数,那我们就自己手动也传递一个,你同时发现了这里并不是直接调用 Base 的析构函数(代理析构函数)。

typedef void (*DctorFun)(int);Base base;base.~Base();long *vptr      = (long *)(&base);long *vftable   = (long *)(*vptr);((Fun)(vftable[0]))();((Fun)(vftable[1]))();((Fun)(vftable[2]))();((DctorFun)(vftable[3]))(0); ; 调用 Base 的虚析构函数

恩,应该没有问题了,开心的跑起来,VS F5 跑起来。但还是抛出异常了…想不出来啥原因,还是看反汇编吧,不断的跟啊跟啊,发现崩溃在~Base 函数里面。

01276EFF  pop          ecx  01276F00  mov         dword ptr [this],ecx  01276F03  mov         eax,dword ptr [this]  01276F06  mov         dword ptr [eax],1282BD8h

这里拿到 ecx,然后又一次赋值虚表地址1282BD8h。但是我们是直接调用虚函数,没有 push ecx,所以这里拿到的 ecx 值是个不确定的值就相当于一个野指针问题。我们知道了为什么崩溃的原因,但是仔细想想这里为什么还需要再一次重新赋值虚表地址,在构造函数的时候不是已经初始化好虚表的地址了?试想下这样的情形:Derive 继承 Base,在调用 Derive 析构函数时,先是调用 Derive 的析构函数,再 Base 调用的析构函数。如果在调用 Base 的析构函数时不重置虚表的话,那么 Base 中可能出现间接的调用 Derive 虚函数,而此时 Derive 的已经执行过析构函数,里面的数据已经是不可靠的,存在安全隐患。同时得出结论析构函数和构造函数中调用虚函数并没有多态的特性。关于间接的调用虚函数说明:我们知道,在构造函数中直接调用虚函数的话,是没有多态的特性,但是如果我们写一个函数,这个函数再去调用虚函数,这个时候生成的汇编代码,会根据虚表找函数地址,就有了多态的特性。有兴趣的同学可以自己反汇编验证下。

4.vptr 和 vftable 的初始化问题

4.1 vptr 什么时候初始化?

1.对象初始化时

2.拷贝构造调用

我们从汇编的角度看,在对象初始化时如何设置 vftable 的地址。

在 vs2013 在调试状态下,右键转到反汇编就可以看到汇编代码

// c++代码MultiDerive multiderive;

这下面三个都是汇编代码

; MultiDerive multiderive;; multiderive 的地址放到 ecx 寄存器中00FC9CD8  lea           ecx,[multiderive]; 调用 MultiDerive 构造函数00FC9CDB  call          virtual_fun_table::MultiDerive::MultiDerive (0FC12BCh) 
; 跳转到 virtual_fun_table::MultiDerive::MultiDerive:00FC12BC  jmp         virtual_fun_table::MultiDerive::MultiDerive (0FC4180h) 
00FC4180  push        ebp  00FC4181  mov        ebp,esp  00FC4183  sub         esp,0CCh  00FC4189  push        ebx  00FC418A  push        esi  00FC418B  push        edi  00FC418C  push        ecx  00FC418D  lea          edi,[ebp-0CCh]  00FC4193  mov         ecx,33h  00FC4198  mov         eax,0CCCCCCCCh  00FC419D  rep stos    dword ptr es:[edi]  ; 从 00FC4180~00FC419D 都是做函数调用过程初始化准备00FC419F  pop         ecx                       ; 拿到 this 指针00FC41A0  mov        dword ptr [this],ecx  00FC41A3  mov        ecx,dword ptr [this]  00FC41A6  call         virtual_fun_table::BaseOne::BaseOne (0FC1235h)  ; 调用 BaseOne 的构造函数00FC41AB  mov        ecx,dword ptr [this]  00FC41AE  add         ecx,8                 ; 调整 this 指针偏移,并将 this 指针传递给调用 Base 构造函数00FC41B1  call          virtual_fun_table::Base::Base (0FC15D2h)  00FC41B6  mov         eax,dword ptr [this]  00FC41B9  mov         dword ptr [eax],0FD1B54h  ; 将 vftable 的地址设置到第一个 vptr 中00FC41BF  mov         eax,dword ptr [this]              00FC41C2  mov         dword ptr [eax+8],0FD1B6Ch ;跳过 8 字节,设置第二个虚表地址放到第二个 vptr 中00FC41C9  mov         eax,dword ptr [this]  00FC41CC  mov         dword ptr [eax+10h],0  00FC41D3  mov         eax,dword ptr [this]  00FC41D6  pop         edi  00FC41D7  pop         esi  00FC41D8  pop         ebx  00FC41D9  add         esp,0CCh  00FC41DF  cmp         ebp,esp  00FC41E1  call          __RTC_CheckEsp (0FC1488h)  00FC41E6  mov         esp,ebp  00FC41E8  pop          ebp  00FC41E9  ret  

关于这里的汇编代码做下说明:

  1. c++中类成员的函数调用叫做 thiscall 方式,就是 this 指针通过 ecx 寄存器来传递的。
  2. 注意看下旁边的注释行文字
  3. 代码中红色加粗的两个地址0FD1B54h0FD1B6Ch就是关键的 vftable 地址
  4. 这里我只带领大家看了构造函数的,那么拷贝构造原理是一样的大家可以自己尝试。如果大家的汇编看不懂,就看看下面的这张调用图便于大家的理解:

image

4.2 vftable 里面的内容什么时候初始化?

这个编译器在编译期间就已经初始化好了,为每个类确定好了虚表里面对应的内容。

5.到底是怎么实现多态特性

前面,我们详细介绍了虚函数的实现细节,但是子类为什么以父类引用或指针的身份出现就会有多态的特性,总感觉还有一层窗户纸没有捅破。上代码,我们看下面的这段代码

;  MultiDerive multiderive;  c++代码01289FC8  lea         ecx,[multiderive]  01289FCB  call        virtual_fun_table::MultiDerive::MultiDerive (012812BCh)   ;   multiderive.m(); c++代码 ; 注意看这里以普通的身份调用虚函数,就两行汇编代码,直接调用传递 this 指针01289FD0  lea         ecx,[multiderive]  01289FD3  call        virtual_fun_table::MultiDerive::m (012812F8h)  ; MultiDerive *multiderive2 = new MultiDerive(); c++代码; 这里从 0x01289FD8~到 0x0128A014 主要是进行了申请堆内存; 申请成功了调用构造函数,申请失败跳过构造函数01289FD8  push        14h  01289FDA  call         operator new (01281587h)  01289FDF  add         esp,4  01289FE2  mov         dword ptr [ebp-13Ch],eax  01289FE8  cmp         dword ptr [ebp-13Ch],0  01289FEF  je            virtual_fun_table::test_multi_inhert_virtual_fun+64h (0128A004h)  01289FF1  mov         ecx,dword ptr [ebp-13Ch]  01289FF7  call          virtual_fun_table::MultiDerive::MultiDerive (012812BCh)  01289FFC  mov         dword ptr [ebp-144h],eax  0128A002  jmp         virtual_fun_table::test_multi_inhert_virtual_fun+6Eh (0128A00Eh)  0128A004  mov         dword ptr [ebp-144h],0  0128A00E  mov         eax,dword ptr [ebp-144h]  0128A014  mov         dword ptr [multiderive2],eax  ; multiderive2->m(); c++代码; 这里是我们需要看的重点内容; 将 multiderive2 的首地址,放到 eax 寄存器0128A017  mov         eax,dword ptr [multiderive2]  ; eax 就是我们之前说的 vptr,对 vptr 解引用获取到 vftable 地址放到 edx0128A01A  mov         edx,dword ptr [eax]  0128A01C  mov         esi,esp  ; thiscall 方式调用,通过 ecx 传递 this 指针地址0128A01E  mov          ecx,dword ptr [multiderive2]  ; edx 里面存放了 vftable 的地址+偏移地址; 还原成高级语言就是数组取值 eax = vftable[0CH],每个函数指针是 4 个字节,4*3=12byte; eax 中就是存放了虚函数 m 的地址0128A021  mov         eax,dword ptr [edx+0Ch]  0128A024  call          eax  0128A026  cmp         esi,esp  0128A028  call          __RTC_CheckEsp (01281488h)  

我们简化下来对比下代码

multiderive.m(); 01289FD0 lea ecx,[multiderive]
01289FD3 call virtualfuntable::MultiDerive::m (012812F8h)
multiderive2->m(); 0128A017 mov eax,dword ptr [multiderive2]
0128A01A mov edx,dword ptr [eax]
0128A01C mov esi,esp
0128A01E mov ecx,dword ptr [multiderive2]
0128A021 mov eax,dword ptr [edx+0Ch]
0128A024 call eax

如果不是引用或者指针,虚函数的调用则是直接寻址,2 行汇编代码搞定。
如果是指针或者是引用,看出虚函数的寻址并不是那么简单的。先是找到 vptr再找到 vftable在加上偏移地址(偏移量是在编译器就已经确定的)最后才是真正的函数调用地址,用了 6 行代码。
现在你能理解为什么虚函数的调用效率比较慢了吧。

image

6.纯虚函数理解

6.1 含义:

如果我们在虚函数原型的后面加上=0(virtual void func()= 0),同时这个函数是没有实现的。

6.2 作用

有纯虚函数的类表示这是一个抽象类,既然是抽象的,那么肯定就是不能实例化。关于抽象类不能实例化可以从逻辑上理解也是合理的,比如说:动物,老虎,狮子,人都是动物。但是你说动物没人能理解你说的动物到底指的是什么东西。

由纯虚函数的引出了抽象类,抽象类的出现是为了解决什么问题?

抽象类就是为了被继承的,它为子类实例化提供蓝图。在相关的组织继承层次中,它来提供一个公共的根。其他相关子类都是这里衍生出来。

它与接口的区别是什么?

接口是对动作的抽象,抽象类是对根源的抽象。比如说人,有五官,有其他属性。但是吃这个动作应该定义为接口更合适。因为其他动物也有吃的动作。

6.3 从汇编角度看纯虚函数特别之处

1.间接的查看 AbstractBase 虚表内容

// #define test_call_abstract_virtual_fun 1class AbstractBase{public:#ifdef test_call_abstract_virtual_funAbstractBase()      { CallAbsFunc(); }void CallAbsFunc()  { AbsFunc(); }#else    AbstractBase()      { }#endif // test_call_abstract_virtual_fun    virtual void AbsFunc()  = 0;    virtual void AbsFunc2() = 0;};class Child : public AbstractBase{public:    Child()         { AbsFunc(); }    void AbsFunc()  { cout << "" << endl; }    void AbsFunc2() { cout << "" << endl; }};void test_abstract_virtual_fun(){    // 因为抽象类不能直接实例化,通过子类实例化,反汇编找到 AbstractBase 找到构造函数    AbstractBase *child = new Child();    child->AbsFunc();}

在 24 行代码处下断点->启动 vs->右键菜单->反汇编->快捷键 F11 单步调试。反汇编的代码比较多,我就挑出重点的代码画图解释下:

image

在反汇编的时候,我们拿到了虚表的地址0x0F82D98,我们把这个地址放到 vs2013 中的内存窗口中。根据我们前面的学到的知识,就知道 AbstractBase 应该有两个虚函数,那么表中应该有两个函数指针,如下图所示。但是你会惊讶的发现表中的两个函数指针都是0x00f714ab

image

2.纯虚类虚表中不仅有内容还是一样的?

现在我们很好奇,为什么虚表中的内容的是一样的。还有纯虚类的虚函数都没有任何的实现的,为什么虚表中还有内容。还有这个地址到底是个什么?干啥的?

那我就在想,如果我可以拿到这个地址直接转成函数指针,通过函数指针调用就可以了。

  1. 方案一:在纯虚父类构造函数中,直接调用纯虚函数,编译失败 error LNK2019: 无法解析的外部符号(因为纯虚函数没有实现,直接调用没有任何意义)。方案否决。

  2. 方案二:尝试拿到纯虚父类的 vftable 地址,但是发现纯虚父类不能实例化。方案否决。

  3. 方案三:尝试在子类构造函数,拿到 this 指针的,在根据 this 指针拿到虚表地址。反汇编的代码看编译器的代码先于我的代码执行,就是说等到了执行我的代码时候,这个 this 指针已经不是纯虚父类的 vftable,而是子类的 vftable 了。貌似进入死胡同,没有方案了。

问题回到刚开始时候,我现在是怎么拿到纯虚父类的 vftable。我是实例化了子类,然后反汇编 F11 一步步跟过去的。就是说我是间接的通过子类去获取纯虚父类的 vftable,等等,是不是有思路了。

  1. 方案四:在纯虚父类中写一个普通的函数,在构造函数->调用普通的函数->调用纯虚函数

在上面的代码将宏放开#define testcallabstractvirtualfun 1,编译通过 ok,接下来可以愉快的玩耍了。代码整理好,反汇编走起来。我把重要的调用过程画图表示出来,便于理解。

image

从纯虚函数的调用过程来看,调用纯虚函数->__purecall->call 0FE51470(19h) ->抛出异常

image

现在我们大概的猜一猜上面提出的疑问了:

1.纯虚函数的确是没有实现的,而虚表的内容时编译器塞进去的。

2.纯虚函数本来就是不能让我们调用的,我们现在通过某种手段绕过编译器了。如果我们直接调用纯虚函数,编译器能够检查出来,会报错 error LNK2019: 无法解析的外部符号。而如果我们间接的调用了纯虚函数,编译器也无能为力,但是编译器还是道高一尺,它知道自己可能在编译期间解析不出来,所以编译器就在虚表中插入purecall 函数,你有几个虚函数我就插入几个purecall 函数,当你在运行时调用,我就让你调用__purecall 抛出异常。让你不能调用你就是不能调用,强行调用我就给你 shutdown。现在能够解释为什么虚表的内容是一样的。

7.虚函数的缺点

天使与魔鬼是并存的,虚函数在带来超强的多态特性,但是不可避免的带来了其他缺点。

1.间接寻址造成的效率慢,在怎么实现多态特性上汇编角度可以看出来,引入时间复杂度。
2.继承关系带来的强耦合关系,父类动子类可能地动山摇,对象关系复杂度上升。
3.体积的增加,尤其是多继承时体现的更明显,引入额外的空间复杂。

但是在软件开发的角度看大大降低软件的开发周期和维护成本,总的来说瑕不掩瑜。与带来的多态特性相比,我觉得还是值得。

8.虚函数的外延探索

  1. 在多态中,我们通过对象->虚表指针->虚表->虚函数,最终找到我们想要的函数。简化的看对象->(中间操作)->虚函数,对象经过中间的一系列操作得到虚函数。这种思路称为间接思维,当我们想要某种东西的时候,可能没法直接获取或者是直接获取的成本太高,但是通过间接轻松的获取。这种思路随处可见,比如:我要吃饭要通过钱去等价交换,我去公司上班通过地铁过去,计算机中的缓存作用。

  2. 关于实现多态的特性,网上还有利用模板的编译期多态的特性,有兴趣同学可以搜一下。

  3. 设计模式的里面的套路,就是基于的虚函数多态的特性。

9.扩展资料

《C++深入理解对象模型》《C++反汇编与逆向分析》

10.总结

image

阅读全文: http://gitbook.cn/gitchat/activity/5e522e38abb3244dfe14aabf

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

发布了3730 篇原创文章 · 获赞 3504 · 访问量 330万+

猜你喜欢

转载自blog.csdn.net/valada/article/details/104470025