C语言陷阱与技巧第12节,重要数据怎么保存?如何判断数据是否损坏?

C语言中的结构体是非常有用的复合数据类型,正是有了结构体,C语言在描述复杂问题时才能够得心应手。事实上,当初 Dennis Ritchie 开发C语言用于替换 B 语言,其中一个主要原因就是 B 语言不支持“结构体”式数据结构。

C语言中的结构体非常有用

例如,利用C语言描述人的身高、体重、年龄、性别、姓名时,使用结构体时非常方便的,相关C语言代码可以如下定义:

struct person{
    float       height;
    float       weight;
    int         age;
    char        gender;
    char        name[128];
};

上面的C语言代码定义了 person 结构体,用于描述要求统计的每个人信息。一般来说,统计信息常常需要记录在磁盘里,如果这些信息比较重要,往往还需要记录不止一份。这样在数据损坏时,可以从备份将损坏数据修复。

如何判断数据是否损坏

但是,C语言程序怎么能知道存在磁盘里的数据有没有损坏呢?这其实就需要借助于校验了,一个非常常用的校验方法是 crc32 校验。crc32 校验可以根据一段长度(若干字节)的数据生成一个 32bit 的数,理想情况下,数据不同,生成的校验值也不同。

所以上面的 person 结构体最好加上一个成员 crc32,相关C语言代码如下,请看:

struct person{
    float       height;
    float       weight;
    int         age;
    char        gender;
    char        name[128];
    int     crc32;
};
person 结构体假设 int 类型占 4 字节内存空间。

这样在记录数据的时候,先计算出这段数据的 crc32 校验值,然后将数据和 crc32 校验值一起存储。以后读取数据时,可以再计算一次 crc32 校验值,并与原先记录的旧 crc32 校验值比较,若相等,则可以认为数据没有损坏;若不相等,就说明数据损坏,可以启动数据修复逻辑了。

上面这种判断数据是否损坏的方法,其实是有可能误判(现实与理想总是有差距)的,但是几率比较小,因此 crc32 仍然是一个不错的数据校验方法。

怎样计算结构体的校验值

计算 crc32 的方法不是本节的重点,而且网络上资源很多。这里直接假设获取一段数据的 crc32 校验值的函数的原型如下,请看C语言代码:

int get_crc32(char *buf, int size);

此时,计算 person 的校验值的C语言代码似乎可以这么写:

char buf[256];
int size = 0;
memcpy(buf, s.height);
size += sizeof(s.height);
memcpy(buf+size, s.weight);
...
size += sizeof(gender);
memcpy(buf+size, s.name);
size += sizeof(name);
s.crc32 = get_crc32(buf, size);
想想看,为什么不能直接这么计算 crc32 校验值呢:s.crc32 = get_crc32(&s, sizeof(struct test s));

显然, 这么计算太麻烦了,若是结构体的成员非常多,估计要把C语言程序员累死。而且,要是以后为结构体添加新成员,或者删除旧成员,这段计算 crc32 校验值的C语言代码也需修改,可见,这样计算 crc32 校验值的代码维护起来也是非常的麻烦,还容易出错。

因此,计算结构体的校验值的代码一般都不像上面那样写,那该怎么写呢?如果能够直接获取 crc32 成员在结构体 test 中的偏移量 offset,那计算校验值的C语言代码就很好写了:

s.crc32 = get_crc32(&s, offset);

那么,offset 等于多少呢?很多C语言初学者会认为:

offset = sizeof(s.height)+sizeof(s.height)+...+sizeof(name);

姑且不管这样计算 crc32 校验值一样会带来代码维护困难、容易出错又麻烦的问题。这样计算的 offset 都不等于 crc32 成员在结构体 test 中的偏移量,因此这样计算校验值是不合适的。

还记得结构体的“内存对齐”相关的陷阱吗?(可以参考我的专栏《C语言经典面试题详解》)

计算结构体某成员偏移量的小技巧

我们都知道,C语言中结构体的各个成员在内存中其实也是先后存储的,结构体 s 的成员 crc32 肯定是排在 s 之后的,因此计算结构体中某个成员的偏移量,其实可以采用“地址相减法”:

offset = &s.crc32 - &s;
s.crc32 = get_crc32(&s, offset);

知道原理了,我们完全可以自己定义一个宏,用于计算结构体某成员在结构体中的偏移量,相关C语言代码如下,请看:

#define     offset(type, v)     (size_t)(&(((type*)0)->v))

既然结构体成员地址减去结构体地址就等于该成员的偏移量,那如果结构体地址为 0,该成员的地址就恰好等于它在结构体中的偏移量了,现在我们编写测试用例,相关C语言代码如下,请看:

#include <stdio.h>

struct test{
    char    a;
    double  b;
    int     c;
};
#define        offset(type, v)     (size_t)(&(((type*)0)->v))
int main()
{
    struct test s;
    printf("%ld %ld %ld\n", offset(struct test, a), 
                            offset(struct test, b), 
                            offset(struct test, c));
    return 0;
}


编译并执行这段C语言代码,得到如下结果:

# gcc t.c
# ./a.out 
0 8 16

一切与预期一致。现在利用 offset 宏计算结构体 person 的校验值就方便了,请看下面的C语言代码:

s.crc32 = get_crc32(&s, offset(struct test, crc32));

而且,无论以后如何调整 person 的成员,删除也好,新增也好,只要保证 crc32 是它的最后一个成员,计算校验值的代码就无需改动,这样的C语言代码维护起来也是非常的省心的。

小结

在C语言程序开发中,若需记录在磁盘中的数据非常重要,则应该保存不止一份,这样才能在尽可能的确保数据不损坏。关于如何判断数据是否损坏,本节介绍了一种常用的 crc32 校验法,在此基础上,讨论了一种计算结构体成员偏移量的方法,并将其封装成宏,特别有利于之后C语言代码的维护。

阅读更多:   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