字串操作函式
1. 字串操作函式 請點評
程式按功能劃分可分為數值運算、符號處理和I/O操作三類,符號處理程式佔相當大的比例,符號處理程式無處不在,編譯器、瀏覽器、Office套件等程式的主要功能都是符號處理。無論多複雜的符號處理都是由各種基本的字串操作組成的,本節介紹如何用C語言的庫函式做字串初始化、取長度、複製、連線、比較、搜尋等基本操作。
1.1. 初始化字串 請點評
#includevoid *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. 取字串的長度 請點評
#includesize_t strlen(const char *s); 返回值:字串的長度
strlen
函式返回s
所指的字串的長度。該函式從s
所指的第一個字元開始找''
字元,一旦找到就返回,返回的長度不包括''
字元在內。例如定義char buf[] = "hello";
,則strlen(buf)
的值是5,但要注意,如果定義char buf[5] = "hello";
,則呼叫strlen(buf)
是危險的,會造成陣列訪問越界。
1.3. 複製字串 請點評
在第 1 節 “本章的預備知識”中介紹了strcpy
和strncpy
函式,複製以''
結尾的字串,strncpy
還帶一個引數指定最多複製多少個位元組,此外,strncpy
並不保證緩衝區以''
結尾。現在介紹memcpy
和memmove
函式。
#includevoid *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
並不是遇到''
就結束,而是一定會複製完n
個位元組。這裡的命名規律是,以str
開頭的函式處理以''
結尾的字串,而以mem
開頭的函式則不關心''
字元,或者說這些函式並不把引數當字串看待,因此引數的指標型別是void *
而非char *
。
memmove
也是從src
所指的記憶體地址複製n
個位元組到dest
所指的記憶體地址,雖然叫move但其實也是複製而非移動。但是和memcpy
有一點不同,memcpy
的兩個引數src
和dest
所指的記憶體區間如果重疊則無法保證正確複製,而memmove
卻可以正確複製。假設定義了一個陣列char buf[20] = "hello worldn";
,如果想把其中的字串往後移動一個位元組(變成"hhello worldn"
),呼叫memcpy(buf + 1, buf, 13)
是無法保證正確複製的:
例 25.1. 錯誤的memcpy呼叫
#include#include int main(void) { char buf[20] = "hello worldn"; memcpy(buf + 1, buf, 13); printf("%s", 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個位元組也只需要一條指令,為了提高複製的效率,我們先處理完零頭然後4個位元組4個位元組地複製。注意這個實現並不正確,把void *
指標轉成int *
指標來訪問應該考慮對齊的問題,請讀者自己實現一個更完善的版本。
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標準。
#includechar *strdup(const char *s); 返回值:指向新分配的字串
這個函式呼叫malloc
動態分配記憶體,把字串s
複製到新分配的記憶體中然後返回。用這個函式省去了事先為新字串分配記憶體的麻煩,但是用完之後要記得呼叫free
釋放新字串的記憶體。
1.4. 連線字串 請點評
#includechar *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 %sn", d, s);
呼叫strcat
函式後,緩衝區s
的內容沒變,緩衝區d
中儲存著字串"foobar"
,注意原來"foo"
後面的''
被連線上來的字串"bar"
覆蓋掉了,"bar"
後面的''
仍保留。
strcat
和strcpy
有同樣的問題,呼叫者必須確保dest
緩衝區足夠大,否則會導致緩衝區溢位錯誤。strncat
函式透過引數n
指定一個長度,就可以避免緩衝區溢位錯誤。注意這個引數n
的含義和strncpy
的引數n
不同,它並不是緩衝區dest
的長度,而是表示最多從src
緩衝區中取n
個字元(不包括結尾的''
)連線到dest
後面。如果src
中前n
個字元沒有出現''
,則取前n
個字元再加一個''
連線到dest
後面,所以strncat
總是保證dest
緩衝區以''
結尾,這一點又和strncpy
不同,strncpy
並不保證dest
緩衝區以''
結尾。所以,提供給strncat
函式的dest
緩衝區的大小至少應該是strlen(dest)+n+1
個位元組,才能保證不溢位。
1.5. 比較字串 請點評
#includeint 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大於s2
memcmp
從前到後逐個比較緩衝區s1
和s2
的前n
個位元組(不管裡面有沒有''
),如果s1
和s2
的前n
個位元組全都一樣就返回0,如果遇到不一樣的位元組,s1
的位元組比s2
小就返回負值,s1
的位元組比s2
大就返回正值。
strcmp
把s1
和s2
當字串比較,在其中一個字串中遇到''
時結束,按照上面的比較準則,"ABC"
比"abc"
小,"ABCD"
比"ABC"
大,"123A9"
比"123B2"
小。
strncmp
的比較結束條件是:要麼在其中一個字串中遇到''
結束(類似於strcmp
),要麼比較完n
個字元結束(類似於memcmp
)。例如,strncmp("ABCD", "ABC", 3)
的返回值是0,strncmp("ABCD", "ABC", 4)
的返回值是正值。
#includeint 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. 搜尋字串 請點評
#includechar *strchr(const char *s, int c); char *strrchr(const char *s, int c); 返回值:如果找到字元c,返回字串s中指向字元c的指標,如果找不到就返回NULL
strchr
在字串s
中從前到後查詢字元c
,找到字元c
第一次出現的位置時就返回,返回值指向這個位置,如果找不到字元c
就返回NULL
。strrchr
和strchr
類似,但是從右向左找字元c
,找到字元c
第一次出現的位置就返回,函式名中間多了一個字母r可以理解為Reverse。
#includechar *strstr(const char *haystack, const char *needle); 返回值:如果找到子串,返回值指向子串的開頭,如果找不到就返回NULL
strstr
在一個長字串中從前到後找一個子串(Substring),找到子串第一次出現的位置就返回,返回值指向子串的開頭,如果找不到就返回NULL。這兩個引數名很形象,在乾草堆haystack
中找一根針needle
,按中文的說法叫大海撈針,顯然haystack
是長字串,needle
是要找的子串。
搜尋子串有一個顯而易見的演算法,可以用兩層的迴圈,外層迴圈把haystack
中的每一個字元的位置依次假定為子串的開頭,內層迴圈從這個位置開始逐個比較haystack
和needle
的每個字元是否相同。想想這個演算法最多需要做多少次比較?其實有比這個演算法高效得多的演算法,有興趣的讀者可以參考[演算法導論]。
1.7. 分割字串 請點評
很多檔案格式或協議格式中會規定一些分隔符或者叫界定符(Delimiter),例如/etc/passwd
檔案中儲存著系統的帳號資訊:
$ cat /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/bin/sh bin:x:2:2:bin:/bin:/bin/sh ...
每條記錄佔一行,也就是說記錄之間的分隔符是換行符,每條記錄又由若干個欄位組成,這些欄位包括使用者名稱、密碼、使用者id、組id、個人資訊、主目錄、登入Shell,欄位之間的分隔符是:號。解析這樣的字串需要根據分隔符把字串分割成幾段,C標準庫提供的strtok
函式可以很方便地完成分割字串的操作。tok是Token的縮寫,分割出來的每一段字串稱為一個Token。
#includechar *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#include int main(void) { char str[] = "root:x::0:root:/root:/bin/bash:"; char *token; token = strtok(str, ":"); printf("%sn", token); while ( (token = strtok(NULL, ":")) != NULL) printf("%sn", token); return 0; }
$ ./a.out root x 0 root /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
中的一個分隔符改成''
,分割出一個小字串,並返回這個小字串的首地址。
(gdb) start Breakpoint 1 at 0x8048415: file main.c, line 5. Starting program: /home/akaedu/a.out main () at main.c:5 5 { (gdb) n 6 char str[] = "root:x::0:root:/root:/bin/bash:"; (gdb) 9 token = strtok(str, ":"); (gdb) display str 1: str = "root:x::0:root:/root:/bin/bash:" (gdb) n 10 printf("%sn", token); 1: str = "root00x::0:root:/root:/bin/bash:" (gdb) root 11 while ( (token = strtok(NULL, ":")) != NULL) 1: str = "root00x::0:root:/root:/bin/bash:" (gdb) 12 printf("%sn", token); 1: str = "root00x00:0:root:/root:/bin/bash:" (gdb) x 11 while ( (token = strtok(NULL, ":")) != NULL) 1: str = "root00x00: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#include #include 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 subdelimn", 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: %sn", j, token); for (str2 = token; ; str2 = NULL) { subtoken = strtok_r(str2, argv[3], &saveptr2); if (subtoken == NULL) break; printf(" --> %sn", subtoken); } } exit(EXIT_SUCCESS); }
$ ./a.out 'a/bbb///cc;xxx:yyy:' ':;' '/' 1: a/bbb///cc --> a --> bbb --> cc 2: xxx --> xxx 3: yyy --> yyy
a/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
段會產生段錯誤在做了分割之後,字串中的分隔符就被
''
覆蓋了strtok
函式使用了靜態變數,它不是執行緒安全的,必要時應該用可重入的strtok_r
函式,以後再詳細介紹“可重入”和“執行緒安全”這兩個概念
習題 請點評
1、出於練習的目的,strtok
和strtok_r
函式非常值得自己動手實現一遍,在這個過程中不僅可以更深刻地理解這兩個函式的工作原理,也為以後理解“可重入”和“執行緒安全”這兩個重要概念打下基礎。
2、解析URL中的路徑和查詢字串。動態網頁的URL末尾通常帶有查詢,例如:
比如上面第一個例子,是路徑部分,?號後面的
complete=1&hl=zh-CN&ie=GB2312&q=linux&meta=
是查詢字串,由五個“key=value”形式的鍵值對(Key-value Pair)組成,以&隔開,有些鍵對應的值可能是空字串,比如這個例子中的鍵meta
。
現在要求實現一個函式,傳入一個帶查詢字串的URL,首先檢查輸入格式的合法性,然後對URL進行切分,將路徑部分和各鍵值對分別傳出,請仔細設計函式介面以便傳出這些字串。如果函式中有動態分配記憶體的操作,還要另外實現一個釋放記憶體的函式。完成之後,為自己設計的函式寫一個Man Page。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/24790158/viewspace-1042844/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- T-SQL——函式——字串操作函式SQL函式字串
- Js字串操作函式大全JS字串函式
- 手撕字串操作函式字串函式
- 2020.12.04 重寫字串操作函式字串函式
- Lesson12——NumPy 字串函式之 Part1:字串操作函式字串函式
- Python學習-字串函式操作1Python字串函式
- Python學習-字串函式操作3Python字串函式
- MySQL(四)日期函式 NULL函式 字串函式MySql函式Null字串
- Oracle 字串函式Oracle字串函式
- Oracle 字串函式Oracle字串函式
- 字串函式 metaphone ()字串函式
- 字串函式 print ()字串函式
- 字串函式 explode ()字串函式
- 字串函式 ord ()字串函式
- 字串函式 ltrim ()字串函式
- 字串函式 levenshtein ()字串函式
- 字串函式 lcfirst ()字串函式
- 字串函式 implode ()字串函式
- 字串函式 fprintf ()字串函式
- 字串函式 htmlentities ()字串函式HTML
- 字串函式 htmlspecialchars ()字串函式HTML
- PHP字串函式PHP字串函式
- MySQL 字串函式:字串擷取MySql字串函式
- PHP 每日一函式 — 字串函式 crypt ()PHP函式字串
- PHP 每日一函式 — 字串函式 chr ()PHP函式字串
- PHP 每日一函式 — 字串函式 addslashes ()PHP函式字串
- PHP 每日一函式 — 字串函式 addcslashes ()PHP函式字串
- MySQL函式學習(一)-----字串函式MySql函式字串
- python函式教程:Python 字串操作(string替換、擷取等)Python函式字串
- MySQL函式大全(字串函式,數學函式,日期函式,系統級函式,聚合函式)MySql函式字串
- Lesson14——NumPy 字串函式之 Par3:字串資訊函式字串函式
- PHP 每日一函式 — 字串函式 chunk_split ()PHP函式字串
- PHP 每日一函式 — 字串函式 crc32 ()PHP函式字串
- PHP 每日一函式 — 字串函式 count_chars ()PHP函式字串
- 字串函式 md5 ()字串函式
- 字串函式學習三字串函式
- 字串函式學習二字串函式
- 字串函式學習一字串函式
- 字串函式 parse_str ()字串函式