C++内存布局(二)

在《C++内存布局(一)》 中,我们介绍了C++内存布局的基本知识,本篇我们仍着重探讨C++类的内存布局,尤其是 多重继承钻石继承(菱形继承)场景下的虚函数表的情况。

一、多重继承

1.1 示例

class A
{
public:
    virtual void da(){MYTRACE();}
};

class B
{
public:
    virtual void db(){MYTRACE();}
};

class C: public A,  public B
{
public:
    virtual void dc(){MYTRACE();}
};

int main()
{
    A a;
    B b;
    C c;

    return 0;
}

C++内存布局(二)_第1张图片

C 中只有两张表
[A::vptr] 继承自基类A中的虚函数表不仅记录了class A的虚函数地址,同时记录了
class B中的虚函数地址,而且还记录了class C 自己定义的虚函数地址。另外还记录一些其他的关联信息。
[B:vptr] 只记录了class B中虚函数的地址

1.2 示例

class A
{
public:
    virtual void da(){MYTRACE();}
};

class B
{
public:
    virtual void db(){MYTRACE();}
};

 相比1.1中的代码示例,我们在这里交互继承顺序
class C: public B,  public A
{
public:
    virtual void dc(){MYTRACE();}
};


int main()
{
    A a;
    B b;
    C c;

    return 0;
}

C++内存布局(二)_第2张图片
好家伙,多重继承掉个顺序,这下子表的顺序也调整了。
看来,在多重继承中,如果继承的多个基类存在多张虚函数表,会按照继承顺序,优先选择第一个基类中的虚函数表作为[基表] (自创词汇,勿cue)将所有关联的虚函数信息记录在此表中。但是表的数量仍取决于基类中虚函数表的总数。

1.3 虚继承1

class A
{
public:
    virtual void da(){MYTRACE();}
};

class B
{
public:
    virtual void db(){MYTRACE();}
};

/// 虚继承自A
class C: public virtual A,  public B
{
public:
    virtual void dc(){MYTRACE();}
};


int main()
{
    A a;
    B b;
    C c;

    return 0;
}

C++内存布局(二)_第3张图片
这种情况(多重继承非全虚继承)下,还是只有两张虚函数表(取决于所有基类中虚函数表总数);不过有趣的是,这次竟然没有按照继承顺序优先选择【基表】,而是在未采用虚继承的class BB::vptr里优先记录了所有的虚函数地址及关联的一些信息。所以我们貌似总结出了一些规律:
虚继承 > 继承顺序

1.4 虚继承2

同样,在1.3的基础上,我们把虚继承换个顺序,看看结果怎么样

class A
{
public:
    virtual void da(){MYTRACE();}
};

class B
{
public:
    virtual void db(){MYTRACE();}
};

class C: public  A, virtual  public B
{
public:
    virtual void dc(){MYTRACE();}
};

int main()
{
    A a;
    B b;
    C c;

    return 0;
}

C++内存布局(二)_第4张图片
结果得到验证,与我们在1.3中得到规律的预期一致。表还是两张,在未采用虚继承的class AA::vptr里优先记录了所有的虚函数地址及关联的一些信息。

1.5 虚继承3 : 交换继承顺序

class A
{
public:
    virtual void da(){MYTRACE();}
};

class B
{
public:
    virtual void db(){MYTRACE();}
};

class C: virtual  public B, public  A
{
public:
    virtual void dc(){MYTRACE();}
};

int main()
{
    A a;
    B b;
    C c;
    return 0;
}

C++内存布局(二)_第5张图片
交换了顺序,与我们在1.4中得到的结果依然一致。可见虚继承 优于 继承顺序,在我们目前用于验证的VS2019 x64 的编译工具链上确实是这样的。

1.6 全虚继承

这次我们in-all 了,多重继承的全部改为虚继承,我们再来探究下。

class A
{
public:
    virtual void da(){MYTRACE();}
};

class B
{
public:
    virtual void db(){MYTRACE();}
};

class C: virtual  public A, virtual public  B
{
public:
    virtual void dc(){MYTRACE();}
    virtual void dc1(){MYTRACE();}
};

int main()
{
    A a;
    B b;
    C c;

    return 0;
}

C++内存布局(二)_第6张图片
喔嚯,这次大不同啊,大不同!在全部采用虚继承的情况下,编译器为class C创建了一张新的表C::vptr,用于记录class Aclass Bclass C中的虚函数地址以及其他关联信息。三张表啊三张表。

1.7 更加复杂一些的多重继承

class A
{
public:
    virtual void da(){MYTRACE();}
};

class B
{
public:
    virtual void db(){MYTRACE();}
};

class C: virtual  public A, virtual public  B
{
public:
    virtual void dc(){MYTRACE();}
    virtual void dc1(){MYTRACE();}
};


class D :  public C
{
};

int main()
{
    A a;
    B b;
    C c;
    D d;

    return 0;
}

C++内存布局(二)_第7张图片

我们看菱形继承到class D这儿,D的实例化对象d中包含了多张虚函数表,包含
A::vptr
B::vptr
C::vptr
C::A::vptr ⇒ 这里我只是用来表明层级关系,::在此并不表示作用域,别纠结
C::B::vptr

是不是很妙。

如果class D也定义了虚函数呢,会创建新的表么?如下,显然没有,D的虚函数地址被记录在了继承自CC::vptr下。
C++内存布局(二)_第8张图片

如果菱形继承采用虚继承呢,结果又会怎么样?
C++内存布局(二)_第9张图片

C++内存布局(二)_第10张图片
果如其然,虚继承情况下创建了一张新的表,用于单独记录class D 中虚函数地址。

二、菱形继承

2.1

class A
{
public:
    virtual void d(){MYTRACE();}
    virtual void da(){MYTRACE();}
};

class B : virtual public A
{
public:
    virtual void db(){MYTRACE();}
};

class C:  virtual  public  A
{
public:
    virtual void dc(){MYTRACE();}
};


/// 非虚继承
class D :  public B,  public C
{
public:
    virtual void dd(){}

};

int main()
{
    D d;
    d.d();

    return 0;
}

C++内存布局(二)_第11张图片

2.2

		A
	/	 	 \
B			  C
    \        /
		D
class A
{
public:
    virtual void d(){MYTRACE();}
    virtual void da(){MYTRACE();}
};

/// 菱形继承必须使用虚继承
class B : virtual public A
{
public:
    virtual void db(){MYTRACE();}
};

/// 菱形继承必须使用虚继承
class C:  virtual  public  A
{
public:
    virtual void dc(){MYTRACE();}
};

/// 全虚继承
class D :  virtual public B, virtual public C
{
public:
    virtual void dd(){}

};

int main()
{
    D d;
    d.d();

    return 0;
}

C++内存布局(二)_第12张图片

三、总结

  • 对于多重继承,且所有虚基类均不被虚继承的情况下,虚函数表(虚表指针vptr)的数量取决于所有继承的父类中分别包含的虚函数表的总数。如果子类也有自定义的虚函数,按照基类继承顺序的优先级, 此虚函数地址被记录在继承的第一个基类的虚函数表中;
  • 对于多重继承,且仅只有部分虚基类被虚继承的情况下,虚函数表(虚表指针vptr)的数量取决于所有继承的父类中分别包含的虚函数表的总数N。如果子类也有自定义的虚函数,按照基类继承顺序的优先级, 此虚函数地址被记录在继承的第一个被非虚继承的虚基类的虚函数表中,子类中包含的虚函数表总数N;如果第一个被非虚继承的虚基类不存在,则会为子类新建一张虚函数表vptr[],此时子类中包含的虚函数表总数N+1;
  • 对于多重继承,且所有虚基类均被虚继承的情况下,虚函数表(虚表指针vptr)的数量取决于所有继承的父类中分别包含的虚函数表的总数N。如果子类也有自定义的虚函数,那编译器会为子类单独创建一张虚函数表,用于记录子类中自己定义的虚函数地址。此时,子类中包含的虚函数表的总数为N+1
  • 菱形继承是一种特殊的多重继承,处于中间层的父类必须使用virtual虚继承,避免函数名重复导致编译错误;虚函数表及地址的记录规则与上面几条保持一致。

以上,属于个人总结,纰漏指出请不吝指正~~

你可能感兴趣的:(C++,c++,开发语言,多重继承,菱形继承,虚函数表,虚表指针)