利用C语言编写一个十六进制日志转换工具
作者:小范好好学习
当然可以!下面是对您提供的 C 程序 逐行添加详细注释,并附上整体功能说明,帮助你彻底理解每一部分的作用。
整体功能总结
该程序用于 处理带时间戳的十六进制日志文件(如串口/调试日志),自动跳过时间戳部分(如 15:00:30.625 - ),仅提取其后的合法十六进制字节(如 AA 55 36...),并将其转换为 C 语言风格的十六进制格式(如 0xAA 0x55 0x36...),输出到一个 .dat 文本文件中。
- 不生成二进制文件,输出仍是人类可读的文本
- 严格丢弃时间戳,避免误解析(如
15被当作0x15)
带详细注释的完整代码
// 引入标准库:输入输出、内存操作、字符串处理、字符类型判断
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
// 函数:将单个十六进制字符('0'-'9', 'A'-'F', 'a'-'f')转换为对应的数值(0-15)
// 例如:'A' → 10, 'f' → 15, '3' → 3
// 若输入非法字符(如 'G', 'Z', '@'),返回 -1
int hexCharToValue(char c) {
if (c >= '0' && c <= '9') return c - '0'; // 数字字符转数值
if (c >= 'a' && c <= 'f') return c - 'a' + 10; // 小写 a-f 转 10-15
if (c >= 'A' && c <= 'F') return c - 'A' + 10; // 大写 A-F 转 10-15
return -1; // 非法字符
}
// 主函数
int main() {
// 定义文件指针,用于读取输入和写入输出
FILE *inputFile, *outputFile;
// 存储用户输入的文件名(不含扩展名)
char inputFileName[256], outputFileName[256];
// 存储完整的带扩展名的文件路径(预留一点额外空间防溢出)
char fullInputFileName[260], fullOutputFileName[260];
// 用于存储从输入文件读取的每一行内容
char line[4096]; // 支持非常长的日志行
// 提示用户输入输入文件名(不带 .txt)
printf("请输入输入文件名(不含.txt): ");
scanf("%s", inputFileName); // 注意:不能包含空格
// 提示用户输入输出文件名(不带 .dat)
printf("请输入输出文件名(不含.dat): ");
scanf("%s", outputFileName);
// 构造完整的输入文件名:拼接 ".txt"
strcpy(fullInputFileName, inputFileName); // 先复制基础名
strcat(fullInputFileName, ".txt"); // 再追加扩展名
// 构造完整的输出文件名:拼接 ".dat"
strcpy(fullOutputFileName, outputFileName);
strcat(fullOutputFileName, ".dat");
// 以文本模式打开输入文件(只读)
inputFile = fopen(fullInputFileName, "r");
if (!inputFile) { // 如果打开失败(文件不存在/权限问题等)
perror("无法打开输入文件"); // 打印系统错误信息(如 "No such file")
return 1; // 退出程序,返回错误码
}
// 以文本模式创建/覆盖输出文件(只写)
outputFile = fopen(fullOutputFileName, "w");
if (!outputFile) {
perror("无法创建输出文件");
fclose(inputFile); // 关闭已打开的输入文件,避免资源泄漏
return 1;
}
// 行号计数器(可用于调试或日志)
int lineNumber = 0;
// 循环读取输入文件的每一行,直到文件结束
while (fgets(line, sizeof(line), inputFile)) {
lineNumber++;
// 在当前行中查找分隔符 " - "
// strstr 返回首次出现 " - " 的位置指针,若未找到则返回 NULL
char *pos = strstr(line, " - ");
if (!pos) {
// 如果当前行没有 " - ",说明不是标准日志格式,跳过该行
continue;
}
// 计算十六进制数据开始的位置:跳过 " - "(共3个字符:空格、减号、空格)
char *hexStart = pos + 3;
// 使用 strtok 按空白字符(空格、制表符、换行等)分割 hexStart 中的内容
// 第一次调用传入字符串,后续传入 NULL
char *token = strtok(hexStart, " \t\r\n");
int first = 1; // 标记是否是当前行的第一个有效十六进制值(用于控制空格)
// 循环处理每一个分割出的 token(即可能的十六进制字节字符串)
while (token) {
// 只有当 token 长度恰好为2,且两个字符都是合法十六进制字符时才处理
if (strlen(token) == 2 &&
hexCharToValue(token[0]) != -1 &&
hexCharToValue(token[1]) != -1) {
// 将两个十六进制字符组合成一个字节:
// 高4位 = 第一个字符的值 << 4,低4位 = 第二个字符的值
int byteVal = (hexCharToValue(token[0]) << 4) | hexCharToValue(token[1]);
// 如果不是第一个值,先输出一个空格(保证格式:0xAA 0x55 ...)
if (!first) {
fprintf(outputFile, " ");
}
// 以 "0xXX" 格式写入输出文件(%02X 表示两位大写十六进制,不足补0)
fprintf(outputFile, "0x%02X", byteVal);
first = 0; // 标记已输出至少一个值
}
// 继续获取下一个 token(strtok 内部维护状态)
token = strtok(NULL, " \t\r\n");
}
// 如果当前行成功解析出至少一个十六进制值,则在行尾添加换行符
if (!first) {
fprintf(outputFile, "\n");
}
}
// 关闭文件,释放资源
fclose(inputFile);
fclose(outputFile);
// 打印成功提示
printf(" 处理完成!已严格丢弃所有时间戳,仅保留 ' - ' 后的十六进制数据。\n");
printf("结果保存至: %s\n", fullOutputFileName);
return 0; // 正常退出
}工具准备清单
- 编译器:安装 GCC(Windows 可用 MinGW 或 WSL)
- 编辑器:VS Code + C/C++ 插件
- 终端:Git Bash(Windows)或 Terminal(Mac/Linux)
- 调试:学会用
printf打印中间变量(最简单有效!)
最后鼓励
你现在的状态非常好——不是从抽象语法开始学,而是从一个真实问题出发。这种“做中学”的方式效率最高!
记住:每一个资深程序员,都曾卡在 fopen 返回 NULL 的问题上。
如果你愿意,我可以:
- 为每一章提供 精简笔记 + 练习题
- 给你一个 可交互的学习计划表(带打卡)
- 提供 log 转 bin 的完整进阶代码
这是一份为你量身定制的 「以十六进制日志解析器」为项目驱动的C语言学习路线图。整个路线紧密围绕你的代码展开,逐层深入,每个阶段都有明确的实践目标。
阶段一:夯实C语言基础(1-2周)
章节1.1:开发环境搭建与调试入门
目标:能编译运行你的代码,并理解每一步执行过程。
知识点:GCC编译命令、调试器GDB基础、断点、单步执行、观察变量
实践任务:
- 手动编译你的代码:
gcc your_code.c -o hexparser -Wall -Wextra -g - 用GDB在
hexCharToValue函数入口设置断点,观察'A'、'f'、'9'的转换过程 - 在
strtok循环中设置断点,观察每个token的拆分结果
关键技能:学会用调试器验证你的假设,这是最高效的学习方式
章节1.2:C程序结构解剖 —— 你的代码是第一课
目标:彻底理解代码骨架,能复述每行作用。
知识点:#include预处理器、main函数签名、返回值意义、注释规范
实践任务:
- 删除某个
#include,观察编译错误信息(理解每个头文件的作用) - 将
int main()改为void main(),观察警告信息 - 在
main函数不同位置return不同的值(0, 1, 2),用echo $?查看退出码
关联代码:你代码中的#include、函数定义、返回值
章节1.3:数据类型与变量 —— 内存的抽象
目标:理解你代码中每个变量的内存布局和大小。
知识点:int、char、数组的本质、sizeof运算符
实践任务:
- 打印所有数组和变量的大小:
printf("line size: %zu\n", sizeof(line)); - 创建测试函数,验证
char的取值范围(-128~127)和无符号版本 - 理解你的代码为什么用
int byteVal而不是char byteVal(防止溢出和位运算提升)
关联你的代码:char line[4096]、int byteVal、FILE *
章节1.4:运算符与表达式 —— 位运算专项
目标:亲手重写并理解十六进制转换的核心逻辑。
知识点:位移<<、按位或|、算术转换、运算符优先级
实践任务:
拆解表达式:int byteVal = (hexCharToValue(token[0]) << 4) | hexCharToValue(token[1]);
为token[0] = 'A', token[1] = 'F'时,在纸上画出每一步的位变化
重写hexCharToValue函数,用switch-case代替if链(理解不同实现)
手动实现一个decCharToValue函数,转换十进制字符'0'-'9'
核心洞察:位运算是C高效处理底层数据的精髓
章节1.5:控制流 —— 代码的决策逻辑
目标:画出你代码的执行流程图。
知识点:if-else链、while循环、continue、break
实践任务:
- 为
main函数画流程图(包括文件打开失败、循环读取、token处理分支) - 将
while(fgets(...))改造为do-while循环,思考何时用哪种 - 在
strtok循环中添加break,测试在什么条件下跳出最合适
关联代码:if (!inputFile)、while (token)、if (strlen(token)==2)
阶段二:字符串与内存操作精讲(2-3周)
章节2.1:字符操作深度实践
目标:重写并优化hexCharToValue函数。
知识点:ASCII码表、ctype.h库(isxdigit、tolower)、字符与数字关系
实践任务:
- 用标准库重写函数:
if (isxdigit(c)) { return (isdigit(c) ? c-'0' : tolower(c)-'a'+10); } - 对比你的实现和标准库实现的优劣(依赖性、性能、可读性)
- 扩展:实现一个
valueToHexChar反向函数(0-15 → '0'-'F')
关键理解:字符在C中就是小整数,'A'比'a'小32
章节2.2:字符串基础与string.h全家桶
目标:从代码出发,掌握所有用到的字符串函数。
知识点:strcpy、strcat、strstr、strlen、strtok的行为与陷阱
实践任务:
- 安全改造:将
strcpy(fullInputFileName, inputFileName)改为strncpy并手动补\0 - 对比实验:
strstrvs.strchr,用strchr(line, '-')能否替代strstr(line, " - ")? - strtok的坑:在一个循环中同时处理两行数据(会失败),理解其静态内部状态
- 内存重叠实验:用
strcpy复制重叠内存区域,观察未定义行为
必读手册:man strtok时特别注意"WARNING"部分
章节2.3:字符串高级操作与内存安全
目标:让你的代码健壮到能处理恶意输入。
知识点:缓冲区溢出、strncpy/snprintf、strnlen、防御性编程
实践任务:
- 算一笔账:如果用户输入
inputFileName为255个字符,你的代码会溢出吗?(会!strcat后超256) - 重构:用
snprintf(fullInputFileName, sizeof(fullInputFileName), "%s.txt", inputFileName);替代strcpy+strcat - 防御:在
hexCharToValue开头加断言:if (c == '\0') return -1; - 模糊测试:创建一个
.txt文件,包含超长行、空行、" - "后无内容、非法十六进制字符,测试你的程序崩溃点
核心价值:C程序员的第一职责是保护内存
章节2.4:十六进制与字节流专项
目标:扩展代码,支持更多格式。
知识点:十六进制字符串、字节序、二进制表示
实践任务:
- 支持前缀:修改代码,能识别
0xAA、0Xbb、AAh等多种格式 - 反向操作:编写一个
valueToHexString函数,将字节数组转为"AA BB CC"字符串 - 批量转换:将输出格式改为纯二进制文件(
fopen(..., "wb")+fwrite) - 计算器:写一个命令行十六进制计算器,输入
1A + 2B,输出45
关联代码:你的核心转换逻辑是项目基石
章节2.5:缓冲区管理艺术
目标:彻底理解fgets(line, sizeof(line), inputFile)的边界行为。
知识点:fgets与gets的区别、缓冲区大小计算、行截断处理
实践任务:
- 超长行测试:创建一个4097字符的行(不含
\n),观察fgets行为(会截断) - 残行处理:检测到行被截断时(
strlen==4095且末位不是\n),如何继续读取剩余部分? - 内存占用:尝试将
line改为char *line = NULL; size_t len = 0;并用getline(&line, &len, inputFile)自动分配 - 性能考量:对比固定缓冲区 vs.
getline在处理1GB文件时的内存和速度差异
关键理解:C中没有"字符串类型",只有字符数组和指针约定
阶段三:文件与I/O系统实战(1-2周)
章节3.1:标准I/O流深度解析
目标:理解printf、scanf、fprintf的底层。
知识点:stdin/stdout/stderr、缓冲区机制、格式字符串
实践任务:
- 重定向实验:
./hexparser < input.txt > output.dat 2>error.log,观察perror输出到哪 - 格式控制:将
fprintf(outputFile, "0x%02X", byteVal);改为小写%02x,再改为输出十进制%03d - scanf的坑:输入文件名带空格会怎样?改用
scanf("%255s", ...)限制长度 - 非交互模式:用
getenv("INPUT_FILE")环境变量替代scanf,实现无人值守批处理
关联代码:所有I/O函数都是FILE*操作的特例
章节3.2:文件操作API全掌握
目标:用多种方式重写文件读写逻辑。
知识点:fopen模式字符串、fclose必要性、perror与strerror
实践任务:
- 模式实验:将
"r"改为"rb",在Windows和Linux下对比处理\r\n的差异 - 错误细分:用
if (errno == ENOENT)判断文件不存在,给出更友好的提示 - 返回值检查:检查
fprintf的返回值(写入字符数),确保数据完整 - 原子写入:写入临时文件,成功后
rename为最终文件名,防止程序崩溃产生半文件
关键习惯:每个系统调用后检查返回值
章节3.3:文本处理管线模式
目标:模仿Unix工具链,让程序更通用。
知识点:管道、过滤器模式、行处理架构
实践任务:
- Unix化改造:支持从
stdin读取和写入stdout,文件名留空时自动切换 - 工具组合:
cat log.txt | ./hexparser | wc -c统计转换后的字节数 - 多格式输出:增加命令行选项
-o bin输出二进制,-o c输出C数组格式 - 日志轮转:处理多个输入文件
*.txt,生成对应*.dat
设计哲学:一个好程序应该能嵌入更大的流程
章节3.4:错误处理与健壮性工程
目标:让你的程序在极端环境下也不崩溃。
知识点:错误码设计、资源清理(RAII模式)、断言assert
实践任务:
- 资源泄漏检测:注释掉
fclose,用valgrind ./hexparser检测泄漏 - 异常路径清理:在任意
return 1前确保所有已分配资源被释放(用goto cleanup模式) - 输入验证:在
hexCharToValue中,用assert(isxdigit(c))捕获开发期错误 - 压力测试:处理10万行、每行1MB的输入文件,监控内存占用是否稳定
最佳实践:错误处理代码应占30%以上
阶段四:代码重构与进阶技术(2-3周)
章节4.1:指针进阶 —— 从数组到指针的跃迁
目标:将代码中所有数组操作改为指针操作。
知识点:数组名退化、ptr++、指针算术、const指针
实践任务:
- 指针化:将
line[0]访问改为*line,token[0]改为*token - 遍历优化:用
for(char *p = line; *p; p++)替代strtok,手动分割token - const correctness:
const char *hexString作为输入参数,防止意外修改 - 函数指针:创建一个函数指针数组,支持多种解析格式(十六进制、十进制、二进制)
核心理解:指针是C的灵魂,也是最大的风险源
章节4.2:动态内存管理
目标:摆脱固定大小缓冲区限制。
知识点:malloc/free/realloc、sizeof陷阱、内存泄漏
实践任务:
- 动态行:用
getline替代固定line[4096],处理任意长度行 - 动态数组:将解析出的字节存入
malloc分配的数组,最后统一写入 - 内存泄漏实验:故意不写
free,用valgrind --leak-check=full观察 - 缓冲区增长策略:实现一个动态字符串,自动
realloc增长(类似C++std::string)
关键原则:谁malloc,谁free;配对出现
章节4.3:模块化与接口设计
目标:将代码拆分为可复用的模块。
知识点:头文件.h、实现文件.c、API设计、封装
实践任务:
- 拆分解析器:创建
hex_parser.h和hex_parser.c,提供parse_hex_line(const char *line, uint8_t *output) - 拆文件模块:创建
file_utils.h,封装open_file_safely、write_hex_array等函数 - 单元测试:为
hexCharToValue写测试用例,覆盖所有分支 - Makefile:编写Makefile,支持
make、make clean、make test
工程意义:这是从"脚本"到"软件"的转变
章节4.4:性能优化实战
目标:处理GB级日志文件速度提升10倍。
知识点:缓冲区大小调优、mmap内存映射、减少系统调用
实践任务:
- 基准测试:用
time命令测试处理100MB文件的时间 - 批量I/O:将
fprintf改为sprintf到缓冲区,每8192字节写入一次 - mmap改造:用
mmap映射整个文件,指针遍历替代fgets(进阶) - 并行化:用
pthreads多线程处理文件的不同段(超进阶)
优化信条:先测量,再优化;算法 > 实现 > 编译器
阶段五:项目扩展与工程化(持续进行)
章节5.1:命令行工具化
目标:支持./hexparser -i input.log -o output.dat --format=binary。
知识点:argc/argv、getopt_long、配置文件
扩展项目:
- 支持通配符
./hexparser *.log - 支持从ZIP包中读取文件(集成
libzip)
章节5.2:多格式支持
目标:成为通用十六进制转换工具。
扩展项目:
- 支持Motorola S-record、Intel HEX格式
- 支持ASCII转十六进制:
./hexparser --reverse将0xAA 0xBB转回AA BB - 支持校验和计算(Checksum, CRC32)
章节5.3:跨平台与国际化
目标:在Windows/Linux/macOS无差别运行。
知识点:条件编译_WIN32、Unicode路径、编码转换
实践:
- 用
#ifdef _WIN32处理路径分隔符/vs\ - 支持UTF-8文件名(Windows下需
_wfopen)
章节5.4:测试与持续集成
目标:写出一个"自信"的程序。
知识点:单元测试框架(Check/CUnit)、自动化测试
实践项目:
- 创建
test/目录,覆盖所有边界条件 - 集成GitHub Actions,每次提交自动测试
- 生成代码覆盖率报告:
gcov+lcov
学习进度建议与检查点
| 周数 | 攻克章节 | 产出物 | 自我检测 |
|---|---|---|---|
| 1 | 1.1-1.5 | 能调试代码,画出流程图 | 不看代码能复述hexCharToValue逻辑 |
| 2-3 | 2.1-2.5 | 代码无缓冲区溢出隐患 | 用valgrind检测零错误 |
| 4 | 3.1-3.4 | 支持管道操作,错误处理完善 | 输入不存在的文件,输出清晰错误 |
| 5-6 | 4.1-4.4 | 模块化代码 + Makefile | make test通过所有用例 |
| 7+ | 5.x | 支持命令行参数,处理GB文件 | 处理1GB文件内存稳定 < 10MB |
终极挑战项目
用你积累的所有知识,重写本项目并满足:
- 零警告编译(
-Wall -Wextra -Werror) - 通过所有边界测试(空文件、超长行、恶意输入)
- 处理1GB文件内存占用<10MB,速度比原版快5倍
- 输出同时支持文本
0xAA和二进制两种格式 - 有完整单元测试,覆盖率>90%
当你完成时,你已经是合格的C语言系统程序员了!
记住,C语言学习的唯一捷径就是亲手敲每一行代码,用调试器看每一个变量的变化。你的这段代码是绝佳的起点,因为它涵盖了C语言90%的核心知识点。坚持按路线图实践,3个月后你会感谢现在的自己!
到此这篇关于利用C语言编写一个十六进制日志转换工具的文章就介绍到这了,更多相关C语言十六进制日志转换内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
