虚函数/虚继承对对象内存大小的影响,凌乱中......求指引

2015-06-22 11:35:37 +08:00
 codercai
class A
{
public:
    int a;
    virtual void f1(){}
    virtual void f2(){}
};

class E : public A
{
public:
    void virtual f3(){}
};

class F : virtual public A
{

};

class G : virtual public A
{
    void virtual f4(){}
};
int main()
{
    cout<<sizeof(A)<<endl;//8
    cout<<sizeof(E)<<endl;//8   
    cout<<sizeof(F)<<endl;//12
    cout<<sizeof(G)<<endl;//12
}

如上,私以为:
1、显然,A中因为虚函数的存在,增加了一个指向虚表的指针,所以大小为4(int a)+ 4(指针)=8
2、从E的大小可以看出,一般派生时,派生类中增加虚函数并没有导致派生类变大,这说明派生类 和基类应该是公用了同一张虚函数表,他们的虚函数地址都放在里面,所以没有必要在派生类中 同样增加一个指针指向虚表。因此E的大小不变。
3、从F的大小可以看出,虚继承时,派生类中会增加一个指针指向他的父类,因此派生类的大小增 加4字节。
4、class G 是为了进一步印证2、3点。

所以我是这么总结的:
1、如果基类、派生类均含有虚函数,他们是公用一张虚表的
2、虚继承会在派生类中额外增加一个指向父类的指针

然而,有这么一条博文说基类派生类有各自不同的虚表,和我的推论相悖,但却视乎证据确凿,我竟无言以对,所以凌乱了。求各位指点啊。http://blog.csdn.net/kangroger/article/details/38313461

1688 次点击
所在节点    C
10 条回复
ini
2015-06-22 12:48:33 +08:00
ilotuo
2015-06-22 13:25:00 +08:00
应该是继承的时候复制了基类的虚函数表吧。
每个类有自己的虚函数表,只不过根据有没有重新实现的话改变里面函数指针的值。
有本书叫 深入探索cpp对象模型
josephpei
2015-06-22 14:04:30 +08:00
Lippman 《Inside C++ Object Model》深度探索C++对象模型,有详细解释。
codercai
2015-06-22 16:40:29 +08:00
@ini
非常感谢层煮的分享!
博主的文章基本能看懂,那么问题还是存在的呀,如果确实是基类派生类各自有其虚表,那么也就是个自有一个虚表指针,这样的话,上面的输出就说不通了,应该是
cout<<sizeof(E)<<endl;//12。
这个作何解释呀?
secondwtq
2015-06-22 18:11:58 +08:00
@codercai 我细节很多地方都忘了,不过这块还是有点记忆的。

无论是基类还是派生类的对象实例,都只会有一个 vptr,占据对象布局中相同的位置。
所不同的只是其指向的虚表。

你想啊,加一个继承层次,就在其所有实例对象上加一个 vptr,这样做,和所有需要 vtable 的实例对象都有且只有一个 vptr,哪个成本低。
况且还有一个关键问题就是,如果子类实例对象有多个 vptr(单继承),当你使用基类指针调用虚函数的时候,是根本没有办法判断该指针所指对象是不是拥有楼主所推断出的“多出来”的那个 vptr 的。

实际情况是,基类和派生类的对象实例,都有且只有一个 vptr,并占据对象布局中相同的位置(当然是有虚函数的时候)。而在不同类型的对象实例中,这个指针指向不同的 vtable。

举个栗子,如果你知道了不同类型对象虚表的地址,在你对 C++ 搞出来的可执行程序做动态汇编调试的时候,找到一个对象的位置,那么在 hex view 里面瞄一眼这个对象的开始几个字节(vptr 的位置),就能知道这个对象是什么类型,我管这个叫人肉 RTTI :)

C++ 运行期多态的精髓,窃以为就在这个虚指针上。另外我个人一般是把这个 vptr 当作一个 data member 来看的。
secondwtq
2015-06-22 18:19:13 +08:00
@codercai 另外,之所以基类和派生类不共用同一张虚函数表,我个人认为是因为同一个基类可能会派生出多个子类,并且它们可能会增加、覆盖不同的虚函数。

而在进行虚函数调用的时候,你所拥有的信息只有:一个 vptr 及该 vptr 指向的 vtable;类型定义;指针/引用的类型(不一定是对象实际类型,但是就算不是也一定是其基类);由类型信息所推导出的,编译器写入可执行代码的一个索引。

这些信息,除了第一项是运行时的,后三项全部是编译时确定的。我觉得如果共用一张 vtable 的话,在以上条件下,是无法处理第一段所描述的复杂情况的。
codercai
2015-06-22 19:15:09 +08:00
@secondwtq
又查了一些资料,加上层所言,基类派生类确实是各有其虚表,那么问题就是更奇怪了,为什么输出是这样的呢?
cout<<sizeof(A)<<endl;//8
cout<<sizeof(E)<<endl;//8
然而不应该是
cout<<sizeof(A)<<endl;//8
cout<<sizeof(E)<<endl;//12
这样的么? 都应该有一个指向虚表的指针啊~~~
secondwtq
2015-06-23 01:51:16 +08:00
@codercai 你也许把“子类实例是子类的父类部分与子类部分所拼接起来的”和这个搞混了
子类和父类实例使用不同的 vtable,其 vptr 的值也相应是不一样的。但是每个实例中只有一个 vptr(对于单继承来说是这样的)。

可以这样说,vtable 不能简单被分为“基类”和“子类”两个部分,一个对象的 vptr 所指 vtable,其中包含了该对象实际类型中所有可以用指针/引用直接调用虚函数(包括父类中未被子类 override 掉的,子类新定义的,和子类 override 掉父类的)。因此任何一个实例,只有一个 vptr 的位置(依然是只针对单继承)。

我个人平常的理解是,这个 vptr 相当于对象的一个“类型信息”或者类型的 ID,据我所知某些动态语言的实现中,对象实际在内存中的大小是不定的,但是每个对象头部的结构是确定的,这个头部中存储了必要的类型信息,据此可推导出对象的具体“类型”,以及该对象究竟符合哪一种布局。

C++ 中一个实例对象的实际类型可能有好几层继承,涉及到若干个基类,但是其类型总是能唯一确定的。
skydiver
2015-06-23 03:09:06 +08:00
@livid 为什么这个主题页面也需要登录才能查看。。
codercai
2015-06-23 08:56:35 +08:00
@secondwtq
读到你的第一句就恍然大悟,非常感谢啊,这个问题困扰我一两天了。我确实是将基类vptr单纯继承到了子类了,结合前面几位
@ini
@ilotuo
给的博文链接和你的解答,现在已经非常清晰了,感谢~~~~

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/200304

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX