首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 开发语言 > C++ >

请问:关于内存对齐有关问题。请解释以下程序运行结果

2012-03-23 
请教:关于内存对齐问题。请解释以下程序运行结果。程序1:#include iostreamusing namespace stdclass B0

请教:关于内存对齐问题。请解释以下程序运行结果。
程序1:
#include <iostream>
using namespace std;
class B0 //声明基类B0
{ public: //外部接口
char nV;
char n;
//void fun(){cout<<"Member of B0"<<endl;}
};
class B1: virtual public B0 //B0为虚基类,派生B1类
{ public: //新增外部接口
int nV1;
};
class B2: virtual public B0 //B0为虚基类,派生B2类
{ public: //新增外部接口
int nV2;
};

class D1: public B1, public B2 //派生类D1声明
{
public: //新增外部接口
int nVd;
//void fund(){cout<<"Member of D1"<<endl;}
};
void main() //程序主函数
{
B0 b0;
B1 b1;
B2 b2;

D1 d1; //声明D1类对象d1
d1.nV=2; //使用最远基类成员
//d1.fun();
cout<<sizeof(B0)<<" "<<sizeof(B1)<<" "<<sizeof(B2)<<" "<<sizeof(D1)<<endl;
cout<<sizeof(b0)<<" "<<sizeof(b1)<<" "<<sizeof(b2)<<" "<<sizeof(d1);



运行结果:
2 10 10 22
2 10 10 22

程序2:
#include <iostream>
using namespace std;
class X1
 {
  int i;//4个字节
  char c1;//1个字节
  char c2;//1个字节
  };
 
class X2
 {
  char c1;//1个字节
  int i;//4个字节
  char c2;//1个字节
 };
 
 class X3
 {
  char c1;//1个字节
  char c2;//1个字节
  int i;//4个字节
 };
int main()
 {  
  cout<<"long "<<sizeof(long)<<"\n";
  cout<<"float "<<sizeof(float)<<"\n";
  cout<<"int "<<sizeof(int)<<"\n";
  cout<<"char "<<sizeof(char)<<"\n";
 
  X1 x1;
  X2 x2;  
X3 x3;
  cout<<"x1 的大小 "<<sizeof(x1)<<"\n";
  cout<<"x2 的大小 "<<sizeof(x2)<<"\n";
  cout<<"x3 的大小 "<<sizeof(x3)<<"\n";
  return 0;
}

运行结果:
long 4
float 4
int 4
char 1
x1 的大小 8
x2 的大小 12
x3 的大小 8

在visual c++6.0中运行。
对于程序1,由于使用virtual继承,会增加“内部调整指针”,以免使子类重复拷贝同一个数据。由此B0:2个char,2个字节;B1:BO继承来的的2个char+内部调整指针(4字节)+自身int nV1(4字节)=10字节;同理B2也是10字节;D1:从B1和B2那里来的两个“内部调整指针”(8字节)+nV1(4字节)+nV2(4字节)+nVd(4字节)=20字节,其中函数由于存放在代码区,不参与类的sizeof计算。由此好像根本没有考虑内存对齐的问题。

对于程序2,显示考虑了内存对齐。而且好像是4字节对齐。
但是我在project->setting->c/c++->category/code generation->structure member alignment 里,无论怎么设置,结果都一样。

不知道我上面的分析有没有错。
到底什么时候对齐,什么时候不对齐,具体对齐是怎么个对齐法呢?迷惑中。。。。
欢迎大家发表意见。希望了解的高手们能够抽出点点时间帮我解除疑惑,感激不尽!



[解决办法]
所有的基本类型都有相应的对齐参数,编译器在编译时,会用全局的对齐参数和当前类型的对齐参数中较小的一个进行对齐.
比如,编译时你指定按8字节对齐,可以你的类型是char,由于char的对齐参数是1,所以最后还是按1字节对齐.
对于复合类型,所有的成员都按各自的对齐参数和全局对齐参数中较小的一处进行对齐.
比如:
struct {
 char a;
 short b;
 long c;
}
这个结构里,a就对齐在1字节边界,b就对齐在2字节边界,c就对齐在4字节边界.最后这个结构就占8个字节...
对于这个结构来说,它的对齐参数则是它所有成员里最大的一个对齐参数,也就是说,这个结构在对齐的时候,就会按4字节对齐...
[解决办法]
还忘说了一点.
对于上面结构来说,对齐之后还要补齐,目的就是为了在设定这个类型的数组时,所有的项都可以正确的对齐到相应的位置.
就像你的例子里的
class X1
{
int i;//4个字节
char c1;//1个字节
char c2;//1个字节
}; 
虽然这个结构只占了6个字节,但由于它的成员i是4字节对齐的,也就要求结构按4字节对齐,如果它sizeof是6的话,就会导致定义数组时,第二项无法对齐.所以这时编译器会给结构补两个字节.以保证结构的大小是它对齐参数的整数倍.
[解决办法]
ls说的好。
对于第一个因为都是char,所以就不需要补齐了,只需要对齐就可以,所以为2,
两个10增加的是一个int和一个指针(指向vtbl)(从你的结果猜测是这样)


后面的22,因为是virtual继承,原来的两个char不变,分别增加了B1 B2 比B多的那部分就是 2+(12-2)+(12-2),可以搜索virtual 继承或者“菱形继承”
[解决办法]
综上所述:

Win32平台下的微软 编译器(cl.exe for 80×86)的对齐策略:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节(trailing padding)。
备注:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。
[解决办法]
UP
[解决办法]
up
记得c++必知必会中说过,最好保持对类实现的良好的不知情;否则用POD风格
MARK
[解决办法]
c/c++内存对齐
http://blog.csdn.net/sharpdew/articles/772504.aspx
[解决办法]
引用楼主:对于程序2,显示考虑了内存对齐。而且好像是4字节对齐。 
但是我在project->setting->c/c++->category/code generation->structure member alignment 里,无论怎么设置,结果都一样

PS: 你加个在主函数前面 #pragma pack(1)
试试
[解决办法]
还有一种对齐方式,那就是强制对齐。
强制对齐与自然对齐取小的值,就是实际对齐的方式。
当确定实际对齐方式以后,如果在前一个成员剩余的对齐空间不足以放下该成员时,则该成员放到下一个存储空间。

探讨
综上所述:

Win32平台下的微软 编译器(cl.exe for 80×86)的对齐策略:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整…

[解决办法]
内存对齐,说的太多了,不想说了!

lz看看网上的一些资料!
[解决办法]
D1:从B1和B2那里来的两个“内部调整指针”(8字节)+nV1(4字节)+nV2(4字节)+nVd(4字节)+ BO继承来的2个char = 22;

B1和B2都重B0继承2个char,但是因为B1和B2是虚继承B0,所以,D1的父类中,会共享B0的空间,所以只从B0继承来的2个char 而不是4个。
[解决办法]
在vc6中栈的对齐方式不受结构成员对齐选项的影响。(本来就是两码事)。它总是保持对齐,而且对齐在4字节边界上。

[解决办法]
我的理解:为什么要内存对齐?

CPU与内存通过地址总线与数据总线进行联系. 地址总线传输内存的地址. 假如数据总线是32位,则一次可传输4个字节数据. 虽然内存是按字节编址的, 但是其实地址总线里只会出现偶数地址(或4的倍数地址等). 所以如果一个数据的起始地址与结束地址不在(同一个)偶数地址, 那么可能就需要读取两次内存, 如果数据超过了数据总线的宽度,那肯定要读取多次内存. 
所以简单的判断方法是:
class X1 

int i;//4个字节 
char c1;//1个字节 
char c2;//1个字节 
}; 
假如是4字节对齐, 即地址总线只出现4的倍数地址. 那么读取i 是读取一次内存, 读取c1与c2也是一次足够, 因为c1的地址肯定是4的倍数. 末尾补齐2字节,保证其他对象/数据 也是4字节对齐. 所以是 4 + 2 + 2 = 8字节

class X2 

char c1;//1个字节 
int i;//4个字节 
char c2;//1个字节 
}; 
为了能读取一次内存就能读取到整个 i 数据, 对c1进行补齐,即c1 占了4字节, 从而使i的起始地址是4的倍数, 可直接放到地址总线.
所以是4+4+4 = 12字节


[解决办法]
参考:
http://super-jiju.spaces.live.com/blog/cns!806C498DDEE76B61!315.entry
[解决办法]
探讨
上面各位对于单独的类的内存对齐问题分析得很好很透彻。非常感谢!

可是我对于继承的类的内存分配对齐问题仍存在疑惑。。。。

将程序1修改如下:

#include <iostream>
using namespace std;
class B0 //声明基类B0
{ public: //外部接口
char nV;
double n;
//void fun(){cout < <"Member of B0" < <endl;}


};
class B1: virtual public B0 //B0为虚基类,派生B1类
{ public: //新增外部接口
ch…


[解决办法]
楼主,你用virtual继承,编译器会在对象里放一些其它的信息来保证其行为...这些东西在标准里没有规定,编译器可以对它进行任意排布.原则上,对齐只对POD类型有意义.其它类型很难保证其布局...比如:编译器甚至可以对类的私有成员进行重排...
所以,很多时候你会发现,我们定义数据包多用使用而不是继承...
比如:
struct a {
...
};
struct b { //通常是这样
a _member;
....
};
struct c : public a { // 这样在绝大多数情况下是可以的,但不能保证
...
};
[解决办法]
探讨
上面各位对于单独的类的内存对齐问题分析得很好很透彻。非常感谢!

可是我对于继承的类的内存分配对齐问题仍存在疑惑。。。。

将程序1修改如下:

#include <iostream>
using namespace std;
class B0 //声明基类B0
{ public: //外部接口
char nV;
double n;
//void fun(){cout < <"Member of B0" < <endl;}
};
class B1: virtual public B0 //B0为虚基类,派生B1类
{ public: //新增外部接口
ch…

[解决办法]
B0:
char nV; 8
double n; 8
B1:
1个调整指针 + char i + int nV1 16
B2:
1个调整指针 + int nV2 8
D1:
int nVd; 8
[解决办法]
程序1: 
#include <iostream> 
using namespace std; 
class B0 //声明基类B0 
{ public: //外部接口 
char nV; 
char n; 
//void fun(){cout < <"Member of B0" < <endl;} 
}; 
class B1: virtual public B0 //B0为虚基类,派生B1类 
{ public: //新增外部接口 
int nV1; 
}; 

个人感觉sizeof(B1)应为12
B0 2 (被继承后应该在B1里补齐为4吧)
1个调整指针 4
int nV1 4

4+4+4=12

请教下,为什么B0在程序里为2而不是4
[解决办法]
10:
指针4字节(指向一个位移信息表)
本身数据4字节
虚基类的数据2字节
共10字节

22:
作为B1的对象,8个字节,其中4字节的指针(指向一个位移信息表),4字节的数据
作为B2的对象,8个字节,其中4字节的指针(指向一个位移信息表),4字节的数据
本身的数据 4字节
虚基类的数据 2字节

(由于这些乱七八糟的数据,大小已经不是由以前的什么对齐规则所限了)

为什么会有一个位移信息表呢?
注意下面的代码
B2* p = &d1;
p本身的的和d1的地址相等吗?
明显不等,p指向了d1这个对象偏移八个位置的地方:即B2
也许现在还看不出问题.
接着:
p->nV = 一个值;
于是要通过这4字节的指针,得到虚基类的数据的偏移地址.

注意到,如果p是作为B1的指针呢?
p->nV = 一个值;
同样需要一个偏移地址.
两者的偏移地址区别如下.


一个纯粹的B1对象,虚基类的数据的偏移地址为8
一个纯粹的B2对象,虚基类的数据的偏移地址为8

一个D1对象,虚基类的数据的偏移地址为8+8+4 = 20;
现在d1作为一个B1对象,虚基类的数据的偏移地址为20(即,这个指针指向的表的第一项的值为20)
现在d2作为一个B2的对象,虚基类的数据偏移地址为12(即,这个指针指向的表的第一项的值为12)

在上面,两个纯粹的B1或B2对象的虚基类的数据偏移地址为8
而不是纯粹的话,就可能是20,12.

有虚函数的时候差不多.
这也就是传说中的,有多继承,有虚基类的时候指针重设问题.

这里只是VC6的实现,换个编译器可能有别人方案.
[解决办法]
mark.
[解决办法]
学习!~~~
[解决办法]
探讨
redleaves您的回答比较有意思,“乱放的”?

[解决办法]
up
[解决办法]
Mark 学习
[解决办法]
upup
[解决办法]
内存对其很重要,否则程序会偶尔出现一些莫名其妙的错误结果
[解决办法]
如果不是搞嵌入式开发,就搞一字节对齐算了,损失掉性能,避免了设计结构的复杂性,成员顺序随你怎么排.


[解决办法]
mark,不错的一个论题
[解决办法]
4楼说得不错,支持
[解决办法]
程序1:
b0:| char1 | char1 | == 2

b1,b2:| vptr4 | int4 | char1 | char1 | = 10

d1:| vptr(b1)4 | int(b1)4 | vptr(b2)4 | int(b2)4 | int4 | char1 | char1 | = 22

最后的两个char型,编译器是不给予补齐的

程序2:
x1:| int4 | char1 | char1 | 补2 | = 8

x2:| char1 | 对齐3 | int4 | char1 | 补3 | = 12

x3:| char1 | char2 | 对齐2 | int4 | = 8

程序1改:
b0:| char1 | 对齐7 | double8 | = 16

b1:| vptr4 | char1 | 对齐3 | int4 | 对齐4 | (b0)16 = 32 //最后的B0是8字节一块的,所以B0的首地址要对齐到8字节的边缘

b2:| vptr4 | int4 | (b0)16 | = 24 //先前的4+4=8已经让B0首地址对齐到8字节边缘了,所以不用添加对齐字节

d1:| (b1)32-(b0)16 = 16 | (b2)24-(b0)16 = 8 | int4 | 对齐4 | (b0)16 = 48 //前两部分是b1和b2扣掉各自后面的b0,然后跟上d1自己的数

据,这样总共是16+8+4=28,不在8的整数倍,所以补4,等于32,再把b0放上,就是48
[解决办法]
我想知道这是物理内存还是c语言中的内存

[解决办法]

探讨
我想知道这是物理内存还是c语言中的内存

[解决办法]
探讨
我想知道这是物理内存还是c语言中的内存

[解决办法]
mark
[解决办法]
编译器有对齐指令的,能否讨论下
[解决办法]
我们一般都选择1字节对齐
[解决办法]
mark
[解决办法]
18楼的很好,就是好像D1:nvd 和 B0 的顺序错了。
应该改为:

对于D1:

+--------+----------+
| VB10 | B1:i |
+--------+----------+
| B1:nv1 |
+-------------------+
| VB20 | B2:nv2 |
+--------+----------+
| D1:nvd |
+-------------------+
| B0:nv |
+-------------------+
| B0:n |
+-------------------+

每一行是8字节的,因为最大的是double,八字节!

6*8 = 48
|
|

[解决办法]
但是在gcc下编译运行的结果却是:
8
16 32 24 40
16 32 24 40
0022FF40 0022FF44 0022FF48 0022FF50 0022FF58
0022FEF0 0022FEF4 0022FEF8 0022FF00 0022FF04 0022FF08 0022FF10

也就是可以这样推测:
vtrp(4)+ i(1)+ 填充(3)+ nv1(4)+ 填充(4)+ nv2(4)+ nvd(4)+ nv(1)+ 填充(7)+ n(8)= 40

也就是说,在gcc中,只有一个vtrp在最开头占了4个字节
[解决办法]
学习
[解决办法]
学习
[解决办法]
mark.
[解决办法]
mark
[解决办法]
学习

热点排行