原帖:向日葵智能
结构体
在各种编程语言中,都是建立自定义数据体的一种非常好的途径。但是有时忽略结构体成员自动对齐,带来的结果会让人迷惑。此外,获取结构体成员在结构体中的偏移,方法很多。最近常用C语言,今天发现了一种非常不错的获取结构体成员偏移的方法,仅仅根据结构体成员名即可计算出偏移。
结构体成员自动对齐,引起写到文件“错误”
这里的“错误”加了引号,说明并不是真正的错误,而是看着“好像错了”,执行下面代码:
我们编译,并且执行:
按理说,data.bin
文件里的数据应该是
1 2 0 0 0 4 0
但是,我们查看 data.bin
,发现数据居然是:
长度跟想象的不一样,而且 06 40
哪里来的啊?
这个就是结构体成员自动对齐
的原因了。这里只说上面的例子中的对齐:
- char short 和 int 分别占 1 2 4 字节内存
- TEST 结构体第一个成员先占了 1 字节内存
- 第二个成员 4 字节,必须 4 字节对齐,于是在第一个成员后面补了 3 位
- 第三个成员 2 字节,由于已经 4 字节对齐,所以在后面补了 2 位
- 虽然第一个成员补了 3 位,但是有效的只有原来的 1 位,第三个成员也如此。所以
06 40
都是残留在内存中的野值。
这么一解释,输出结果似乎又正确了哈。
取消结构体成员自动对齐,使数据更加紧凑
虽然结构体的成员自动对齐,可以提升访问速度,但是也会带来一些不方便。例如,从上面的 data.bin
文件中读出 TEST
结构体的 mint
成员,可以先读出整个 TEST
结构体,然后通过 TEST.mint 得到数据。这当然可行,但是,如果结构体非常复杂,全部读出来,只获取一个 int 型的成员数据,有些得不偿失。所以咱们尝试加上成员偏移,直接读出 mint
成员。现在问题的关键就是得到 mint
成员在结构体里的偏移了,咋一看,似乎是 1
,因为 char
占了1字节,但是代码却告诉我们这是不对的。
编译执行,得到的好像是野值:
这还是因为结构体成员自动对齐的缘故,按照上面分析,偏移应该是 4,所以应该是
lseek(fd, 4, SEEK_SET);
再编译执行,发现输出正确了。
但是计算对齐后的偏移,很容易出错。当然有解决办法,其实只要在声明结构体时,加入关键字 __attribute__((packed))
,结构体的成员就不会再自动对齐了,数据变得紧凑。
编译执行,发现偏移是 1,正确读出结果了,写入的数据也变得紧凑了。
计算结构体成员偏移的方法
上面取消结构体成员自动对齐后,虽然成员的偏移变得更加容易看出,但是仍然容易出错。而且如果后期添加结构体成员,偏移可能会变,例如,当在 TEST 结构体添加成员:
此时,mint
成员的偏移变为 2。代码里所有的偏移都得改成2,这很麻烦,也比较容易出错。
那么,有方便的获取成员偏移的方法吗?肯定是有的,C语言可以直接取变量地址,利用这点,就可以计算出结构体的成员偏移。
编译执行,得到:
发现,我们仅仅根据成员名
就得到了成员在结构体中的偏移量。不过上面的代码有些臃肿,仅为了得到成员的偏移,似乎太兴师动众了。
其实,结构体本身的地址,我们并不关心,因为最后总是要减掉的,那么,如果test
的地址为 0,就可以免去减掉
操作。基于这样的思想,下面是精简版的获取成员偏移的方法:
编译执行,发现一样成功获得了mint
的偏移。
$ ./t
mint offset: 1
如此以来,获取结构体成员的偏移,完全可以定义成一个宏:
#define OFFSET(type, member) ( (size_t)( &( ((type*)0)->member) ) )
我们将其应用到上面的例子中,试试效果。
编译执行,发现成功了:
这个宏是通过成员名获取的偏移
,所以,即使结构体新增成员,宏获取的偏移不会受影响,代码的其他位置无需修改,很方便,而且不容易出错。