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。
說明:指標是一種普通的變數,從訪問上沒有什麼不同於其他變數的特性。其儲存的數值是個整型資料,和整型變數不同的是,這個整型資料指向的是一段記憶體地址。
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不會溢位。程式中儘量使用這些安全的版本。良好的習慣是一個程式穩定與健壯的保證,而良好的習慣都是使用這些常用的函式養成的,所以一定要主要這些字串函式的使用。