程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 關於C語言字符串函數的思考

關於C語言字符串函數的思考

編輯:關於C語言

C語言並不是一種很方便的語言,它的字符串就是一例。按照C語言的定義,“字符串就是一段內存空間,裡面包含ASCII字符,並且,以“\0”結尾,總共能存放n-1個字符。”按照這個描述,字符串處理確實很麻煩,還很容易出錯。

為了方便用戶,C語言標准庫向用戶提供了一些字符串函數,如字符串拷貝、構造、清空等函數,在一定程度上方便了用戶的使用。但是,我無意中發現,這些函數還是有些隱患的。

事情很簡單,我注意到我寫的一些程序,老是有內存讀寫錯誤,但是,經過仔細檢查我所有的數據Buffer,以及相關的處理函數,又沒有找到什麼錯誤。於是我把懷疑的目光投向我常用的一些字符串處理函數上,如strcpy、sprintf等。在經過幾次仔細地跟蹤之後,我發現內存錯誤出自於此。於是,我開始研究如何安全地使用字符串這個話題。

1.字符串拷貝函數

1.1 不安全的strcpy

首先,我寫了這樣一個測試函數:

     void strcpyTest0()
    {
    int i;
    char szBuf[128];
    for(i=0;i<128;i++) szBuf[i]='*';
    szBuf[127]='\0';     //構造一個全部是*的字符串
    char szBuf2[256];
    for(i=0;i<256;i++) szBuf2[i]='#';
    szBuf2[255]='\0';   //構造一個全部是#的字符串
    strcpy(szBuf,szBuf2);
    printf("%s\n",szBuf);
    }

很簡單,把一個字符串拷貝到另外一個空間,但是,很不幸,源字符串比目標地址要長,因此,程序很悲慘地死去了。

1.2 還是不安全的strncpy

通過上例,我發現我需要在拷貝時多輸入一個參數,來標明目的地址有多長,檢查C語言的庫函數說明,有一個strncpy可以達到這個目的,這個函數的原型如下:

char *strncpy( char *strDest, const char *strSource, size_t count );

好了,這下我們的問題解決了,我寫出了如下代碼:

 void strcpyTest1()
{
       int i;
       char szBuf[128];
       for(i=0;i<128;i++) szBuf[i]='*';
       szBuf[127]='\0';
       char szBuf2[256];
       for(i=0;i<256;i++) szBuf2[i]='#';
       szBuf2[255]='\0';
       strncpy(szBuf,szBuf2,128);
       printf("%s\n",szBuf);
}

一切都顯得很好,但是,當我輸出結果的時候,發現了問題,字符串後面有時會跟幾個奇怪的字符,好像沒有用“\0”結束,於是我把上面的拷貝語句改成“strncpy(szBuf,szBuf2,8);”,只拷貝8個字符,問題出現了,程序輸出如下:

########***********************************************************************************************************************

果然,當請求的目標地址空間比源字符串空間要小的時候,strncpy將不再用“\0”來結束字符串。巨大的隱患。

1.3 安全地字符串拷貝函數

我仔細想了想,我認為我需要如下一個字符串拷貝函數:

1、允許用一個整數界定目標地址空間尺寸。

2、當目標地址空間nD小於源字符串長度nS時,應該只拷貝nD個字節。

3、任何情況下,目標地址空間均應該以“\0”結束,保持一個合法的字符串身份。因此,得到的字符串最大長度為nD-1.

於是,我寫了這麼一個字符串拷貝函數:

 void xg_strncpy1(char *pD, char *pS,int nDestSize)
{
       memcpy(pD,pS,nDestSize);
       *(pD+nDestSize-1)='\0';
}

很EASY是不,將這個拷貝函數代入上面的例子,只輸出7個“#”, 結果正確。

1.4 內存讀錯誤的思考

本來以為可以就此打住了,不過,沒多久,我就發現一個奇怪的現象,這個函數在VC的Debug模式下有錯誤,但是Release模式下卻一切正常。

我奇怪了很久,終於有一天我忍不住了,決定解決這個問題,我把上面的memcpy用自己的一個復制循環代替,單步跟蹤,想看看究竟怎麼回事?

原因找到了,我希望拷貝一個256字節長的字符串,但是,拷貝到第33字節時出錯,檢查程序,發現我的源字符串空間只有32 Bytes,原來,我上面的代碼只是防止了內存寫出界,但沒有針對讀出界進行檢查,在VC的Debug模式下,內存讀出界也是一種非法錯誤,因此被報錯。

知道了原因,解決就很簡單了,我把上面的拷貝函數改成如下形狀:

 void xg_strncpy2(char *pD, char *pS,int nDestSize)
{
       int nLen=strlen(pS)+1;
       if(nLen>nDestSize) nLen=nDestSize;
       memcpy(pD,pS,nLen);
       *(pD+nLen-1)='\0';
}

一切OK.

2.字符串構造函數

2.1 不安全的sprintf

如同上例,我在修改拷貝函數的同時,我也想到了另外一個我常用的字符串構造函數sprintf,顯然,這個函數沒有界定目標地址空間的尺寸,也是不安全的,下面的代碼將會造成崩潰:

 void sprintfTest0()
{
       int i;
       char szBuf[128];
       for(i=0;i<128;i++) szBuf[i]='*';
       szBuf[127]='\0';
       char szBuf2[256];
       for(i=0;i<256;i++) szBuf2[i]='#';
       szBuf2[255]='\0';
       sprintf(szBuf,szBuf2);
       printf("%s\n",szBuf);
}

2.2 還是不安全的_snprintf

查閱庫函數手冊,找到這麼一個函數_snprintf,其函數原型如下:

int _snprintf( char *buffer, size_t count, const char *format [, argument] …… );

這個函數允許界定目標地址尺寸,但是,由於研究拷貝函數的經驗,我懷疑它也有strncpy相同的問題,因此,我寫了這麼一段代碼測試:

 void sprintfTest1()
{
       int i;
       char szBuf[128];
       for(i=0;i<128;i++) szBuf[i]='*';
       szBuf[127]='\0';

       char szBuf2[256];
       for(i=0;i<256;i++) szBuf2[i]='#';
       szBuf2[255]='\0';

       _snprintf(szBuf,8,szBuf2);
       printf("%s\n",szBuf);
}

果然,程序輸出如下:

########***********************************************************************************************************************

同樣的錯誤,沒有用“\0”結束,我必須另外想方法。

另外,還發現了另外一個不足,就是這個時候,_snprintf函數返回-1,不再返回打印的字符數,那麼,我們如果使用如下代碼將會造成邏輯錯誤,甚至可能崩潰:

char szBuf[256];
int nCount=0;
while(1)  //這裡表示循環構造
{
       nCount+=_snprintf(szBuf+nCount,256-nCount,”... ...”);  //多個字符串構造成一個字符串
}

注意,代碼利用_snprintf返回的值,來確定下一個起始點,這很常用,但是,當_snprintf返回-1的時候,有可能會寫到*(szBuf-1)的位置上,典型的內存寫出界。

2.3 安全地字符串構造函數

經過仔細思考,我構造了如下一個函數:

 int xg_printf(char* szBuf,int nDestSize,char *szFormat, ...)
{
       int nListCount=0;
       va_list pArgList;
       va_start (pArgList,szFormat);
       nListCount+=_vsnprintf(szBuf+nListCount,
              nDestSize-nListCount,szFormat,pArgList);
       va_end(pArgList);
       *(szBuf+nDestSize-1)='\0';
       return strlen(szBuf);
}

注意,這裡我采用了變參函數設計,為的是和sprintf一樣方便,另外,最後一個return也非常重要,因為很多場合,我們需要知道究竟打印了多少字符。將這段函數代入上面的例子後一切正常。

總結:C語言字符串庫函數可能是出於提高性能目的,在一旦條件不夠的時候,往往直接返回,忘了采用“\0”結束字符串。這會造成下一次讀取字符串時,數據邊界不可控。格式化打印函數,返回值設計不合理,不永遠是一個正整數,會造成邏輯隱患。因此,建議大家有興趣可以參考一下我提供的兩個函數。

另外,以上僅為我個人測試之作,限於本人水平所限,肯定還有沒考慮到的地方,歡迎大家展開討論。如果大家需要上面的源代碼,請和我聯系。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved