1. 程式人生 > >linux C --深入理解字串處理函式 strlen() strcpy() strcat() strcmp()

linux C --深入理解字串處理函式 strlen() strcpy() strcat() strcmp()


    在linux C 程式設計中,我們經常遇到字串的處理,最多的就是字串的長度、拷貝字串、比較字串等;當然現在的C庫中為我們提供了很多字串處理函式。熟練的運用這些函式,可以減少程式設計工作量,這裡介紹幾個常用的字串函式,並編寫一些程式,如果沒有這些庫函式,我們將如何實現其功能;

1 求字串長度函式 strlen

標頭檔案:string.h

函式原型:size_t strlen(const char *s)

功能:求字串長度(不含字串結束標誌'\0')

如果沒有這個函式,我們如何實現strlen呢?

程式如下:

#include <stdio.h>
#include <string.h>

int mystrlen(const char *p)
{
	int i = 0;
	while(p[i])
		i++;

	return i;
}

int main()
{
	int len;
	char str[] = "Helloworld";
	len = mystrlen(str);
	printf("len = %d\n",len);

	return 0;
}

執行結果如下:

fs@ubuntu:~/qiang/string$ gcc -o strlen strlen.c
fs@ubuntu:~/qiang/string$ ./strlen
len = 10


同樣可以實現求字串長度功能。

既然在講strlen(),在這裡多說明一下,注意strlen()與sizeof()的區別:

sizeof和strlen有以下區別:
 sizeof是一個操作符,strlen是庫函式。
 sizeof的引數可以是資料的型別,也可以是變數,而strlen只能以結尾為‘\0‘的字串作引數。
 編譯器在編譯時就計算出了sizeof的結果。而strlen函式必須在執行時才能計算出來。並且sizeof計算的是資料型別佔記憶體的大小,而strlen計算的是字串實際的長度。

 陣列做sizeof的引數不退化,傳遞給strlen就退化為指標了。
注意:有些是操作符看起來像是函式,而有些函式名看起來又像操作符,這類容易混淆的名稱一定要加以區分,否則遇到陣列名這類特殊資料型別作引數時就很容易出錯。最容易混淆為函式的操作符就是sizeof。
說明:指標是一種普通的變數,從訪問上沒有什麼不同於其他變數的特性。其儲存的數值是個整型資料,和整型變數不同的是,這個整型資料指向的是一段記憶體地址。

2、字串拷貝函式strcpy()

標頭檔案:string.h

函式原型:char *strcpy(char *dest,const char *src)

功能: 字串拷貝

引數:src為源串的起始地址,dest為目標串的起始地址

如果沒有這個函式,我們將如何實現呢?程式如下:

#include <stdio.h>

char *mystrcpy(char *dest,const char *src)
{
	char *p;
	p = dest;
	while(*src)
	{
		*dest++ = *src++;
	}
	*dest = '\0';

	return p;
}

int main()
{
	const char str1[] = "Helloworld";
	char str2[30];
	mystrcpy(str2,str1);
	printf("str2 = %s\n",str2);

	return 0;
}

執行結果如下:

fs@ubuntu:~/qiang/string$ ./strcpy 
str2 = Helloworld


同樣能夠得到結果,當然有了strcpy()會很方便;

3、字串連線接函式strcat

標頭檔案:string.h

函式原型:char  *strcat(char *dest,const char *src)

功能:把字串src連線到字串dest的後面

實現方法:

#include <stdio.h>

char *mystrcat(char *dest,const char *src)
{
	char *p;
	p = dest;
	while(*dest)
		dest++;
	while(*src)
	{
		*dest++ = *src++;
	}

	*dest = '\0';

	return p;
}

int main()
{	
	char str1[] = "hello";
	char str2[] = "world";
	mystrcat(str1,str2);

	printf("str1 = %s\n",str1);

	return 0;

}

執行結果如下:

fs@ubuntu:~/qiang/string$ gcc -o strcat strcat.c
fs@ubuntu:~/qiang/string$ ./strcat
str1 = helloworld

在使用strcat函式時,需要注意,目標陣列應該有足夠的空間,連線源串。注意,目標字串'\0'被刪除,然後連線源串。如果越界會發生什麼呢?我們可以來驗證一下:

#include <stdio.h>
#include <string.h>

int main()
{
	char str2[6] = "world";
	char str1[6] = "hello";
	strcat(str1,str2);
	printf("str1 = %s\n",str1);

	return 0;
}

執行結果如下:

fs@ubuntu:~/qiang/string$ gcc -o strcat1 strcat1.c 
fs@ubuntu:~/qiang/string$ ./strcat1
str1 = helloworld
*** stack smashing detected ***: ./strcat1 terminated
======= Backtrace: =========
/lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x45)[0xb76cbd95]
/lib/i386-linux-gnu/libc.so.6(+0x103d4a)[0xb76cbd4a]
./strcat1[0x80484d7]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0xb75e14d3]
./strcat1[0x80483d1]
======= Memory map: ========
08048000-08049000 r-xp 00000000 08:01 830810     /home/fs/qiang/string/strcat1

當然,下面還有好多,這裡就不展示了,我們來看看結果,helloworld能打印出來,編譯時也沒錯誤,但執行時告知溢位了。

我們稍微修改程式,大家注意區別:

#include <stdio.h>
#include <string.h>

int main()
{
	char str1[6] = "hello";
	char str2[6] = "world";
	strcat(str1,str2);
	printf("str1 = %s\n",str1);
	printf("str2 = %s\n",str2);
	
	return 0;
}

輸出結果如下:

fs@ubuntu:~/qiang/string$ gcc -o strcat1 strcat1.c 
fs@ubuntu:~/qiang/string$ ./strcat1
str1 = helloworld
str2 = orld

並沒有溢位!(實際是溢位了!!!!!!!)

是不是很有意思?大家看看兩者程式的區別,只是六七行交換了位置,但一個溢位,一個卻不溢位,為什麼呢?大家應該知道資料儲存的方式吧,第一個程式中:

char str2[6] = "world";
char str1[6] = "hello";
	

先定義了str2[],程式為其分配了一段連續的空間,接著定義了str1[],程式會在剛才為str2[]定義的地址後面接著定義一段連續空間,如果接著將str2 接在str1後面,str1原來只定義了6個位元組,需要連線在一起需要11個位元組,肯定超出了我們定義的地址空間,後面是一片未知區域,會發生溢位;但為什麼程式2卻沒有溢位呢?

char str1[6] = "hello";
char str2[6] = "world";

這裡就比較巧了,因為str2是在str1後面定義的,str2接在str1後面確實會溢位,但溢位後的一片空間,正好是str2的地址空間,區域是可知的,只是helloworld覆蓋掉了原來str2的東西。所以不會溢位,這樣說,大家明白吧?

繼續看,大家有木有發現,我在程式二中對str2的值進行了列印,不再是原來的world,變成了orld,按道理來講,str2的值不會改變的啊?大家在這裡要清楚str2只是一個地址而已,只負責輸出當前地址以後的字串,以'\0'結束;所以這裡的orld是strcat(str1,str2)後str1的值,但為什麼是“orld”呢?因為目標字串的'\0'被刪除,然後連線串;

此時str1後面的orld覆蓋了原world,但str2原來指向的是w的地址,現在原存放'w'的地址處存放的是'o',所以會輸出"orld"!大家是否還有疑問,後面好像還有個'd'沒有被覆蓋啊,為什麼輸出的不是"orldd"呢?大家應該明白字串有個結束符'\0'吧,它將'd'覆蓋了,如果大家覺得不好理解,可以畫一下圖,就比較清楚了;

其實這只是個特例,讓大家看一下如果資料溢位造成的後果!

4、字串比較函式strcmp

標頭檔案:string.h

函式原型:int strcmp(const char *s1,const char *s2)

功能:按照ASCII碼順序比較字串s1和字串s2的大小

如果沒有這個函式,我們如下實現:

#include <stdio.h>

int mystrcmp(const char *s1,const char *s2)
{
	int i = 0;
	while(*s1 || *s2)
	{
		if(*s1 > *s2)
		{
			return 1;
		}
		else if(*s1 < *s2)
		{
			return -1;
		}
		else
		{
			s1++;
			s2++;
		}	
	}
	return 0;
}

int main()
{
	int n;
	char str1[] = "hell";
	char str2[] = "hello";
	n = mystrcmp(str1,str2);
	printf("n = %d\n",n);

	return 0;
}

執行結果如下:

fs@ubuntu:~/qiang/string$ gcc -o strcmp strcmp.c 
fs@ubuntu:~/qiang/string$ ./strcmp
n = -1

剛才提到的函式功能:比較兩字串的大小,好像比較抽象,我們其實是比較兩個字串是否相等;下面我們看個題目:

題目:計算字串中子串出現的次數

什麼意思呢?就是helloworldhehehehellowo中,比如說子串"hello"在字串中出現的次數,如果單純的用getchar()獲取每個字元並比較,會很麻煩,在這裡我們可以用strcmp來實現,會很方便,大家可以看看strcmp的具體應用,實現程式如下:

#include <stdio.h>
#include <string.h>

int main()
{
	int i = 0;
	int count = 0;
	int len1,len2;
	char str1[100] = {'\0'};
	char str2[20] = {'\0'};
	printf("Please input two strings!\n");
	scanf("%s%s",str1,str2);
	len1 = strlen(str1);
	len2 = strlen(str2);

	while(i + len2 <= len1)
	{
		if(!(strncmp(&str1[i],str2,len2)))
		{
			count++;
			i += len2;
		}
		else
			i++;
	}
	
	printf("count = %d\n",count);

	return 0;
}

執行結果如下:

fs@ubuntu:~/qiang/string$ ./zichuan 
Please input two strings!
xiaoqiangxiqiangxiaoxiaqiang
xiao
count = 2
fs@ubuntu:~/qiang/string$ 


大家看看結果是不是正確的。

附:(轉載)

    strcmp 字串比較函式,strcpy 字串拷貝函式, strlen 字串測長函式, strcat字串連線函式,sprintf格式化字串拷貝函式等等。因為字串就是以‘\0’結束的一段記憶體,這些函式實質上也就是操作記憶體的函式,所以避免不了的與指標打交道,使得這些函式充滿了陷阱,如果這些函式使用不當,很有可能在程式中埋伏下危險的陷阱,使程式的穩定性遭受重創。下面我就字串使用中一些常見的問題來進行舉例說明。

一. strcpy:極度危險的函式,一不小心就會中招,危險指數:四星

       strcpy的原型是這樣的: char *strcpy(char *dest, const char *src) 作為常見的字串複製函式,C庫中的實現是不安全的,因為它不做字串的檢查,以至於如果引數傳入了非法指標,比如:src不是指向字串的指標。後果就不堪設想,程式會一直複製,直到遇到‘\0’才結束,這樣很有可能就會使得dest指向的記憶體區域緩衝區溢位,使得導致不程式相干的部分出現錯誤,這種錯誤也許就是致命的。所以使用這個函式一定確保第二個引數傳入合法的指標。
例子:

#include <string.h>
#include <stdlib.h>
#include <stdio.h>

char dest[5] = {'D'};
char mydata[7] = {'m','y','d','a','t','a','\0'};

int main(void)
{
        char i;
        char source[5];
        char bound[5] = {'&','&','&','&','&'};

        for (i = 0; i < 5; i++)
                source[i] = 'S';

        printf("before strcopy, mydata is %s\n", mydata);
        strcpy(dest, source);
        printf("dest is %s\n", dest);
        printf("after strcopy, mydata is %s\n", mydata);

}

  程式中定義了兩個全域性陣列,我們知道C語言的全域性變數要放在DATA段,而dest與mydata因為定義相連,所以其記憶體地址是相鄰的。程式的目的是複製一個字串到dest陣列,而程式中忘了給source陣列最後加上'\0'。所以source就不是一個字串,用它傳遞給strcpy就會造成意想不到的後果。本程式中strcpy一直複製記憶體到dest,直到在遇到‘\0’, 這樣就會多複製很多資料到dest,從而意外的覆蓋mydata,甚至有時還會導致程式崩潰。在拷貝之前, mydata的資料是 "mydata", 而在拷貝之後造成了意外的修改。

二. strcat 造成緩衝區溢位的隱形殺手,危險指數 三星

        strcat 是將一個字串連線到另外一個字串上,其函式原型為char *strcat(char *dest,char *src)。這個函式也很危險,因為C語言的實現也是不安全的,傳入非法的指標有可能會造成程式的崩潰。首先保證兩個指標都應該指向字串,其次dest指標指向的空間要足以容的下src指向的字串,否則會造成緩衝區溢位而破壞其他程式資料。
例子1:

#include <string.h>
#include <stdlib.h>
#include <stdio.h>

char dest[5] = {'D', '\0'};
char mydata[7] = {'m','y','d','a','t','a','\0'};

int main(void)
{
        char i;
        char source[5];
        char bound[5] = {'&','&','&','&','&'};

        for (i = 0; i < 4; i++)
                source[i] = 'S';
        source[4] = '\0';
        printf("before strcat, mydata is %s\n", mydata);
        strcat(dest, source);
        printf("dest is %s\n", dest);
        printf("after strcat, mydata is %s\n", mydata);

}

這個例子因為目標dest只有5個位元組大小,並且資料佔了一個位元組,只剩下四個位元組位置,而源資料字串長度為4個字元加一個‘\0’有五個位元組大小,所以會多出一個位元組覆蓋了mydata的資料,多出的‘\0’成為了mydata的第一個位元組,導致呼叫strcat後輸出mydata為空。
例子2 :

#include <string.h>
#include <stdlib.h>
#include <stdio.h>

char dest[5] = {'D', 'D', 'D', 'D', 'D'};
char mydata[7] = {'m','y','d','a','t','a','\0'};

int main(void)
{
        char i;
        char source[5];
        char bound[5] = {'&','&','&','&','&'};

        for (i = 0; i < 4; i++)
                source[i] = 'S';
        source[4] = '\0';
        printf("before strcat, mydata is %s\n", mydata);
        strcat(dest, source);
        printf("dest is %s\n", dest);
        printf("after strcat, mydata is %s\n", mydata);

}

    這個例子中,dest不是字串(沒有‘\0’結尾),導致strcat從地址dest處開始找'\0',找到'\0'後並在此地址上覆制source的資料,在本程式中就將source連線到了mydata後面,導致mydata變成了“mydataSSSS”,這樣也破壞了程式無關的資料,本程式中還好是破壞的DATA中的資料,如果是其他的資料那麼後果不不僅僅是資料改變這麼簡單了。

三.  strlen 很多malloc函式緩衝區溢位問題的始作俑者 危險指數 二星

     strlen是字串求長函式,但是它求出的長度不包括‘\0’,所以在用malloc分配記憶體的時候,很容易少分配一個位元組,就這小小的一個位元組就會造成緩衝區溢位,我們知道malloc分配的記憶體區域是有一個頭的,這樣就有可能破壞其他malloc的頭使得記憶體釋放失敗,帶來一系列連鎖反映。因為malloc函式的實現與系統有關,這個不好用程式模擬,但是這種情況確實存在。因此如果用strlen求字串長度用於malloc一定要記住要加1。

四.  sprintf 同樣可以造成緩衝區溢位,危險指數 一星

     sprintf是格式化字元拷貝函式,函式原型是int sprintf( char *buffer, const char *format, … ) 。這個函式的實現也是不安全的,使用這個函式要確保buffer足夠大,否則這個函式在不做任何提示的情況下就將buffer溢位,這個函式雖然返回複製的位元組數,可以通過這個檢查複製了多少個位元組,以確定是否緩衝區溢位。但這種亡羊補牢的做法其實沒有實際意義。緩衝區溢位的錯誤已經發生也許會是程式崩潰,檢測的時間也許都沒有,就算有檢測時間,也只是用於提示程式的BUG,在正式的程式中沒有多大用處。
例子: 

#include <string.h>
#include <stdlib.h>
#include <stdio.h>

char dest[2] = {'D'};
char mydata[7] = {'m','y','d','a','t','a','\0'};

int main(void)
{
        char i;
        char source[5];
        char bound[5] = {'&','&','&','&','&'};

        for (i = 0; i < 4; i++)
                source[i] = 'S';
        source[4] = '\0';
        printf("before sprintf, mydata is %s\n", mydata);
        sprintf(dest, "%s", source);
        printf("dest is %s\n", dest);
        printf("after sprintf, mydata is %s\n", mydata);

}

 這個例子中,目標緩衝區只有兩個位元組的大小,而源字串卻是五個位元組,sprintf在不進行任何提示的情況下,默默的覆蓋了mydata的資料。
總結      

        總上所述,C語言字串操作函式一般都不對引數做檢查,需要呼叫者確保引數的合法性。如果傳入不正確的引數,就會造成緩衝區溢位。輕則資料被修改,重則程式崩潰。最鬱悶的是影響到程式中不相關的部分。我前面舉的例子都很簡單,很容易一眼看出問題的所在,但是大型程式就不會這麼簡單了,這些錯誤就是致命的。所以使用C語言的字串函式時一定要養成良好的習慣,自己檢查引數的合法性,然後再呼叫。目前C語言中這些字串操作函式都有一些安全的版本就是帶n的系列,比如:strncpy,strncat,snprintf。這些函式規定了源字串的大小,對緩衝區溢位的預防有一定的作用,比如:snprintf,其函式原型是int snprintf(char *str, size_t size, const char *format, ...) 第二個引數size,可以保證複製size個位元組,如果要複製的字串大於size就會截短,從而保證str不會溢位。程式中儘量使用這些安全的版本。良好的習慣是一個程式穩定與健壯的保證,而良好的習慣都是使用這些常用的函式養成的,所以一定要主要這些字串函式的使用。