C語言的本質(22)——C標準庫之字串操作

尹成發表於2014-07-17

 

編譯器、瀏覽器、Office套件等程式的主要功能都是符號處理,符號處理功能在程式中佔相當大的比例,無論多複雜的符號處理都是由各種基本的字串操作組成的,下面介紹如何用C語言的庫函式做字串初始化、取長度、拷貝、連線、比較、搜尋等基本操作。

 

1. 初始化字串

標頭檔案:string.h

函式原型:

void *memset(void *s, int c, size_t n);

memset函式將s所指向的某一塊記憶體中的前n個 位元組的內容全部設定為ch指定的ASCII值, 塊的大小由第三個引數指定,這個函式通常為新申請的記憶體做初始化工作, 其返回值為指向s的指標。

 例如定義char buf[10];,如果它是全域性變數或靜態變數,則自動初始化為0,如果它是函式的區域性變數,則初值不確定,可以用memset(buf, 0, 10)清零,由malloc分配的記憶體初值也是不確定的,也可以用memset清零。

 

#include<string.h>
#include<stdio.h>
 
int main(void)
{
   buf[]="Hello world!\n";
   printf("Buffer before memset:%s\n",buf);
   memset(buffer,'*',strlen(buf));
   printf("Buffer after memset:%s\n",buf);
   return0;
}

 

2. 取字串的長度

標頭檔案:string.h

函式原型:

size_t strlen(const char *s);

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

#include<string.h>
#include<stdio.h>
int main(void)
{
   char *s="Hello world!\n";
   printf("%s has %d chars",s,strlen(s));
   return0;
}


strlen與sizeof的區別

 

strlen(char*)函式求的是字串的實際長度,它求得方法是從開始到遇到第一個'\0',如果你只定義沒有給它賦初值,這個結果是不定的,它會從aa首地址一直找下去,直到遇到'\0'停止。

若char aa[10]; 則strlen(aa)結果是不定的

若char aa[10]={'\0'}; 則strlen(aa)則結果為0

若char aa[10]="jun"; 則strlen(aa)則結果為3

 

而sizeof()返回的是變數宣告後所佔的記憶體數,不是實際長度,此外sizeof不是函式,僅僅是一個操作符,strlen是函式。

sizeof(aa) 返回10

int a[10]; sizeof(a) 返回40

 

1、sizeof操作符的結果型別是size_t,它在標頭檔案中typedef為unsigned int型別。

該型別保證能容納實現所建立的最大物件的位元組大小。

2、sizeof是操作符(關鍵字),strlen是函式。

3、sizeof可以用型別做引數,strlen只能用char*做引數,且必須是以''\0''結尾的。

sizeof還可以用函式做引數,比如:

short f();
printf("%d\n",sizeof(f()));

輸出的結果是sizeof(short),即2。

4、陣列做sizeof的引數不退化,傳遞給strlen就退化為指標了。

5、大部分編譯程式 在編譯的時候就把sizeof計算過了是型別或是變數的長度這就是sizeof(x)可以用來定義陣列維數的原因

char str[20]="0123456789";

int a=strlen(str); //a=10;

int b=sizeof(str); //而b=20;

6、strlen的結果要在執行的時候才能計算出來,是用來計算字串的長度,不是型別佔記憶體的大小。

7、sizeof後如果是型別必須加括弧,如果是變數名可以不加括弧。這是因為sizeof是個操作符不是個函式。

8、當適用了於一個結構型別時或變數,sizeof 返回實際的大小,

當適用一靜態地空間陣列, sizeof 歸還全部陣列的尺寸。

sizeof 操作符不能返回動態地被分派了的陣列或外部的陣列的尺寸

9、陣列作為引數傳給函式時傳的是指標而不是陣列,傳遞的是陣列的首地址,如:

fun(char [8])
fun(char [])

都等價於 fun(char *)

3. 拷貝字串

前面的博文介紹了strcpy和strncpy函式,拷貝以'\0'結尾的字串,strncpy還帶一個引數指定最多拷貝多少個位元組,此外,strncpy並不保證緩衝區以'\0'結尾。現在介紹memcpy和memmove函式。

 

標頭檔案: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)是無法保證正確拷貝的:

 錯誤的memcpy呼叫

 

#include <stdio.h>
#include <string.h>
 
int main(void)
{
         charbuf[20] = "hello world\n";
         memcpy(buf+ 1, buf, 13);
         printf("%s",buf);
         return0;
}

執行結果:

hhelloo wold

 

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個位元組,才能保證不溢位。

  

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標準中定義的。

 

 

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是要找的子串。

 

7. 分割字串

標頭檔案:#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遇到其中任何一個分隔符就會分割字串。看下面的例子。

 

很多檔案格式或協議格式中會規定一些分隔符或者叫界定符(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。 

#include <stdio.h>
#include <string.h>
 
int main(void)
{
         charstr[] = "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);
        
         return0;
}

$ ./a.out
root
x
0
root
/root
/bin/bash


從"root:x::0:root:/root:/bin/bash:"這個例子可以看出,如果在字串開頭或結尾出現分隔符會被忽略,如果字串中連續出現兩個分隔符就認為是一個分隔符,而不會認為兩個分隔符中間有一個空字串的Token。第一次呼叫時把字串傳給strtok,以後每次呼叫時第一個引數只要傳NULL就可以了,strtok函式自己會記住上次處理到字串的什麼位置(顯然這是通過strtok函式中的一個靜態指標變數記住的)。

 

相關文章