strtok拆分字符串

大家好,我是惊觉,今天聊聊字符串。字符串的使用场景非常之多,人机交互和双机通信都会用到。比如:

  • 通过串口向单片机发送指令,以执行操作或配置参数。
  • 单片机读取传感器数据,数据格式是字符串。一般GPS数据就是字符格式。
  • 有些场景需要使用多个处理器协同工作,比如单片机+openmv,它们之间需要通信,可以采用字符格式的编码方式。

操作字符串,无非是两件事儿:生成字符串与解析字符串,后者往往更复杂一些。Java,Python之类的高级编程语言自带了强大的字符串处理库,提供非常丰富的操作。下图是Java的String类函数,密密麻麻有木有,这还只是一部分。
在这里插入图片描述
相对而言,标准C库提供的功能有限。大家熟知的功能可能有:

  • 字符串复制追加(strcpy,strcat)
  • 字符串查找比较(strstr,strcmp)
  • 字符串转数字(atoi,strtol)

有两个非常有用但是可能被大家忽略的函数,介绍给大家。

任务:解析经纬度

让我们以解析GPS中的RMC消息为例,数据如下:

$GNRMC,122921.000,A,3204.862246,N,11845.911047,E,0.099,191.76,280521,,E,A*00

GPS的各字段以逗号分隔。我们需要提取经纬度信息,集中在:A,3204.862246,N,11845.911047,E。A表示经纬度有效,3204.862246是纬度,11845.911047是纬度,具体的解释参见下图:
在这里插入图片描述

拆分字符串strtok_r

由于GPS中各字段以逗号分隔,大家最先想到的可能是用strstr或strchr去查找逗号的位置,再一一处理。如果有一个函数可以帮我们完成拆分,效果如下图,那将会很方便后续处理。
在这里插入图片描述
这个函数是有的,而且就在C标准库中,那就是strtok。

char *strtok(char *source, const char *delimiters);

其根据提供的分隔符集delimiters,对source进行拆分。

  • source 待拆分的字符串。
  • delimiters 分隔符集,可以包含多个字符。比如"\r\n\t "表示以换行,tab等字符进行拆分。
  • return 返回指向子字符串的指针。

在拆分一个字符串时,需要多次调用该方法:

  • 初次调用时,source为待拆分字符串,delimiters为分隔符。函数返回第一个子字符串地址。
  • 之后的调用,source为NULL,delimiters为分隔符,分隔符的内容并不需要与之前的一致。函数返回下一个子字符串地址。
  • 当某次调用后返回NULL时,整个拆分就结束了。

其实过程并不复杂,请看拆分GPS的代码:

#define GPS_RMC "$GNRMC,122921.000,A,3204.862246,N,11845.911047,E,0.099,191.76,280521,,E,A*00"

void split_string_example(void)
{
    char buf[128];
    int buf_len;
    char *token = NULL;
    char *saveptr = NULL;
    const char *delim = ",*";

    LOG_I("test split string");

    buf_len = snprintf(buf, sizeof(buf), "%s", GPS_RMC);

    token = strtok_r(buf, delim, &saveptr);
    while (token)
    {
        LOG_D("%s", token);
        token = strtok_r(NULL, delim, &saveptr);
    }

    LOG_HEX_V(buf, buf_len, "finally, buf:");
}

结果刚才已经放过了,这次放一个更完整的。
在这里插入图片描述
示例中用宏GPS_RMC来定义GPS的内容,再用snprintf把它打印到buf之中?

buf_len = snprintf(buf, sizeof(buf), "%s", GPS_RMC);

这可不是笔者多此一举,而是因为strtok在拆分字符串时会修改其内容。以下两点需要牢记:

  • strtok并不是重新分配内存以存放子字符串,其返回的子字符串直接指向待拆分字符串中的相应位置。没有任何的内存分配。
  • 所谓的拆分,是将字符串中的分隔符替换为’\0’,也只有这样,你才能进行后续操作。上图的结尾展示了拆分后的buf的内容,红框都是’\0’。因此,待拆分字符串必须是可被修改的,必须是变量,而不能是常量

笔者用的不是strtok,而是strtok_r。C语言中很多函数有两种版本,一种不带_r,一种带_r,_r表示可重入。可重入的概念可以单独写一篇文章,这里就不多说了。strtok_r比strtok多了一个参数,其为char *指针,用于保存拆分的状态。其实用法很简单,定义一个指针变量并传入就行,不需要关注它的值。

优化一下

我们再看下GPS的数据,如果想提取其中的A3204.86224611845.911047,直接使用strtok并不方便。

$GNRMC,122921.000,A,3204.862246,N,11845.911047,E,0.099,191.76,280521,,E,A*00

如果使用Java的话,如下几行代码即可完成提取。

String gps = "$GNRMC,122921.000,A,3204.862246,N,11845.911047,E,0.099,191.76,280521,,E,A*00";
String[] sub = gps.split(",");
if (sub.length < 6) {
	System.out.println("parse fail");
} else {
	System.out.println(String.format(
			"parse succeed, valid:%s, longitude:%s, latitude:%s", 
			sub[2], sub[3], sub[5]));
}

输出结果:

parse succeed, valid:A, longitude:3204.862246, latitude:11845.911047

Java之所以方便,关键在于split函数返回了拆分后的字符串数组,可直接通过下标提取相关字段。

C语言没有这样的函数,那我们就自己写一个。

static int split_string(char *str, const char *delim, char *sub_ptr[], int size)
{
    char *token = NULL;
    char *saveptr = NULL;
    int idx = 0;


    token = strtok_r(str, delim, &saveptr);
    while (token && idx < size)
    {
        sub_ptr[idx++] = token;
        token = strtok_r(NULL, delim, &saveptr);
    }

    return idx;
}

split_string将拆分的结果写入sub_ptr之中,并返回子字符串个数。有了这个函数,提取就如Java一样方便了。

void split_string_example2(void)
{
    char buf[128];
    char *sub_buf[20];
    int num;

    LOG_I("test split string 2");

    snprintf(buf, sizeof(buf), "%s", GPS_RMC);
    num = split_string(buf, ",", sub_buf, ARRAY_SIZE(sub_buf));

    if (num < 7)
    {
        LOG_E("fail");
        return;
    }

    LOG_D("succeed, valid:%s, latitude:%s, longitude:%s", sub_buf[2], sub_buf[3], sub_buf[5]);

}

使用strtok或者是split_string仅仅是提取出目标字符串,想得到经纬度数值的话,还需要转换成浮点数,可使用atof函数。其实还有一种更为简单的方法,咱明天继续。

文中完整的示例代码,参见笔者基于stm32f407创建的demo工程:

地址:git@gitee.com:wenbodong/mcu_demo.git
示例:examples/05_string/example.c
使用时需要打开examples/examples.h中的EXAMPLE_SHOW_STRING。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值