PAYATHON 2026

C 中如何将每行十六进制字符串解析为 16 字节数组?

数据阿明

结论

将每两个十六进制字符分别转换为 0~15 的数值,再通过 (high << 4) | low 合并成一个字节。建议使用 unsigned char key[16] 保存结果。

a 看起来从 9 变成了 ^@,是因为代码中有两个同名变量:分支内声明的 a 遮蔽了外层的 a。外层变量始终未初始化,读取它会导致未定义行为,因此不能从调试输出推断它的值。

为什么原代码会出错?

分支里的声明创建了另一个变量

外层声明了:

unsigned char a;

而 else 分支中写的是:

char a = c_buffer;

这条声明创建了一个新变量,作用域仅限该分支。调试时打印的字符 9 来自内层变量;下一次循环进入另一个分支时,访问的是未初始化的外层变量。

要更新外层变量,应使用赋值:

a = c_buffer;

但修复变量遮蔽还不能完成解析。

字符编码不能直接当作十六进制数值

读取到的 '9' 和 '5' 是字符,不是数值。以 ASCII 为例,它们的编码分别是 0x39 和 0x35,而不是 9 和 5。

所以下面的表达式不能直接得到预期字节:

(a << 4) | c_buffer

需要先转换字符。例如,将 '9' 和 '5' 转换为数值后,才能组合得到 0x95。

fgetc 的返回值必须先保存在 int 中

fgetc 返回 int。读取成功时,它返回按 unsigned char 解释后转换为 int 的字符值;读取结束或失败时返回 EOF。

原代码先将结果存入 unsigned char,可能丢失 EOF 信息。应先用 int 保存返回值:

int ch;
while ((ch = fgetc(fptr)) != EOF) {
    /* 处理 ch */
}

换行、数组边界和输出方式也需要修正

原代码遇到换行后仍会执行配对逻辑,且 len 没有按行重置,因此换行符会干扰后续字符的配对。写入数组前也没有检查边界,展示的代码片段中还缺少 keyarr 的声明。

字节数组不能直接使用 %s 输出。%s 要求以空字符结尾的字符串,而密钥可能包含 0x00,也不保证末尾有字符串终止符,使用 %s 可能导致输出截断或越界读取。调试字节数据时,应逐字节使用 %02x 输出。

按行读取并转换的完整示例

下面假设输入是普通文本,每行恰好包含 32 个十六进制字符,不含空格、前缀 0x 或内嵌空字符。示例支持大小写十六进制字母、LF 和 CRLF 换行,也支持最后一行没有换行符。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int hex_value(char c)
{
    if (c >= '0' && c <= '9') {
        return c - '0';
    }

    switch (c) {
    case 'a': case 'A': return 10;
    case 'b': case 'B': return 11;
    case 'c': case 'C': return 12;
    case 'd': case 'D': return 13;
    case 'e': case 'E': return 14;
    case 'f': case 'F': return 15;
    default: return -1;
    }
}

static int parse_key(const char *text, unsigned char key[16])
{
    if (strlen(text) != 32) {
        return 0;
    }

    for (size_t i = 0; i < 16; ++i) {
        int high = hex_value(text[2 * i]);
        int low = hex_value(text[2 * i + 1]);

        if (high < 0 || low < 0) {
            return 0;
        }

        key[i] = (unsigned char)((high << 4) | low);
    }

    return 1;
}

int main(void)
{
    FILE *fptr = fopen("../keys.txt", "r");
    if (fptr == NULL) {
        perror("fopen");
        return EXIT_FAILURE;
    }

    /* 32 个十六进制字符 + CRLF + 字符串终止符。 */
    char line[35];
    unsigned char key[16];
    size_t line_number = 0;
    int status = EXIT_SUCCESS;

    while (fgets(line, sizeof line, fptr) != NULL) {
        ++line_number;
        size_t length = strlen(line);

        if (length > 0 && line[length - 1] == '\n') {
            line[--length] = '\0';
        }
        if (length > 0 && line[length - 1] == '\r') {
            line[--length] = '\0';
        }

        if (!parse_key(line, key)) {
            fprintf(stderr,
                    "Invalid key at line %zu: expected 32 hex characters\n",
                    line_number);
            status = EXIT_FAILURE;
            break;
        }

        printf("Key %zu:", line_number);
        for (size_t i = 0; i < sizeof key; ++i) {
            printf(" %02x", (unsigned int)key[i]);
        }
        putchar('\n');

        /* 在这里使用当前 key,下一行会覆盖该数组。 */
    }

    if (ferror(fptr)) {
        fprintf(stderr, "Error reading keys.txt\n");
        status = EXIT_FAILURE;
    }

    if (fclose(fptr) == EOF) {
        fprintf(stderr, "Error closing keys.txt\n");
        status = EXIT_FAILURE;
    }

    return status;
}

第一行的转换从下面两组字符开始:

'6', '4' -> (6 << 4) | 4   -> 0x64
'6', 'e' -> (6 << 4) | 14  -> 0x6e

数组保存的是数值字节。0x64 是该数值在源代码中的十六进制写法,运行时不需要额外保存 0x 或逗号。

使用时的注意事项

  • unsigned char 能直接表达这里所需的 0~255 数值,避免普通 char 的有符号性差异影响处理。
  • 遇到空行、长度不符或包含非法字符的行时,示例会报错并停止,不会自动忽略空格或空行。
  • 如果 parse_key 返回失败,数组可能已经被部分写入,调用方应丢弃本次结果。
  • 示例每次只保留一行密钥。需要保存全部密钥时,应在解析成功后将其复制到另一个有容量管理的数组或容器中。
  • 传给其他函数时,应同时传递长度 16,不要用 strlen 计算密钥长度。

备注:内容仅供参考。