6

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

 3 years ago
source link: https://blog.popkx.com/c-programming-new-method-to-get-member-offset-of-struct-convinient-to-handle-binary-data/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK