并没有按照原文逐字逐句翻译,按照我的理解重新组织了文章。水平有限,如有错误还请指正。
原文链接
引例:写出下面这个程序的运行结果
#include
// Alignment requirements
// (typical 32 bit machine)
// char 1 byte
// short int 2 bytes
// int 4 bytes
// double 8 bytes
// structure A
typedef struct structa_tag
{
char c;
short int s;
} structa_t;
// structure B
typedef struct structb_tag
{
short int s;
char c;
int i;
} structb_t;
// structure C
typedef struct structc_tag
{
char c;
double d;
int s;
} structc_t;
// structure D
typedef struct structd_tag
{
double d;
int s;
char c;
} structd_t;
int main()
{
printf("sizeof(structa_t) = %d\n", sizeof(structa_t));
printf("sizeof(structb_t) = %d\n", sizeof(structb_t));
printf("sizeof(structc_t) = %d\n", sizeof(structc_t));
printf("sizeof(structd_t) = %d\n", sizeof(structd_t));
return 0;
}
在公布答案之前,让我们来深入研究一下其中的原理。
c/c++的数据类型要求边界对齐(实际上这是由存储器的结构)决定的,并不是语言本身的特性)。
处理器的数据线的根数决定了处理器的处理字长。
如上图左侧所示,内存是按字节为单位编址并按顺序排列,如果内存按照“一个存储体,每个单元的宽度”为一个字节”的方式来组织的话,处理器需要4个总线周期来读取一个4字节数据。
但实际上,并没有必要花4个总线周期来读取,我们有办法让处理器用1个总线周期完成读取——采用低位多体交叉存储结构(如上图右侧所示)。
尽管我们把内存空间分成了4个部分,但寻址的时候是仍然是把内存空间当成上图左侧那样连续的空间。
如果把双字数据的最低的8位放在Bank 0的X地址(X是一个能被16整除的偶地址)
X+3 | X+2 | X+1 | X |
---|
多体交叉存储器需要CPU的4根地址线作为片选线来选择对应的存储体,通常会挪用低4位的地址线,低4位默认为0
如果一个int类型的数据的起始存储位置能够被4整除,处理器在一个总线周期就能够读取完整个int。
然而,如果一个int的起始存储位置的地址不能被4整除,如下图
处理器读取时,就需要扫描两行,需要2个总线周期完成。
不同数据类型的边界对齐要求
short ( 2 bytes ) | int ( 4 bytes ) | double ( 8 btyes ) |
---|---|---|
存储在Bank 0和Bank 1 | 占用存储体一行的空间,从低位到高位,依次存放在Bank 0,Bank 1,Bank 2,Bank 3 | 32位机器:占用存储体两行的空间。需要两个总线周期来读取。 |
存储在Bank 2和Bank 3 | 64位机器:存储体分成8个部分,一次能够处理64位数据,只需要1个总线周期。 |
方便起见,假设每个结构体的起始地址是0x0000
// structure A
typedef struct structa_tag
{
char c;
short int s;
} structa_t;
如果按照下图方式存储
那么short就从奇地址开始存储,此时short就是一个非规则字
实际上,编译器会在char和short之间插入一个空白的字节
以此来确保short的起始位置能够被2整除。
综上所述:结构体A的大小 = sizeof(char) + 1(插入空白字节) + sizeof(short) = 4字节
// structure B
typedef struct structb_tag
{
short int s;
char c;
int i;
} structb_t;
这里需要注意的是,short和int之间char并没有对齐要求
// structure C
typedef struct structc_tag
{
char c;
double d;
int s;
} structc_t;
按照上面的分析,
这个结构的大小应该是 sizeof(char) + 7 + sizeof(double) + sizeof(int) = 1 +7 + 8 + 4 = 20
然而,正确答案是24
译者注:
2013/05/01作者更新称,发现某些处理器会给结构C分配16字节的空间,作者在AMD Athlon X2处理器,GCC 4.7环境下运行得到结果是24。结构体的大小很大程度上取决于内存空间的组织方式,这是硬件层面的问题。
为了更好的解释这个问题,下面定义一个新的结构数组
structc_t structc_array[3];
按照我们之前的理解
结构数组的第二成员(下标为1)的起初存储位置是0x0014(十进制20),结构体内的double的起始存储位置是0x001C(十进制28),28不能被8整除,与double的边界对齐要求规则冲突。
所以,所有结构的起始存储位置必须是结构中对边界要求最严格的数据类型所要求的位置。
// structure D
typedef struct structd_tag
{
double d;
int s;
char c;
} structd_t;
读到这,相信你已经有所领悟,如果不是出于可读性和可维护性的原因,程序员根据结构体成员各自的边界对结构体进行排列,以减少因为边界对齐而损失的内存空间。
对于要求边界对齐的机器,malloc函数所返回的内存的起始位置将始终能够满足对边界对齐要求最严格的类型的要求——《c和指针》
Q:堆栈当中也有边界对齐的要求吗?
A:有。堆栈一样是在内存当中,SS中存储的堆栈段基地址就是对齐好的。举例来说,一个32位字长的处理器,SS和SP得到的堆栈位置能够被4整除。一般来说,处理器并不会去检查堆栈的边界,这是程序员的责任去确保堆栈的边界已经对齐。
Q:如果char并没有放在Bank 0,它就会被错误的地址线读取。处理器是怎么解决有关char类型的读取问题的?
A:通常,处理器会识别结构中的数据类型(例如ARM处理器的LDRB)。根据类型在内存中的存储情况,处理器将用最不重要的数据线来读取他
Q:当参数在堆栈中传递时,它们还需要对齐吗?
A:需要。例如,如果一个16位的值压入了32位宽的堆栈,这个值将自动填0来和32对齐。考虑下面这段代码
void argument_alignment_check( char c1, char c2 )
{
// Considering downward stack
// (on upward stack the output will be negative)
printf("Displacement %d\n", (int)&c2 - (int)&c1);
}
在32位机器上运行,结果是4。
Q:假如我非要读取一个没有对齐的数据会发生什么事?
A:这取决于处理器。有些处理器会用几个总线周期来读取,并按照正确的方式来组合DB上传来的数据。有些处理器并没有最后两条地址线,则意味着他们无法访问奇地址(二进制数的奇偶在于最后一位是否为1),这样读取来的数据是不完整的。
struct {
int a;
short b;
int c;
char d;
}A;
struct {
double a;
short b;
int c;
char d;
}B;
在32位机器上用GCC编译以上代码,求sizeof(A)和sizeof(B)
答案:16和24
结构B图解
下文部分内容来自《C和指针》一书
为什么char的后面会有这么多空白的地方?用我们之前结构数组的分析思路,假如后面紧接着一个相同的结构,那么他的起始存储位置需满足结构中对边界要求最严格的的数据类型要求的位置,也就是double。而系统禁止编译器在一个结构的起始位置跳过几个字节来满足边界对齐要求。所以空出来的字节也要包括在结构体当中。
分析结构体的大小时,按照顺序,按照“字起始地址能被占用空间的大小整除”的原则为成员分配空间,不满足原则就插入空白字节。结尾的时候要注意,按照结构数组的分析思路,看是否需要在末尾继续插入空白字节。