Featured image of post C++面向对象高级开发-兼谈对象模型-复合继承关系下的构造和析构、对象模型ObjectModel关于vptr与vtbl、this指针、动态绑定

C++面向对象高级开发-兼谈对象模型-复合继承关系下的构造和析构、对象模型ObjectModel关于vptr与vtbl、this指针、动态绑定

复合继承关系下的构造和析构, 对象模型(Object Model):关于vptr和vtbl, 关于this指针, 关于Dynamic Binding

C++面向对象高级开发-兼谈对象模型-复合继承关系下的构造和析构、对象模型ObjectModel关于vptr与vtbl、this指针、动态绑定

Notes

  • 复合继承关系下的构造和析构
  • 对象模型(Object Model):关于vptr和vtbl
  • 对象模型(Object Model):关于this指针
  • 对象模型(Object Model):关于Dynamic Binding

复合继承关系下的构造和析构

对象与对象之间的关系,可分为复合Composition、继承Inheritance、委托Delegation。

Inheritance继承关系下的构造和析构

当子类继承自父类时,子类对象中存在父类的部分。基类的析构函数必须为虚函数,否则会出现未定义行为。继承关系下的构造函数是由内而外的,子类的构造函数被执行会首先调用父类的默认构造函数,执行完成后才会调用子类自身的构造函数。而子类的析构函数被执行会首先执行自身的析构函数,而后再调用父类的析构函数。

Composition复合关系下的构造和析构

若存在Container类中复合(或含有)Component类时,Container对象中含有Component的部分。构造函数与析构函数与继承关系中的构造函数和析构函数同理,Container的构造函数首先调用Component的默认构造函数后,才调用Container自身的构造函数。而Container的析构函数首先调用自身的析构函数而后调用Component的析构函数执行。

Inheritance+Composition关系下的构造和析构

若存在派生类Derived和基类Base,且派生类Derived复合Component类时,派生类对象中不仅有基类部分,还有Component复合类部分。构造函数仍然是由内而外,析构函数仍然是由外而内。派生类Derived的构造函数首先调用基类Base的默认构造函数,继而调用Component复合类的默认构造函数,最后调用派生类自身的构造函数。派生类Derived的析构函数首先调用自身的析构函数,而后调用Component复合类的析构函数,最后调用基类Base的的析构函数。其代码表现形式为:

Derived::Derived(...) : Base(), Component() {...}
Derived::Derived(...) {... ~Component(); ~Base();}
#include <iostream>
#include <cstdlib>

class Base {
public:
    Base() {
        std::cout << "Base Constructor" << std::endl;
    }
    virtual ~Base() {
        std::cout << "Base Deconstructor" << std::endl;
    }
};

class Component {
public:
    Component() {
        std::cout << "Component Constructor" << std::endl;
    }
    ~Component() {
        std::cout << "Component Deconstructor" << std::endl;
    }
};

class Derived : public Base {
private:
    Component* component;
public:
    Derived() {
        component = new Component();
        std::cout << "Derived Constructor" << std::endl;
    }
    ~Derived() {
        std::cout << "Derived Deconstructor" << std::endl;
        component->~Component();
    }
};

int main(int argc, const char * argv[]) {
    Derived* derived = new Derived();
    derived->~Derived();
    return EXIT_SUCCESS;
}

在不同的编译器中顺序可能存在不同,根据上面的代码测试所获的的结果与描述相符。

Base Constructor
Component Constructor
Derived Constructor
Derived Deconstructor
Component Deconstructor
Base Deconstructor
Program ended with exit code: 0

对象模型(Object Model):关于vprt和vtbl

vptr vtbl

由图已知类A、B、C。B公开继承自A,C公开继承自B。类对象占用的内存一般为数据,若类中定义虚函数(1个或无穷多个),类对象占用的内存会增加1个指针即4个字节,此指针即为虚指针。函数的继承是指继承函数的调用权,并非继承函数的所占的空间或大小。若父类存在虚函数,则子类一定存在虚函数。所以虚指针一定存在,故子类对象中一定存在父类成分。B类中继承自A的两个虚函数,classB::vfunc1()函数override覆写classA::vfunc1()函数。classA::vfunc2()classB继承。classCclassB同理。此时内存中共存在4个非虚函数与4个虚函数。classA有虚函数,故A类对象中存在虚指针,虚指针指向虚表,虚表中存放的即为函数指针,指向函数所在的位置,即为图中中间列。如图中A类对象中所含虚指针地址为0x409004并指向虚表,虚表中分别存放A类对象中两个虚函数的地址,即0x401ED00x401F10,分别指向classA::vfunc1()classA::vfunc2()。B类与C类中继承了A类中的classA::vfunc2(),故除本类中覆写父类的虚函数指针0x401F800x401FF0外,classA::vfunc2()的虚指针地址0x401F10依旧继承。故虚函数的调用即依靠虚指针和虚表实现。若有语句C* p();,意为指针p指向C类对象或p为C类对象的指针,通过指针p调用classC::vfunc1(),C语言中的实现通过条件判断语句或逻辑结构等通过call命令实现调用,此种调用方式称之为静态绑定。当通过指针调用虚函数时,通过指向类对象的指针中的存放虚指针的地址获取虚指针,而后通过虚指针访问虚表,通过虚表访问实际指向的虚函数。并非静态绑定中通过call指令访问固定地址,此种调用方式称之为动态绑定。若将动态绑定通过指针调用虚函数的形式改写为C语言静态绑定的形式,代码可为:(*(p-&gt;vptr)[n])(p);(* p-&gt;vptr[n] )(p);。其中n的含义为在虚表中的位次。位次由编译器在编译时生成。 vptr vtbl

对上述示例中的A、B、C类实例化,考虑定义Shape抽象父类,B、C类可考虑为正方形、圆形等。或考虑父类为长方形,子类为正方形,或考虑父类为椭圆形,子类为圆形等。在进行类设计时,若要在一个容器内可装载各种子类对象,则必须在容器定义中存放指针,若不存放指针,则每个对象的大小不一致,即无法存放。指针必须指向基类,指针为4个字节,所以基类指针(如Shape)可以指向任意派生类(如Rectangle),对基类的虚函数重写(如图形重绘),通过取出指向类对象的虚指针,调用派生类的虚函数执行。(调用派生类的draw函数)。图中的draw函数与vfunc1原理一致。故此时有指针指向什么类型,即可以调用此种类型自身的方法(指针指向Rectangle,即可调用长方形的虚函数draw方法)。静态绑定的C语言需要通过逻辑判断实现对不同类型方法的调用。当派生类的类型不断增加,逻辑判断就必须改变,判断逻辑的代码就必须重写。故在上述示例中,静态绑定的设计方法劣于动态绑定。

动态绑定的必备条件:

  • 通过指针调用
  • 指针必为向上转型(up-cast)
  • 调用虚函数

动态绑定的形式又可称为虚机制(Virtual Mechanism)。虚函数的此种用法又可称为多态。

对象模型(Object Model):关于this指针

this指针

通过对象调用函数,对象的地址就是this指针。虚函数的使用方法分为多态和模板方法。以图中代码为例,定义CMyDoc类继承自MFC类库CDocument, CDocument类中有OnFileOpen()方法,在方法中调用了Serialize()函数,在CDocument类中该函数为虚函数,其实现必须由派生类完成。此时创建一个CMyDoc对象,通过该对象调用OnFileOpen()方法(子类对象调用父类方法),执行至Serialize()函数处,虚函数调用派生类覆写的实现执行后续代码。关于语句myDoc.OnFileOpen();将被编译器解析为CDocument::OnFileOpen(&amp;myDoc);,其中myDoc的地址即为this指针。this指针指向的对象也被称为this对象。

C++中所有的成员函数均有隐藏参数this指针作为参数。

成员函数中有this指针传入时,所有的函数调用语句都被编译器改编,原语句Serialize()被改编为this-&gt;Serialize();,此时满足编译器对动态绑定的3个条件(通过指针调用、this指向子类对象、调用虚函数)。故语句this-&gt;Serialize()可通过动态绑定转化为(*(this-&gt;vptr)[n])(this);。故此时根据动态绑定通过vptr与vtbl虚指针与虚表调用子类的虚函数实现。

对象模型(Object Model):关于Dynamic Binding

动态绑定

动态绑定

A、B、C的继承关系与上述讲解部分一致。分析代码块:

B b;
A a = (A)b;
a.vfunc1();

A* pa = new B;
pa->vfunc1();

pa = &b;
pa->vfunc1();

语句A a = (A)b;创建了A类型的对象a,初值由B类型的对象b转换而来,语句a.vfunc1();为通过类对象调用函数,故此时为静态绑定。其汇编形态为call @ILT+420(A::vfunc1)(004011a9),即为call xxxx。后两组语句中的pa-&gt;vfunc1();则与上述情况不同,通过指针pa调用vfunc1函数,pa指针为从B类型至A类型的向上转型,且vfunc1为虚函数。此时的汇编形态已与静态绑定的call xxx(xxx为固定地址)不同,call dword ptr [edx]不为固定地址,其执行等价语句为(*(p-&gt;vptr)[n])(p);(* p-&gt;vptr[n] ) (p);。通过指针p找到虚指针vptr,访问虚表取出第n个地址来调用。由于通过p调用,所以此时p为this指针。核心仍为通过之前讲述的通过虚指针与虚表调用到派生类虚函数中的地址。 可完整执行的上述C++代码可参考:

//
//  main.cpp
//  DynamicBindingAssembleTest
//
//  Created by Fa1c0n on 2020/4/30.
//  Copyright © 2020 Fa1c0n. All rights reserved.
//

#include <iostream>
#include <cstdlib>

class A {
public:
    virtual void vfunc1 () const {
        std::cout << "classA vfunc1 called." << std::endl;
    }
};

class B : public A {
public:
    void vfunc1 () const {
        std::cout << "classB vfunc1 called." << std::endl;
    }

};

class C : public B {

};

int main(int argc, const char * argv[]) {
    B b;
    A a = (A)b;
    a.vfunc1();

    A* pa = new B;
    pa->vfunc1();

    pa = &b;
    pa->vfunc1();
    return EXIT_SUCCESS;
}

运行结果:

classA vfunc1 called.
classB vfunc1 called.
classB vfunc1 called.
Program ended with exit code: 0

程序汇编:

DynamicBindingAssembleTest`main:
    0x100001030 <+0>:   pushq  %rbp
    0x100001031 <+1>:   movq   %rsp, %rbp
    0x100001034 <+4>:   subq   $0x30, %rsp
    0x100001038 <+8>:   movl   $0x0, -0x4(%rbp)
    0x10000103f <+15>:  movl   %edi, -0x8(%rbp)
    0x100001042 <+18>:  movq   %rsi, -0x10(%rbp)
    0x100001046 <+22>:  leaq   -0x18(%rbp), %rdi
    0x10000104a <+26>:  callq  0x1000010b0               ; B::B at main.cpp:18
    0x10000104f <+31>:  leaq   -0x18(%rbp), %rsi
    0x100001053 <+35>:  leaq   -0x20(%rbp), %rdi
    0x100001057 <+39>:  callq  0x1000010d0               ; A::A at main.cpp:11
    0x10000105c <+44>:  leaq   -0x20(%rbp), %rdi
    0x100001060 <+48>:  callq  0x100001100               ; A::vfunc1 at main.cpp:13
    0x100001065 <+53>:  movl   $0x8, %edi
    0x10000106a <+58>:  callq  0x100001dea               ; symbol stub for: operator new(unsigned long)
    0x10000106f <+63>:  movq   %rax, %rdi
    0x100001072 <+66>:  movq   %rax, -0x30(%rbp)
    0x100001076 <+70>:  callq  0x1000010b0               ; B::B at main.cpp:18
    0x10000107b <+75>:  movq   -0x30(%rbp), %rax
    0x10000107f <+79>:  movq   %rax, -0x28(%rbp)
    0x100001083 <+83>:  movq   -0x28(%rbp), %rax
    0x100001087 <+87>:  movq   (%rax), %rsi
    0x10000108a <+90>:  movq   %rax, %rdi
    0x10000108d <+93>:  callq  *(%rsi)
    0x10000108f <+95>:  leaq   -0x18(%rbp), %rax
    0x100001093 <+99>:  movq   %rax, -0x28(%rbp)
    0x100001097 <+103>: movq   -0x28(%rbp), %rax
    0x10000109b <+107>: movq   (%rax), %rsi
    0x10000109e <+110>: movq   %rax, %rdi
    0x1000010a1 <+113>: callq  *(%rsi)
    0x1000010a3 <+115>: xorl   %eax, %eax
->  0x1000010a5 <+117>: addq   $0x30, %rsp
    0x1000010a9 <+121>: popq   %rbp
    0x1000010aa <+122>: retq