C/C++字节对齐

对 C/C++ 字节对齐、位域和柔型数组成员的总结与一些示例分析

字节对齐

什么是字节对齐

现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

为什么有字节对齐

字节对齐主要是通过牺牲一部分存储空间来换取时间的方式,可以通过下面的例子来理解字节对齐的意义:
图1_1

  • 图1是没有对齐的情况,要取出double型的数据,需要进行三次寻址。然后将三次取得的数据拼接成完整的数据。
  • 图2是有对齐的情况,只需寻址两次,但相应的char后面的3字节空间不能够使用了。

四个概念

在考虑字节对齐相关的问题时,需要遵循下面四点:

  1. 基本数据类型的自身对齐值
    char 1
    short 2
    int 4
    long 4
    float 4
    double 4

  2. 程序的指定对齐值

    可以通过预编译命令认为指定程序的对齐值

    1
    2
    3
    4
    // 指定对齐值为4
    #pragma pack(4)
    // 恢复默认对齐值
    #progma pack()
  3. 自定义类型的自身对齐值,取内部成员对齐值最大的一个。

    1
    2
    3
    4
    5
    6
    struct Test
    {
    int a;
    double b;
    char c;
    };

    结构体Test的对齐值为 8 ,大小为 24

  4. 自定义类型的有效对齐值,程序指定对齐值和自身对齐中最小的一个。

    1
    2
    3
    4
    5
    6
    7
    #progma pack(4)
    struct Test
    {
    char a;
    short b;
    char c;
    };

    结构体Test的对齐值为 2 ,大小为 6

字节对齐问题实例分析

下面的例子指针为 4 字节大小,默认对齐值为 8

例 1 结构体嵌套

1
2
3
4
5
6
7
8
9
10
11
12
// sizeof(Test) = 40
typedef struct Test
{
short a; // 2 +6 -> 8
struct
{
int b; // 4 +4 -> 8
double c; // 8
char d; // 1 +7 -> 8
};
long e; // 4 +4 -> 8
}Test;

解释:b、c、d 依然是结构体 Test 的成员,结构体对齐值为 8

例 2 程序指定对齐值

1
2
3
4
5
6
7
8
9
10
11
12
13
// sizeof(Test) = 24
#pragma pack(4)
typedef struct Test
{
short a; // 2 +2 -> 4
struct
{
int b; // 4
double c; // 8
char d; // 1 +3 -> 4
};
long e; // 4
}Test;

解释:指定了对齐值为 4,比 4 小的补齐,比 4 大的需为 4 的整数倍

例 3 嵌套结构体定义的坑

1
2
3
4
5
6
7
8
9
10
11
12
13
// sizeof(Test) = 8
#pragma pack(8)
typedef struct Test
{
short a; // 2 +2 -> 4
struct t // 这里结构体有名,相当于没有
{
int b;
double c;
char d;
};
long e; // 4
}Test;

解释:struct t有名就是定义结构体,不占空间。

例 4 数组成员

1
2
3
4
5
6
7
8
9
10
11
12
// sizeof(Test) = 112
typedef struct Test
{
short a; // 2 +6 -> 8
struct
{
int b; // 4 +4 -> 8
double c[10]; // 80
char d; // 1 +7 -> 8
};
long e; // 4 +4 -> 8
}Test;

解释:数组可以理解为 10 个连续的 double 成员

例 5 柔性数组成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// sizeof(Test) = 8
typedef struct Test
{
short a;
int b;
char c[0];
}Test;

int main(int argc, char *argv[])
{
const char *str = "abcdefghijklmn";
int sz = sizeof(Test) + sizeof(char) * (strlen(str)+1);
Test *pt = (Test*)malloc(sz);

pt->a = 1;
pt->b = 2;
strcpy(pt->c, str);
printf("pt->a = %d\npt->b = %d\npt->c = %s\n",
pt->a, pt->b, pt->c);

free(pt);
return 0;
}

解释:柔型数组成员不占空间,只起到一个占位符的作用,在需要的时候,可以分配给此结构体更大的空间,然后用柔性数组成员作为变量名进行访问。并且不用像指针成员那样考虑内存释放。

如上代码中的做法就是用柔型数组成员来存储字符串,图示如下:
图1_2

例 6 位域

位域对一个字节的内存空间进行了更加精细的划分,指定变量只能占用一个字节数据中的指定位数

  • 位域不能跨字节存储
  • 位域不能跨类型存储

看下面两个例子:

1
2
3
4
5
6
7
8
9
// sizeof(Test) = 2
typedef struct Test
{
char a : 1; // 位域 只占 1 个 bit位
char b : 3; // 占 3 个 bit 位
char c : 3; // 占 3 个 bit 位
char d : 2; // 占 2 个 bit 位 另起炉灶
// 位域不夸字节存储
}Test;

如图所示:
图1_3

1
2
3
4
5
6
// sizeof(Test) = 8
typedef struct Test
{
char a : 4; // 位域不能够跨类型存储
int b : 5;
}Test;

例 7 联合体 union

1
2
3
4
5
6
7
// sizeof(Test) = 8
typedef union Test
{
char a;
double b;
int c;
}Test;

解释:联合体取最大的一个大小,在笔试时务必看清是 union 还是 struct.

1
2
3
4
5
6
7
8
9
10
11
12
// 96
typedef union Test
{
short a;
struct
{
int b;
double c[10];
char d;
};
long e;
}Test;

解释:联合体中嵌套结构体,必须保证能够被整个存下。

例 9 枚举 enum

1
2
3
4
5
6
7
8
9
// sizeof(Test) = 4
typedef enum Test
{
ADD=-100,
SUB, // -99
MUL=100,
DIV, // 101
MOD
};

解释:枚举类型占4个字节,

  • 枚举值默认从0开始
  • 若自定义了其中一个的值,则下面的值在上面的基础上+1

例 10 静态成员

1
2
3
4
5
6
7
8
9
10
// sizeof(Test) = 24
class Test
{
public:
static int a;
public:
char a;
double b;
int *p;
};

解释:静态成员不占结构体空间

例 11 继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// sizeof(Test) = 48
class Base
{
private:
char x;
double y;
int *z;
};
class Test : public Base
{
public:
char a;
double b;
int *p;
};

解释:无论何种继承方式,子类都会继承父类的成员,不同的只是访问权限。

例 12 虚函数

1
2
3
4
5
6
7
8
9
10
11
// sizeof(Test) = 16
class Test
{
public:
Test(){}
virtual ~Test(){}
private:
char a;
int b;
int *p;
}

解释:出现虚函数时,虚表指针会占一个指针大小的空间