引言
在嵌入式开发与底层 C 语言编程中,字符串处理和进制转换是两项非常基础却又高频使用的技能。无论是解析串口指令、处理通信协议报文,还是将二进制数据以可读形式输出,都离不开这两类操作。本文将从基础概念出发,先深入剖析strtok系列字符串分割函数的原理与注意事项,再介绍如何手写一个高效的 16 进制整数转字符串工具函数,最后通过一个综合实战示例串联起两个知识点,帮助读者从原理到实践完整掌握。
文章目录
- 引言
- 一、x_strtok
- 1.定义
- 2.原型
- 3.说明
- (1)包含的分割字符时
- (2)返回值
- (3)注意
- 示例代码
- 二、16进制整数转字符串
- 1. 应用场景
- 2. 实现思路
- 3. 边界条件分析
- 4. 改进后的带参数校验版本
- 示例代码
- 三、实战示例:解析并打印十六进制数据帧
- 四、总结
在深入阅读正文之前,先通过下表快速了解strtok系列各变体的核心差异,便于按需选用:
| 函数 | 线程安全 | 可重入性 | 是否破坏原字符串 | 适用平台 | 使用场景 |
|---|---|---|---|---|---|
strtok | 否(内部静态变量) | 否 | 是 | C89/C99 标准库 | 单线程、简单字符串分割,代码量最少 |
strtok_r | 是(调用方传入saveptr) | 是 | 是 | POSIX(Linux/Unix) | 多线程环境下的字符串分割,需自行维护上下文 |
strtok_s | 是(调用方传入上下文) | 是 | 是 | C11 标准(可选)/ Windows | 跨平台多线程分割,接口与strtok_r类似 |
x_strtok_s | 是(调用方传入lasts) | 是 | 是 | 嵌入式 / 自定义实现 | 不依赖平台扩展,可移植到裸机或 RTOS 环境 |
说明:
strtok因使用静态变量保存分割状态,多线程并发调用会互相覆盖,导致结果错乱;strtok_r、strtok_s与x_strtok_s均通过调用方提供的指针保存状态,因此线程安全且可重入。四者都会把分隔符替换为\0,从而破坏原字符串,若需保留原文请先拷贝一份。
一、x_strtok
1.定义
分解字符串为一组字符串。s为要分解的字符,delim为分隔符字符(如果传入字符串,则传入的字符串中每个字符均为分割符)。首次调用时,s指向要分解的字符串,之后再次调用要把s设成NULL。在头文件#include<string.h>中。
2.原型
char *strtok(char s[], const char *delim);
3.说明
(1)包含的分割字符时
当strtok()在参数s的字符串中发现参数delim中包含的分割字符时,则会将该字符改为\0 字符。在第一次调用时,strtok()必需给予参数s字符串,往后的调用则将参数s设置成NULL。每次调用成功则返回指向被分割出片段的指针。
(2)返回值
从s开头开始的一个个被分割的串。当s中的字符查找到末尾时,返回NULL。如果查找不到delim中的字符时,返回当前strtok的字符串的指针。所有delim中包含的字符都会被滤掉,并将被滤掉的地方设为一处分割的节点。
(3)注意
需要注意的是,使用该函数进行字符串分割时,会破坏被分解字符串的完整,调用前和调用后的s已经不一样了。第一次分割之后,原字符串str是分割完成之后的第一个字符串,剩余的字符串存储在一个静态变量中,因此多线程同时访问该静态变量时,则会出现错误。
示例代码
#include<string.h>#include"x_strtok.h"char*_x_strtok_s(char*s,constchar*delim,char**lasts,intskip_leading_delim){registerchar*spanp;registerintc,sc;char*tok;if(s==NULL&&(s=*lasts)==NULL)return(NULL);/* * Skip (span) leading delimiters (s += strspn(s, delim), sort of). */cont:c=*s++;for(spanp=(char*)delim;(sc=*spanp++)!=0;){if(c==sc){if(skip_leading_delim){gotocont;}else{*lasts=s;s[-1]=0;return(s-1);}}}if(c==0){/* no non-delimiter characters */*lasts=NULL;return(NULL);}tok=s-1;/* * Scan token (scan for delimiters: s += strcspn(s, delim), sort of). * Note that delim must have one NUL; we stop if we see that, too. */for(;;){c=*s++;spanp=(char*)delim;do{if((sc=*spanp++)==c){if(c==0)s=NULL;elses[-1]=0;*lasts=s;return(tok);}}while(sc!=0);}/* NOTREACHED */}char*x_strtok_s(char*s,constchar*delim,char**lasts){return_x_strtok_s(s,delim,lasts,1);}#ifndef_X_STRTOK_H_#define_X_STRTOK_H_char*x_strtok_s(char*s,constchar*delim,char**lasts);#endif// _X_STRTOK_H_二、16进制整数转字符串
1. 应用场景
在嵌入式日志输出、调试信息打印、蓝牙或网络数据帧展示等场景中,经常需要把原始字节流转换为可读的十六进制字符串。例如将传感器采集到的温度、湿度原始数据以0x3A 0x1F的形式打印出来,便于开发者直观核对。
2. 实现思路
核心思路是逐字节处理:将每个字节的高 4 位和低 4 位分别取出,映射为对应的十六进制字符(0-9、a-f),依次写入目标缓冲区,最后补上字符串结束符\0。
3. 边界条件分析
上面的hex_to_str实现虽然简洁,但在实际工程中还需考虑以下边界情况:
len为 0 时:循环体不会执行,函数直接执行target[len] = '\0',即target[0] = '\0',此时target仅写入一个字符串结束符,得到一个空字符串。这在语义上是合理的,但要求调用方保证target至少能容纳 1 个字节。source或target为 NULL 时:函数会直接对空指针解引用,触发空指针异常(段错误)。在嵌入式或对健壮性要求较高的场景中,应在函数入口处增加参数校验,非法输入时提前返回。len较大时:每个字节会转换为 2 个十六进制字符,加上末尾的'\0',共需2 * len + 1个字节。若调用方提供的target缓冲区小于该值,写入时就会发生缓冲区越界,破坏相邻内存数据,甚至引发安全漏洞。因此调用方必须确保缓冲区大小充足。
4. 改进后的带参数校验版本
针对上述边界条件,下面给出一个带参数校验的改进版本。函数增加返回值,用0表示成功、-1表示参数非法,便于调用方判断处理:
#include<stdint.h>/** * @brief 将字节数组转换为十六进制字符串 * @param source 源字节数组指针 * @param len 源字节数 * @param target 目标缓冲区指针 * @param cap 目标缓冲区容量(字节数) * @return 0 成功;-1 参数非法(source/target 为 NULL,或缓冲区容量不足) */inthex_to_str(uint8_t*source,uint32_tlen,uint8_t*target,uint32_tcap){uint8_tddl,ddh;uint32_ti;/* 参数校验:空指针直接返回 */if(source==NULL||target==NULL)return-1;/* 参数校验:目标缓冲区至少需要 2*len+1 字节(含结束符) */if(cap<(uint32_t)(2*len+1))return-1;/* len 为 0 时,循环不执行,仅写入结束符,得到空字符串 */for(i=0;i<len;i++){ddh=('0'+source[i]/16);ddl=('0'+source[i]%16);if(ddh>'9')ddh=(ddh+('a'-'9'-1));if(ddl>'9')ddl=(ddl+('a'-'9'-1));target[i*2]=ddh;target[i*2+1]=ddl;}target[len*2]='\0';return0;}说明:改进后的版本通过
cap参数显式传入目标缓冲区容量,在写入前完成容量校验,从根本上避免越界风险;同时增加空指针检查,使函数在异常输入下也能安全返回。调用方在使用时,应传入sizeof(target_buf)作为cap,并检查返回值是否为0。
示例代码
staticvoidhex_to_str(uint8_t*source,uint32_tlen,uint8_t*target){uint8_tddl,ddh;uint32_ti;for(i=0;i<len;i++){ddh=('0'+source[i]/16);ddl=('0'+source[i]%16);if(ddh>'9')ddh=(ddh+('a'-'9'-1));if(ddl>'9')ddl=(ddl+('a'-'9'-1));target[i*2]=ddh;target[i*2+1]=ddl;}target[len]='\0';}三、实战示例:解析并打印十六进制数据帧
下面综合运用x_strtok_s与hex_to_str,实现一个简单的数据帧解析与打印工具:从串口接收到的字符串中按逗号分割出若干十六进制片段,再将其转换为可读的十六进制字符串输出。
#include<stdio.h>#include<string.h>#include<stdint.h>#include<stdlib.h>#include<errno.h>#include"x_strtok.h"/* 目标缓冲区大小:最多 4 个字节,每个字节转成 2 个十六进制字符,再加结束符 */#defineMAX_BYTES4#defineHEX_STR_SIZE(MAX_BYTES*2+1)staticvoidhex_to_str(uint8_t*source,uint32_tlen,uint8_t*target){uint8_tddl,ddh;uint32_ti;for(i=0;i<len;i++){ddh=('0'+source[i]/16);ddl=('0'+source[i]%16);if(ddh>'9')ddh=(ddh+('a'-'9'-1));if(ddl>'9')ddl=(ddl+('a'-'9'-1));target[i*2]=ddh;target[i*2+1]=ddl;}target[len]='\0';}intmain(void){charframe[]="0x3A,0x1F,0x2C,0x4D";char*token=NULL;char*saveptr=NULL;uint8_tbytes[MAX_BYTES];uint8_thex_str[HEX_STR_SIZE];intidx=0;token=x_strtok_s(frame,",",&saveptr);while(token!=NULL){char*endptr=NULL;longval;/* 检查是否超出缓冲区容量,防止数组越界写入 */if(idx>=MAX_BYTES){printf("Error: too many tokens, buffer overflow avoided.\n");break;}/* 清空 errno,以便区分 strtol 是否因溢出而失败 */errno=0;val=strtol(token,&endptr,16);/* 转换失败:没有任何有效数字被解析 */if(endptr==token){printf("Error: invalid hex token \"%s\".\n",token);break;}/* 转换失败:token 尾部存在非法字符(如 "0xZZ" 中的 'Z') */if(*endptr!='\0'){printf("Error: trailing garbage in token \"%s\".\n",token);break;}/* 转换失败:数值超出 long 范围(errno 被置为 ERANGE) */if(errno==ERANGE){printf("Error: token \"%s\" out of range.\n",token);break;}/* 转换失败:数值超出 uint8_t 可表示范围(0~255) */if(val<0||val>0xFF){printf("Error: token \"%s\" out of uint8_t range.\n",token);break;}bytes[idx++]=(uint8_t)val;token=x_strtok_s(NULL,",",&saveptr);}/* 只有完整解析出全部字节时才执行转换与打印 */if(idx==MAX_BYTES){hex_to_str(bytes,idx,hex_str);printf("Parsed hex string: %s\n",hex_str);}else{printf("Parsing failed, only %d/%d bytes valid.\n",idx,MAX_BYTES);}return0;}四、总结
本文从strtok系列函数的基本定义、原型与注意事项出发,梳理了字符串分割的底层机制;随后介绍了 16 进制整数转字符串的常见实现思路,并给出了可直接复用的工具函数;最后通过一个综合实战示例,演示了如何将两者结合用于实际的数据帧解析与格式化输出。掌握这两个基础工具,能显著提升嵌入式与 C 语言开发中的字符串处理效率。