C语言结构体写入文件“错误”,根据结构体成员名计算偏移的方法

结构体在各种编程语言中,都是建立自定义数据体的一种非常好的途径。但是有时忽略结构体成员自动对齐,带来的结果会让人迷惑。此外,获取结构体成员在结构体中的偏移,方法很多。最近常用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

这个宏是通过成员名获取的偏移,所以,即使结构体新增成员,宏获取的偏移不会受影响,代码的其他位置无需修改,很方便,而且不容易出错。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif