大家好,我是驚覺,今天聊聊字元串。字元串的使用場景非常之多,人機互動和雙機通信都會用到。比如:
- 通過序列槽向單片機發送指令,以執行操作或配置參數。
- 單片機讀取傳感器資料,資料格式是字元串。一般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。
其根據提供的分隔符集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的資料,如果想提取其中的
A
,
3204.862246
和
11845.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工程:
位址:[email protected]:wenbodong/mcu_demo.git
示例:examples/05_string/example.c
使用時需要打開examples/examples.h中的EXAMPLE_SHOW_STRING。