1. 程式人生 > >字串的一些常用操作函式

字串的一些常用操作函式

1. 字串操作函式

  程式按功能劃分可分為數值運算、符號處理和I/O操作三類,符號處理程式佔相當大的比例,符號處理程式無處不在,編譯器、瀏覽器、Office套件等程式的主要功能都是符號處理。無論多複雜的符號處理都是由各種基本的字串操作組成的,本節介紹如何用C語言的庫函式做字串初始化、取長度、拷貝、連線、比較、搜尋等基本操作。

  1.1. 初始化字串

  #include <string.h>void *memset(void *s, int c, size_t n);返回值:s指向哪,返回的指標就指向哪memset函式把s所指的記憶體地址開始的n個位元組都填充為c的值。通常c的值為0,把一塊記憶體區清零。例如定義char buf[10];,如果它是全域性變數或靜態變數,則自動初始化為0(位於.bss段),如果它是函式的區域性變數,則初值不確定,可以用memset(buf, 0, 10)清零,由malloc分配的記憶體初值也是不確定的,也可以用memset清零。

  1.2. 取字串的長度

  #include <string.h>size_t strlen(const char *s);返回值:字串的長度strlen函式返回s所指的字串的長度。該函式從s所指的第一個字元開始找'/0'字元,一旦找到就返回,返回的長度不包括'/0'字元在內。例如定義char buf[] = "hello";,則strlen(buf)的值是5,但要注意,如果定義char buf[5] = "hello";,則呼叫strlen(buf)是危險的,會造成陣列訪問越界。

  1.3. 拷貝字串

  在第 1 節 “本章的預備知識”中介紹了strcpy和strncpy函式,拷貝以'/0'結尾的字串,strncpy還帶一個引數指定最多拷貝多少個位元組,此外,strncpy並不保證緩衝區以'/0'結尾。現在介紹memcpy和memmove函式。

  #include <string.h>void *memcpy(void *dest, const void *src, size_t n);void *memmove(void *dest, const void *src, size_t n);返回值:dest指向哪,返回的指標就指向哪memcpy函式從src所指的記憶體地址拷貝n個位元組到dest所指的記憶體地址,和strncpy不同,memcpy並不是遇到'/0'就結束,而是一定會拷貝完n個位元組。這裡的命名規律是,以str開頭的函式處理以'/0'結尾的字串,而以mem開頭的函式則不關心'/0'字元,或者說這些函式並不把引數當字串看待,因此引數的指標型別是void *而非char *。

  memmove也是從src所指的記憶體地址拷貝n個位元組到dest所指的記憶體地址,雖然叫move但其實也是拷貝而非移動。但是和memcpy有一點不同,memcpy的兩個引數src和dest所指的記憶體區間如果重疊則無法保證正確拷貝,而memmove卻可以正確拷貝。假設定義了一個數組char buf[20] = "hello world/n";,如果想把其中的字串往後移動一個位元組(變成"hhello world/n"),呼叫memcpy(buf + 1, buf, 13)是無法保證正確拷貝的:

  例 25.1. 錯誤的memcpy呼叫

  #include <stdio.h>#include <string.h>int main(void){  char buf[20] = "hello world/n";  memcpy(buf + 1, buf, 13);  printf(buf);  return 0;}

  在我的機器上執行的結果是hhhllooworrd。如果把程式碼中的memcpy改成memmove則可以保證正確拷貝。memmove可以這樣實現:

  void *memmove(void *dest, const void *src, size_t n){  char temp[n];  int i;  char *d = dest;  const char *s = src;  for (i = 0; i < n; i++)    temp[i] = s[i];  for (i = 0; i < n; i++)    d[i] = temp[i];  return dest;}藉助於一個臨時緩衝區temp,即使src和dest所指的記憶體區間有重疊也能正確拷貝。思考一下,如果不借助於臨時緩衝區能不能正確處理重疊記憶體區間的拷貝?

  用memcpy如果得到的結果是hhhhhhhhhhhhhh倒不奇怪,可為什麼會得到hhhllooworrd這個奇怪的結果呢?根據這個結果猜測的一種可能的實現是:

  void *memcpy(void *dest, const void *src, size_t n){  char *d = dest;  const char *s = src;  int *di;  const int *si;  int r = n % 4;  while (r--)    *d++ = *s++;  di = (int *)d;  si = (const int *)s;  n /= 4;  while (n--)    *di++ = *si++;  return dest;}在32位的x86平臺上,每次拷貝1個位元組需要一條指令,每次拷貝4個位元組也只需要一條指令,memcpy函式的實現儘可能4個位元組4個位元組地拷貝,因而得到上述結果。

  C99的restrict關鍵字

  我們來看一個跟memcpy/memmove類似的問題。下面的函式將兩個陣列中對應的元素相加,結果儲存在第三個陣列中。

  void vector_add(const double *x, const double *y, double *result){  int i;  for (i = 0; i < 64; ++i)    result[i] = x[i] + y[i];}如果這個函式要在多處理器的計算機上執行,編譯器可以做這樣的優化:把這一個迴圈拆成兩個迴圈,一個處理器計算i值從0到31的迴圈,另一個處理器計算i值從32到63的迴圈,這樣兩個處理器可以同時工作,使計算時間縮短一半。但是這樣的編譯優化能保證得出正確結果嗎?假如result和x所指的記憶體區間是重疊的,result[0]其實是x[1],result[i]其實是x[i+1],這兩個處理器就不能各幹各的事情了,因為第二個處理器的工作依賴於第一個處理器的最終計算結果,這種情況下編譯優化的結果是錯的。這樣看來編譯器是不敢隨便做優化了,那麼多處理器提供的並行性就無法利用,豈不可惜?為此,C99引入restrict關鍵字,如果程式設計師把上面的函式宣告為void vector_add(const double *restrict x, const double *restrict y, double *restrict result),就是告訴編譯器可以放心地對這個函式做優化,程式設計師自己會保證這些指標所指的記憶體區間互不重疊。

  由於restrict是C99引入的新關鍵字,目前Linux的Man Page還沒有更新,所以都沒有restrict關鍵字,本書的函式原型都取自Man Page,所以也都沒有restrict關鍵字。但在C99標準中庫函式的原型都在必要的地方加了restrict關鍵字,在C99中memcpy的原型是void *memcpy(void * restrict s1, const void * restrict s2, size_t n);,就是告訴呼叫者,這個函式的實現可能會做些優化,編譯器也可能會做些優化,傳進來的指標不允許指向重疊的記憶體區間,否則結果可能是錯的,而memmove的原型是void *memmove(void *s1, const void *s2, size_t n);,沒有restrict關鍵字,說明傳給這個函式的指標允許指向重疊的記憶體區間。在restrict關鍵字出現之前都是用自然語言描述哪些函式的引數不允許指向重疊的記憶體區間,例如在C89標準的庫函式一章開頭提到,本章描述的所有函式,除非特別說明,都不應該接收兩個指標引數指向重疊的記憶體區間,例如呼叫sprintf時傳進來的格式化字串和結果字串的首地址相同,諸如此類的呼叫都是非法的。本書也遵循這一慣例,除非像memmove這樣特別說明之外,都表示“不允許”。

  關於restrict關鍵字更詳細的解釋可以參考[BeganFORTRAN]。

  字串的拷貝也可以用strdup(3)函式,這個函式不屬於C標準庫,是POSIX標準中定義的,POSIX標準定義了UNIX系統的各種介面,包含C標準庫的所有函式和很多其它的系統函式,在第 2 節 “C標準I/O庫函式與Unbuffered I/O函式”將詳細介紹POSIX標準。

  #include <string.h>char *strdup(const char *s);返回值:指向新分配的字串這個函式呼叫malloc動態分配記憶體,把字串s拷貝到新分配的記憶體中然後返回。用這個函式省去了事先為新字串分配記憶體的麻煩,但是用完之後要記得呼叫free釋放新字串的記憶體。

  1.4. 連線字串

  #include <string.h>char *strcat(char *dest, const char *src);char *strncat(char *dest, const char *src, size_t n);返回值:dest指向哪,返回的指標就指向哪strcat把src所指的字串連線到dest所指的字串後面,例如:

  char d[10] = "foo";char s[10] = "bar";strcat(d, s);printf("%s %s/n", d, s);呼叫strcat函式後,緩衝區s的內容沒變,緩衝區d中儲存著字串"foobar",注意原來"foo"後面的'/0'被連線上來的字串"bar"覆蓋掉了,"bar"後面的'/0'仍保留。

  strcat和strcpy有同樣的問題,呼叫者必須確保dest緩衝區足夠大,否則會導致緩衝區溢位錯誤。strncat函式通過引數n指定一個長度,就可以避免緩衝區溢位錯誤。注意這個引數n的含義和strncpy的引數n不同,它並不是緩衝區dest的長度,而是表示最多從src緩衝區中取n個字元(不包括結尾的'/0')連線到dest後面。如果src中前n個字元沒有出現'/0',則取前n個字元再加一個'/0'連線到dest後面,所以strncat總是保證dest緩衝區以'/0'結尾,這一點又和strncpy不同,strncpy並不保證dest緩衝區以'/0'結尾。所以,提供給strncat函式的dest緩衝區的大小至少應該是strlen(dest)+n+1個位元組,才能保證不溢位。

  1.5. 比較字串

  #include <string.h>int memcmp(const void *s1, const void *s2, size_t n);int strcmp(const char *s1, const char *s2);int strncmp(const char *s1, const char *s2, size_t n);返回值:負值表示s1小於s2,0表示s1等於s2,正值表示s1大於s2memcmp從前到後逐個比較緩衝區s1和s2的前n個位元組(不管裡面有沒有'/0'),如果s1和s2的前n個位元組全都一樣就返回0,如果遇到不一樣的位元組,s1的位元組比s2小就返回負值,s1的位元組比s2大就返回正值。

  strcmp把s1和s2當字串比較,在其中一個字串中遇到'/0'時結束,按照上面的比較準則,"ABC"比"abc"小,"ABCD"比"ABC"大,"123A9"比"123B2"小。

  strncmp的比較結束條件是:要麼在其中一個字串中遇到'/0'結束(類似於strcmp),要麼比較完n個字元結束(類似於memcmp)。例如,strncmp("ABCD", "ABC", 3)的返回值是0,strncmp("ABCD", "ABC", 4)的返回值是正值。

  #include <strings.h>int strcasecmp(const char *s1, const char *s2);int strncasecmp(const char *s1, const char *s2, size_t n);返回值:負值表示s1小於s2,0表示s1等於s2,正值表示s1大於s2這兩個函式和strcmp/strncmp類似,但在比較過程中忽略大小寫,大寫字母A和小寫字母a認為是相等的。這兩個函式不屬於C標準庫,是POSIX標準中定義的。

  1.6. 搜尋字串

  #include <string.h>char *strchr(const char *s, int c);char *strrchr(const char *s, int c);返回值:如果找到字元c,返回字串s中指向字元c的指標,如果找不到就返回NULLstrchr在字串s中從前到後查詢字元c,找到字元c第一次出現的位置時就返回,返回值指向這個位置,如果找不到字元c就返回NULL。strrchr和strchr類似,但是從右向左找字元c,找到字元c第一次出現的位置就返回,函式名中間多了一個字母r可以理解為Right-to-left。

  #include <string.h>char *strstr(const char *haystack, const char *needle);返回值:如果找到子串,返回值指向子串的開頭,如果找不到就返回NULLstrstr在一個長字串中從前到後找一個子串(Substring) ,找到子串第一次出現的位置就返回,返回值指向子串的開頭,如果找不到就返回NULL。這兩個引數名很形象,在乾草堆haystack中找一根針needle,按中文的說法叫大海撈針,顯然haystack是長字串,needle是要找的子串。

  搜尋子串有一個顯而易見的演算法,可以用兩層的迴圈,外層迴圈把haystack中的每一個字元的位置依次假定為子串的開頭,內層迴圈從這個位置開始逐個比較haystack和needle的每個字元是否相同。想想這個演算法最多需要做多少次比較?其實有比這個演算法高效得多的演算法,有興趣的讀者可以參考[演算法導論]。

  1.7. 分割字串

  很多檔案格式或協議格式中會規定一些分隔符或者叫界定符(Delimiter) ,例如/etc/passwd檔案中儲存著系統的帳號資訊:

  $ cat /etc/passwdroot:x:0:0:root:/root:/bin/bashdaemon:x:1:1:daemon:/usr/sbin:/bin/shbin:x:2:2:bin:/bin:/bin/sh...每條記錄佔一行,也就是說記錄之間的分隔符是換行符,每條記錄又由若干個欄位組成,這些欄位包括使用者名稱、密碼、使用者id、組id、個人資訊、主目錄、登入Shell,欄位之間的分隔符是:號。解析這樣的字串需要根據分隔符把字串分割成幾段,C標準庫提供的strtok函式可以很方便地完成分割字串的操作。tok是Token的縮寫,分割出來的每一段字串稱為一個Token。

  #include <string.h>char *strtok(char *str, const char *delim);char *strtok_r(char *str, const char *delim, char **saveptr);返回值:返回指向下一個Token的指標,如果沒有下一個Token了就返回NULL引數str是待分割的字串,delim是分隔符,可以指定一個或多個分隔符,strtok遇到其中任何一個分隔符就會分割字串。看下面的例子。

  例 25.2. strtok

  #include <stdio.h>#include <string.h>int main(void){  char str[] = "root:x::0:root:/root:/bin/bash:";  char *token;  token = strtok(str, ":");  printf("%s/n", token);  while ( (token = strtok(NULL, ":")) != NULL)    printf("%s/n", token);  return 0;}

  $ ./a.outrootx0root/root/bin/bash結合這個例子,strtok的行為可以這樣理解:冒號是分隔符,把"root:x::0:root:/root:/bin/bash:"這個字串分隔成"root"、"x"、""、"0"、"root"、"/root"、"/bin/bash"、""等幾個Token,但空字串的Token被忽略。第一次呼叫要把字串首地址傳給strtok的第一個引數,以後每次呼叫第一個引數只要傳NULL就可以了,strtok函式自己會記住上次處理到字串的什麼位置(顯然這是通過strtok函式中的一個靜態指標變數記住的)。

  用gdb跟蹤這個程式,會發現str字串被strtok不斷修改,每次呼叫strtok把str中的一個分隔符改成'/0',分割出一個小字串,並返回這個小字串的首地址。

  (gdb) startBreakpoint 1 at 0x8048415: file main.c, line 5.Starting program: /home/akaedu/a.outmain () at main.c:55 {(gdb) n6   char str[] = "root:x::0:root:/root:/bin/bash:";(gdb)9   token = strtok(str, ":");(gdb) display str1: str = "root:x::0:root:/root:/bin/bash:"(gdb) n10    printf("%s/n", token);1: str = "root/000x::0:root:/root:/bin/bash:"(gdb)root11    while ( (token = strtok(NULL, ":")) != NULL)1: str = "root/000x::0:root:/root:/bin/bash:"(gdb)12      printf("%s/n", token);1: str = "root/000x/000:0:root:/root:/bin/bash:"(gdb)x11    while ( (token = strtok(NULL, ":")) != NULL)1: str = "root/000x/000:0:root:/root:/bin/bash:"剛才提到在strtok函式中應該有一個靜態指標變數記住上次處理到字串中的什麼位置,所以不需要每次呼叫時都把字串中的當前處理位置傳給strtok,但是在函式中使用靜態變數是不好的,以後會講到這樣的函式是不可重入的。strtok_r函式則不存在這個問題,它的內部沒有靜態變數,呼叫者需要自己分配一個指標變數來維護字串中的當前處理位置,每次呼叫時把這個指標變數的地址傳給strtok_r的第三個引數,告訴strtok_r從哪裡開始處理,strtok_r返回時再把新的處理位置寫回到這個指標變數中(這是一個Value-result引數)。strtok_r末尾的r就表示可重入(Reentrant),這個函式不屬於C標準庫,是在POSIX標準中定義的。關於strtok_r的用法Man Page上有一個很好的例子:

  例 25.3. strtok_r

  #include <stdio.h>#include <stdlib.h>#include <string.h>int main(int argc, char *argv[]){  char *str1, *str2, *token, *subtoken;  char *saveptr1, *saveptr2;  int j;  if (argc != 4) {    fprintf(stderr, "Usage: %s string delim subdelim/n",      argv[0]);    exit(EXIT_FAILURE);  }  for (j = 1, str1 = argv[1]; ; j++, str1 = NULL) {    token = strtok_r(str1, argv[2], &saveptr1);    if (token == NULL)      break;    printf("%d: %s/n", j, token);    for (str2 = token; ; str2 = NULL) {      subtoken = strtok_r(str2, argv[3], &saveptr2);      if (subtoken == NULL)        break;      printf(" --> %s/n", subtoken);    }  }  exit(EXIT_SUCCESS);}

  $ ./a.out 'a/bbb///cc;xxx:yyy:' ':;' '/'1: a/bbb///cc --> a --> bbb --> cc2: xxx --> xxx3: yyy --> yyya/bbb///cc;xxx:yyy:這個字串有兩級分隔符,一級分隔符是:號或;號,把這個字串分割成a/bbb///cc、xxx、yyy三個子串,二級分隔符是/,只有第一個子串中有二級分隔符,它被進一步分割成a、bbb、cc三個子串。由於strtok_r不使用靜態變數,而是要求呼叫者自己儲存字串的當前處理位置,所以這個例子可以在按一級分隔符分割整個字串的過程中穿插著用二級分隔符分割其中的每個子串。建議讀者用gdb的display命令跟蹤argv[1]、saveptr1和saveptr2,以理解strtok_r函式的工作方式。

  Man Page的BUGS部分指出了用strtok和strtok_r函式需要注意的問題:

  這兩個函式要改寫字串以達到分割的效果

  這兩個函式不能用於常量字串,因為試圖改寫.rodata段會產生段錯誤

  在做了分割之後,字串中的分隔符就被'/0'覆蓋了

  strtok函式使用了靜態變數,它不是執行緒安全的,必要時應該用可重入的strtok_r函式,以後再詳細介紹“可重入”和“執行緒安全”這兩個概念