含虚基类的类构造顺序深度理解

前言

前文C++虚函数调用过程深度理解(二)笔者探讨验证了一些关于C++虚函数调用过程概念的深度理解,希望能帮助你更了解cpp或激起你探索cpp的兴趣,那便是笔者的更新动力。

笔者将探讨验证关于含有虚基类的类构造顺序

结论

基本概念:

  • msvcmicrosoft visual c++编译器,应用于windows平台。
  • gccgnu c compilerg++是其cpp版本,一般应用于类unixlinux平台。
  • clang也是一种编译器,本文没有使用到。

结论:

  • 类的构造顺序是一致的,不管是在gcc编译器还是msvc编译器。
    • 笔者认为是cpp标准制定了这方面的规则。
  • 虽然从结果上看是一致的,但实现细节还是有差异。
    • 虚基表实现有差异,gcc实现的类布局相对紧凑对象携带的虚基表指针更少,即占用内存更少。
      • 但关于对象虚基表指针详细实现,笔者还没找到详细资料,在微软的c++参考手册上略有提及,但未深入涉及实现细节。

验证

本次验证使用如下示例代码

#include <iostream>
#include <string>

class A {
public:
    A(const std::string& s = std::string()) {
        std::cout << "A()" << " " << s << std::endl;
    }

    ~A() {
        std::cout << "~A()" << std::endl;
    }
};

class B {
public:
    B() {
        std::cout << "B()" << std::endl;
    }

    ~B() {
        std::cout << "~B()" << std::endl;
    }
};

class C : public virtual A, public virtual B {
public:
    C() {
        std::cout << "C()" << " : public virtual A, public virtual B" << std::endl;
    }

    ~C() {
        std::cout << "~C()" << std::endl;
    }
};

class D : public virtual A, public virtual B {
public:
    D() {
        std::cout << "D()" << " : public virtual A, public virtual B" << std::endl;
    }

    ~D() {
        std::cout << "~D()" << std::endl;
    }
};

class E : public C, public D, virtual public B {
public:
    E() {
        std::cout << "E() : public C, public D, virtual public B" << std::endl;
    }

    ~E() {
        std::cout << "~E()" << std::endl;
    }
};

class AB :
    public
    virtual //当注释这行时,在DCB中,A实例有两份,一份是public引入, 一份是public virtual引入
    A {
public:
    AB() : A("called_from_AB()") {
        std::cout << "AB() : public virtual A" << std::endl;
    }

    ~AB() {
        std::cout << "~AB()" << std::endl;
    }
};

class AC : virtual public A {
public:
    AC() : A("called_from_AC()") {
        std::cout << "AC() : virtual public A" << std::endl;
    }

    ~AC() {
        std::cout << "~AC()" << std::endl;
    }
};

class DCB : public AB, public AC {
public:
    DCB() :
        AC::A("called_from_DCB()"),     //msvc gcc
        //AB::A("called_from_DCB"),     //msvc gcc
        //A("called_from_DCB"),         //gcc 上这样写可以编译通过;msvc这样写编译不通过
        AB(), AC() {
        std::cout << "DCB() : : public AB, public AC" << std::endl;
    }

    ~DCB() {
        std::cout << "~DCB()" << std::endl;
    }
};

class F {
public:
    F() {
        std::cout << "F()" << std::endl;
    }

    ~F() {
        std::cout << "~F()" << std::endl;
    }
};

class G : public E, virtual public F {
public:
    G() : E(), F() {
        std::cout << "G() : public E, virtual public F" << std::endl;
    }

    ~G() {
        std::cout << "~G()" << std::endl;
    }
};

class G_NoVirutalF : public E, public F {
public:
    G_NoVirutalF() : E(), F() {
        std::cout << "G_NoVirutalF() : public E, public F" << std::endl;
    }

    ~G_NoVirutalF() {
        std::cout << "~G_NoVirutalF()" << std::endl;
    }
};

int main() {
    A a;
    std::cout << "sizeof A = " << sizeof(a) << std::endl;

    B b;
    std::cout << "sizeof B = " << sizeof(b) << std::endl;

    C c;
    std::cout << "sizeof C = " << sizeof(c) << std::endl;

    D d;
    std::cout << "sizeof D = " << sizeof(d) << std::endl;

    E e;
    std::cout << "sizeof E = " << sizeof(e) << std::endl;

    G g;
    std::cout << "sizeof G = " << sizeof(g) << std::endl;

    G_NoVirutalF g_novf;
    std::cout << "sizeof G_NoVirutalF = " << sizeof(g_novf) << std::endl;

    DCB dcb;
    std::cout << "sizeof DCB = " << sizeof(dcb) << std::endl;

    return 0;
}
  • virtual可在public前后,如virtual publicpublic virtual

执行结果差异如下:
在这里插入图片描述

从上图可以看出:

  • msvcgcc实现差异导致对象大小不同。

还有个额外的知识点:

  • 空类对象的大小为1字节,编译器为了将对象区分默认做的设置。

绘制的继承关系图及构造顺序图如下:
在这里插入图片描述

如何确定含虚基类的类构造顺序,笔者根据实际验证情况及个人理解,有如下推断过程:

A 

B

C = [A, B]
D = [A, B]

E = [C, D, B] = [[[A, B], C], [[A, B], D], [B]]
           = [A, B, [C], [[A, B], D], [B]]
           = [A, B, C, [[A, B], D], [B]]
           # 解D中的[A, B],因为A, B是虚基类,且列表最前面已有A, B虚基类,则这里去除A, B
           = [A, B, C, [D], [B]]
           # 解D
           = [A, B, C, D, [B]]
           # 解[B],因为[B]为vitrual public即虚基类,且列表前面已有B,则这里消去[B]
           = [A, B, C, D]
E = [A, B, C, D]

F

G = [E, F] = [[[C, D, B], E], F]
           = [[[[A, B], C], [[A, B], D], [B]], F]
           #这部分过程与上面推断E的过程一致,可得到
           = [A, B, C, D, [E], [F]]
           # 解[E]
           = [A, B, C, D, E, [F]]
           # 因为F为virtual public即虚基类,则在列表中检查是否存储虚基类F。F不存在,则将其加入到前面虚基类部分[A, B]的尾部
           = [A, B, F, C, D, E]

# 这部分与G推断过程不同的是最后一步F的推断
G_NoVirutalF= [E, F]
    = [[[[A, B], C], [[A, B], D], [B]], F]
    #这部分过程与上面推断E的过程一致,可得到
    = [A, B, C, D, [E], [F]]
    # 解[E]
    = [A, B, C, D, E, [F]]
    # 因为F为public,不是虚基类,所以维持它的位置
    = [A, B, C, D, E, F]

根据上述推断,可得到大致的推断规则(也适用于不含虚基类的类构造顺序):

  1. 先将类继承关系继承顺序逐级展开
    1. E继承于C, D, B => E = [C, D, B]
    2. 其中C = [A, B]D = [A, B]AB为最底层基类则展开结束。代入使用 [[A, B], C][[A, B], D]
      • => E = [[[A, B], C], [[A, B], D] , [B]]
    3. [[A, B], C]中的[A, B]展开,=> E = [A, B, [C], [[A, B], D], [B]], 其中A, B为虚基类
    4. 展开C => E = [A, B, C, [[A, B], D], [B]]
    5. 展开[[A, B], D],因为D虚继承于A, B,故A B为虚基类,又因为A B已存在,故这里消除。若不是虚继承,则保留。
      • => E = [A, B, C, [D], [B]]
    6. 因为D为非虚继承,前面的C也是同理,这里保留。
      • => E = [A, B, C, D, [B]]
      • 假如这里最后面的[B][X]且为虚继承,则需要将X移动到前面虚基类部分[A, B]后面得到E = [A, B, F, C, D]
    7. 因为后面的B为虚继承,所以需要检查前面是否存在虚基类B。因已存在,故这里消除。
      • => E = [A, B, C, D]
    8. 加上E部分,则其最后构造顺序为[A, B, C, D, E]
  2. 后面虚基类移动至列表前面虚基类最后,按继承相对顺序

结尾

  • 行文至此,相关内容已分享结束。当然关于gccmsvc对于虚基表实现有详细描述的较正式文档可在评论区留言,我去看看。

    • 关于这块及相关详细信息可能会在后续更新。
  • 如果觉得还不错,你的点赞关注加收藏便是笔者继续更新的最大动力哦^_^

  • 23
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值