字节存储单元及struct内存分配

二进制

当今的计算机系统使用的基本上都是由18世纪德国数理哲学大师莱布尼兹发现的二进制系统。二进制数字系统中只有两种二进制数码——0和1。

“bit”(比特)被创造出来代表“binary digit”,1bit代表一个二进制数位。

为方便起见,我们不妨将“比特”简单地理解为数字逻辑电路中的开关,键控导致电路的通断产生两种状态:断电(低电平)为0,上电(高电平)为1。

存储单元

2个比特可以组合出4(2^2)种状态,可表示无符号数值范围[0,3];32个比特可以组合出4294967296(2^32)种状态,可表示无符号数值范围[0,4294967295];……。

在有限范围内的可计量数值几乎都可以用二进制数码串组合表示,计算机的内存由数以亿万计的比特位存储单元(晶体管)组成。由于一个位只能表示二元数值,所以单独一位的用处不大。通常将固定位数的位串作为一个基本存储单位,这样就可以存储范围较大的值。

存储单元(byte)的地址,就像门牌号,敬请参考《指针》。

打印字节串

1byte=8bit,底层都是二进制位串进行移位实现相关操作。

标准C++中的<bitset>提供了二进制位串操作接口,以下为打印单字节和通用数据类型二进制位串的示例程序,以直观地查看数据的二进制位串。

typedef unsigned char uchar;

// 枚举整数x二进制串中含有多少个1,也可以不停右移除以2看有多少个余数
int enum_filled_bits(int x)
{
    int countx = 0;

    while (x)
    {
       countx++;
       x = x & (x - 1);
    }

    return countx;
}

// 打印单字节数的二进制位串
void binary_print_byte(uchar c)
{
    for(int i = 0; i < 8; ++i)
    {
       if((c << i) & 0x80) // 左移
           cout << '1';
       else
           cout << '0';
    }
    
    cout << ' ';
}

// 打印通用类型的二进制位串
template <class T>
void binary_print_multibytes(T val)
{
    void *f = &val; // 取地址
    size_t sz = sizeof(T);
    uchar *pByte = new uchar[sz];
    int i;

    for(i = 0; i < sz; i++)
       pByte[i] = *((uchar*)&f + i);   

#ifdef _BIG_ENDIAN
    for(i = 0; i != sz; i++)
       binary_print_byte(pByte[i]);
#else // for windoze(Intel X86)
    for(i = sz; i != 0; i--)
       binary_print_byte(pByte[i-1]);
#endif 

    delete[] pByte;

    cout << endl;
}

字节串的组合析取

以下测试小程序展示了三种字节析取情况:

#include <stdio.h>
#include <stdint.h>

int main(int argc, const char *argv[])
{
    int i;
    int8_t byte[9] = {48, 49, 50, 51, 52, 53, 54, 55, 0};
    puts("每个byte的16进制值:");
    for (i = 0; i < 9; i++)
    {
        printf("byte[%d] = 0x%x\n", i, byte[i]);
    }

    int8_t *pByte = byte;
    puts("----------------------------------");
    printf("字符串byte[9] = %s\n", pByte);

    puts("----------------------------------");
    puts("每2个byte组合而成的16进制值:");
    int16_t *pi16 = (int16_t *)pByte;
    for (i = 0; i < 4; i++)
    {
        int16_t i16 = *(pi16 + i);
        printf("*(pi16 + %d) = 0x%4x\n", i, *(pi16 + i));
    }

    puts("----------------------------------");
    puts("每4个byte组合而成的16进制值:");
    int32_t *pi32 = (int32_t *)pByte;
    for (i = 0; i < 2; i++)
    {
        int32_t i32 = *(pi32 + i);
        printf("*(pi32 + %d) = %#x\n", i, *(pi32 + i));
    }
    puts("----------------------------------");

    return 0;
}

说明:*(pi16 + 0) = 0x3130 而不是 0x3031,这是因为x86架构体系的Windows操作系统为小尾端(little endian)系统,也即在起始地址处存放整数的低序号字节(低地址低字节)。关于字节的大小端问题,网络编程中将有所涉及,在嵌入式开发中经常遇到。

这些位置的每一个都被称为字节(byte),每个字节都包含了存储一个字符所需要的位数

100

101

102

103

104

105

106

107

在很多现代的机器上,每个字节包含8个位,可以存储无符号值0至255,或者有符号值-128 至127,典型的如ASCII码。每个字节通过地址来标识,如上图中的数字所示。

为了存储更大的值,我们把两个或更多个字节合在一起作为一个更大的内存单位。例如,很多机器以字为单位存储整数,每个字一般由2或4个字节组成。下图所示内存位置与上图相同,但这次它以4个字节的字来表示。

100101102103104105106107
                                                

在 32 位机器上,尽管一个字包含了4个字节,它仍然只有一个地址。至于它的地址是它最左边那个字节的位置还是最右边那个字节的位置,不同的机器有不同的规定。另一个需要注意的硬件事项是边界对齐(boundary alignment)。在要求边界对齐的机器上,整型值存储的起始位置只能是某些特定的字节,通常是2或4的倍数。但这些问题是硬件设计者的事情,它们很少影响C程序员。我们只对两件事情感兴趣:

1)内存中的每个位置有一个独一无二的地址标识。

2)内存中的每个位置都包含一个值。

在C程序中,我们经常根据需要借助指针对一块内存进行操作,再按字节组合析取出所需数据,平时的程序中经常用到通用指针 void* 的妙处就在于可以按照需要操作一块内存,以取所需值类型。

强制类型转换

下图为 MSDN 中 C++ Type System (Modern C++) 的 Fundamental (built-in) types 按字节宽度的层砌图(Layout of Source Language Data Types):

一个8byte宽的存储单元,可以存储1个 long long(__int64)/double、2个连续的 long(int)/float、4个连续的 wchart_t(short)、8个连续的byte(char)。
按照实际存储的有效数据单元,可按需对字节串进行组合析取或类型转换。
以下程序示例了两种强制类型转换:
// 多字节的截取(容易造成数据的丢失!)
int i1 = 0x12345678; // 小序存放顺序4byte:0x78,0x56,0x34,0x12
short s1 = (short)i1; // 析取低位2byte:0x5678
char c1 = (char)i1; // 析取低位1byte:0x78
// 短字节的扩展
short s2 = 0x5678; // 小序存放顺序2byte:0x78,0x56
int i2 = (int)s2; // 扩展2byte,高位补0:0x00005678

关于字节序

Endianness: In computing, endianness is the order in which bytes within a word of digital data are transmitted over a data communication medium or addressed (by rising addresses) in computer memory, counting only byte significance compared to earliness.

Endianness is primarily expressed as big-endian (BE) or little-endian (LE), terms introduced by Danny Cohen into computer science for data ordering in an Internet Experiment Note published in 1980.

在移动嵌入式领域,统治市场的 MIPS 和 ARM 处理器可通过配置寄存器采用不同的字节序,默认采用 Little-Endian。

但 ARM 始终采用 Big-Endian 存储浮点数。早期使用 PowerPC 处理器的 Mac 采用大字节序,如今的 Mac 同 Windows PC 一样都采用 Intel x86 芯片,因此也都是小字节序存储的。

TCP/IP协议统一规定采用大端方式封装解析传输数据,也称为网络字节顺序(network byte order,TCP/IP-endian)。因此,在进行网络数据的收发时,都需要执行字节序转换

以下为 MSDN 中关于 Packet byte/bit order 的阐述:

For packets, the bit numbering convention followed is the same as that used in RFCs, namely: the high (most significant) bit of the first byte to hit the wire is in packet bit 0, and the low bit of the last byte to hit the wire is in packet bit 31 (so that the bits are shown from left-to-right in the order they naturally appear over the network).

clang/gcc 执行 -E 预处理,-x 指定 c 语言,-dM 选项 dump Macros,打印出来的预定义宏中字节序相关定义:

# $ clang -dM -E -x c /dev/null
$ gcc -dM -E -x c /dev/null

#define __BYTE_ORDER__ __ORDER_LITTLE_ENDIAN__
#define __ORDER_BIG_ENDIAN__ 4321
#define __ORDER_LITTLE_ENDIAN__ 1234
#define __ORDER_PDP_ENDIAN__ 3412

以下C程序用于测试系统的字节序:

  • isBigEndian: 无符号短整型 us 占用 2 个字节,字节指针 (unsigned char *)&us 指向第1个字节。如果是大端顺序存放,第1个字节存放高位 0x34。
  • isLittleEndian: 联合类型 c,最长单元为短整型 a,占用 2 个字节。c.a、c.b 都指向数据结构的起始位置,即内存中的第 1 个字节。c.a=0x0001,当小端存放时,第 1 个字节存放低位 0x01,即 c.b=0x1。
#include <stdio.h>

typedef enum
{
    PDP_ENDIAN = 0x3412,    // __ORDER_PDP_ENDIAN__
    BIG_ENDIAN = 0x4321,    // __ORDER_BIG_ENDIAN__
    LITTLE_ENDIAN = 0x1234, // __ORDER_LITTLE_ENDIAN__
} ByteOrder; // __BYTE_ORDER__

// 顺序存储
int isBigEndian()
{
    unsigned short us = PDP_ENDIAN;
    return (*((unsigned char *)&us) == 0x34);
}

// 高位存储低权字节,则为小端
ByteOrder getByteOrder()
{
    static ByteOrder bo = PDP_ENDIAN;

    if (bo == PDP_ENDIAN)
    {
        union u
        {
            short a; // 2 byte
            char b;  // 1 byte
        } c;
        c.a = 1; // 0x0001
        bo = (c.b ? LITTLE_ENDIAN : BIG_ENDIAN);
    }

    return bo;
}

int main(int argc, char **argv)
{
    printf("isBigEndian = %d\n", isBigEndian());
    printf("isLittleEndian = %d\n", getByteOrder() == LITTLE_ENDIAN);

    return 0;
}

以下程序用于测试输出 OS X/iOS 系统的字节序

#import <Foundation/Foundation.h>
#import <Foundation/NSByteOrder.h>

// 预编译警告信息将在build report log中输出
#if __DARWIN_BYTE_ORDER == __DARWIN_BIG_ENDIAN
#pragma message("__DARWIN_BIG_ENDIAN")
#elif __DARWIN_BYTE_ORDER == __DARWIN_LITTLE_ENDIAN
#pragma message("__DARWIN_LITTLE_ENDIAN")
#endif

#if defined(__BIG_ENDIAN__)
#pragma message("__BIG_ENDIAN__")
#elif defined(__LITTLE_ENDIAN__)
#pragma message("__LITTLE_ENDIAN__")
#endif

BOOL isBigEndian() {
    unsigned short v = 0x3412; // __ORDER_PDP_ENDIAN__
    return (*((unsigned char*)&v) == 0x34);
}

BOOL isLittleEndian()
{
    static CFByteOrder bo = CFByteOrderUnknown;
    
    if (bo == CFByteOrderUnknown) { // run only once
        union w
        {
            short a;    // 2 byte
            char b;     // 1 byte
        } c;
        c.a = 1;
        bo = (c.b?CFByteOrderLittleEndian:CFByteOrderBigEndian); // 高位存储低权字节,则为小端
    }
    
    return bo;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"isBigEndian = %d", isBigEndian()); // 0
        NSLog(@"isLittleEndian = %d", isLittleEndian()); // 1
        NSLog(@"NSHostByteOrder = %ld", NSHostByteOrder()); // NS_LittleEndian=CFByteOrderLittleEndian=1
    }
    return 0;
}

以下简单梳理以下 OS X 和 iOS SDK 提供的字节序处理接口。

(1)OS X和iOS SDK的usr/inclue/i386/endian.h、usr/inclue/arm/endian.h中定义了__DARWIN_BYTE_ORDER

#define __DARWIN_BYTE_ORDER __DARWIN_LITTLE_ENDIAN

(2)OS X和iOS SDK的<libkern/OSByteOrder.h>中的OSSwap*操作接口:

_OSSwapInt32 // __builtin_bswap32,<libkern/i386/_OSByteOrder.h>、<libkern/arm/OSByteOrder.h>
#define __DARWIN_OSSwapInt32(x) _OSSwapInt32(x) // <libkern/_OSByteOrder.h>
#define OSSwapInt32(x) __DARWIN_OSSwapInt32(x) // <libkern/OSByteOrder.h>
#define ntohl(x) __DARWIN_OSSwapInt32(x)
#define htonl(x) __DARWIN_OSSwapInt32(x)

<arpa/inet.h>中包含了<machine/endian.h>和<sys/_endian.h>。
(3)Frameworks/CoreFoundation/CFByteOrder.h还是CF_USE_OSBYTEORDER_H,封装了系列CFSwap*操作接口。

CF_INLINE uint32_t CFSwapInt32(uint32_t arg) {
#if CF_USE_OSBYTEORDER_H
return OSSwapInt32(arg);
#else
uint32_t result;
result = ((arg & 0xFF) << 24) | ((arg & 0xFF00) << 8) | ((arg >> 8) & 0xFF00) | ((arg >> 24) & 0xFF);
return result;
#endif
}

(4)OS X和iOS SDK的Frameworks/Foundation/NSByteOrder.h中定义了基于CFSwap*操作接口进一步封装了NSSwap*操作接口。

NS_INLINE unsigned int NSSwapInt(unsigned int inv) {
return CFSwapInt32(inv);
}

(5)OS X和iOS SDK的usr/inclue/sys/_endian.h中定义了ntohs/htonsntohl/htonl等宏:

结构体 struct 存储对齐

请看下面的结构体:

struct MyStruct1
{
    double d;
    char c;
    int i;
};

使用 sizeof 运算符计算结构体 MyStruct1 占用的字节数,是多少呢?

也许初学者会这样求:sizeof(MyStruct1)=sizeof(double)+sizeof(char)+sizeof(int)=8+1+4=13

给这个结构体定义加上属性限定 __attribute__((__packed__)),不考虑内存地址对齐问题,自然紧凑排列,其 size 确实是 13。

但测试结果是 sizeof(MyStruct1)=16,你知道为什么会得出这个结果吗?

成员变量地址偏移量对齐自身大小限制

其实,许多计算机系统对基本数据类型的合法地址做出了一些限制, 要求某种类型对象的地址必须是某个值 K( 通常是 2、4 或 8) 的倍数。这种对齐限制简化了形成处理器和内存系统之间接口的硬件设计 。 例如, 假设一个处理器总是从内存中取 8 个 字节,则地址必须为 8 的倍数 。 如果我们能保证将所有的 double 类型数据的地址对齐成 8 的倍数, 那么就可以用一个内存操作来读或者写值了。 否则, 我们可能需要执行两次内存访问, 因为对象可能被分放在两个 8 字节内存块中。

无论数据是否对齐, x86-64 硬件都能正确工作。 不过,Intel 还是建议要对齐数据以 提高内存系统的性能。 对齐原则是任何 K 字节的基本对象的地址必须是 K 的倍数。

即对结构体成员变量的起始地址做了“对齐”处理:在默认情况下,各成员变量存放的起始地址相对结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数

下面列出常用类型的对齐方式(vs6.0&vs8.0,32位系统)。

类型

对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)

char

偏移量必须为sizeof(char),即1的倍数

short

偏移量必须为sizeof(short),即2的倍数

int

偏移量必须为sizeof(int),即4的倍数

float

偏移量必须为sizeof(float),即4的倍数

double

偏移量必须为sizeof(double),即8的倍数

各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节会自动填充。同时为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。

下面以 struct MyStruct1 为例来说明结构体的存储布局。

  1. 先为第一个成员d分配空间,其起始地址跟结构的起始地址相同,偏移量0为sizeof(double)=8的倍数,该成员变量占用8个字节;
  2. 接下来为第二个成员c分配空间,相对结构起始地址的偏移量为8,是sizeof(char)=1的倍数,地址偏移量满足对齐方式,该成员变量占用1个字节;
  3. 继续为第三个成员i分配空间,相对结构起始地址的偏移量为9,不是sizeof(int)=4的倍数。为满足对齐限制,将自动填充3个字节,地址偏移量为12的地址开始存放 i,该成员变量占用4个字节;
  4. 至此,整个结构的成员变量都已经分配了空间,sizeof(MyStruct1) = 8+1+(3)+4=16。
  5. 可以调用 <stddef.h> 中定义的宏 offsetof 来读取各个成员的偏移量。

结构体整体大小及地址边界对齐

交换一下 MyStruct1 的成员变量d和c的位置,新的结构 MyStruct2 占用的空间为多大呢?

struct MyStruct2
{
    char c;
    double d;
    int i;
};

结合上面提到的地址对齐原则,分析下怎样为上面的结构分配空间。

  1. 先为第一个成员c分配空间,其起始地址跟结构的起始地址相同,偏移量0为sizeof(char)=1的倍数,该成员变量占用1个字节;
  2. 接下来为第二个成员d分配空间,相对结构起始地址的偏移量为1, 不是sizeof(double)=8的倍数。先填充 7 个字节,再从地址偏移量为 8 的地址处存放d,该成员变量占用8个字节;
  3. 接下来为第三个成员i分配空间,相对结构起始地址的偏移量为16,是sizeof(int)=4的倍数,满足对齐限制,所以i存放在偏移量为16的地址处,该成员变量占用4个字节;
  4. 至此,整个结构的成员变量都已经分配了空间,总共占用的空间大小为:1+(7)+8+4=20。

似乎已经讨论完毕,但是考虑一下定义一个结构体数组

struct MyStruct2 ms[4];

第一个结构体 ms[0] 的地址分配如上,假设数组(第一个结构体)的起始地址为 Sa,则第二、三、四个结构体的地址分别为 Sa+20, Sa+40, Sa+60,不可能满足 ms 每个元素内部的对齐限制。

实际上,为了使结构体数组中的每个元素采用相同的地址对齐规则且保证所占空间一致,编译器在为 MyStruct2 分配空间时,除了使每个成员变量满足偏移量地址对齐,还必须保证结构所占的总字节数是结构中最长类型所占字节数(这里是sizeof(double)=8)的倍数。这样,只要确保结构数组起始地址 Sa 是 8 的倍数,就能够保证每个元素都满足对齐限制。

具体来说,为第三个成员分配空间后,总共占空间 1+(7)+8+4=20,还必须再填充 4 个字节,使总大小是 sizeof(double)=8 的倍数。这样,结构体的总大小 sizeof(MyStruct2)=1+(7)+8+4+(4)=24。

然后,编译器为结构数组ms分配地址时,确保分配一个 8 的倍数的起始地址。这种特殊的存储处理可以提高CPU存取变量的速度,但有时候也带来了一些麻烦,我们也可以屏蔽掉变量默认的对齐方式,自行设定变量的对齐方式。

编译器默认对齐参数

MSVC/Zp (Struct Member Alignment) | Microsoft Learn:Controls how the members of a structure are packed into memory and specifies the same packing for all structures in a module.

  • Packs structures on 8-byte boundaries (default for x86, ARM, and ARM64).
  • Packs structures on 16-byte boundaries (default for x64 and ARM64EC).

gcc6.34.1 Common Variable Attributes:GCC also provides a target specific macro BIGGEST_ALIGNMENT, which is the largest alignment ever used for any data type on the target machine you are compiling for.

可以借助 clang/gcc -E 预编译,打印 __BIGGEST_ALIGNMENT__ 的大小:armv7l、arm64 下为 8;x86_64 和 aarch64(ARM64EC)下为 16。

# for old iPhone with 32bit chip
$ clang -dM -E -arch armv7 -x c /dev/null | grep '__BIGGEST_ALIGNMENT__'
#define __BIGGEST_ALIGNMENT__ 4

# macOS Apple Silicon arm64
$ clang -dM -E -arch arm64 -x c /dev/null | grep '__BIGGEST_ALIGNMENT__'
#define __BIGGEST_ALIGNMENT__ 8

# macOS intel x86_64
$ clang -dM -E -arch x86_64 -x c /dev/null | grep '__BIGGEST_ALIGNMENT__'
#define __BIGGEST_ALIGNMENT__ 16

# rpi3b-raspbian - armv7l
$ gcc -dM -E -x c /dev/null | grep '__BIGGEST_ALIGNMENT__'
#define __BIGGEST_ALIGNMENT__ 8

# rpi4b-ubuntu - aarch64
$ gcc -dM -E -x c /dev/null | grep '__BIGGEST_ALIGNMENT__'
#define __BIGGEST_ALIGNMENT__ 16

#pragma pack(show) 测试代码:

#pragma pack(show)

int main(int argc, char *argv[]) {
    return 0;
}

macOS(Intel x86\_64、Apple Silicon arm64)编译输出警告:

$ gcc showpack.c -o showpack && ./showpack
showpack.c:1:9: warning: value of #pragma pack(show) == 8
#pragma pack(show)
        ^
1 warning generated.

rpi4b-ubuntu/arm64 下不支持该预编译指令。

32、64 位下的基本数据类型占用最大空间是 8 字节(sizeof(long long)、sizeof(double)),这个也是自然对齐的最大参数。

参考 gcc - 5.30 Inquiring on Alignment of Types or Variables,可以调用类 sizeof 的运算符 __alignof__ 读取基本类型或结构体类型的对齐参数,例如:

  • __alignof__(int))=4
  • __alignof__(struct MyStruct2))=8

#pragma pack 修改字节对齐方式

MSVC和 gcc 等编译器中都支持通过预编译处理指令 #pragma pack(n) 来改变编译器的默认对齐方式。

#pragma pack(n) // 编译器将按照 n 字节对齐,或 #pragma pack(push, n)

// pragma pack 作用区间

#pragma pack() // 编译器将取消自定义字节对齐方式,或 #pragma pack(pop)

#pragma pack(n) 和 #pragma pack() 之间的代码按 n 字节对齐。

成员对齐有一个重要的条件,即每个成员按自己的方式对齐,也就是说虽然指定了按 n 字节对齐,但并不是所有的成员都是以 n 字节对齐。其对齐的规则是:每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是n字节)中较小的一个对齐,即 min(n, sizeof(item)),并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。

1.  如果 n >= sizeof(item),成员变量按默认的对齐方式,即按照其 size 对齐,结构对齐后的总大小必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐。
2.  如果 n < sizeof(item),成员变量的偏移量取 n 的倍数,不用满足默认的对齐方式,结构的总大小必须为n的倍数。

默认 n=8,满足第一种情况的自然对齐;当 n>8,不再有实际影响。

下面举例说明 #pragma pack(n) 对结构存储布局的影响。

#pragma pack(push) // 保存对齐状态
#pragma pack(4) // 设定为4字节对齐
struct MyStruct3
{
    char c;
    double d;
    int i;
};
#pragma pack(pop) // 恢复对齐状态
  1. 首先,为c分配空间,其偏移量为0,满足n=4字节对齐方式,c占1个字节。
  2. 其次,为d分配空间,这时其偏移量为1,需补足3个字节,对齐n=4(不必按sizeof(double)=8 对齐),d占8个字节。
  3. 最后,为i分配空间,这时其偏移量为12,满足为n=4的倍数,i占4个字节。
  4. 三个成员共分配了16个字节,满足为n=4的倍数,故 sizeof(MyStruct3) = 16。

如果把 #pragma pack(4) 改为 #pragma pack(16),n>8,那么结构的大小就是自然对齐下的24,相当于不受影响。

在MSVC中,Project Settings->C/C++->Struct member alignment中默认值为8Bytes *。Struct member alignment 用以指定数据结构中的成员变量在内存中是按几字节对齐的,根据计算机数据总线的位数,不同的对齐方式存取数据的速度不一样。这个参数对数据包网络传输等应用尤为重要,不是存取速度问题,而是数据位的精确定义问题,一般可在程序中使用 #pragma pack 预处理来指定。

gcc 的 Options for Code Generation Conventions 支持编译选项 -fpack-struct[=n] 指定对齐字节数。另外,也可以在代码中使用 Common Variable Attributes 提供的 aligned 属性(__attribute__)修饰变量。

需要注意的是,#pragma pack(16) 定义 MyStruct2 和为 MyStruct2 指定__attribute__((aligned(16))),后者必须满足整体大小为 n=16 的倍数,sizeof 测算出来的大小为 32。

以下是综合测试代码,可通过 gcc -E 查看宏展开。

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

// __attribute__ ((__packed__))
// struct's members are packed closely together,
// but the internal layout of its s member is not packed
#define DEFINE_STRUCT_PAIR(StructName, StructBody) \
	struct StructName StructBody \
	struct __attribute__((__packed__)) StructName##_packed StructBody
#define GET_PACKED_SIZE(StructName) sizeof(struct StructName##_packed)
#define GET_SIZE(StructName) sizeof(struct StructName)

// aligned (alignment): specifies a minimum alignment
#define DEFINE_STRUCT_ALIGNED(n, StructName, StructBody) \
	struct __attribute__((aligned(n))) StructName##_aligned StructBody
#define GET_ALIGNED_SIZE(StructName) sizeof(struct StructName##_aligned)

DEFINE_STRUCT_PAIR(MyStruct1, {
	double d;
	char c;
	int i;
};)

DEFINE_STRUCT_PAIR(MyStruct2, {
	char c;
	double d;
	int i;
};)

#pragma pack(push, 4)
struct MyStruct3
{
	char c;
	double d;
	int i;
};
#pragma pack(pop)

DEFINE_STRUCT_ALIGNED(4, MyStruct3, {
	char c;
	double d;
	int i;
};)

#pragma pack(push, 16)
struct MyStruct4
{
	char c;
	double d;
	int i;
};
#pragma pack(pop)

// on a 16-byte boundary.
DEFINE_STRUCT_ALIGNED(16, MyStruct4, {
	char c;
	double d;
	int i;
};)

int main(int argc, char *argv[])
{
	printf("sizeof pointer=%zu\n", sizeof(void *));

	printf("alignof(char)=%tu\n", __alignof__(char));
	printf("alignof(short)=%tu\n", __alignof__(short));
	printf("alignof(int)=%tu\n", __alignof__(int));
	printf("alignof(long)=%tu\n", __alignof__(long));
	printf("alignof(long long)=%tu\n", __alignof__(long long));
	printf("alignof(double)=%tu\n", __alignof__(double));

	printf("sizeof(MyStruct1)=%zu,%zu, alignof(MyStruct1)=%tu\n",
		   GET_PACKED_SIZE(MyStruct1), GET_SIZE(MyStruct1),
		   __alignof__(struct MyStruct1));
	printf("sizeof(MyStruct2)=%zu,%zu, alignof(MyStruct2)=%tu\n",
		   GET_PACKED_SIZE(MyStruct2), GET_SIZE(MyStruct2),
		   __alignof__(struct MyStruct2));

	struct MyStruct2 ms2;
	printf("MyStruct1 offsets: c=%tu, d=%tu, i=%tu\n",
		   offsetof(struct MyStruct2, c),
		   offsetof(struct MyStruct2, d),
		   (uintptr_t)&ms2.i - (uintptr_t)&ms2);

	printf("sizeof(MyStruct2): pack(4)=%zu, aligned(4)=%zu\n",
		   sizeof(struct MyStruct3),
		   GET_ALIGNED_SIZE(MyStruct3));
	printf("alignof(MyStruct2): pack(4)=%zu, aligned(4)=%zu\n",
		   __alignof__(struct MyStruct3),
		   __alignof__(struct MyStruct3_aligned));

	printf("sizeof(MyStruct2): pack(16)=%zu, aligned(16)=%zu\n",
		   sizeof(struct MyStruct4),
		   GET_ALIGNED_SIZE(MyStruct4));
	printf("alignof(MyStruct2): pack(16)=%zu, aligned(16)=%zu\n",
		   __alignof__(struct MyStruct4),
		   __alignof__(struct MyStruct4_aligned));

	return 0;
}

参考:

C/C++基本数据类型

Pointers》《Pointers and Memory》《Pointers in C》

字节那些事儿》《字节序》《ARM Endian

轻松记住大小端》《大端模式和小端模式

大端与小端详解》《详解大端模式和小端模式

The sizeof Operator

VC中的sizeof的用法总结

什么是内存对齐

Data structure alignment

C Structure Padding Initialization | Interrupt
How Struct Memory Alignment Works in C | by Mazin Mohamed | Level Up Coding
 

Alignment | Microsoft Learn

pack pragma | Microsoft Learn

5.30 Inquiring on Alignment of Types or Variables

5.31 Specifying Attributes of Variables

5.32 Specifying Attributes of Types

6.32.1 Common Variable Attributes

6.34.1 Common Variable Attributes

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值