深入理解 C/C++ 结构体 <4>:以空间换时间,结构体中的成员对齐之道 By Long Luo
在开始今天的文章之前,请先看下面一道面试题:
问题: 阅读下面一段代码并回答题目之后的问题:1
2
3
4
5
struct ALIGN
{
int mA;
int mB;
};
请问在 32 位系统下 sizeof(ALIGN)
的结果是多少?
当然这道题目是难不到广大程序员同学们滴!
在 32 位机器上 int 类型占 4 个字节,Struct ALIGN 里面有 2 个 int 型变量,那么总共就是 8 个字节喽!
Bingo!在这个例子中,sizeof(ALIGN)
的结果的确是 8 。
下面,我们把代码修改一下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using namespace std;
struct ALIGN
{
int mA;
int mB;
};
struct ALIGN1
{
int mA;
short mB;
};
int main()
{
cout<<sizeof(short)<<endl;
cout<<sizeof(ALIGN1)<<endl;
getchar();
return 0;
}
请问输出是多少?
这还不简单,小case嘛!
mA占4个字节,mB占2个字节,所以Struct ALGN1应该是4+2=6个字节,所以答案是2和6。
—你确定么? (小丫的语言) —我确定!
好的,请看大屏幕:
咦?
结构体的大小不是将结构体元素单纯相加就可以的吗?
怎么结果却变成 8 了呢?
要回答这个问题,需要从计算机的地址对齐讲起。至于为什么需要对齐,当然是对齐能够带来很多好处的。
第一,可以大大简化处理器和存储器之间接口的硬件设计;
第二,提高处理器访问数据的效率。
首先讲下,对齐(alignment)就是计算机系统对基本数据类型的可允许地址做了限制,某种类型的对象的地址必须是某个值k的倍数。
以IA32为例,在自然对齐方式下,基本数据类型(如short,int,double)变量的地址必须被他们的大小整除。
通俗的说,对于int类型的变量,因为宽度为4,因此存放int类型变量的起始地址必须能被4整除
,宽度为2的基本数据类型(short等)则位于能被2整除的地址上
,以此类推对于char和bool类型的变量,由于其只占用一个字节,则没有特别要求
。
我们修改下程序,让其输出成员变量的地址:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using namespace std;
struct ALIGN
{
int mA;
int mB;
};
struct ALIGN1
{
int mA;
short mB;
};
int main()
{
ALIGN aln0;
ALIGN1 aln1;
cout<<"\n"<<&aln0.mA<<"\t"<<&aln0.mB<<endl;
cout<<"\n"<<&aln1.mA<<"\t"<<&aln1.mB<<endl;
getchar();
return 0;
}
程序调试我们可以看到:
从上述结果中,可以看出在struct ALIGN1
中,int mA 的起始地址为 0x0012ff4c 可以被 4 整除,short mB 的起始地址为 0x0012ff50 可以被 2 整除。
再看下列代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
using namespace std;
struct ALIGN
{
int mA;
int mB;
};
struct ALIGN1
{
int mA;
short mB;
};
struct ALIGN2
{
char mA;
int mB;
short mC;
};
struct ALIGN3
{
int mB;
char mA;
short mC;
};
int main(void)
{
ALIGN aln0;
ALIGN1 aln1;
ALIGN2 aln2;
ALIGN3 aln3;
cout<<"The size of struct ALIGN is:"<<sizeof(ALIGN)<<endl;
cout<<"\t"<<&aln0.mA<<"\t"<<&aln0.mB<<endl;
cout<<"The size of struct ALIGN1 is:"<<sizeof(ALIGN1)<<endl;
cout<<"\t"<<&aln1.mA<<"\t"<<&aln1.mB<<endl;
cout<<"The size of struct ALIGN2 is:"<<sizeof(ALIGN2)<<endl;
cout<<"\t"<<&aln2.mA<<"\t"<<&aln2.mB<<"\t"<<&aln2.mC<<endl;
cout<<"The size of struct ALIGN3 is:"<<sizeof(ALIGN3)<<endl;
cout<<"\t"<<&aln3.mA<<"\t"<<&aln3.mB<<"\t"<<&aln3.mC<<endl;
getchar();
return 0;
}
输出结果如下:
是不是觉得很奇怪?
ALIGN2 和 ALIGN3 都是 1 个 int 型, 1 个 char 型,1 个 short 型,可是它们所占的空间却1个是 8 ,一个是 12 ?
这非常非常不科学啊!
2个结构体都拥有一样的成员变量,可是所占的大小却有很大的区别。这一切的一切,是计算机中的幽灵在作祟,还是另有隐情? 编译器如此厚此薄彼,到底是为什么?被偷去的内存,到底去了哪里?
下一篇我们将使用gcc编译,分析编译生成的每一步,了解编译器具体是怎么做的,为什么需要这么做,为你揭开这些谜团!!!
为什么会有这样的区别呢?
通过上篇关于对齐的介绍,我们已经猜测这是因为编译器对其作了对齐的处理所致,但是编译器处理的细节具体是什么呢?
在这一篇里,我们将对程序的编译,汇编,链接进行逐一分析,解开这个谜团。
不要走开,下面更精彩!
编译过程:
一般情况下,C
程序的编译过程为
- 预处理
- 编译成汇编代码
- 汇编成目标代码
- 链接
这一篇我们将使用gcc
对上述几个过程进行仔细分析,了解其处理细节。
首先我们看一个例子,源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/************************************************************************************
** File: - Z:\code\c\Alignment\Align.c
**
** Copyright (C), Long.Luo, All Rights Reserved!
**
** Description:
** Align.c --- To learn the details of Alignment by the compiler.
**
** Version: 1.1
** Date created: 22:33:50,10/12/2012
** Author: Long.Luo
**
** --------------------------- Revision History: --------------------------------
** <author> <data> <desc>
**
************************************************************************************/
struct ALIGN2
{
char mA;
int mB;
short mC;
};
struct ALIGN3
{
int mB;
char mA;
short mC;
};
int main(void)
{
struct ALIGN2 aln2;
struct ALIGN3 aln3;
printf("The size of struct ALIGN2 is: %d\n", sizeof(aln2));
printf("\t aln2.mA=0x%x, aln2.mB=0x%x, aln2.mC=0x%x\n", &aln2.mA, &aln2.mB, &aln2.mC);
printf("The size of struct ALIGN3 is: %d\n", sizeof(aln3));
printf("\t aln3.mA=0x%x, aln3.mB=0x%x, aln3.mC=0x%x\n", &aln3.mA, &aln3.mB, &aln3.mC);
return 0;
}
接下来我们对Align.c
按照C程序的编译流程进行一一分析,如下所示:
1. 预处理
输出文件的后缀为:*.cpp
文件。
2. 编译成汇编代码
使用-x参数说明根据指定的步骤进行工作,cpp-output
指明从预处理得到的文件开始编译;
使用-S说明生成汇编代码后停止工作
1
gcc –x cpp-output –S –o align.s align.cpp
我们也可以直接编译到汇编代码:1
gcc –S Align.c
得到align.s
文件之后,在最开始之处我们可以看到:1
2
3
.file "Align.c"
.section .rodata
.align 4
其中的.align 4
就表明了其后面所有的数据都遵守4字节对齐的限制。
3. 编译成目标代码
汇编代码—>目标代码
4. 编译成执行代码
目标代码–>执行代码
最终的输出结果如下所示:
内存分析
我们可以绘出struct ALIGN2
和struct ALIGN3
的内存分配图:
假如我们不对齐?
上图是内存对齐的struct ALIGN2
和struct ALIGN3
的内存分配情况,但是假如我们不对齐呢?
其内存分配如下所示:
很明显,int mB
和short mC
都不满足对齐要求。
对齐的好处是什么呢?
通过上一节,我们知道了如果不对齐,我们可以节省出几个byte的内存空间,在计算机世界中,可以对齐也可以不对齐,但是实际中,都做了对齐。
那么,对齐的好处是什么呢?
答案是:对齐是在时间和空间之间做了一个tradeoff!
对齐可以提高取数据的效率!
在IA32架构中,数据总线是32位,即一次可以存取4个字节的数据。
在对齐的情况下,struct ALIGN2
的每个成员都可以在一个指令周期内完成;
而假设我们的struct ALIGN2
没有对齐,那么对于struct ALIGN2
中char mA
,CPU可以一次取出4个字节获得低位的一个字节,同时需要将高位的3个字节保存在寄存器中,之后的int mB
,CPU必须再取得低位的1个字节并通之前保存在寄存器中的数据结果组合在一起,每一个都需要好几条指令,是不是相当麻烦?
如何自定义对齐?
那肯定有同学要问了,有没有办法让处理器按照自己的要求进行地址对齐呢?
—当然可以!
我们可以通过预编译命令#pragma pack(n),n=1,2,4,8,16
来改变这一系数,其中的n
就是你要指定的“对齐系数”。
比如,我想让处理器按照1个字节的方式对齐,则代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/************************************************************************************
** File: - Z:\code\c\Alignment\AlignPackOne.c
**
** Copyright (C), Long.Luo, All Rights Reserved!
**
** Description:
** Align.c --- To learn the details of Alignment by the compiler.
**
** Version: 1.1
** Date created: 23:39:05,10/12/2012
** Author: Long.Luo
**
** --------------------------- Revision History: --------------------------------
** <author> <data> <desc>
**
************************************************************************************/
struct ALIGN2
{
char mA;
int mB;
short mC;
};
struct ALIGN3
{
int mB;
char mA;
short mC;
};
int main(void)
{
struct ALIGN2 aln2;
struct ALIGN3 aln3;
printf("The size of struct ALIGN2 is: %d\n", sizeof(aln2));
printf("\t aln2.mA=0x%x, aln2.mB=0x%x, aln2.mC=0x%x\n", &aln2.mA, &aln2.mB, &aln2.mC);
printf("The size of struct ALIGN3 is: %d\n", sizeof(aln3));
printf("\t aln3.mA=0x%x, aln3.mB=0x%x, aln3.mC=0x%x\n", &aln3.mA, &aln3.mB, &aln3.mC);
return 0;
}
编译之后输出结果如下:
可以看出,在我们要求的1字节对齐方式下,struct ALIGN2
和struct ALIGN3
的结果都是7,只占了4+2+1个字节,内存空间一个字节都利用到极致。
至此,关于内存对齐就到此告一段落了,你弄明白了吗?
之前的系列文章,我们的struct
都有成员变量,那么你有没有考虑过如果过struct
完全是空的情况呢?
下一篇我们将重点探讨这个问题!
不要走开,后面更精彩!
Updated by Long Luo at 2016-6-11 03:44:26 @Shenzhen, China. Updated by Long Luo at 2018年9月28日23点23分 @Hangzhou, China.
By Long Luo transfer at 2016-6-8 21:51:38 @Shenzhen, China. Modified By Long Luo at 2018年9月28日23点28分 @Hangzhou, China.
By Long Luo
在开始今天的文章之前,请先看下面一道面试题:
问题: 阅读下面一段代码并回答题目之后的问题:1
2
3
4
5struct ALIGN
{
int mA;
int mB;
};
请问在 32 位系统下 sizeof(ALIGN)
的结果是多少?
当然这道题目是难不到广大程序员同学们滴!
在 32 位机器上 int 类型占 4 个字节,Struct ALIGN 里面有 2 个 int 型变量,那么总共就是 8 个字节喽!
Bingo!在这个例子中,sizeof(ALIGN)
的结果的确是 8 。
下面,我们把代码修改一下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using namespace std;
struct ALIGN
{
int mA;
int mB;
};
struct ALIGN1
{
int mA;
short mB;
};
int main()
{
cout<<sizeof(short)<<endl;
cout<<sizeof(ALIGN1)<<endl;
getchar();
return 0;
}
请问输出是多少?
这还不简单,小case嘛!
mA占4个字节,mB占2个字节,所以Struct ALGN1应该是4+2=6个字节,所以答案是2和6。
—你确定么? (小丫的语言) —我确定!
好的,请看大屏幕:
咦?
结构体的大小不是将结构体元素单纯相加就可以的吗?
怎么结果却变成 8 了呢?
要回答这个问题,需要从计算机的地址对齐讲起。至于为什么需要对齐,当然是对齐能够带来很多好处的。
第一,可以大大简化处理器和存储器之间接口的硬件设计;
第二,提高处理器访问数据的效率。
首先讲下,对齐(alignment)就是计算机系统对基本数据类型的可允许地址做了限制,某种类型的对象的地址必须是某个值k的倍数。
以IA32为例,在自然对齐方式下,基本数据类型(如short,int,double)变量的地址必须被他们的大小整除。
通俗的说,对于int类型的变量,因为宽度为4,因此存放int类型变量的起始地址必须能被4整除
,宽度为2的基本数据类型(short等)则位于能被2整除的地址上
,以此类推对于char和bool类型的变量,由于其只占用一个字节,则没有特别要求
。
我们修改下程序,让其输出成员变量的地址:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using namespace std;
struct ALIGN
{
int mA;
int mB;
};
struct ALIGN1
{
int mA;
short mB;
};
int main()
{
ALIGN aln0;
ALIGN1 aln1;
cout<<"\n"<<&aln0.mA<<"\t"<<&aln0.mB<<endl;
cout<<"\n"<<&aln1.mA<<"\t"<<&aln1.mB<<endl;
getchar();
return 0;
}
程序调试我们可以看到:
从上述结果中,可以看出在struct ALIGN1
中,int mA 的起始地址为 0x0012ff4c 可以被 4 整除,short mB 的起始地址为 0x0012ff50 可以被 2 整除。
再看下列代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
using namespace std;
struct ALIGN
{
int mA;
int mB;
};
struct ALIGN1
{
int mA;
short mB;
};
struct ALIGN2
{
char mA;
int mB;
short mC;
};
struct ALIGN3
{
int mB;
char mA;
short mC;
};
int main(void)
{
ALIGN aln0;
ALIGN1 aln1;
ALIGN2 aln2;
ALIGN3 aln3;
cout<<"The size of struct ALIGN is:"<<sizeof(ALIGN)<<endl;
cout<<"\t"<<&aln0.mA<<"\t"<<&aln0.mB<<endl;
cout<<"The size of struct ALIGN1 is:"<<sizeof(ALIGN1)<<endl;
cout<<"\t"<<&aln1.mA<<"\t"<<&aln1.mB<<endl;
cout<<"The size of struct ALIGN2 is:"<<sizeof(ALIGN2)<<endl;
cout<<"\t"<<&aln2.mA<<"\t"<<&aln2.mB<<"\t"<<&aln2.mC<<endl;
cout<<"The size of struct ALIGN3 is:"<<sizeof(ALIGN3)<<endl;
cout<<"\t"<<&aln3.mA<<"\t"<<&aln3.mB<<"\t"<<&aln3.mC<<endl;
getchar();
return 0;
}
输出结果如下:
是不是觉得很奇怪?
ALIGN2 和 ALIGN3 都是 1 个 int 型, 1 个 char 型,1 个 short 型,可是它们所占的空间却1个是 8 ,一个是 12 ?
这非常非常不科学啊!
2个结构体都拥有一样的成员变量,可是所占的大小却有很大的区别。这一切的一切,是计算机中的幽灵在作祟,还是另有隐情? 编译器如此厚此薄彼,到底是为什么?被偷去的内存,到底去了哪里?
下一篇我们将使用gcc编译,分析编译生成的每一步,了解编译器具体是怎么做的,为什么需要这么做,为你揭开这些谜团!!!
为什么会有这样的区别呢?
通过上篇关于对齐的介绍,我们已经猜测这是因为编译器对其作了对齐的处理所致,但是编译器处理的细节具体是什么呢?
在这一篇里,我们将对程序的编译,汇编,链接进行逐一分析,解开这个谜团。
不要走开,下面更精彩!
编译过程:
一般情况下,C
程序的编译过程为
- 预处理
- 编译成汇编代码
- 汇编成目标代码
- 链接
这一篇我们将使用gcc
对上述几个过程进行仔细分析,了解其处理细节。
首先我们看一个例子,源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47/************************************************************************************
** File: - Z:\code\c\Alignment\Align.c
**
** Copyright (C), Long.Luo, All Rights Reserved!
**
** Description:
** Align.c --- To learn the details of Alignment by the compiler.
**
** Version: 1.1
** Date created: 22:33:50,10/12/2012
** Author: Long.Luo
**
** --------------------------- Revision History: --------------------------------
** <author> <data> <desc>
**
************************************************************************************/
struct ALIGN2
{
char mA;
int mB;
short mC;
};
struct ALIGN3
{
int mB;
char mA;
short mC;
};
int main(void)
{
struct ALIGN2 aln2;
struct ALIGN3 aln3;
printf("The size of struct ALIGN2 is: %d\n", sizeof(aln2));
printf("\t aln2.mA=0x%x, aln2.mB=0x%x, aln2.mC=0x%x\n", &aln2.mA, &aln2.mB, &aln2.mC);
printf("The size of struct ALIGN3 is: %d\n", sizeof(aln3));
printf("\t aln3.mA=0x%x, aln3.mB=0x%x, aln3.mC=0x%x\n", &aln3.mA, &aln3.mB, &aln3.mC);
return 0;
}
接下来我们对Align.c
按照C程序的编译流程进行一一分析,如下所示:
1. 预处理
输出文件的后缀为:*.cpp
文件。
2. 编译成汇编代码
使用-x参数说明根据指定的步骤进行工作,
cpp-output
指明从预处理得到的文件开始编译;使用-S说明生成汇编代码后停止工作
1 | gcc –x cpp-output –S –o align.s align.cpp |
我们也可以直接编译到汇编代码:1
gcc –S Align.c
得到align.s
文件之后,在最开始之处我们可以看到:1
2
3.file "Align.c"
.section .rodata
.align 4
其中的.align 4
就表明了其后面所有的数据都遵守4字节对齐的限制。
3. 编译成目标代码
汇编代码—>目标代码
4. 编译成执行代码
目标代码–>执行代码
最终的输出结果如下所示:
内存分析
我们可以绘出struct ALIGN2
和struct ALIGN3
的内存分配图:
假如我们不对齐?
上图是内存对齐的struct ALIGN2
和struct ALIGN3
的内存分配情况,但是假如我们不对齐呢?
其内存分配如下所示:
很明显,int mB
和short mC
都不满足对齐要求。
对齐的好处是什么呢?
通过上一节,我们知道了如果不对齐,我们可以节省出几个byte的内存空间,在计算机世界中,可以对齐也可以不对齐,但是实际中,都做了对齐。
那么,对齐的好处是什么呢?
答案是:对齐是在时间和空间之间做了一个tradeoff!
对齐可以提高取数据的效率!
在IA32架构中,数据总线是32位,即一次可以存取4个字节的数据。
在对齐的情况下,struct ALIGN2
的每个成员都可以在一个指令周期内完成;
而假设我们的struct ALIGN2
没有对齐,那么对于struct ALIGN2
中char mA
,CPU可以一次取出4个字节获得低位的一个字节,同时需要将高位的3个字节保存在寄存器中,之后的int mB
,CPU必须再取得低位的1个字节并通之前保存在寄存器中的数据结果组合在一起,每一个都需要好几条指令,是不是相当麻烦?
如何自定义对齐?
那肯定有同学要问了,有没有办法让处理器按照自己的要求进行地址对齐呢?
—当然可以!
我们可以通过预编译命令#pragma pack(n),n=1,2,4,8,16
来改变这一系数,其中的n
就是你要指定的“对齐系数”。
比如,我想让处理器按照1个字节的方式对齐,则代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50/************************************************************************************
** File: - Z:\code\c\Alignment\AlignPackOne.c
**
** Copyright (C), Long.Luo, All Rights Reserved!
**
** Description:
** Align.c --- To learn the details of Alignment by the compiler.
**
** Version: 1.1
** Date created: 23:39:05,10/12/2012
** Author: Long.Luo
**
** --------------------------- Revision History: --------------------------------
** <author> <data> <desc>
**
************************************************************************************/
struct ALIGN2
{
char mA;
int mB;
short mC;
};
struct ALIGN3
{
int mB;
char mA;
short mC;
};
int main(void)
{
struct ALIGN2 aln2;
struct ALIGN3 aln3;
printf("The size of struct ALIGN2 is: %d\n", sizeof(aln2));
printf("\t aln2.mA=0x%x, aln2.mB=0x%x, aln2.mC=0x%x\n", &aln2.mA, &aln2.mB, &aln2.mC);
printf("The size of struct ALIGN3 is: %d\n", sizeof(aln3));
printf("\t aln3.mA=0x%x, aln3.mB=0x%x, aln3.mC=0x%x\n", &aln3.mA, &aln3.mB, &aln3.mC);
return 0;
}
编译之后输出结果如下:
可以看出,在我们要求的1字节对齐方式下,struct ALIGN2
和struct ALIGN3
的结果都是7,只占了4+2+1个字节,内存空间一个字节都利用到极致。
至此,关于内存对齐就到此告一段落了,你弄明白了吗?
之前的系列文章,我们的struct
都有成员变量,那么你有没有考虑过如果过struct
完全是空的情况呢?
下一篇我们将重点探讨这个问题!
不要走开,后面更精彩!
Updated by Long Luo at 2016-6-11 03:44:26 @Shenzhen, China. Updated by Long Luo at 2018年9月28日23点23分 @Hangzhou, China.
By Long Luo transfer at 2016-6-8 21:51:38 @Shenzhen, China. Modified By Long Luo at 2018年9月28日23点28分 @Hangzhou, China.