c++中的数据对齐和数据结构体填充

  在socks5代理浅识中对代理请求认证等协议分别定义相关的结构体类型,在向代理服务发送请求协议的部分中,通过sizeof获取结构体大小来指定要发送的数据字节数,这恰恰隐藏了一个严总的问题,尤其是同学们对C++数据对齐不甚了解的前提下。

一、你想的结构体大小不是你想的"结构体大小"

  下面是代码中定义的一个结构体:

1 struct ProxyRequest
2 {
3     uint8_t ver;
4     uint8_t cmd;
5     uint8_t rsv = 0x00;
6     uint8_t atyp;
7     uint32_t dstAddr;
8     uint16_t dstPort;
9 };

  对于上面的结构体大小,同学们会立即得出10字节大小的答案,那通过sizeof获取的大小会是你想的那样吗?下面是VC编译器调用sizeof获取ProxyRequest结构体的大小:

      

  结果并不是你想的那样,为什么会这样呢?下面一步一步解开它的面纱。

二、数据对齐

  在32位系统上面,CPU读取一次数据大小是4个字节,如果8字节的数据类型,必须读取两次。CPU在执行程序指令的时候,会从指定的内存中获取一定长度的数据,为了减少读取数据的次数,会对数据进行必要的对齐操作,来提高程序的性能。在C++中,数据类型主要有基础类型:char,shor,int,float,double,long等;自定义类型通过一些基础类型,包裹自定义类型包装在一起形成的新类型,编译器在编译的时候自动对自定义类型进行数据对齐操作。

三、数据结构体填充

  为了能够实现数据对齐,就必须对数据结构体进行额外的字节填充。那什么情况下会发生数据结构体填充呢?

  1.数据类型一致:

 1 struct DataChar
 2 {
 3     char a;
 4     char b;
 5     
 6 };
 7 
 8 struct DataShort
 9 {
10     short a;
11     short b;
12     
13 };
14 
15 struct DataComposition
16 {
17     DataChar char_c1;
18     DataChar char_c2;
19 };

  

   编译器并不会进行内存对齐,内存大小:类型大小 * N。

  2.数据类型不一致:

 1 struct Data1    // 4 bytes
 2 {
 3     char  a;    // padding 1 byte ==> (1 + 1) + 2 = 4
 4     short b;
 5 };
 6 
 7 struct Data2    // 4 bytes
 8 {
 9     short a;
10     char  b;  // padding 1 byte ==> 2 + (1 + 1) = 4
11 };
12 
13 
14 struct Data3   // 8byytes
15 {
16     int   a;
17     short b;
18     char  c;   // padding 1 byte ==> 4 + 2 + (1 + 1) = 8
19 };
20 
21 struct Data4  // 12 bytes
22 {
23     short a;   // padding 2 bytes
24     int   b;
25     char  c;  //  padding 3 bytes ==> (2 + 2) + 4 + (1 + 3) = 12
26 };
27 
28 struct Data5  // 8 bytes
29 {
30     char  a;  // padding 1 byte  ==> (1 + 1) + 2 + 4 = 8 bytes
31     short b; 
32     int   c;
33 };
34 
35 struct Data6  // 16 bytes
36 {
37     int64_t a;
38     int     b;
39     short   c; // padding 2 bytes
40     char    d; // padding 3 bytes ==> 8 + 4 + (2 + 2) + (1 + 3) = 16
41 };
42 
43 struct Data7  // 24 bytes
44 {
45     short   a;  // padding 2 bytes
46     int     b;  
47     char    c; // padding 7 bytes ==> (2 + 2) + 4 + (1 + 7) + 8 = 24
48     int64_t d;
49 };

  

   同学们是不是觉得很奇怪:Data1和Data2结构体类型一样,获取的字节大小一样可以理解;Data3,4,5结构体类型一样,Data6,7结构体类型一样,但是为什么出现获取字节大小不一样的情况呢?如果想搞清楚原因,我们必须得清楚的知道编译器是怎么进行字节填充的。最简单的方式就是通过编译器的内存窗口观察结构体中变量的赋值的大小,下面进行详细介绍:

  1.Data1

1 int main()
2 {
3     Data1 data1;
4     data1.a = 0x01;
5     data1.b = 0x0002;
6     
7     return 0;
8 }

  

    

1     ----------------------------
2     | 01 | padding | 02 | 00 |     0x00BFF8F4
3     ----------------------------

  2.Data2

1 int main()
2 {
3     Data2 data2;
4     data2.a = 0x0001;
5     data2.b = 0x02;
6     return 0;
7 }

  

     

1 ----------------------------
2 | 01 | 00 | 02 | padding |     0x0118FB38
3  ----------------------------

  3.Data3

1 int main()
2 {
3     Data3 data3;
4     data3.a = 0x00000001;
5     data3.b = 0x0002;
6     data3.c = 0x03;
7 
8     return 0;
9 }

   

      

   

1 ----------------------------
2  | 01 | 00 | 00 |  00      |     0x006FFD88
3 ----------------------------
4  |  02 | 00 | 03 | padding |     0X06FFDBC
5 ----------------------------

  4.Data4

1 int main()
2 {
3     Data4 data4;
4     data4.a = 0x0001;
5     data4.b = 0x00000002;
6     data4.c = 0x03;
7 
8     return 0;
9 }

  

    

    

1 -------------------------------------
2 | 01 | 00 | padding |  padding      |    0x0073FDB0
3 -------------------------------------
4 |  02 | 00 | 00        | 00         |    0x0073FDB4
5 -------------------------------------
6 |  03 | padding | padding | padding |    0x0073FDB8
7 -------------------------------------

  5.Data5

1 int main()
2 {
3     Data5 data5;
4     data5.a = 0x01;
5     data5.b = 0x0002;
6     data5.c = 0x00000003;
7 
8     return 0;
9 }

  

       

       

1 --------------------------
2 | 01 | padding | 02 | 00 |     0x0055FC48
3 --------------------------
4 | 03 | 00      | 00 | 00 |     0X0055FC4C
5 --------------------------

  6.Data6

 1 int main()
 2 {
 3     Data6 data6;
 4     data6.a = 0x0000000000000001;
 5     data6.b = 0x00000002;
 6     data6.c = 0x0003;
 7     data6.d = 0x01;
 8 
 9     return 0;
10 }

  

  

  

    

1 ----------------------------
2 | 01 | 00 | 00 |  00        |    0x012FF70C
3 ----------------------------
4 | 00 | 00 | 00 | 00         |    0x012FF710
5 -----------------------------
6 |  02 | 00 | 00 | 00        |    0x012FF714
7 -----------------------------
8 |  03 | 00 | 01 | padding |    0x012FF718
9 -----------------------------

  7.Data7

 1 int main()
 2 {
 3     Data7 data7;
 4     data7.a = 0x001;
 5     data7.b = 0x00000002;
 6     data7.c = 0x03;
 7     data7.d = 0x0000000000000004;
 8 
 9     return 0;
10 }

  

    

      

     

 1  ----------------------------------------
 2 | 01 | 00       | padding | padding     |         0x00EFF800
 3  ----------------------------------------
 4 | 02 | 00       | 00      | 00          |         0x00EFF804
 5  ----------------------------------------
 6 | 03 | padding  | padding | padding     |         0x00EFF808
 7  ----------------------------------------
 8 | padding | padding | padding | padding |        0x00EFF80C
 9  ----------------------------------------
10 | 04      | 00      | 00      | 00      |        0x00EFF810
11 -----------------------------------------
12 | 00      | 00      | 00      | 00      |        0x00EFF814
13  ----------------------------------------

  通过上面详细的分析,对结构体或类中成员变量进行合理的位置定义可以减少因数据对齐导致的内存占用。结构体填充后的字节大小最大字节数类型的倍数,一般合理的排序是按照类型字节从大到小或从小到大顺序定义,这样填充字节最小,其他情况的排序都会比这个大。当然,在实际编程中,为了提高编程效率,一般不会要求这么严格。

四、你想的结构体大小是你想的"结构体大小"

  有些情况下,为了保证通过sizeof获取到的类型大小是正确的,即:你想的结构体大小就是你想的的大小,比如socks5代理相关请求认证结构体,该如何解决呢?

  在VC编译器中,可以通过定义预处理#pragma pack(n),告诉编译器按照指定字节大小进行数据对齐操作,

 1 #pragma pack(1)
 2 struct Data1
 3 {
 4     char  a;
 5     short b;
 6 };
 7 
 8 struct Data2
 9 {
10     short a;
11     char  b;
12 };
13 
14 
15 struct Data3
16 {
17     int   a;
18     short b;
19     char  c;
20 };
21 
22 struct Data4
23 {
24     short a;
25     int   b;
26     char  c;
27 };
28 
29 struct Data5
30 {
31     char  a;
32     short b;
33     int   c;
34 };
35 
36 struct Data6
37 {
38     int64_t a;
39     int     b;
40     short   c;
41     char    d;
42 };
43 
44 struct Data7
45 {
46     short   a;
47     int     b;
48     char    c;
49     int64_t d;
50 };
51 #pragma pack()

  

   pack(n) :表示以n个字节进行数据对齐,pack()取消之前的设置。这是以性能为代价,一般在实际开发过程中,尽量不要对大量的结构体处理。

五、编程中的一点建议

  1.在实际编程中,如果想获取结构体对象(自定义类型)的字节大小,谨慎使用sizeof获取结构体大小,可以通过对结构体体中每个类型单独获取大小;可以对部分结构体采用pack方式,减少多于字节的内存占用。

  2.在实际编程中,对结构体(类)中的数据成员进行合理的排序,减少数据对齐导致的内存占用问题。

参考:Data Structure alignment - GeeksforGeeks

posted @ 2021-07-05 01:01  blackstar666  阅读(767)  评论(0编辑  收藏  举报