因為一個函數strtok踩坑,我被老工程師無情嘲笑了(一)

2021-03-02 技術讓夢想更偉大

在用C/C++實現字符串切割中,strtok函數經常用到,其主要作用是按照給定的字符集分隔字符串,並返回各子字符串。

但是實際上,可不止有strtok(),還有strtok、strtok_s、strtok_r 函數,我們本篇文章作為基礎篇,來一些簡單的介紹。因為濫用了這個函數,我可是被老工程師嘲笑的無地自容了。

strtok()函數詳解描述

該函數用來將字符串分割成一個個片段,並返回各子字符串。

函數原型
char *strtok(char *str, const char *delim)

參數返回值

該函數返回被分解的第一個子字符串,如果沒有可檢索的字符串,則返回一個空指針。

實例
//https://tool.lu/coderunner/
//來源:技術讓夢想更偉大
//作者:李肖遙
#include <string.h>
#include <stdio.h>
#define INFO_MAX_SZ 80

int main () {
   char str[INFO_MAX_SZ] = "dream - coder - lixiaoyao";
   const char delim[2] = "-";
   char *token;
   
   //獲取第一個子字符串
   token = strtok(str,delim);
   
   //繼續獲取其他的子字符串
   while( token != NULL ) 
   {
      printf( "%s\n", token );
    
      token = strtok(NULL, delim);
   }
   
   return(0);
}

運行的結果如下:

注意事項

使用該函數進行字符串分割時,會破壞被分解字符串的完整,調用前和調用後的s已經不一樣了。第一次分割之後,原字符串str是分割完成之後的第一個字符串,剩餘的字符串存儲在一個靜態變量中。

strtok函數在提取字符串時使用了靜態緩衝區,因此,它是線程不安全的,多線程同時訪問該靜態變量時,則會出現錯誤。本篇為基礎篇,在後續中將進一步剖析

拓展一個應用實例

網絡上一個比較經典的例子是將字符串切分,存入結構體中,我整理了一下,看代碼

//https://tool.lu/coderunner/
//來源:技術讓夢想更偉大
//作者:李肖遙

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define INFO_MAX_SZ 80
 
typedef struct person{ 
 char name[25]; 
 char sex[10]; 
 char age[4]; 
}Person;

int main()
{
 int in=0;  
 int j;
 char buffer[INFO_MAX_SZ]="Aob male 18,Bob male 19,Cob female 20";      
 char *p[20];  
 char *buf = buffer;  
 while((p[in]=strtok(buf,","))!=NULL)//先以,為分界符,將三個人的信息分開
 {  
  buf=p[in];//調用strtok,先將子串先一一保存到字符串指針數組中,  
  while((p[in]=strtok(buf," "))!=NULL)//以空格為分界符
  {  
   in++;  
   buf=NULL;  
  }  
  buf=NULL;  
 }  
 printf("Here we have %d strings\n", in);  
 for (j=0; j<in; j++)  
 {  
    //列印指針數組中保存的所有子串
  printf(">%s<\n",p[j]);  
 }  
    return 0;
}

運行結果如下

按照這個結果並沒有得到我們想要的結果,僅僅提取出了第一個人的信息。

那麼出現了什麼問題呢?

我們分析得到,其實在第一次循環中,strtok函數將第一個人信息後的這個逗號,改為了'\0,這時strtok內部的this指針指向的是逗號的後一個字符。

而在第一個循環結束後,函數第一個參數被設定為NULL,strtok將以this指針指向的位置作為分解起始位置,此時this指針指向的是'\0』,strtok對一個空串無法切分,返回NULL,所以得到上面的結果。

那麼我們怎麼解決這個問題呢?

我們看一下代碼來實現這個想要的結果

//https://tool.lu/coderunner/
//來源:技術讓夢想更偉大
//作者:李肖遙

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define INFO_MAX_SZ 80
 
typedef struct person{ 
 char name[25]; 
 char sex[10]; 
 char age[4]; 
}Person;

int main()
{
 int in=0;  
 int j;
 char buffer[INFO_MAX_SZ]="Aob male 18,Bob male 19,Cob female 20";      
 char *p[20];  
 char *buf = buffer;  
 while ((p[in] = strtok(buf, " ,")) != NULL)//同時以逗號和空格為分界符
 {  
  switch (in % 3)  
  {  
  case 0:  
   printf("第%d個人:Name!\n", in/3+1);  
   break;  
  case 1:  
   printf("第%d個人:Sex!\n", in/3+1);  
   break;  
  case 2:  
   printf("第%d個人:Age!\n", in/3+1);  
   break;  
  }  
  in++;  
  buf = NULL;  
 }  
 printf("Here we have %d strings\n", in);  
 for (j=0; j<in; j++)  
 {     
  printf(">%s<\n",p[j]);  
 } 
    return 0;
}

最終運行的結果如下

額,這樣的代碼我看不下去了,要實現我們必須提前知道一個結構體中究竟包含了幾個數據成員,那麼有沒有合適的函數能夠代替strtok呢?

有的,它就是strtok_r。

Linux下的strtok_r函數描述

strtok_r是linux平臺下的strtok函數的線程安全版。windows的string.h中並不包含它。要想使用這個函數,找到linux下的實現源碼,複製到你的程序中即,或者使用GNU C Library。

strtok_r函數是strtok函數的可重入版本。char **saveptr參數是一個指向char *的指針變量,用來在strtok_r內部保存切分時的上下文,以應對連續調用分解相同源字符串。

第一次調用strtok_r時,str參數必須指向待提取的字符串,saveptr參數的值可以忽略。連續調用時,str賦值為NULL,saveptr為上次調用後返回的值,不要修改。

一系列不同的字符串可能會同時連續調用strtok_r進行提取,要為不同的調用傳遞不同的saveptr參數。

strtok_r實際上就是將strtok內部隱式保存的this指針,以參數的形式與函數外部進行交互。由調用者進行傳遞、保存甚至是修改。需要調用者在連續切分相同源字符串時,除了將str參數賦值為NULL,還要傳遞上次切分時保存下的saveptr。

函數原型如下
char *strtok_r(char *str, const char *delim, char **saveptr);

源碼
/* Parse S into tokens separated by characters in DELIM.
   If S is NULL, the saved pointer in SAVE_PTR is used as
   the next starting point.  For example:
        char s[] = "-abc-=-def";
        char *sp;
        x = strtok_r(s, "-", &sp);      // x = "abc", sp = "=-def"
        x = strtok_r(NULL, "-=", &sp);  // x = "def", sp = NULL
        x = strtok_r(NULL, "=", &sp);   // x = NULL
                // s = "abc\0-def\0"
*/
char *strtok_r(char *s, const char *delim, char **save_ptr) {
    char *token;
   /*判斷參數s是否為NULL,如果是NULL就以傳遞進來的save_ptr作為起始分解位置;若不是NULL,則以s開始切分*/
    if (s == NULL) s = *save_ptr;
 
    /* Scan leading delimiters.  */
    s += strspn(s, delim);
    /*判斷當前待分解的位置是否為'\0',若是則返回NULL(聯繫到(一)中所說對返回值為NULL的解釋);不是則繼續。*/
    if (*s == '\0') 
  return NULL;
 
    /* Find the end of the token.  */
    token = s;
    s = strpbrk(token, delim);
    if (s == NULL)
        /* This token finishes the string.  */
        *save_ptr = strchr(token, '\0');
    else {
        /* Terminate the token and make *SAVE_PTR point past it.  */
        *s = '\0';
        *save_ptr = s + 1;
    }
 
    return token;
}

實現以上實例

調用strtok_r的代碼比調用strtok的代碼多了兩個指針,outer_ptr和inner_ptr。outer_ptr用於標記每個人的提取位置,即外循環;inner_ptr用於標記每個人內部每項信息的提取位置,即內循環。

strtok_r將原內部指針顯示化,提供了saveptr這個參數。增加了函數的靈活性和安全性。

//https://tool.lu/coderunner/
//來源:技術讓夢想更偉大
//作者:李肖遙

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define INFO_MAX_SZ 80
 
typedef struct person{ 
 char name[25]; 
 char sex[10]; 
 char age[4]; 
}Person;

int main()
{
 int in=0;  
 int j;
 char buffer[INFO_MAX_SZ]="Aob male 18,Bob male 19,Cob female 20";      
 char *p[20];  
 char *buf=buffer;  
 char *outer_ptr=NULL;  
 char *inner_ptr=NULL;  
 while((p[in] = strtok_r(buf, ",", &outer_ptr))!=NULL)   
 {  
  buf=p[in];  
  while((p[in]=strtok_r(buf, " ", &inner_ptr))!=NULL)   
  {  
   in++;  
   buf=NULL;  
  }  
  buf=NULL;  
 }  
 printf("Here we have %d strings\n",in);  
 for (j=0; j<in; j++)  
 {     
  printf(">%s<\n",p[j]);  
 } 
    return 0;
}

編譯結果如下

注意事項

該函數也會破壞帶分解字符串的完整性,但是其將剩餘的字符串保存在saveptr變量中,保證了安全性。

Windows下的strtok_s函數描述

strtok_s是windows下的一個分割字符串安全函數,

原型
char *strtok_s( char *strToken, const char *strDelimit, char **buf);

char * strtok_s(char * restrict str,rsize_t * restrict strmax,const char * restrict delim,char ** restrict ptr);

在由str指向的以空字符結尾的字節字符串中查找下一個標記。分隔符字符由delim指向的以空字符結尾的字節字符串標識。

該函數被設計為被稱為倍數時間以從相同的字符串獲得連續的令牌。

這裡大家可以參考,我在這裡不多講了。

https://cloud.tencent.com/developer/section/1009645

巨人的肩膀

https://blog.csdn.net/bobyangsmile/article/details/38420985

https://www.runoob.com/cprogramming/c-function-strtok.html

最後

這裡先簡單介紹下這幾個函數的基本使用以及一些優缺點等等,後續會根據自己踩的坑來解讀strtok()的隱含特性,下一期,我們再見!

相關焦點

  • 因為一個函數strtok踩坑,我懂得了看源碼的重要性
    在上篇因為一個函數strtok踩坑,我被老工程師無情嘲笑了(一),我們分析了strtok()函數,以及windos、Linux下的線程安全版,那麼這篇中我們著重分析下解讀strtok()的隱含特性,到底strtok有哪些坑。
  • C語言字符串處理函數之字符串轉換、查詢函數
    strchr()遇到'\0'會停止,例子:再來做一個簡單的拓展,寫個小函數,實現功能呢就是把給定字符串中某個字符出現的所有位置都用另外一個字替換,如下例子,將"abcabc"中的'c'替換為'@':memchr
  • 年終總結:這一年踩過的坑
    踩坑,是項技術活。有的人踩坑,踩的七歪八扭,深淺不一。這種踩最後就只剩下坑。我呢,稍微踩的好點,踩的坑都圍繞著同一個基點:教育行業的產品經理。這個詞可以分成兩截理解:教育行業、產品經理。就像我當年跨考傳播學時,那些深奧的社科理論,過上幾十遍,一個理論看幾十篇相關的論文,早晚有搞懂的一天。但我很怕,當我搞懂的時候,產品經理本身的技能,已經被別人甩一大截了。學習python和深度學習的那段時間,我一邊猶豫,一邊投入時間。我一邊告訴自己,這是深不見底的坑。一邊又不甘心,畢竟投入了大半年的時間,就這麼放棄,自己都要嘲笑自己。是不是因為不行了,所以找那麼多藉口?
  • SQL常用函數(多平臺對比)
    SQL常用函數(多平臺對比)一、數值型函數(一)Teradata
  • 關於IF函數公式,多條件判斷,你一定踩過的坑 - Excel自學成才
    在工作中,IF函數是必不可少的一個存在,解決了我們工作中很多的問題,但是還是有不少的小夥伴然後不會使用,我們模擬數據舉一個例子:下表是公司員工數據,現在公司對於工齡5-10年的員工,有額外的補貼1000元
  • 爸爸姓「沙」,隨口給孩子取名差點踩坑,沙溢:多虧我爸當年阻攔
    文 | 小鴻兒媽媽(此文為原創 ,版權歸屬本作者所有,歡迎個人轉發分享)導語有些父母在給孩子取名時,沒有注意到自己姓氏的特殊,就隨意給孩子取名,殊不知這樣的名字是很容易踩坑的孩子也可能因此受到其他人的嘲笑
  • 作為踩過坑的老教師,我想給新手教師這三個建議
    我於90年代初大學畢業,一開始分配到鄉村偏遠的初中教書,也先後在幾所公立和私立學校呆過。在教育領域默默耕耘了近30年,中間踩過坑,由當初的「娃娃頭」,後來成為區級骨幹教師,然後是市級先進教師,直到成為省優秀教師。
  • 使用Vookup函數總是出現錯誤值?盤點那些你一定踩過的坑!
    Vlookup函數是一個使用頻率很高的查找匹配函數。但是為什麼有時明明用檢查窗口可以查詢到的數據,vlookup函數卻實現錯誤值?這篇文章就和朋友們一起盤點那些你一定踩過的坑。一.VLOOKUP簡介:1.語法:VLOOKUP(查找值,查找區域,返回查找區域第N列,查找模式)2.各參數意義:(1)查找值可以理解為第我們要查找什麼,可以使用通配符。(2)查找區域就是我們要在哪個區域查找查找值。(3)在查找區域找到查找值所在的行數後,第三個參數需要明確返回的數據在查找區域的第幾列。
  • COUNTIF函數這3個坑,讓很多老學員鬱悶死
    今天,VIP群有幾個老學員在鬱悶中,明明公式是對的,結果卻錯的,百思不得其解。統計C列每個區間的次數,<1年本來只有1個,結果卻為26。=COUNTIFS($C2:$C50,G2)COUNTIF是一個很特殊的函數,經常會存在各種陷阱
  • 專欄 | Detectron精讀系列之一:學習率的調節和踩坑
    但是面對龐大的 caffe2 和 detectron 函數庫,多少會感覺有些迷茫。Detectron 精讀系列會從細小的調參開始,到一些重要的函數分析,最後掌握 Detectron 函數庫的全貌。在這個過程中,我們也會幫大家提前踩坑,希望大家可以從 Detectron 函數庫學到更多通用的計算機視覺技能。
  • 「到底愛我什麼」?怎麼才能不踩坑的回答,今天我剛知道
    剛看到一個關於愛情的表述,亟不可待分享給大家。這是一位叫金文聲的已故天津老評書藝人,曾經在評書裡說過的一句閒話,大意是這樣的:兩個人談戀愛,你要問問對方,他愛你什麼。我不知道大家是否明白,但是我看到老先生說的這段話之後,醍醐灌頂。我今天才知道有這樣一位老先生,而且我很有幸跟金先生的名字同音不同字,所以感觸更深。這段話讓我回想起我自己戀愛生涯中踩過的很多坑。
  • Golang 需要避免踩的 50 個坑(一)
    前言Go 是一門簡單有趣的程式語言,與其他語言一樣,在使用時不免會遇到很多坑,不過它們大多不是 Go 本身的設計缺陷。如果你剛從其他語言轉到 Go,那這篇文章裡的坑多半會踩到。如果花時間學習官方 doc、wiki、討論郵件列表、 Rob Pike 的大量文章以及 Go 的源碼,會發現這篇文章中的坑是很常見的,新手跳過這些坑,能減少大量調試代碼的時間。
  • 美術生踩過的畫室收費坑,這次我不允許你踩了
    爸媽我的學費準備好了嗎,咱去交學費吧!對於迎接新生活的興奮,往往讓人失去理智。很多人容易踩學費的坑。居心不良的畫室像渣男,讓人盲目。我想告訴他既然踩了不試學,就直接交學費的坑,那麼你就得好好看看合同條款。某些畫室規定在兩個月內是可以退還部分剩餘的學費。想退費就好像會過期的鳳梨罐頭,超出兩個月就會過期。我之前說學畫畫就像談戀愛,早發現畫室不合適,其實就得早離開,是要先試學是一個道理,因為人要學會及時止損。時間太久了你再說你不學了,對於你自己,對於畫室而言都是雙輸的局面。你因此浪費了學畫畫的時間。畫室因此浪費了為你調配的教學資源。
  • 一個Android資深工程師的自我提升
    看到一篇文章中提到「最近幾年國內的初級Android程式設計師已經很多了,但是中高級的Android技術人才仍然稀缺「,這的確不假,從我在百度所進行的一些面試來看,找一個適合的高級Android工程師的確不容易,一般需要進行大量的面試才能挑選出一個比較滿意的。為什麼中高級Android程式設計師不多呢?
  • 張嘉倪好心推薦護膚品,她一開口,就遭到了網友的無情嘲笑
    張嘉倪好心推薦護膚品,她一開口,就遭到了網友的無情嘲笑。藝圈的很多女明星喜歡滿推薦化妝品,像範冰冰和林允一樣,平時會分享一些化妝品,因為有明星效應,所以商品能力很高,網絡用戶的反饋也很好。因為印象很好,所以氣質很好,張嘉倪推薦的護膚品,在網絡用戶心中也很受歡迎。張嘉倪親切地在某美化妝品平臺上分享了短片視頻,這次,張嘉倪推薦的護膚品是美白專用的,結果,她一開口就被網友冷笑了,很多網友認為,張嘉倪自己的皮膚比較黑。
  • 窗臺石該如何「選擇」,老師傅教你,輕鬆選擇不「踩坑」
    窗臺石該如何「選擇」,老師傅教你,輕鬆選擇不「踩坑」。裝修時我們在做室內裝飾時,喜歡把窗臺鋪上各種的石材。前兩天一位粉絲私信我說,它朋友勸它買窗臺石一定要買好的,劣質的容易風化開裂,更換起來超級麻煩。問我家中的窗臺石應該選擇哪種石材那,才不會像朋友說的那樣風化。
  • 不瞞你說,在分級閱讀上我也踩過不少坑
    你說我有啥?!一個才上小班。。。帥氣的兒子?!自卑!太自卑了!!後來我想通了,我是難得一見的養成系的啊!你看,我陪你們一起雞娃,娃跟你們娃一起長大,碰到好的資源就齊分享,踩過的坑也絕對不藏著掖著。所以,我就來聊聊,跟分級閱讀碰撞的兩年裡,我踩過的一些坑,你們的很多疑問可能就在我的這些坑裡。第一坑,貪多。我很慶幸,在剛開始接觸分級閱讀的時候,還不認識「現在的我」。
  • Deep Learning模型優化編譯器 TVM 踩坑記錄,強烈推薦!
    頭都炸了的我在打算手擼 OpenCL 調優之前,去問了下我們組的 CV 大神該怎麼辦,大神微微一笑,轉身隨風而去,只聽雲端傳來 3 個字:「T~V~M~~~~~"於是我就開始 TVM 的研究 (踩坑) 之路, 到今天為止終於把所有的路都踩平了之後,成功把我們的 Pytorch 模型用 Auto-TVM 調優成功且部署在了我們的 android 系統上,性能整整提高了
  • 作為新手媽媽我踩過的購物坑
    期待孩子的到來是一件非常幸福的事情,為了讓孩子不受委屈,可能每個待產媽媽都會像我一樣在生產前就為孩子準備我們所認為的必須品,信息的來源可能是網上、專家、甚至是產品的推銷廣告。但往往就是「不能讓孩子受委屈」這一個心理就讓我們踩中了無數的坑。
  • 借一個例子,說說我在做活動產品時踩過的坑
    大家好,本來已經寫了《一個例子,說說我在做活動產品時踩過的坑》上篇和下篇,以為該說的,該總結的都寫完了。但在這個活動結束後,一看數據發現了有很多可以復盤的東西,所以筆者就拿出來跟大家分享下。先來回顧一下活動流程:一.