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

下面我将从最简单的求和校验开始,逐步介绍更复杂的算法,并提供完整的 C 语言代码示例。
简单求和校验
这是最直观的校验和算法,将所有字节相加,通常用一个较大的数据类型(如 unsigned int 或 uint32_t)来存储总和,以防止溢出。
算法步骤:
- 初始化一个变量
sum为 0。 - 遍历数据块的每一个字节。
- 将每个字节的值加到
sum上。 - 最终的
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 为例):
- 初始化两个 16 位累加器
sum1和sum2为 0。 - 遍历数据块的每一个字节
B。 - 更新
sum1:sum1 = (sum1 + B) % 255 - 更新
sum2:sum2 = (sum2 + sum1) % 255 - 最终的校验码是
(sum2 << 8) | sum1。
这个算法对字节顺序的变化非常敏感,因此比简单求和更可靠。

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函数是一个简化示例。(图片来源网络,侵删)
总结与如何选择
| 算法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 简单求和 | 算术累加 | 实现最简单,速度最快 | 可靠性差,易漏检多位错误 | 对错误要求不高的场景,如快速内部一致性检查。 |
| Fletcher | 滚动累加 | 可靠性高于简单求和,对字节顺序敏感 | 计算比简单求和稍复杂,可靠性仍不及 CRC | 网络协议(如 Zmodem)、文件系统,是速度和可靠性的一个折中。 |
| CRC | 多项式除法(模2运算) | 可靠性极高,能有效检测突发错误和多位错误 | 实现相对复杂,需要理解多项式等概念 | 行业标准,网络(以太网、PPP)、存储设备(硬盘、光盘)、文件格式(PNG, ZIP, JPEG)、通信协议。 |
如何选择?
- 如果只是需要一个非常快速的、非关键性的检查:使用简单求和。
- 如果需要比简单求和更好的可靠性,但又不想实现复杂的 CRC:使用Fletcher 校验和。
- 如果数据完整性至关重要,尤其是在网络传输或存储介质中:必须使用 CRC,根据具体需求选择标准 CRC 类型(如 CRC-16, CRC-32)。
对于绝大多数需要高可靠性的应用,直接使用成熟的库(如 zlib 提供的 CRC32)是最佳选择,而不是自己从头实现。

