结构体
在各种编程语言中,都是建立自定义数据体的一种非常好的途径。但是有时忽略结构体成员自动对齐,带来的结果会让人迷惑。此外,获取结构体成员在结构体中的偏移,方法很多。最近常用C语言,今天发现了一种非常不错的获取结构体成员偏移的方法,仅仅根据结构体成员名即可计算出偏移。
结构体成员自动对齐,引起写到文件“错误”
这里的“错误”加了引号,说明并不是真正的错误,而是看着“好像错了”,执行下面代码:
// 文件名 t.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct __TEST
{
char mchar;
int mint;
short mshort;
}TEST;
int main()
{
TEST test;
test.mchar = 1;
test.mshort = 2;
test.mint = 4;
int fd = open("data.bin", O_RDWR|O_CREAT);
write(fd, &test, sizeof(TEST));
close(fd);
return 0;
}
我们编译,并且执行:
$ gcc t.c -o t
$ ./t
按理说,data.bin
文件里的数据应该是
1 2 0 0 0 4 0
但是,我们查看 data.bin
,发现数据居然是:
$ od -tx1 -Ax data.bin
000000 01 06 40 00 04 00 00 00 02 00 40 00
00000c
长度跟想象的不一样,而且 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字节,但是代码却告诉我们这是不对的。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct __TEST
{
char mchar;
int mint;
short mshort;
}TEST;
int main()
{
TEST test;
int res = 0;
int fd = open("data.bin", O_RDWR|O_CREAT);
// mint 的偏移似乎在 1 字节(char占1字节)
lseek(fd, 1, SEEK_SET);
read(fd, &res, 1);
close(fd);
printf("res: %d\n", res);
return 0;
}
编译执行,得到的好像是野值:
$ gcc t.c -o t
$ ./t
res: 6
这还是因为结构体成员自动对齐的缘故,按照上面分析,偏移应该是 4,所以应该是
lseek(fd, 4, SEEK_SET);
再编译执行,发现输出正确了。
$ gcc t.c -o t
$ ./t
res: 4
但是计算对齐后的偏移,很容易出错。当然有解决办法,其实只要在声明结构体时,加入关键字 __attribute__((packed))
,结构体的成员就不会再自动对齐了,数据变得紧凑。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct __TEST
{
char mchar;
int mint;
short mshort;
}__attribute__((packed)) TEST;
int main()
{
TEST test;
int res = 0;
test.mchar = 1;
test.mshort = 2;
test.mint = 4;
int fd = open("data.bin", O_RDWR|O_CREAT|O_TRUNC);
write(fd, &test, sizeof(TEST));
// mint 的偏移 1
lseek(fd, 1, SEEK_SET);
read(fd, &res, 1);
close(fd);
printf("res: %d\n", res);
return 0;
}
编译执行,发现偏移是 1,正确读出结果了,写入的数据也变得紧凑了。
$ gcc t.c -o t
$ ./t
res: 4
$ od -tx1 -Ax data.bin
000000 01 04 00 00 00 02 00
000007
计算结构体成员偏移的方法
上面取消结构体成员自动对齐后,虽然成员的偏移变得更加容易看出,但是仍然容易出错。而且如果后期添加结构体成员,偏移可能会变,例如,当在 TEST 结构体添加成员:
typedef struct __TEST
{
char mchar;
char mchar2;
int mint;
short mshort;
}__attribute__((packed)) TEST;
此时,mint
成员的偏移变为 2。代码里所有的偏移都得改成2,这很麻烦,也比较容易出错。
那么,有方便的获取成员偏移的方法吗?肯定是有的,C语言可以直接取变量地址,利用这点,就可以计算出结构体的成员偏移。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct __TEST
{
char mchar;
int mint;
short mshort;
}__attribute__((packed)) TEST;
int main()
{
TEST test;
size_t baseAddr = 0, mintAddr = 0;
baseAddr = (size_t)(&test);
mintAddr = (size_t)(&(test.mint));
printf("baseAddr: %ld, mintAddr: %ld\n", baseAddr, mintAddr);
printf("mint offset: %ld\n", mintAddr - baseAddr);
return 0;
}
编译执行,得到:
$ gcc t.c -o t
$ ./t
baseAddr: 140723789483952, mintAddr: 140723789483953
mint offset: 1
发现,我们仅仅根据成员名
就得到了成员在结构体中的偏移量。不过上面的代码有些臃肿,仅为了得到成员的偏移,似乎太兴师动众了。
其实,结构体本身的地址,我们并不关心,因为最后总是要减掉的,那么,如果test
的地址为 0,就可以免去减掉
操作。基于这样的思想,下面是精简版的获取成员偏移的方法:
#include <stdio.h>
typedef struct __TEST
{
char mchar;
int mint;
short mshort;
}__attribute__((packed)) TEST;
int main()
{
size_t offset = (size_t)( &( ((TEST*)0)->mint) );
printf("mint offset: %ld\n", offset);
return 0;
}
编译执行,发现一样成功获得了mint
的偏移。
$ ./t
mint offset: 1
如此以来,获取结构体成员的偏移,完全可以定义成一个宏:
#define OFFSET(type, member) ( (size_t)( &( ((type*)0)->member) ) )
我们将其应用到上面的例子中,试试效果。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define OFFSET(type, member) ( (size_t)( &( ((type*)0)->member) ) )
typedef struct __TEST
{
char mchar;
int mint;
short mshort;
}__attribute__((packed)) TEST;
int main()
{
TEST test;
int res = 0;
test.mchar = 1;
test.mshort = 2;
test.mint = 4;
int fd = open("data.bin", O_RDWR|O_CREAT|O_TRUNC);
write(fd, &test, sizeof(TEST));
// 使用 宏 OFFSET 获得偏移
lseek(fd, OFFSET(TEST, mint), SEEK_SET);
read(fd, &res, 1);
close(fd);
printf("res: %d\n", res);
return 0;
}
编译执行,发现成功了:
$ gcc t.c -o t
$ ./t
res: 4
这个宏是通过成员名获取的偏移
,所以,即使结构体新增成员,宏获取的偏移不会受影响,代码的其他位置无需修改,很方便,而且不容易出错。