C语言checksum如何实现?

99ANYc3cd6
预计阅读时长 23 分钟
位置: 首页 C语言 正文

校验和是一种简单但广泛使用的错误检测机制,它的核心思想是:将数据块(如一个文件、一串数据包)中的所有字节相加,得到一个总和,然后将这个总和(或其某种变换形式)作为校验码一起发送或存储,当接收方或读取方再次计算校验和时,如果计算结果与之前存储的校验码不匹配,就说明数据在传输或存储过程中可能发生了损坏。

c语言checksum
(图片来源网络,侵删)

下面我将从最简单的求和校验开始,逐步介绍更复杂的算法,并提供完整的 C 语言代码示例。


简单求和校验

这是最直观的校验和算法,将所有字节相加,通常用一个较大的数据类型(如 unsigned intuint32_t)来存储总和,以防止溢出。

算法步骤:

  1. 初始化一个变量 sum 为 0。
  2. 遍历数据块的每一个字节。
  3. 将每个字节的值加到 sum 上。
  4. 最终的 sum 就是校验和。

C 语言实现

#include <stdio.h>
#include <stdint.h> // 用于 uint32_t
/**
 * @brief 计算一个字节数组的简单求和校验和
 * @param data 指向数据数组的指针
 * @param length 数据数组的长度
 * @return uint32_t 类型的校验和
 */
uint32_t simple_checksum(const uint8_t *data, size_t length) {
    uint32_t sum = 0;
    for (size_t i = 0; i < length; i++) {
        sum += data[i];
    }
    return sum;
}
int main() {
    // 示例数据
    uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05};
    size_t data_length = sizeof(data) / sizeof(data[0]);
    // 计算校验和
    uint32_t checksum = simple_checksum(data, data_length);
    printf("原始数据: ");
    for (size_t i = 0; i < data_length; i++) {
        printf("%02X ", data[i]);
    }
    printf("\n");
    printf("简单求和校验和: %08X\n", checksum);
    // --- 模拟数据损坏 ---
    uint8_t corrupted_data[] = {0x01, 0x02, 0x03, 0x07, 0x05}; // 第4个字节从0x04变成了0x07
    uint32_t corrupted_checksum = simple_checksum(corrupted_data, data_length);
    printf("\n损坏后的数据: ");
    for (size_t i = 0; i < data_length; i++) {
        printf("%02X ", corrupted_data[i]);
    }
    printf("\n");
    printf("损坏数据的校验和: %08X\n", corrupted_checksum);
    // --- 验证 ---
    if (checksum == corrupted_checksum) {
        printf("校验通过: 数据未损坏,\n");
    } else {
        printf("校验失败: 数据已损坏!\n");
    }
    return 0;
}

优点与缺点:

  • 优点: 实现简单,计算速度快。
  • 缺点: 可靠性较低,如果数据中发生了两个或多个字节的变化,但它们的增减量相互抵消(一个字节 +1,另一个字节 -1),校验和将无法检测到这种错误。

Fletcher 校验和

为了解决简单求和校验的缺点,Fletcher 校验和引入了“滚动累加”的概念,对错误检测能力有显著提升,最常见的是 Fletcher-16 和 Fletcher-32。

算法步骤 (以 Fletcher-16 为例):

  1. 初始化两个 16 位累加器 sum1sum2 为 0。
  2. 遍历数据块的每一个字节 B
  3. 更新 sum1: sum1 = (sum1 + B) % 255
  4. 更新 sum2: sum2 = (sum2 + sum1) % 255
  5. 最终的校验码是 (sum2 << 8) | sum1

这个算法对字节顺序的变化非常敏感,因此比简单求和更可靠。

c语言checksum
(图片来源网络,侵删)

C 语言实现 (Fletcher-16)

#include <stdio.h>
#include <stdint.h>
/**
 * @brief 计算 Fletcher-16 校验和
 * @param data 数据指针
 * @param length 数据长度
 * @return uint16_t 类型的校验和
 */
uint16_t fletcher16(const uint8_t *data, size_t length) {
    uint16_t sum1 = 0;
    uint16_t sum2 = 0;
    for (size_t i = 0; i < length; i++) {
        sum1 = (sum1 + data[i]) % 255;
        sum2 = (sum2 + sum1) % 255;
    }
    return (sum2 << 8) | sum1;
}
int main() {
    uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05};
    size_t data_length = sizeof(data) / sizeof(data[0]);
    uint16_t checksum = fletcher16(data, data_length);
    printf("Fletcher-16 校验和: %04X\n", checksum);
    // --- 模拟数据损坏 ---
    uint8_t corrupted_data[] = {0x01, 0x02, 0x03, 0x07, 0x05};
    uint16_t corrupted_checksum = fletcher16(corrupted_data, data_length);
    printf("损坏数据的 Fletcher-16 校验和: %04X\n", corrupted_checksum);
    if (checksum == corrupted_checksum) {
        printf("校验通过: 数据未损坏,\n");
    } else {
        printf("校验失败: 数据已损坏!\n");
    }
    return 0;
}

循环冗余校验

CRC 是目前最强大、应用最广泛的校验算法之一,它不是基于简单的算术求和,而是基于多项式除法在有限域(模2运算)上的计算,CRC 能以极高的概率检测出多位错误、突发错误(连续的字节损坏)以及某些模式化的错误。

算法思想:

将数据块看作一个巨大的二进制数,然后用一个固定的“生成多项式”去除这个大数,得到的余数就是 CRC 校验码,这个除法过程是通过一系列的位异或和移位来实现的,效率很高。

C 语言实现 (CRC-8)

实现 CRC 通常使用查表法来优化性能,因为直接计算循环效率较低,这里我们展示一个通用的计算函数,并附上一个简单的查表示例。

#include <stdio.h>
#include <stdint.h>
// CRC-8 多项式 (Dallas-Maxim 的 CRC8)
#define CRC8_POLYNOMIAL 0x07
// 初始值
#define CRC8_INIT 0x00
/**
 * @brief 计算 CRC-8 校验和 (不使用查表法,用于演示原理)
 * @param data 数据指针
 * @param length 数据长度
 * @return uint8_t 类型的校验和
 */
uint8_t crc8(const uint8_t *data, size_t length) {
    uint8_t crc = CRC8_INIT;
    for (size_t i = 0; i < length; i++) {
        crc ^= data[i]; // 将数据和 CRC 值异或
        for (int j = 0; j < 8; j++) {
            if (crc & 0x80) { // 如果最高位是1
                crc = (crc << 1) ^ CRC8_POLYNOMIAL;
            } else {
                crc <<= 1;
            }
        }
    }
    return crc;
}
// --- 使用查表法优化 CRC-8 计算 ---
// 预先生成的 CRC-8 查表
static const uint8_t crc8_table[256] = {
    0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
    // ... (完整的表格有256项,这里省略,实际使用时需要完整填充)
};
uint8_t crc8_table_lookup(const uint8_t *data, size_t length) {
    uint8_t crc = CRC8_INIT;
    for (size_t i = 0; i < length; i++) {
        crc = crc8_table[(crc ^ data[i]) & 0xFF]; // 只取低8位查表
    }
    return crc;
}
int main() {
    uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05};
    size_t data_length = sizeof(data) / sizeof(data[0]);
    uint8_t checksum = crc8(data, data_length);
    printf("CRC-8 校验和: %02X\n", checksum);
    // --- 模拟数据损坏 ---
    uint8_t corrupted_data[] = {0x01, 0x02, 0x03, 0x07, 0x05};
    uint8_t corrupted_checksum = crc8(corrupted_data, data_length);
    printf("损坏数据的 CRC-8 校验和: %02X\n", corrupted_checksum);
    if (checksum == corrupted_checksum) {
        printf("校验通过: 数据未损坏,\n");
    } else {
        printf("校验失败: 数据已损坏!\n");
    }
    return 0;
}

注意: 实际应用 CRC 时,必须明确所使用的多项式、初始值、输入数据是否反转、结果是否反转以及最终结果是否异或一个特定值,这些参数组合定义了不同的 CRC 标准(如 CRC-8, CRC-16-CCITT, CRC32),上面的 crc8 函数是一个简化示例。

c语言checksum
(图片来源网络,侵删)

总结与如何选择

算法 原理 优点 缺点 适用场景
简单求和 算术累加 实现最简单,速度最快 可靠性差,易漏检多位错误 对错误要求不高的场景,如快速内部一致性检查。
Fletcher 滚动累加 可靠性高于简单求和,对字节顺序敏感 计算比简单求和稍复杂,可靠性仍不及 CRC 网络协议(如 Zmodem)、文件系统,是速度和可靠性的一个折中。
CRC 多项式除法(模2运算) 可靠性极高,能有效检测突发错误和多位错误 实现相对复杂,需要理解多项式等概念 行业标准,网络(以太网、PPP)、存储设备(硬盘、光盘)、文件格式(PNG, ZIP, JPEG)、通信协议。

如何选择?

  • 如果只是需要一个非常快速的、非关键性的检查:使用简单求和
  • 如果需要比简单求和更好的可靠性,但又不想实现复杂的 CRC:使用Fletcher 校验和
  • 如果数据完整性至关重要,尤其是在网络传输或存储介质中必须使用 CRC,根据具体需求选择标准 CRC 类型(如 CRC-16, CRC-32)。

对于绝大多数需要高可靠性的应用,直接使用成熟的库(如 zlib 提供的 CRC32)是最佳选择,而不是自己从头实现。

-- 展开阅读全文 --
头像
织梦缩略图为何添加不上?
« 上一篇 04-16
C语言65分,是基础薄弱还是方法不对?
下一篇 » 04-16

相关文章

取消
微信二维码
支付宝二维码

目录[+]