C/C++ 字节对齐

概念

  在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。

  为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的”对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即对齐跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。

  比如在32位cpu下,假设一个整型变量的地址为0x00000004(为4的倍数),那它就是自然对齐的,而如果其地址为0x00000002(非4的倍数)则是非对齐的。

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

为什么要字节对齐

  需要字节对齐的根本原因在于CPU访问数据的效率问题。假设上面整型变量的地址不是自然对齐,比如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的一个short然后组合得到所要的数据,如果变量在0x00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为char,然后组合得到整型数据。

  而如果变量在自然对齐位置上,则只要一次就可以取出数据。一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误,而在x86上就不会出现错误,只是效率下降。

  各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。

  比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐,但其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。

  比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。

  另外字节对齐的作用不仅是便于cpu快速访问,同时合理的利用字节对齐可以==有效地节省存储空间==。

  也即CPU一次访问时,要么读0x01~0x04,要么读0x05~0x08…硬件不支持一次访问就读到0x02~0x05

  例:如果0x02~0x05存了一个int,读取这个int就需要先读0x01~0x04,留下0x02~0x04的内容,再读0x05~0x08,留下0x05的内容,两部分拼接起来才能得到那个int的值,这样读一个int就要两次内存访问,效率就低了。

对齐方式

    对于 struct 或 union 中的 struct 或者 union 来说,它们的字节对齐标准就是它的所有成员中字节数最大的数据的字节数。

一般情况下 C/C++ 的变量所占用的字节数

        char:    1字节;

        short:   2字节;

        int:       4字节;

        long:    4字节;

        long long: 8字节;

        float:    4字节;

        double: 8字节;

        bool:    1字节;

*struct 中字节对齐需要满足的条件:

    1、某个变量存放的起始位置相对于结构的起始位置的偏移量是该变量字节数的整数倍;

    2、结构所占用的总字节数是结构种字节数最长的变量的字节数的整数倍。

    例:

struct Struct
{
    double d1;
    char d2;
    int d3;
}a;

 sizeof(a) = 8 + 1 + 3 + 4 = 16。其中补上的 3 个字节是为了让 int 型数据的起始位置相对于结构起始位置的偏移量为 4 的整数倍。

struct Struct
{
    char d1;
    double d2;
    int d3;
}b;

sizeof(b) = 1 + 7 + 8 + 4 = 20。 20 / 8 = 2 …… 4,所以需要再补上 4 个字节,使之成为 8 的 整数倍

*union 中字节对齐需要满足的两个条件:

    1、unoin 的大小必须足够容纳最宽的成员;

    2、union 的大小需要能够被其所包含的基础成员类型的大小所整除。

 

字节对齐的另一种方式

    VC提供了 #pragma pack(n) 用来自定义字节对齐方式

    有一下两种情况:

        1、n 大于变量的字节数:偏移量只满足默认的字节对齐方式;

        2、n 小于变量所占的字节数:偏移量是 n 的整数倍,不使用默认的字节对齐方式。

    例:

int main()
{
#pragma pack(push)        // 保持对齐状态
#pragma pack(4)            // 设定为 4 字节对齐
	struct test
	{
		char m1;
		double m2;
		int m3;
	}a;
#pragma pack(pop)        // 恢复对齐状态
	//sizeof(a) = 1 + 3 + 8 + 4 = 16  // 其中补上三位是因为 n 小于 8,所以 m2 的起始位置相对于结构起始位置的偏移量是 n,即为 4.
	cout << sizeof(a)<<endl;

#pragma pack(8)
	struct S1
	{
		char a;
		long b;
	};
	struct S2
	{
		char c;
		struct S1 d;
		long long e;
	};
#pragma pack()
	
	cout << "s1:"<<sizeof(S1)<<endl;
	cout << "s2:" << sizeof(S2)<<endl;

	system("pause");
    return 0;
}

1、字节对齐问题的讨论到上边已经结束了。下面再加上我碰到的两道题目作为实例:

#include <stdio.h>
union
{
    char x[5];
    int i;
}a;

int main()
{
    a.x[0] = 10;
    a.x[1] = 1;
    printf("%d\n", a.i);
    printf("%d\n", sizeof(a));
    return 0;
}

输出为 266 8

解析:

对 union 分配内存涉及字节对齐问题,在上方已有详细描述,在此只简单解释一番。union 分配的内存必须是 union 中所有基本数据类型的倍数。
在此题中即为 1 和 4 的倍数,又 char x[5] 占用 5 个字节,故 union 分配的内存大小应为 8 个字节。
windows 系统中高字节在后,低字节在前。而 char x[5] 只有前两个元素有值,即两个值只占 2 个字节,也即 union 中的 int 型数据中只有低两位上有值。
即 i 的二进制表示为:
    00000000 00000000 00000001 00001010
    即: 2^1 + 2^3 + 2^8 = 266
也相当于十六进制 0x010A, 即: 10 * 16^0 + 1 * 16^2 = 266
 

这里补充一下进制转换问题,也是我一并搜集来的:

进制互相转换

十进制0123456789101112131415
十六进制0123456789ABCDEF
二进制0000000100100011010001010110011110001001101010111100110111101111

  十六进制数字转换十进制数字:         

    0x2AF5转换十进制:            

      5 * 16^0 = 5            

      F * 16^1 = 240            

      A * 16^2 = 2560            

      2 * 16^3 = 8192            

      ---------------            

      5 + 240 + 2560 + 8192 = 10997    

  十进制数字转换十六进制数字:        

    2500转换十六进制:            

      2500 / 16 = 156 …… 4            

      156 / 16 = 9 …… 12(C)            

      9 / 16 = 0 …… 9            

      ----------------------            

      得到4C9,倒转9C4,即2500的十六进制表示是: 0x9C4    

  二进制数字转换十六进制数字:        

    101110011011.1001        

    采取四合一法,即以二进制的小数点为分界点,向左(或向右)每四位取一位。        

    组分好以后,对照二进制与十六进制数的对应表,将四位二进制按权相加,得到的数就是一位十六进制数,然后按顺序排列,小数点的位置不变。        

    结果为:B9B.9        

    需要注意的是,在向左(或向右)取四位时,取到最高位(最低位)如果无法凑足四位,就可以在小数点的最左边(或最右边)补0,进行换算。    

  十六进制数字转换二进制数字:  

    对照二进制与十六进制数的对应表,将十六进制数分为二进制数字,用四位二进制数字按权相加,最后得到二进制数字,小数点依旧。

2、

#include <iostream>
using namespace std;

typedef struct A
{
    char aChar;
    int aInt_2;
    short aInt;
}TypeA;

typedef struct B
{
    char bChar[3];
    TypeA bA;
    double bDouble;
}TypeB;

int main()
{
    TypeA a;
    TypeB b;
    cout << sizeof(a) << endl << sizeof(b) << endl;
    return 0;
}

输出为 12 24

解析:

由上述对字节对齐问题的讨论很容易便可以得出此题的答案。
sizeof(TypeA) = 1 + 3 + 4 + 2 = 10。 10 / 4 = 2 …… 2,故需要再补上 2 个字节,即 sizeof(TypeA) = 12;
sizeof(TypeB) = 3 + 1 + 12 + 8 = 24。之所以补上 1 个字节是因为 TypeA 类型在 struct TypeB 中的默认对齐方式
是 4 个字节(即 int 的字节大小,也即 struct TypeA 中最长的字节)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值