下面博客转载自别人的,我也是被这个问题坑了快两天了,关于各种虚基类,虚继承,虚函数以及数据成员等引发的一系列内存对齐的问题再次详细描述

先看下面这片代码。在这里我使用了一个空类K,不要被这个东西所迷惑,我使用这个空类的目的主要是为了让它产生虚基类表指针而又不引入虚基类成员变量,这样我就可以少叙述一些关于虚基类成员排放的东西,而将焦点聚集在引入的那个虚基类表指针之上。这个空类虽然有点特殊,但是在这里它其他的东西和正常的类一样,不要纠结这个。还有,代码我直接指定了对齐参数,是为了不引起混乱。

#include"stdafx.h"
#pragma pack(8)
class K{}; class B :virtual K{
public:
int b;
B() :b(0xbbbbbbbb){}
}; int _tmain(int argc, _TCHAR* argv[])
{
B aa;
return ;
}

我们可以在IDE下打断点,用内存查看窗口轻易地观察到对象bb的内存布局情况,这个大家基本上都应该猜测得到大致的布局,就是在类B的开始插入了一个隐藏成员,虚基类表指针,然后才到B的成员b,因为虚基类没有成员所以其他的就不需要理了,这就是我选空类的原因,少说点话~。在VS2013下我抓到的布局是这样的:

恩,和我们猜测的一样。先是放了虚基类表指针然后才到 B 的成员。看到这里我们视乎可以得出点小结论,就是加入的隐藏成员,虚基类表指针,就是相当于在类里面加入一个正常的指针成员变量一样,即可以把类B看成是这样的等价布局:

class B { void *vb_ptr; int b;}

这样的话字节对齐等各方面都还算是合理的。

那么我们小改一下看下能推翻这个模型么。我们在B里面加一个成员变量double。变成这样:

#include "stdafx.h"
#pragma pack(8)
class K{}; class B :virtual K{
public:
int b;
double b2;
B() :b(0xbbbbbbbb), b2(){}
}; int _tmain(int argc, _TCHAR* argv[])
{
B aa;
return ;
}

按照以上的理论,我们新加入的double类型的成员变量b2刚好会紧紧的排列在成员b的后面,因为字节对齐刚好合适。但是不幸的是抓到的实际内存布局是这样的:

你看,原来在vb_ ptr和成员b之间没有填充字节的,现在却有了4个字节的填充,而b2和b之间的4字节填充倒还可以勉强理解得过去。所以之前的推论肯定是不正确的了。按照现在的布局和之前的布局貌似还可以这样牵强地理解,虚基类表指针不和B是一个结构的,B是一个结构,虚基类表指针自己一个结构,那么就等价于这样的布局:

class temp{ void* vb_ ptr;  B tempB;};

为什么是上面这种布局,不懂的需要看另一篇博客:http://www.cnblogs.com/13224ACMer/p/6287201.html

简单的解释如下:

在 temp 类里面

1.   vb_ptr 是一个指针,大小为 4 字节;

2.   tempB 是一个结构体,tempB 中又包含 int 类型(占 4 字节)的变量 b ;

3.   以及double 类型(占 8 字节)的变量 b2;

class 或 struct 类型的有效对齐参数是指它的成员中,有效对齐参数最大的那个值,对于 temp 类而言,有效对齐参数是 8 ,也就是 tempB 中的 b2 变量(double类型)占用的字节大小。

按照上述模型的话就可以满足上面的两种情况了,而且好像怎么变化B里面的成员,这个模型都可以很好的等价其对齐规则。这个结论当然也是错误的,一般情况其实也就到这里了,但是我说过要让事情变得复杂。

#include"stdafx.h"
#pragma pack(8)
class K{}; class A {
public:
int a;
A() :a(0xaaaaaaaa){}
}; class B :virtual K, A{
public:
int b;
B() :b(0xbbbbbbbb){}
}; int _tmain(int argc, _TCHAR* argv[])
{
A aa;
return ;
}

我们把代码改成如上那样。让B在虚继承的时候还多继承了一个A,而且是实继承A。这个派生类B的对象模型估计很多人也猜得对,按照对象布局规则,先是实基类A,然后是虚基类表指针,然后是成员b。实际上内存布局确实也是这样:

class temp{ A tempA; void* vb_ ptr;  B tempB;};  

但是后面,我们会慢慢的进入噩梦。小改一下代码。把类A的成员变量a的类型改成char类型。3个类现在变成这样,指定对齐仍然是8字节对齐,和VC默认的一样。

#include "stdafx.h"
#pragma pack(8)
class K{}; class A {
public:
char a; // int->char
A() :a(0xaaaaaaaa){}
}; class B :virtual K, A{
public:
int b;
B() :b(0xbbbbbbbb){}
}; int _tmain(int argc, _TCHAR* argv[])
{
B bb;
return ;
}
在IDE下抓取到的B的对象bb的内存布局是这样的:

怎么样,虚函数表指针后面居然填充了4个字节,看起来貌似完全没有必要在和B的成员变量b之间填充4个字节,就算是b直接排放在vb_prt之后也是满足整体的对齐规则的。这样看来,虚基类表指针的对齐规则貌似和排在它之前的对象又有点联系。

我们把指定的对齐参数改成4字节对齐。看下这填充的4个字节会消失掉么。理论上它应该消失,因为填充的字节数必须小于指定的对齐参数的。

#include"stdafx.h"
#pragma pack(4) //pack(8)->pack(4)
class K{}; class A {
public:
char a; //int->char
A() :a(0xaaaaaaaa){}
}; class B :virtualK, A{
public:
int b;
B() :b(0xbbbbbbbb){}
}; int _tmain(int argc, _TCHAR* argv[])
{
B bb;
return ;
}

可以看到,字节的填充完全没有变化,这确实是违背了对齐规则的,这种情况不应该发生~!。这也是苦恼我很久的地方。然而,还有更加苦恼的地方。

把对齐改回8字节对齐,然后再把B的成员b的类型改成double。

#include"stdafx.h"
#pragma pack(8)
class K{}; class A {
public:
char a; //int->char
A() :a(0xaaaaaaaa){}
}; class B :virtualK, A{
public:
double b;//int->double
B() :b(0xbbbbbbbb){}
}; int _tmain(int argc, _TCHAR* argv[])
{
B bb;
return ;
}

把b的类型由int改成了double之后,虚基类表指针和B的成员b之间居然填充了8个字节。实在是匪夷所思了。我尝试了很多猜测,很多看似马上就对得上号了的模型,但是最终都被自己举各种复杂的例子给推翻了~···当然,我想到的那些对齐模型都是很曲扭的,其实连我自己也不相信会是那样的~···

后来,不知道怎么的灵感就来了。

像虚基类表指针和虚函数表指针这些类里面必要的时候会出现的“隐藏成员变量”它们的对齐规则可以总结为一句话:

隐藏成员的加入不能影响在其后的成员的对齐。

这句话怎么理解呢?要想不影响后面的成员的对齐规则和填充的字节,那么因为隐藏成员而插入的总体字节长度就必须是结构里面的各个成员中有效对齐参数最大的那个的整数倍。这样,后面的成员才可以“无视”隐藏成员的存在进行对齐和填充。隐藏的成员确确实实也属于类的成员。

这个结论我已经测试了很多种情况了,都是可以的,我也相信像这种言简意赅的结论才会是正确的那一个,其实我早该想到,而且微软的编译器这样安排也确实很合情合理。我之前的各种憋屈结论立马变成浮云……

我们先按照这个规则小试牛刀一下。

比如最匪夷所思的这个:

#include"stdafx.h"
#pragma pack(4) //pack(8)->pack(4)
class K{}; class A {
public:
char a; //int->char
A() :a(0xaaaaaaaa){}
}; class B :virtualK, A{
public:
int b;
B() :b(0xbbbbbbbb){}
}; int _tmain(int argc, _TCHAR* argv[])
{
B bb;
return ;
}

类A的大小只占一个字节,这已经是它的完整的结构,a和虚基类表指针之间的那3个字节是因为虚基类表指针本身需要4字节对齐而引入的填充字节,在虚基类表指针之后,因为填充的3个字节和vb_ptr本身的4个字节加起来才是7个字节,还不是结构B中的有效对齐参数最大的那个的整数倍,B中成员的最大对齐参数是int b 或者是隐藏成员虚基类表指针本身的4字节对齐,这其实也是类B本身的有效对齐参数。所以还得在vb_ptr后面再补一个字节,这样后面的成员才能相当于无视掉这个隐藏参数带来的对齐影响。而后面那3个框出来的CC,其实是成员b需要4字节对齐而引入的字节填充,所以看起来就像是填充了4个字节而已。其实它并没有违反字节对齐的基本规则。

还有其他几个也是可以一样的推导出来。

虚函数表指针也是一个道理的,只不过虚函数表指针会简单一些,因为在虚函数表前面,要么没有成员,要么肯定就是已经是4字节对齐了,不会出现像虚基类表指针那样的前面补几个字节后面补几个字节这种情况。比如下面那样:

#include"stdafx.h"
#pragma pack(8)
class A {
public:
char a;
virtual void funA(){}
A() :a(0xaaaaaaaa){}
}; int _tmain(int argc, _TCHAR* argv[])
{
A aa;
return ;
}

类A有虚函数,所以会有一个隐藏的成员,虚函数表指针,放在类对象的最开始的地方,因为类A的最大对齐参数就是隐藏成员本身的对齐参数4了,因隐藏成员的加入而引入的总共字节数就自然是它的整数倍了,所以就没必要再隐藏成员后面填充字节了,然后就是成员变量a紧紧跟着在后面,因为A本身也要自身对齐,按照它的成员中最大的那个有效对齐作为自己的自身对齐,也就是隐藏成员虚函数指针,所以A本身要按照4字节对齐,所以在a后面要填充3个字节才满足对齐规则。

#include"stdafx.h"
#pragma pack(8)
class C {
public:
int c;
double c2;
virtual void funC(){}
C() :c(0xaaaaaaaa), c2(){}
}; int _tmain(int argc, _TCHAR* argv[])
{
C aa;
return ;
}

C类也是差不多,但是C有一个double成员,它的有效对齐是8字节的,是C中各个成员最大的一个,所以隐藏成员引入的字节要是8的整数倍。所以虚函数指针要至少填充4个字节然后加上它本身的4字节才能满足要求。

C++ 虚基类表指针字节对齐的更多相关文章

  1. c++ 虚函数多态、纯虚函数、虚函数表指针、虚基类表指针详解

    静态多态.动态多态 静态多态:程序在编译阶段就可以确定调用哪个函数.这种情况叫做静态多态.比如重载,编译器根据传递给函数的参数和函数名决定具体要使用哪一个函数.动态多态:在运行期间才可以确定最终调用的 ...

  2. C++ 虚继承实现原理(虚基类表指针与虚基类表)

    虚继承和虚函数是完全无相关的两个概念. 虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝.这将存在两个问题:其一,浪费存储空间:第二,存在二义性问题,通常可 ...

  3. 【c++内存分布系列】虚基类表

    虚基类表相对于虚函数表要稍微难理解些,故单独提出来. 虚函数表是在对象生成时插入一个虚函数指针,指向虚函数表,这个表中所列就是虚函数. 虚基类表原理与虚函数表类似,不过虚基类表的内容有所不同.表的第一 ...

  4. RTTI、虚函数和虚基类的实现方式、开销分析及使用指导(虚函数的开销很小,就2次操作而已)

    白杨 http://baiy.cn “在正确的场合使用恰当的特性” 对称职的C++程序员来说是一个基本标准.想要做到这点,首先要了解语言中每个特性的实现方式及其开销.本文主要讨论相对于传统 C 而言, ...

  5. C++中虚基类在派生类中的内存布局

    今天重温C++的知识,当看到虚基类这点的时候,那时候也没有太过追究,就是知道虚基类是消除了类继承之间的二义性问题而已,可是很是好奇,它是怎么消除的,内存布局是怎么分配的呢?于是就深入研究了一下,具体的 ...

  6. C++ - 虚基类、虚函数与纯虚函数

    虚基类       在说明其作用前先看一段代码 class A{public:    int iValue;}; class B:public A{public:    void bPrintf(){ ...

  7. 【转】C++ 虚函数&纯虚函数&抽象类&接口&虚基类

    1. 动态多态 在面向对象语言中,接口的多种不同实现方式即为多态.多态是指,用父类的指针指向子类的实例(对象),然后通过父类的指针调用实际子类的成员函数. 多态性就是允许将子类类型的指针赋值给父类类型 ...

  8. C++ 虚函数&纯虚函数&抽象类&接口&虚基类(转)

    http://www.cnblogs.com/fly1988happy/archive/2012/09/25/2701237.html 1. 多态 在面向对象语言中,接口的多种不同实现方式即为多态.多 ...

  9. C++ 由虚基类 虚继承 虚函数 到 虚函数表

    //虚基类:一个类可以在一个类族中既被用作虚基类,也被用作非虚基类. class Base1{ public: Base1(){cout<<"Construct Base1!&q ...

随机推荐

  1. (转)Windows管道(Pipe)重定向stdout,stderr,stdin

    参考: http://qiusuoge.com/11496.html http://www.cnblogs.com/BoyXiao/archive/2011/01/01/1923828.html st ...

  2. hdu_5085_Counting problem(莫队分块思想)

    题目连接:hdu_5085_Counting problem 题意:给你一个计算公式,然后给你一个区间,问这个区间内满足条件的数有多少个 题解:由于这个公式比较特殊,具有可加性,我们考虑讲一个数分为两 ...

  3. 【项目笔记】【bug】数组空指针异常

    package com.example.googleplay.ui.holder; import java.util.ArrayList; import android.view.View; impo ...

  4. 剑指offer 二叉搜索树与双向链表

    html, body { font-size: 15px; } body { font-family: Helvetica, "Hiragino Sans GB", 微软雅黑, & ...

  5. 转 Android:sp与dp(densityDpi与scaledDensity)

    一般在布局上设置控件大小维度的单位采用dp,而定义字体大小的单位采用sp. dp是dip,density independent pixel,即密度无关的像素单位,说白了,就是这个维度相对于不同屏幕的 ...

  6. zf-安徽桐城关于(资源中心-数据录入)上传文件后没有进行处理Excel文件的原因

    上传的文件 是会自动复制到另外一个路径的 如果没有进行处理 那么表示那个路径并没有那个文件 这样就会卡死 导致之后的文件都不会进行处理(后台有个变量是从数据库里获得文件路径的),所以需要去数据库 执行 ...

  7. Apache下的FileUtils.listFiles方法简单使用技巧

    一.引言 Apache提供的很多工具方法非常好用,推荐. 今天在使用的过程中使用到了org.apache.commons.io.FileUtils.listFiles方法,本文主要谈谈这个工具方法的用 ...

  8. 定制化jQuery

    毋庸置疑,jQuery很强大,很方便,但是......越来越臃肿,怎么办?,jquery只基于模块化开发的,可以通过工具定制jquery,选择你需要的模块即可. 下面这个网站可以帮你完成定制 http ...

  9. jQuery中的attr()和prop()使用

    总结:除了checked.seleted这样的属性推荐用prop()外,其他的随意都可以. 原因如下: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML ...

  10. js图片未加载完显示loading效果

    <html> <title>js图片未加载完显示loading效果</title> <body> <style> img{float:lef ...