“ 字符串是一種重要的數據類型,但是C語言並沒有顯式的字符串數據類型,因爲字符串以字符串常量的形式出現或者存儲於字符數組中。字符串常量適用於那些程序不會進行修改的字符串。所有其他字符串必須存儲於字符數組或動態分配的內存中。本文描述處理字符串和字符的庫函數,以及一些相關的,具有類似能力的函數。”
01
—
字符串基礎
首先,我們瞭解下字符串的基礎知識。字符串就是一串零個或多個字符,並且以一個位模式爲全0的NUL字節結尾。因此,字符串包含的字符內部不能出現NUL字節。這個限制很少會引起問題,因爲NUL字節並不存在與它相關聯的可打印字符,這也是它被選爲終止符的原因。NUL字節是字符串的終止符,但它本身並不是字符串的一部分,所以字符串的長度並不包括NUL字節。
02
—
字符串的長度
字符串的長度就是它所包含的字符個數。我們很容易通過對字符進行計數來計算字符串的長度,下面程序就是這樣做的。
#include <stddef.h>
size_t strlen(char const *string)
{
int length;
for (length = 0; *string++ != '\0'; )
length += 1;
return length;
}
這種實現方法說明了處理字符串所使用的的處理過程的類型。但是,事實上你極少需要編寫字符串函數,因爲標準庫所提供的函數通常能完成這些任務。不過,如果你還是希望自己編寫一個字符串函數,請注意標準保留了所有以str開頭的函數名,用於標準庫將來的拓展。
03
—
不受限制的字符串函數
最常用的字符串函數都是“不受限制”的,就是說它們只是通過尋找字符串參數結尾的NUL字節來判斷它的長度。這些函數一般都指定一塊內存用於存放結果字符串。在使用這些函數時,程序員必須保證結果字符串不會溢出這塊內存。這節將做詳細討論。
-
複製字符串
用於複製字符串的函數是strcpy,它的原型如下所示:
char *strcpy(char *dst, char const *src);
這個函數把參數src字符串複製到dst參數,如果src和dst在內存中出現重疊,其結果是未定義的。由於dst參數將進行修改,所以它必須是個字符數組或者是一個指向動態分配內存的指針,不能使用字符串常量。
目標參數的的以前內容將被覆蓋並丟失。即使新的字符串比dst原先的內存更短,由於新字符串是以NUL字節結尾的,所以老字符串最後剩餘的幾個字符也會被有效地刪除。
char messgae[] = "Original messgae";
...
strcpy(message, "Different");
順利執行strcpy之後,數組將包含下面內容:
|'D'|'i'|'f'|'e'|'r'|'e'|'n'|'t'|0|'e'|'s'|'s'|'a'|'g'|'e'|0|
第一個NUL字節後面的幾個字符無法被字符串函數訪問,因此從實際角度來看,它們已經是丟失的了。
警告:
你必須保證目標字符數組的空間足以容納需要複製的字符串,如果字符串比數組長,多餘的字符仍被複制,它們將覆蓋原先存儲於數組後面的內存空間的值,strcpy無法解決這個問題,因爲它無法判斷目標字符數組的長度。例如:
char message[] = "Original messgae";
...
strcpy(message, "A different message");
第二個字符串太長了,無法容納於message數組中。因此,strcpy函數將侵佔數組後面的部分內存空間,改寫原先恰好存儲在那裏的變量。如果你在使用這個函數前確保目標函數足以容納源字符串,就可以避免大量調試工作。
2.連接字符串
要想把一個字符串添加(連接)到另一個字符串的後面,你可以使用strcat函數。它的原型如下:
char *strcat(char *dst, char const *src);
strcat要求dst參數原先已經包含了一個字符串(可以是空字符串),它找到這個字符串的末尾,並把src字符串的一份拷貝添加到這個位置。如果src和dst的位置發生重疊,其結果是未定義的。
下面是它的常見用法,
char name[] = "Jim";
strcpy(message, "Hello ");
strcat(message, name);
strcat(message, ", how are you?");
每個strcat函數的字符串參數都被添加到原先存於message數組的字符串後面,其結果是下面這個字符串:
Hello Jim, how are you?
3.函數的返回值
strcpy和strcat都返回它們第一個參數的一份拷貝,就是一個指向目標字符數組的指針,由於它們都是返回這種類型的值,所以你可以嵌套地調用這些函數,如下所示:
strcat(strcpy(dst, a), b);
strcpy首先執行。它把字符串從a複製到dst並返回dst。然後這個返回值稱爲strcat函數的第一個參數,strcat把b添加到dst後面。
這種嵌套調用的風格較之下面這種可讀性更佳的風格在功能上並無優勢。
strcpy(dst, a);
strcat(dst, b);
事實上,這些函數的的絕大多數調用中,它們的返回值只是被簡單地忽略。
4.字符串比較
比較兩個字符串涉及對兩個字符串對應的字符逐個比較,直到發現不匹配爲止。那個最先不匹配的字符中較“小”(在字符集中的序數較小)的那個字符所在的字符串被認爲小於另外一個字符串。如果其中一個字符串是另一個字符串的前面一部分,那麼它也被認爲小於另外一個字符串,因爲它的NUL結尾字節出現得更早。這種比較被稱爲“詞典比較”,對於只包含大寫字母或只包含小寫字母的字符比較,這種比較過程所給出的結果總是和我們日常所用的字母順序的比較相同。
庫函數strcmp用於比較兩個字符串,它的原型如下:
int strcmp(char const *s1, char const *s2);
如果s1小於s2,函數返回一個小於零的值,如果s1大於s2,函數返回一個大於零的值。如果兩個字符串相等,函數就返回零。
04
—
長度受限的字符串函數
標準庫還包含了一些函數,它們以一種不同的方式處理字符串。這些函數接受一個顯式的長度參數,用於限定進行復制或比較的字符數。這些函數提供了一種方便的機制,可以防止難以預料的長字符從它們的目標數組溢出。
這些函數的原型如下,和它們不受限制的版本一樣,如果源參數與目標參數發生重疊,strncpy和strncat的結果就是未定義的。
char *strncpy(char *dst, char const *src, size_t len);
char *strncat(char *dst, char const *src, size_t len);
int strncmp(char const *s1, char const *s2, size_t len);
和strcpy一樣,strncpy把源字符串的字符複製到目標數組。然而,它總是正好向dst寫入len個字符。如果strlen(src)的值小於len,dst就用額外的NUL字符填充到len長度。如果strlen(src)的值大於等於len,那麼只有len個字符被複制到dst中。注意!它的結果將不會以NUL字節結尾。
警告:
strncpy調用的結果可能不是一個字符串,因爲字符串必須以NUL字節結尾。如果在一個需要字符串的地方(如strlen函數的參數)使用了一個不是以NUL字節結尾的字符序列,會發生什麼情況呢?strlen函數將無法知道NUL字節是沒有的,所以它會繼續查找,直到它發現一個NUL字節爲止。或許找了幾百個字符才找到,而strlen函數的這個返回值從本質上說是一個隨機數。或者,如果函數試圖訪問系統分配給這個程序以外的內存範圍,程序就會崩潰。
儘管strncat也是一個長度受限的函數,但它和strncpy存在不同之處。它從src中最多複製len個字符到目標數組後面。但是,strncat總是在結果字符串後面添加一個NUL字節,且它不會像strncpy那樣對目標數組用NUL字節進行填充。注意目標數組中原先的字符串並沒有算在strncat的長度中。strncat最多向目標數組複製len個字符(再加一個結尾的NUL字節),它不會管目標參數除去原先字符串後剩下的空間夠不夠。
最後,strncmp也用於比較兩個字符串,但它最多比較len個字節。如果兩個字符串在第len個字符之前存在不相等的字符,這個函數就像strcmp一樣停止比較,返回結果。如果兩個字符串的前len個字符相等,函數就返回零。
05
—
字符串查找
標準庫中存在許多函數,它們用各種不同的方法查找字符串。這樣各種各樣的工具給了碼手很大的靈活性。
1.查找一個字符
在一個字符串中查找一個特定字符最容易的方法是使用strchr和strrchr函數,它們的原型如下:
char *strchr(char const *str, int ch);
char *strrchr(char const *str, int ch);
注意它們的第2個參數是一個整型值。但是,它包含了一個字符值。strchr在字符串str中查找字符ch第1次出現的位置,找到後函數返回一個指向該位置的指針。如果該字符串中不存在該字符,函數就返回一個NULL指針。strrchr的功能和strchr基本一致,只是它返回的是一個指向該字符串該字符最後一次出現的位置。
2.查找任何幾個字符
strpbrk是個更爲常見的函數。它並不是查找某個特定的字符,而是查找任何一組字符第一次在字符串中出現的位置。它的原型如下:
char *strpbrk(char const *str, char const *group);
這個函數返回一個指向str中第一個匹配group中任何一個字符的字符位置。如果未找到匹配,函數返回一個NULL指針。
如下代碼:
char string[20] = "Hello there, honey.";
char *ans;
ans = strpbrk(string, "aeiou");
ans指向的位置是string+1,因爲這個位置是第二個參數中的字符第一次出現的位置。和前面一樣,這個函數也是區分大小寫的。
3.查找一個子串
爲了在字符串中查找一個子串,可以使用strstr函數,它的原型如下:
char *strstr(char const *s1, char const *s2);
這個函數s1中查找整個s2第一次出現的起始位置,並返回一個指向該位置的指針。如果s2並沒有完整地出現在s1的任何地方,函數將返回一個NULL指針。如果第二個參數是一個空字符串,函數就返回s1。
標準庫中並不存在strrstr和strrpbrk。不過,如果你需要它們,它們是很容易實現的。