当前位置: 首页 > article >正文

C语言的字节对齐

一、基本概念

1. 什么是自然对齐

对于一个存储在内存中的变量,如果它的内存地址等于该变量长度的整数倍,则称该变量是自然对齐的。

例子:对于32位的CPU,如果一个int型变量A的地址是0x00000004,A是自然对齐。

 

2. 关于CPU存取数据的效率问题

假设有一片连续的内存地址,如下图:

每个地址上可存储1个字节的数据。

 

例子:以32位的CPU为例,32位的CPU,默认是4字节[32 / 8 = 4]对齐。

当整型数据A的存储起始地址为0x2,则它在内存中的数据占据了4个单元,分别对应地址0x2~0x5。CPU对该数据的读取操作如下:

(1) CPU先读取地址0x0~0x3,读出0x2和0x3地址上的数据,一个short类型[2字节]的数

据。

(2) 再读地址0x4~0x7,读出0x4和0x5地址上的数据,一个short类型[2字节]的数据。

(3) 最后将前面两次读取到的数据进行组合,得到原始的int类型数据。

注:实际上读了两次[访问两次内存]才读到真正的数据

 

如果A的存储起始地址为0x3,则它在内存中存储的地址为0x3~0x6。CPU对该数据的读取操作如下:

(1) CPU先读取地址0x0~0x3,读出0x3地址上的数据,一个char类型[1字节]的数据。

(2) 接着访问内存地址0x4~0x7,这里就有两种情况,取决于CPU的读取策略。

A. 先读出0x4上的一个char,B. 再读出0x5~0x6上的一个short;

A. 先读出0x4~0x5上的一个short,B. 再读出0x6上的一个char。

(3) 最后将前面三次读取到的数据进行组合,得到原始的int类型数据。

注:实际上读了三次[访问三次内存]才读到真正的数据

 

如果A存储在自然对其的地址上[如:0x4, 0x8……],CPU只需要读取一次即可。

 

对比以上三种情况进行分析可以,变量在内存中存储的位置影响CPU的存储效率。

 

3. 编译器如何处理字节对齐

C语言的数据类型可分为标准数据类型和构造数据类型。标准数据类型的存储地址为其长度的整数倍即可实现对齐。而对于构造的数据类型:结构体、共用体和数组,对齐的原则是:

(1) 对于数组,因为数组里元素的数据类型都是一样的,所以,第一个元素对齐之后,后面剩余元素自然就对齐了。

(2) 共用体,按照长度最大的成员进行对齐。

(3) 结构体,因为结构体里的数据成员类型可以是不同的,所以,每一个成员都需要对齐。

 

4. 常见的字节对齐方式

有:1字节对齐,2字节对齐,4字节对齐。其中1字节对齐是最小的字节对齐方式。

例如:32位的CPU,是:32/8=4字节对齐,因此比如32位的gcc编译器也是4字节对齐。

重点讨论结构体类型的对齐。

 

二、讨论结构体类型变量的大小

1. C语言的__attribute__选项

__attribute__选项可以设置函数的属性(Function Attribute),变量属性(Variable Attribute),类型属性( Type Attribute)。对于C语言中的结构体类型来说,经常使用该选项来指定结构体类型的字节对齐属性。传入选项中的参数有:aligned(N)、packed。

 

2. 对齐规则

首先,每个成员都先自身对齐(假设数据长度是N字节对齐,则存储地址%N = 0);其次是结构体的总长度要对齐(总长 = 最大长度数据成员的最小整数倍,total(len) = max(data_type) *n )。所以会有填补空间的情况(使用空字符进行填充,ASCII值为0的NULL)。

 

3. 示例

3.1 例子1

32位的CPU,默认按4字节对齐。

struct persion {

         char sex;

         int age;

         char name[10];

};

 

struct persion per_01;

 

int main(void)

{

         printk("sizeof(per_01) = %ld\n", sizeof(per_01));

 

         while (1) {

                   ;

         }

 

         return 0;

}

程序结果:main: sizeof(per_01) = 20

分析:首先考虑的是各个元素自身的对齐,CPU按4字节对齐来访问数据,也就是数据要放在:Address % 4=0,所以,假设第一个元素起始地址位是0x0,只占1个字节,为了能够让第二个成员对齐,在sex的后面会有3个填充的字节;那么第二个元素就得放0x4上,占4个连续的内存空间;第三个元素放0x8上,占10个连续的内存空间地址;最后,再考虑整个结构体的对齐,name后有也有2个填充的字节,整个结构体变量共占用20个字节连续的内存空间(可以这么理解:CPU会读5次,每次读4字节,5 x 4 =20),其中有5个空间(3+2)是无用的空间。该结构体变量的各个成员在内存中的存储情况如下图。

 

再分析CPU读取结构体成员的过程:

(1) 读地址0x0~0x3,读到sex。

(2) 偏移量为4,读地址0x4~0x7,可读到age。

(3) 偏移量为8,读起始地址0x8,可读到name。

以上是理论的分析,接下来,可将每个成员的首地址以及偏移量打印出来,程序如下:

struct persion {

         char sex;

         int age;

         char name[10];

};

 

struct persion per_01;

 

int main(void)

{

         printk("&per_01.sex = %p, offset(sex) = %ld", &per_01.sex, offsetof(struct persion, sex));

         printk("&per_01.age = %p, offset(age) = %ld", &per_01.age, offsetof(struct persion, age));

         printk("per_01.name = %p, offset(name) = %ld", per_01.name, offsetof(struct persion, name));        

         while (1) {

                   ;

         }

 

         return 0;

}

运行结果:

main: &per_01.sex = 0xbfc2480c, offset(sex) = 0

main: &per_01.age = 0xbfc24810, offset(age) = 4

main: &per_01.name = 0xbfc24814, offset(name) = 8

3.1.1 小结

(1) 填充会有两种情况:在数据成员之间填充,目的是为了能让下一个成员能对齐;而在最尾的成员之后填充,考虑的是整个结构体的对齐,在不使用参数__attribute__的情况下,总长 = 最大长度数据成员的最小整数倍。

(2) 填充的空间算是浪费掉的空间,但是相对于目前计算机的存储技术而言,这一点空间算不上什么,这就是典型的“以牺牲空间来换取时间上的效率”。

(3) 每次运行程序,打印出来的数据成员地址可能不一样,但是地址排布规律是一样的。

main: &per_01.sex = 0xbfc24800, offset(sex) = 0

main: &per_01.age = 0xbfc24804, offset(age) = 4

main: &per_01.name = 0xbfc24808, offset(name) = 8

 

main: &per_01.sex = 0xbfc2480c, offset(sex) = 0

main: &per_01.age = 0xbfc24810, offset(age) = 4

main: &per_01.name = 0xbfc24814, offset(name) = 8

(4) 偏移量,指的是第一个元素之后,第二个元素开始,相对于首地址[实际上就是第一个元

素的地址]的偏移量,用当前元素的地址减去首地址就得到偏移量,可以直接用库函数offsetof()求得。

0xbfc24810 - 0xbfc2480c = 4

0xbfc24814 - 0xbfc2480c = 8  

 

3.2 例子2

使用参数__attribute__,如果传入的参数是aligned(N)。

场景1:N比结构体成员中最大数据长度的成员还要小,则要按最大数据成员的长度来对齐[各个成员的对齐情况以及整个结构体的长度对齐]。指定2字节对齐,结构体成员中有int数据类型(32位CPU,默认4字节对齐),示例程序如下:

struct persion {

         char sex;

         int age;

         char name[10];

};

 

struct persion_02 {

         char sex;

         int age;

         char name[10];

}__attribute__ ((aligned(2)));

 

struct persion per_01;

struct persion_02 per_02;

int main(void)

{

         printk("sizeof(per_01) = %ld\n", sizeof(per_01));

         printk ("&per_01.sex = %p, offset(sex) = %ld", &per_01.sex, offsetof(struct persion, sex));

         printk ("&per_01.age = %p, offset(age) = %ld", &per_01.age, offsetof(struct persion, age));

         printk ("per_01.name = %p, offset(name) = %ld", per_01.name, offsetof(struct persion, name));

         printk ("*****************************************************************");

 

         printk ("sizeof(per_02) = %ld\n", sizeof(per_02));

         printk ("&per_02.sex = %p, offset(sex) = %ld", &per_02.sex, offsetof(struct persion_02, sex));

         printk ("&per_02.age = %p, offset(age) = %ld", &per_02.age, offsetof(struct persion_02, age));

         printk ("per_02.name = %p, offset(name) = %ld", per_02.name, offsetof(struct persion_02, name));   

         while (1) {

                   ;

         }

 

         return 0;

}

程序运行结果:

main: sizeof(per_01) = 20

 

main: &per_01.sex = 0xbfc24820, offset(sex) = 0

main: &per_01.age = 0xbfc24824, offset(age) = 4

main: per_01.name = 0xbfc24828, offset(name) = 8

main: ********************************************************************

 

main: sizeof(per_02) = 20

main: &per_02.sex = 0xbfc2480c, offset(sex) = 0

main: &per_02.age = 0xbfc24810, offset(age) = 4

main: per_02.name = 0xbfc24814, offset(name) = 8

注:对于该例子,1字节或者2字节对齐,得到的结果都是一样的,都按4字节来对齐。

 

场景2: N就比结构体成员中最大数据长度的成员还要大,

假设是64位的CPU,默认是8字节对齐。那么传入的参数是aligned(8)。则:各个成员的对齐情况是自然对齐;整个结构体的长度是N对齐,示例程序如下:

struct persion {

         char sex;

         int age;

         char name[10];

};

 

struct persion_02 {

         char sex;

         int age;

         char name[10];

}__attribute__ ((aligned(8))); //aligned(N),N=8

 

struct persion per_01;

struct persion_02 per_02;

 

int main(void)

{

         printk("sizeof(per_01) = %ld\n", sizeof(per_01));

         printk ("&per_01.sex = %p, offset(sex) = %ld", &per_01.sex, offsetof(struct persion, sex));

         printk ("&per_01.age = %p, offset(age) = %ld", &per_01.age, offsetof(struct persion, age));

         printk ("per_01.name = %p, offset(name) = %ld", per_01.name, offsetof(struct persion, name));

         printk ("******************************************************************");

 

         printk ("sizeof(per_02) = %ld\n", sizeof(per_02));

         printk ("&per_02.sex = %p, offset(sex) = %ld", &per_02.sex, offsetof(struct persion_02, sex));

         printk ("&per_02.age = %p, offset(age) = %ld", &per_02.age, offsetof(struct persion_02, age));

         printk ("per_02.name = %p, offset(name) = %ld", per_02.name, offsetof(struct persion_02, name));

 

         while (1) {

                   ;

         }

 

         return 0;

}

程序运行结果:

main: &per_01.sex = 0xbfc24828, offset(sex) = 0

main: &per_01.age = 0xbfc2482c, offset(age) = 4

main: per_01.name = 0xbfc24830, offset(name) = 8

main: ********************************************************************

 

main: sizeof(per_02) = 24

main: &per_02.sex = 0xbfc24810, offset(sex) = 0

main: &per_02.age = 0xbfc24814, offset(age) = 4

main: per_02.name = 0xbfc24818, offset(name) = 8

8字节对齐,则在最后一个数据成员后,补了6个空字符。在第一个数据成员后,填充了3个空字符。可以取出地址上的值,进行验证。下面程序打印第一个字节之后,填充的三个空字符的ASCII值。

struct persion {

         char sex;

         int age;

         char name[10];

};

 

struct persion_02 {

         char sex;

         int age;

         char name[10];

}__attribute__ ((aligned(8)));

 

struct persion per_01;

struct persion_02 per_02={

         'm',

         26,

         "jone"

};

 

int main(void)

{

         char *sex_dat = NULL;

         int *age_dat = NULL;

 

         printk("sizeof(per_01) = %ld\n", sizeof(per_01));

         printk("&per_01.sex = %p, offset(sex) = %ld", &per_01.sex, offsetof(struct persion, sex));

         printk("&per_01.age = %p, offset(age) = %ld", &per_01.age, offsetof(struct persion, age));

         printk("per_01.name = %p, offset(name) = %ld", per_01.name, offsetof(struct persion, name));

         printk("****************************************************************");

 

         printk("sizeof(per_02) = %ld\n", sizeof(per_02));

         printk("&per_02.sex = %p, offset(sex) = %ld", &per_02.sex, offsetof(struct persion_02, sex));

         printk("&per_02.age = %p, offset(age) = %ld", &per_02.age, offsetof(struct persion_02, age));

         printk("per_02.name = %p, offset(name) = %ld", per_02.name, offsetof(struct persion_02, name));

 

         sex_dat = &per_02.sex;

         age_dat = &per_02.age;

 

         printk("*(sex_dat) = %c", *(sex_dat));

         printk("*(sex_dat + 1) = 0x%02x", *(sex_dat + 1));

         printk("*(sex_dat + 1) = 0x%02x", *(sex_dat + 2));

         printk("*(sex_dat + 1) = 0x%02x", *(sex_dat + 3));

 

         printk("*(age_dat) = %d", *(age_dat));

 

         while (1) {

                   ;

         }

 

         return 0;

}

 

程序运行结果:

main: sizeof(per_01) = 20

main: &per_01.sex = 0xbfc24824, offset(sex) = 0

main: &per_01.age = 0xbfc24828, offset(age) = 4

main: per_01.name = 0xbfc2482c, offset(name) = 8

main: ********************************************************************

 

main: sizeof(per_02) = 24

main: &per_02.sex = 0xbfc234b8, offset(sex) = 0

main: &per_02.age = 0xbfc234bc, offset(age) = 4

main: per_02.name = 0xbfc234c0, offset(name) = 8

 

main: *(sex_dat) = m

main: *(sex_dat + 1) = 0x00

main: *(sex_dat + 1) = 0x00

main: *(sex_dat + 1) = 0x00

main: *(age_dat) = 26

内存排布图示:没按实际的来画,但是分布规律相同。

 

 

3.3 例子3

使用参数__attribute__,如果传入的参数是packed。告诉编译器,在编译过程中取消优化对齐。所以,实际占用多少字节就是多少字节,不会有填充。示例程序如下:

#if 0

struct persion {

         char sex;

         int age;

         char name[10];

}__attribute__ ((aligned(1)));

#else

struct persion {

         char sex;

         int age;

         char name[10];

}__attribute__ ((packed));

 

#endif

struct persion per_01;

 

int main(void)

{

         printk("sizeof(per_01) = %ld\n", sizeof(per_01));

         printk("&per_01.sex = %p, offset(sex) = %ld", &per_01.sex, offsetof(struct persion, sex));

         printk("&per_01.age = %p, offset(age) = %ld", &per_01.age, offsetof(struct persion, age));

         printk("per_01.name = %p, offset(name) = %ld", per_01.name, offsetof(struct persion, name));

 

         while (1) {

                   ;

         }

 

         return 0;

}

运行结果:

main: sizeof(per_01) = 15

 

main: &per_01.sex = 0xbfc2480c, offset(sex) = 0

main: &per_01.age = 0xbfc2480d, offset(age) = 1

main: per_01.name = 0xbfc24811, offset(name) = 5

 

通过分析打印的地址信息,是可以知道,编译器并没有做内存上的对齐优化。

内存排布图示(数据成员没有对齐,也没有填充),没按实际的来画,但是分布规律相同。

 

4. 和字节对齐相关的预处理指令

成对使用:告诉编译器,按照N个字节对齐。

#pragma pack(N)  //N字节对齐

………………………

#pragma pack()    //取消N字节对齐

 

示例程序:

#if 0             /* 两种一样的情况 */

struct persion {

         char sex;

         int age;

         char name[10];

}__attribute__ ((packed));

#else

 

#pragma pack(1)

struct persion {

         char sex;

         int age;

         char name[10];

};

 

#pragma pack()

 

#endif

 

struct persion per_01;

 

int main(void)

{

         printk("sizeof(per_01) = %ld\n", sizeof(per_01));

         printk("&per_01.sex = %p, offset(sex) = %ld", &per_01.sex, offsetof(struct persion, sex));

         printk("&per_01.age = %p, offset(age) = %ld", &per_01.age, offsetof(struct persion, age));

         printk("per_01.name = %p, offset(name) = %ld", per_01.name, offsetof(struct persion, name));

 

         while (1) {

                   ;

         }

 

         return 0;

}

 

运行结果:

main: sizeof(per_01) = 15

main: &per_01.sex = 0xbfc2480c, offset(sex) = 0

main: &per_01.age = 0xbfc2480d, offset(age) = 1

main: per_01.name = 0xbfc24811, offset(name) = 5

注意:和使用参数__aligned__(1)的区别[是否存在最大长度大于1的数据成员]。

 

5. 定义结构体类型时如何节省空间

一般性的原则是:相同类型的要靠到一起(在定义的时候,可以做简单的计算)。

例子1:32位的CPU,默认是4字节对齐。

struct dat_type_01 {

         char dat_a;

         int dat_c;

         short dat_b;

};

 

struct dat_type_02 {

         int dat_c;

         char dat_a;

         short dat_b;

};

 

struct dat_type_01 dat_01;

struct dat_type_02 dat_02;

 

int main(void)

{

         printk("sizeof(per_01) = %ld", sizeof(dat_01));

         printk("&dat_01.dat_a = %p, offset(dat_a) = %ld", &dat_01.dat_a, offsetof(struct dat_type_01, dat_a));

         printk("&dat_01.dat_b = %p, offset(dat_b) = %ld", &dat_01.dat_b, offsetof(struct dat_type_01, dat_b));

         printk("&dat_01.dat_c = %p, offset(dat_c) = %ld", &dat_01.dat_c, offsetof(struct dat_type_01, dat_c));

         printk ("***********************************************");

 

         printk ("sizeof(dat_02) = %ld", sizeof(dat_02));

        

         printk("&dat_02.dat_c = %p, offset(dat_c) = %ld", &dat_02.dat_c, offsetof(struct dat_type_02, dat_c));

         printk("&dat_02.dat_a = %p, offset(dat_a) = %ld", &dat_02.dat_a, offsetof(struct dat_type_02, dat_a));

         printk("&dat_02.dat_b = %p, offset(dat_b) = %ld", &dat_02.dat_b, offsetof(struct dat_type_02, dat_b));

 

         while (1) {

                   ;

         }

 

         return 0;

}

运行结果:

main: sizeof(per_01) = 12

main: &dat_01.dat_a = 0xbfc24814, offset(dat_a) = 0

main: &dat_01.dat_b = 0xbfc2481c, offset(dat_b) = 8

main: &dat_01.dat_c = 0xbfc24818, offset(dat_c) = 4

main: ***********************************************

main: sizeof(dat_02) = 8

main: &dat_02.dat_c = 0xbfc2480c, offset(dat_c) = 0

main: &dat_02.dat_a = 0xbfc24810, offset(dat_a) = 4

main: &dat_02.dat_b = 0xbfc24812, offset(dat_b) = 6

注:数据成员的排序,影响到内存排布。第一种情况,填充了5个空字符,而第二种情况,只填充了1个空字符

0 1 2 3   4 5 6 7   8 9   10 11

char       int          short

 

0 1 2 3   4    5   6 7

int          char    short

 

6. 什么是显示提醒

程序中,补上char类型的数据,起到提醒的作用,所以子啊源码中,一般都是用”reserved”做标识符,reserved译为”保留的”,在项目源码中定义的结构体经常碰到。实际上,可以不用补上,编译器会自动分配出空间,也就是填充空间。

示例程序:32位CPU,默认4字节对齐。

struct dat_type_01 {

         char dat_a;

         int dat_c;

         short dat_b;

};

 

struct dat_type_02 {

         char dat_a;

         char reserved_array_01[3];

         int dat_c;

         short dat_b;

         char reserved_array_02[2];

};

 

struct dat_type_01 dat_01;

struct dat_type_02 dat_02;

 

int main(void)

{

         printk("sizeof(per_01) = %ld", sizeof(dat_01));

         printk("&dat_01.dat_a = %p, offset(dat_a) = %ld", &dat_01.dat_a, offsetof(struct dat_type_01, dat_a));

         printk("&dat_01.dat_b = %p, offset(dat_b) = %ld", &dat_01.dat_b, offsetof(struct dat_type_01, dat_b));

         printk("&dat_01.dat_c = %p, offset(dat_c) = %ld", &dat_01.dat_c, offsetof(struct dat_type_01, dat_c));

         printk("***********************************************");

 

         printk("sizeof(dat_02) = %ld", sizeof(dat_02));

         printk("&dat_02.dat_a = %p, offset(dat_a) = %ld", &dat_02.dat_a, offsetof(struct dat_type_02, dat_a));

         printk("&dat_02.dat_b = %p, offset(dat_b) = %ld", &dat_02.dat_b, offsetof(struct dat_type_02, dat_b));

         printk("&dat_02.dat_c = %p, offset(dat_c) = %ld", &dat_02.dat_c, offsetof(struct dat_type_02, dat_c));

 

         while (1) {

                   ;

         }

 

         return 0;

}

运行结果:

main: sizeof(per_01) = 12

main: &dat_01.dat_a = 0xbfc24818, offset(dat_a) = 0

main: &dat_01.dat_b = 0xbfc24820, offset(dat_b) = 8

main: &dat_01.dat_c = 0xbfc2481c, offset(dat_c) = 4

main: ***********************************************

main: sizeof(dat_02) = 12

main: &dat_02.dat_a = 0xbfc2480c, offset(dat_a) = 0

main: &dat_02.dat_b = 0xbfc24814, offset(dat_b) = 8

main: &dat_02.dat_c = 0xbfc24810, offset(dat_c) = 4

 

 

7. 结构体的数据成员是连续存放的吗

不一定,取决于:(1)结构体类型的定义情况 (2)内存字节对齐情况。

 

8. 小结

(1) 对于结构体,首先考虑的是每个数据成员的自然对其,其次,是整个结构体长度的对齐。最好是画个示意图,能更清楚地分析内存排布情况。

(2) 一般来说,对齐的事交给编译器就好了。

http://www.lryc.cn/news/2416595.html

相关文章:

  • 前端必知必会-CSS布局display属性
  • 一篇文章搞懂RAID磁盘阵列
  • Delphi编程---可以进行四则运算的Tcalc类源代码
  • tftpd32+ tftpd64文件传输安装和使用教程【图文并茂】
  • 学生信息管理系统模块ExecuteSql 函数详解
  • 属性PropertyInfo的使用
  • 航天器状态(位置速度)转轨道六根数
  • EPon F4503.0作为交换机使用废旧光猫中国电信光猫改交换机功能作为无线wifi或者交换机使用天翼网关中兴ZXHN免拆机获取破解超级管理员密码
  • AT24C04 eeprom读写测试
  • hotstuff共识算法总结
  • 初学者如何制作一个简单的HTML个人网页
  • IEEE1394(火线)接口全面了解
  • Freemaker指令总结
  • 文本处理正则表达式:grep
  • MySQL字符集的设置
  • C语言“学生信息管理系统”功能详解及代码展示2023级慕课版
  • go二维map_记一次坑爹的golang 二维map判断问题
  • Android Studio 代码混淆(你真的会混淆吗)
  • JSP基于web仓库管理系统v83k3(程序+源码+数据库+调试部署+开发环境)
  • RISC架构
  • 多线程编程java_java多线程编程
  • 递归调用栈溢出问题分析与解决
  • C#的Winform多语言实现(resx文件)
  • 电脑时间老是重置?一招教你轻松解决!
  • 黑色主题个人主页HTML源码
  • 印度电影推荐
  • 教您如何使用WebMatrix创建第一个网页
  • 网络安全笔记-信息安全工程师与网络安全工程师考试大纲(附:Web安全大纲)
  • Windows xp正版验证序列号大全
  • 如何利用CSDN资源来建立技术社区 - 博客篇