乐趣区

面试系列之C的对象布局建议收藏

我们都知道 C ++ 多态是通过虚函数表来实现的,那具体是什么样的大家清楚吗?开篇依旧提出来几个问题:

  • 普通类对象是什么布局?
  • 带虚函数的类对象是什么布局?
  • 单继承下不含有覆盖函数的类对象是什么布局?
  • 单继承下含有覆盖函数的类对象是什么布局?
  • 多继承下不含有覆盖函数的类对象是什么布局?
  • 多继承下含有覆盖函数的类对象的是什么布局?
  • 多继承中不同的继承顺序产生的类对象布局相同吗?
  • 虚继承的类对象是什么布局?
  • 菱形继承下类对象是什么布局?
  • 为什么要引入虚继承?
  • 为什么虚函数表中有两个析构函数?
  • 为什么构造函数不能是虚函数?
  • 为什么基类析构函数需要是虚函数?

要回答上述问题我们首先需要了解什么是多态。

什么是多态

多态可以分为编译时多态和运行时多态。

  • 编译时多态:基于模板和函数重载方式,在编译时就已经确定对象的行为,也称为静态绑定。
  • 运行时多态:面向对象的一大特色,通过继承方式使得程序在运行时才会确定相应调用的方法,也称为动态绑定,它的实现主要是依赖于传说中的虚函数表。

如何查看对象的布局

在 gcc 中可以使用如下命令查看对象布局:

g++ -fdump-class-hierarchy model.cc 后查看生成的文件

在 clang 中可以使用如下命令:

clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.cc
// 查看对象布局
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c model.cc
// 查看虚函数表布局

上面两种方式其实足够了,也可以使用 gdb 来查看内存布局,这里可以看文末相关参考资料。本文都是使用 clang 来查看的对象布局。

接下来让我们一起来探秘下各种继承条件下类对象的布局情况吧~

1. 普通类对象的布局

如下代码:

struct Base {Base() = default;
    ~Base() = default;
    
    void Func() {}

    int a;
    int b;
};

int main() {
    Base a;
    return 0; 
}

// 使用 clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.cc 查看

输出如下:

*** Dumping AST Record Layout
         0 | struct Base
         0 |   int a
         4 |   int b
           | [sizeof=8, dsize=8, align=4,
           |  nvsize=8, nvalign=4]

*** Dumping IRgen Record Layout

画出图如下:

从结果中可以看见,这个普通结构体 Base 的大小为 8 字节,a 占 4 个字节,b 占 4 个字节。

2. 带虚函数的类对象的布局

struct Base {Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {printf("FuncB\n");
    }

    int a;
    int b;
};

int main() {
    Base a;
    return 0; 
}

// 这里可以查看对象的布局和相应虚函数表的布局
clang -Xclang -fdump-record-layouts -stdlib=libc++ -c model.cc
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c model.cc

对象布局如下:

*** Dumping AST Record Layout
         0 | struct Base
         0 |   (Base vtable pointer)
         8 |   int a
        12 |   int b
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping IRgen Record Layout

这个含有虚函数的结构体大小为 16,在对象的头部,前 8 个字节是虚函数表的指针,指向虚函数的相应函数指针地址,a 占 4 个字节,b 占 4 个字节,总大小为 16。

虚函数表布局:

Vtable for 'Base' (5 entries).
   0 | offset_to_top (0)
   1 | Base RTTI
       -- (Base, 0) vtable address --
   2 | Base::~Base() [complete]
   3 | Base::~Base() [deleting]
   4 | void Base::FuncB()

画出对象布局图如下:

我们来探秘下传说中的虚函数表:

offset_to_top(0):表示当前这个虚函数表地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为 0。

RTTI 指针 :指向存储运行时类型信息(type_info) 的地址,用于运行时类型识别,用于 typeid 和 dynamic_cast。

RTTI 下面就是虚函数表指针真正指向的地址啦,存储了类里面所有的虚函数,至于这里为什么会有两个析构函数,大家可以先关注对象的布局,最下面会介绍。

3. 单继承下不含有覆盖函数的类对象的布局

struct Base {Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {printf("Base FuncB\n");
    }

    int a;
    int b;
};

struct Derive : public Base{
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

子类对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct Base (primary base)
         0 |     (Base vtable pointer)
         8 |     int a
        12 |     int b
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping IRgen Record Layout

和上面相同,这个含有虚函数的结构体大小为 16,在对象的头部,前 8 个字节是虚函数表的指针,指向虚函数的相应函数指针地址,a 占 4 个字节,b 占 4 个字节,总大小为 16。

子类虚函数表布局:

Vtable for 'Derive' (5 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (Base, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Base::FuncB()

画图如下:

这个和上面也是相同的,注意下虚函数表这里的 FuncB 函数,还是 Base 类中的 FuncB,因为在子类中没有重写这个函数,那么如果子类重写这个函数后对象布局是什么样的,请继续往下看哈。

4. 单继承下含有覆盖函数的类对象的布局

struct Base {Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {printf("Base FuncB\n");
    }

    int a;
    int b;
};

struct Derive : public Base{void FuncB() override {printf("Derive FuncB \n");
    }
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

子类对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct Base (primary base)
         0 |     (Base vtable pointer)
         8 |     int a
        12 |     int b
           | [sizeof=16, dsize=16, align=8,
           |  nvsize=16, nvalign=8]

*** Dumping IRgen Record Layout

依旧和上面相同,这个含有虚函数的结构体大小为 16,在对象的头部,前 8 个字节是虚函数表的指针,指向虚函数的相应函数指针地址,a 占 4 个字节,b 占 4 个字节,总大小为 16。

子类虚函数表布局:

Vtable for 'Derive' (5 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (Base, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Derive::FuncB()

注意这里虚函数表中的 FuncB 函数已经是 Derive 中的 FuncB 啦,因为在子类中重写了父类的这个函数。

再注意这里的 RTTI 中有了两项,表示 Base 和 Derive 的虚表地址是相同的,Base 类里的虚函数和 Derive 类里的虚函数都在这个链条下,这里可以继续关注下面多继承的情况,看看有何不同。

5. 多继承下不含有覆盖函数的类对象的布局

struct BaseA {BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB {BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseA, public BaseB{
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

类对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseA (primary base)
         0 |     (BaseA vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseB (base)
        16 |     (BaseB vtable pointer)
        24 |     int a
       28 |     int b
          | [sizeof=32, dsize=32, align=8,
          |  nvsize=32, nvalign=8]

Derive 大小为 32,注意这里有了两个虚表指针,因为 Derive 是多继承,一般情况下继承了几个带有虚函数的类,对象布局中就有几个虚表指针,并且子类也会继承基类的数据,一般来说,不考虑内存对齐的话,子类(继承父类)的大小 = 子类(不继承父类)的大小 + 所有父类的大小

虚函数表布局:

Vtable for 'Derive' (10 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (BaseA, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void BaseA::FuncB()
   5 | offset_to_top (-16)
   6 | Derive RTTI
       -- (BaseB, 16) vtable address --
   7 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
   8 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
   9 | void BaseB::FuncC()

可画出对象布局图如下:

offset_to_top(0):表示当前这个虚函数表(BaseA,Derive)地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为 0。

再注意这里的 RTTI 中有了两项,表示 BaseA 和 Derive 的虚表地址是相同的,BaseA 类里的虚函数和 Derive 类里的虚函数都在这个链条下,截至到 offset_to_top(-16)之前都是 BaseA 和 Derive 的虚函数表。

offset_to_top(-16):表示当前这个虚函数表(BaseB)地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为 -16,这里用于 this 指针偏移,下一小节会介绍。

注意下后面的这个 RTTI:只有一项,表示 BaseB 的虚函数表,后面也有两个虚析构函数,为什么有四个 Derive 类的析构函数呢,又是怎么调用呢,请继续往下看~

6. 多继承下含有覆盖函数的类对象的布局

struct BaseA {BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB {BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseA, public BaseB{void FuncB() override {printf("Derive FuncB \n");
    }

    void FuncC() override {printf("Derive FuncC \n");
    }
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseA (primary base)
         0 |     (BaseA vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseB (base)
        16 |     (BaseB vtable pointer)
        24 |     int a
        28 |     int b
           | [sizeof=32, dsize=32, align=8,
           |  nvsize=32, nvalign=8]

*** Dumping IRgen Record Layout

类大小仍然是 32,和上面一样。

虚函数表布局:

Vtable for 'Derive' (11 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (BaseA, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Derive::FuncB()
   5 | void Derive::FuncC()
   6 | offset_to_top (-16)
   7 | Derive RTTI
       -- (BaseB, 16) vtable address --
   8 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
   9 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
  10 | void Derive::FuncC()
       [this adjustment: -16 non-virtual]

offset_to_top(0):表示当前这个虚函数表(BaseA,Derive)地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为 0。

再注意这里的 RTTI 中有了两项,表示 BaseA 和 Derive 的虚表地址是相同的,BaseA 类里的虚函数和 Derive 类里的虚函数都在这个链条下,截至到 offset_to_top(-16) 之前都是 BaseA 和 Derive 的虚函数表。

offset_to_top(-16):表示当前这个虚函数表(BaseB)地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为 -16。当基类 BaseB 的引用或指针 base 实际接受的是 Derive 类型的对象,执行 base->FuncC()时候,由于 FuncC()已经被重写,而此时的 this 指针指向的是 BaseB 类型的对象,需要对 this 指针进行调整,就是 offset_to_top(-16),所以 this 指针向上调整了 16 字节,之后调用 FuncC(),就调用到了被重写后 Derive 虚函数表中的 FuncC() 函数。这些带 adjustment 标记的函数都是需要进行指针调整的。至于上面所说的这里虚函数是怎么调用的,估计您也明白了吧~

7. 多重继承不同的继承顺序导致的类对象的布局相同吗?

struct BaseA {BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB {BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseB, public BaseA{void FuncB() override {printf("Derive FuncB \n");
    }

    void FuncC() override {printf("Derive FuncC \n");
    }
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseB (primary base)
         0 |     (BaseB vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseA (base)
        16 |     (BaseA vtable pointer)
        24 |     int a
        28 |     int b
           | [sizeof=32, dsize=32, align=8,
           |  nvsize=32, nvalign=8]

*** Dumping IRgen Record Layout

这里可见,对象布局和上面的不相同啦,BaseB 的虚函数表指针和数据在上面,BaseA 的虚函数表指针和数据在下面,以 A,B 的顺序继承,对象的布局就是 A 在上 B 在下,以 B,A 的顺序继承,对象的布局就是 B 在上 A 在下。

虚函数表布局:

Vtable for 'Derive' (11 entries).
   0 | offset_to_top (0)
   1 | Derive RTTI
       -- (BaseB, 0) vtable address --
       -- (Derive, 0) vtable address --
   2 | Derive::~Derive() [complete]
   3 | Derive::~Derive() [deleting]
   4 | void Derive::FuncC()
   5 | void Derive::FuncB()
   6 | offset_to_top (-16)
   7 | Derive RTTI
       -- (BaseA, 16) vtable address --
   8 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
   9 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
  10 | void Derive::FuncB()
       [this adjustment: -16 non-virtual]

对象布局图如下:

虚函数表的布局也有所不同,BaseB 和 Derive 共用一个虚表地址,在整个虚表布局的上方,而布局的下半部分是 BaseA 的虚表,可见继承顺序不同,子类的虚表布局也有所不同。

8. 虚继承的布局

struct Base {Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct Derive : virtual public Base{void FuncB() override {printf("Derive FuncB \n");
    }
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   (Derive vtable pointer)
         8 |   struct Base (virtual base)
         8 |     (Base vtable pointer)
        16 |     int a
        20 |     int b
           | [sizeof=24, dsize=24, align=8,
           |  nvsize=8, nvalign=8]

*** Dumping IRgen Record Layout

虚继承下,这里的对象布局和普通单继承有所不同,普通单继承下子类和基类共用一个虚表地址,而在虚继承下,子类和虚基类分别有一个虚表地址的指针,两个指针大小总和为 16,再加上 a 和 b 的大小 8,为 24。

虚函数表:

Vtable for 'Derive' (13 entries).
   0 | vbase_offset (8)
   1 | offset_to_top (0)
   2 | Derive RTTI
       -- (Derive, 0) vtable address --
   3 | void Derive::FuncB()
   4 | Derive::~Derive() [complete]
   5 | Derive::~Derive() [deleting]
   6 | vcall_offset (-8)
   7 | vcall_offset (-8)
   8 | offset_to_top (-8)
   9 | Derive RTTI
       -- (Base, 8) vtable address --
  10 | Derive::~Derive() [complete]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  11 | Derive::~Derive() [deleting]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  12 | void Derive::FuncB()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]

对象布局图如下:

vbase_offset(8):对象在对象布局中与指向虚基类虚函数表的指针地址的偏移量

vcall_offset(-8):当 虚基类 Base 的引用或指针 base 实际接受的是 Derive 类型的对象,执行 base->FuncB() 时候,由于 FuncB()已经被重写,而此时的 this 指针指向的是 Base 类型的对象,需要对 this 指针进行调整,就是 vcall_offset(-8),所以 this 指针向上调整了 8 字节,之后调用 FuncB(),就调用到了被重写后的 FuncB()函数。

9. 虚继承带未覆盖函数的对象布局

struct Base {Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {printf("Base FuncB\n");
    }

    virtual void FuncC() {printf("Base FuncC\n");
    }

    int a;
    int b;
};

struct Derive : virtual public Base{void FuncB() override {printf("Derive FuncB \n");
    }
};

int main() {
    Base a;
    Derive d;
    return 0; 
}

对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   (Derive vtable pointer)
         8 |   struct Base (virtual base)
         8 |     (Base vtable pointer)
        16 |     int a
        20 |     int b
           | [sizeof=24, dsize=24, align=8,
           |  nvsize=8, nvalign=8]

*** Dumping IRgen Record Layout

和上面虚继承情况下相同,普通单继承下子类和基类共用一个虚表地址,而在虚继承下,子类和虚基类分别有一个虚表地址的指针,两个指针大小总和为 16,再加上 a 和 b 的大小 8,为 24。

虚函数表布局:

Vtable for 'Derive' (15 entries).
   0 | vbase_offset (8)
   1 | offset_to_top (0)
   2 | Derive RTTI
       -- (Derive, 0) vtable address --
   3 | void Derive::FuncB()
   4 | Derive::~Derive() [complete]
   5 | Derive::~Derive() [deleting]
   6 | vcall_offset (0)
   7 | vcall_offset (-8)
   8 | vcall_offset (-8)
   9 | offset_to_top (-8)
  10 | Derive RTTI
       -- (Base, 8) vtable address --
  11 | Derive::~Derive() [complete]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  12 | Derive::~Derive() [deleting]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  13 | void Derive::FuncB()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]
  14 | void Base::FuncC()

对象布局图如下:

vbase_offset(8):对象在对象布局中与指向虚基类虚函数表的指针地址的偏移量

vcall_offset(-8):当 虚基类 Base 的引用或指针 base 实际接受的是 Derive 类型的对象,执行 base->FuncB() 时候,由于 FuncB()已经被重写,而此时的 this 指针指向的是 Base 类型的对象,需要对 this 指针进行调整,就是 vcall_offset(-8),所以 this 指针向上调整了 8 字节,之后调用 FuncB(),就调用到了被重写后的 FuncB()函数。

vcall_offset(0):当 Base 的引用或指针 base 实际接受的是 Derive 类型的对象,执行 base->FuncC()时候,由于 FuncC()没有被重写,所以不需要对 this 指针进行调整,就是 vcall_offset(0),之后调用 FuncC()。

10. 菱形继承下类对象的布局

struct Base {Base() = default;
    virtual ~Base() = default;
    
    void FuncA() {}

    virtual void FuncB() {printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseA : virtual public Base {BaseA() = default;
    virtual ~BaseA() = default;
    
    void FuncA() {}

    virtual void FuncB() {printf("BaseA FuncB\n");
    }

    int a;
    int b;
};

struct BaseB : virtual public Base {BaseB() = default;
    virtual ~BaseB() = default;
    
    void FuncA() {}

    virtual void FuncC() {printf("BaseB FuncC\n");
    }

    int a;
    int b;
};

struct Derive : public BaseB, public BaseA{void FuncB() override {printf("Derive FuncB \n");
    }

    void FuncC() override {printf("Derive FuncC \n");
    }
};

int main() {
    BaseA a;
    Derive d;
    return 0; 
}

类对象布局:

*** Dumping AST Record Layout
         0 | struct Derive
         0 |   struct BaseB (primary base)
         0 |     (BaseB vtable pointer)
         8 |     int a
        12 |     int b
        16 |   struct BaseA (base)
        16 |     (BaseA vtable pointer)
        24 |     int a
        28 |     int b
        32 |   struct Base (virtual base)
        32 |     (Base vtable pointer)
        40 |     int a
        44 |     int b
           | [sizeof=48, dsize=48, align=8,
           |  nvsize=32, nvalign=8]

*** Dumping IRgen Record Layout

大小为 48,这里不用做过多介绍啦,相信您已经知道了吧。

虚函数表:

Vtable for 'Derive' (20 entries).
   0 | vbase_offset (32)
   1 | offset_to_top (0)
   2 | Derive RTTI
       -- (BaseB, 0) vtable address --
       -- (Derive, 0) vtable address --
   3 | Derive::~Derive() [complete]
   4 | Derive::~Derive() [deleting]
   5 | void Derive::FuncC()
   6 | void Derive::FuncB()
   7 | vbase_offset (16)
   8 | offset_to_top (-16)
   9 | Derive RTTI
       -- (BaseA, 16) vtable address --
  10 | Derive::~Derive() [complete]
       [this adjustment: -16 non-virtual]
  11 | Derive::~Derive() [deleting]
       [this adjustment: -16 non-virtual]
  12 | void Derive::FuncB()
       [this adjustment: -16 non-virtual]
  13 | vcall_offset (-32)
  14 | vcall_offset (-32)
  15 | offset_to_top (-32)
  16 | Derive RTTI
       -- (Base, 32) vtable address --
  17 | Derive::~Derive() [complete]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  18 | Derive::~Derive() [deleting]
       [this adjustment: 0 non-virtual, -24 vcall offset offset]
  19 | void Derive::FuncB()
       [this adjustment: 0 non-virtual, -32 vcall offset offset]

对象布局图如下:

vbase_offset (32)

vbase_offset (16):对象在对象布局中与指向虚基类虚函数表的指针地址的偏移量

offset_to_top (0)

offset_to_top (-16)

offset_to_top (-32):指向虚函数表的地址与对象顶部地址的偏移量。

vcall_offset(-32):当 虚基类 Base 的引用或指针 base 实际接受的是 Derive 类型的对象,执行 base->FuncB() 时候,由于 FuncB()已经被重写,而此时的 this 指针指向的是 Base 类型的对象,需要对 this 指针进行调整,就是 vcall_offset(-32),所以 this 指针向上调整了 32 字节,之后调用 FuncB(),就调用到了被重写后的 FuncB()函数。

为什么要虚继承

如图:

非虚继承时,显然 D 会继承两次 A,内部就会存储两份 A 的数据浪费空间,而且还有二义性,D 调用 A 的方法时,由于有两个 A,究竟时调用哪个 A 的方法呢,编译器也不知道,就会报错,所以有了虚继承,解决了空间浪费以及二义性问题。在虚拟继承下,只有一个共享的基类子对象被继承,而无论该基类在派生层次中出现多少次。共享的基类子对象被称为虚基类。在虚继承下,基类子对象的复制及由此而引起的二义性都被消除了。

为什么虚函数表中有两个析构函数

前面的代码输出中我们可以看到虚函数表中有两个析构函数,一个标志为 deleting,一个标志为 complete,因为对象有两种构造方式,栈构造和堆构造,所以对应的实现上,对象也有两种析构方式,其中堆上对象的析构和栈上对象的析构不同之处在于,栈内存的析构不需要执行 delete 函数,会自动被回收。

为什么构造函数不能是虚函数。

构造函数就是为了在编译阶段确定对象的类型以及为对象分配空间,如果类中有虚函数,那就会在构造函数中初始化虚函数表,虚函数的执行却需要依赖虚函数表。如果构造函数是虚函数,那它就需要依赖虚函数表才可执行,而只有在构造函数中才会初始化虚函数表,鸡生蛋蛋生鸡的问题,很矛盾,所以构造函数不能是虚函数。

为什么基类析构函数要是虚函数。

一般基类的析构函数都要设置成虚函数,因为如果不设置成虚函数,在析构的过程中只会调用到基类的析构函数而不会调用到子类的析构函数,可能会产生内存泄漏。

小总结

offset_to_top:对象在对象布局中与对象顶部地址的偏移量。

RTTI 指针 :指向存储运行时类型信息(type_info) 的地址,用于运行时类型识别,用于 typeid 和 dynamic_cast。

vbase_offset:对象在对象布局中与指向虚基类虚函数表的指针地址的偏移量。

vcall_offset:父类引用或指针指向子类对象,调用被子类重写的方法时,用于对虚函数执行指针地址调整,方便成功调用被重写的方法。

thunk: 表示上面虚函数表中带有 adjustment 字段的函数调用需要先进行 this 指针调整,才可以调用到被子类重写的函数。

最后通过两张图总结一下对象在 Linux 中的布局:

A *a = new Derive(); // A 为 Derive 的基类

如图:

a 作为对象指针存储在栈中,指向在堆中的类 A 的实例内存,其中实例内存布局中有虚函数表指针,指针指向的虚函数表存放在数据段中,虚函数表中的各个函数指针指向的函数在代码段中。

虚表结构大体如上图,正常的虚表结构中都含有后三项,当有虚继承情况下会有前两个表项。

参考资料:

https://www.cnblogs.com/qg-wh…

https://blog.csdn.net/fuzhong…

https://zhuanlan.zhihu.com/p/…

https://mp.weixin.qq.com/s/sq…

https://jacktang816.github.io…

https://blog.mengy.org/cpp-vi…

https://blog.mengy.org/cpp-vi…

https://blog.mengy.org/extend…

https://www.zhihu.com/questio…

https://www.zhihu.com/questio…

https://zhuanlan.zhihu.com/p/…

https://wizardforcel.gitbooks…

https://www.cnblogs.com/xhb19…

https://www.lagou.com/lgeduar…

更多文章,请关注我的 V X 公 主 号:程序喵大人,欢迎交流。

退出移动版