10

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

 3 years ago
source link: https://blog.popkx.com/c-language-traps-and-techniques-section-12-how-to-save-important-data-how-to-judge-whether-the-data-is-damaged/
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.

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;
}
5163dae9e1d001be620eeaaca6d1d9d3.png

编译并执行这段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语言代码的维护。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK