对于成员对齐,我们要设法减少对象中的空洞,宁愿让末尾留下空洞也不要让中间留下空洞,尽量使所有成员连续存放,并且减少末尾的填充字节。要按照从大到小的顺序从前到后依次声明每一个数据成员,并且尽量使用较小的成员对齐方式。这样,对象的布局及各个成员的偏移就不会随着对齐方式的改变而改变,其布局非常稳定。这一点在一定程度上增加了复合类型的可移植性及其对象的二进制兼容性。这一点在那些包含多个模块的应用中定义模块之间的接口数据类型时是很重要的。如果不同模块恰好使用了不同的对齐方式,而模块间共享的复合数据类型没有显示的指定对齐方式,那么程序出错甚至崩溃的风险就会增加。且看下面的例子:
假设应用程序有一个主程序和一个静态链接库连接而成。
1、sedan.h
#ifndef _SEDAN_H #define _SEDAN_H enum Color{ RED = 0x01, BLUE, GREEN}; typedef unsigned char BYTE; struct Sedan { bool m_hasSkylight; Color m_color; bool m_isAutoShift; double m_price; BYTE m_seatNum; }; //注意,这里没有指定对齐方式 #ifdef __cplusplus extern "C"{ #endif void __cdecl print1(const Sedan *p); #ifdef __cplusplus } #endif #endif //_SEDAN_H
2、sedan.cpp
#include<iostream> #include "sedan.h" using namespace std; #ifdef __cplusplus extern "C" { #endif void __cdecl print1(const Sedan *p) { cout << p->m_isAutoShift << endl; cout << p->m_price << endl; cout << p->m_seatNum << endl; } #ifdef __cplusplus } #endif
我们设置sedan.cpp的成员对齐方式为8字节边界方式,然后编译。
3、main.cpp
#include<iostream> #include "sedan.h" using namespace std; void __cdecl print2(const Sedan *p) { cout << p->m_isAutoShift << endl; cout << p->m_price << endl; cout << p->m_seatNum << endl; } int main(void) { Sedan s; s.m_isAutoShift = true; s.m_price = 100.0; s.m_seatNum = 4; print1(&s); print2(&s); return 0; }
我们设置main.cpp的对齐方式为4。运行结果为s.m_isAutoShift的值是正确的,但是m_price和m_seatNum的值在两个print函数中却不一致。这是什么原因呢?就是因为两个模块使用了不一致的对齐方式,导致它们对Sedan的解释不一样,主要就是理解的对象大小不一致,某些成员的偏移不一致,main.exe认为Sedan的大小为24字节,并且给Sedan.cpp传递了一个24字节大小的对象,而Sedan.cpp认为Sedan的大小为32字节,当然会导致访问错误的内存和输出错误的结果。
如果能够完全按照从大到小排列每一个数据成员,以及每一个嵌套的复合数据成员,虽然可以大大降低这种不一致的风险,但是定义一致的对齐方式可从根本上解决这个问题。能够100%保证一致的方法就是直接在代码中使用编译器提供的方法指定每一个接口数据成员的对齐方式,而不是依赖命令行参数或者其他途径。
COM规范要求不得在接口中定义数据成员,其原因之一就是这可能导致不同模块使用不同编译器而出现二进制不兼容。