你必須知道的指針基礎-4.sizeof計算數組長度與strcpy的安全性問題


一、使用sizeof計算數組長度

1.1 sizeof的基本使用

  如果在作用域內,變量以數組形式聲明,則可以使用sizeof求數組大小,下面一段代碼展示了如何使用sizeof:

    int nums[] = {11,22,33,44,55,66};
    int i;
    // sizeof(nums) 計算nums數組的總字節數
    // sizeof(int) 計算int類型所占用的字節數
    int length = sizeof(nums)/sizeof(int);
    for(i=0;i<length;i++)
    {
        printf("%d ",nums[i]);
    }

  其中sizeof(nums)代表計算nums數組的總字節數,而sizeof(int)則代表計算int類型所占用的字節數(32位系統下是4個字節,64位下可能不同,因此這里使用sizeof(int)可以向程序員屏蔽這個差異),運行結果為:

1.2 sizeof只能在編譯時計算

  假如我們將上面的代碼做一個抽象,將數組的遍歷及打印封裝為一個方法,代碼如下:

void printEach(int* nums)
{
    // sizeof(nums)在這里是計算指針的字節數
    int length = sizeof(nums)/sizeof(int);
    printf("The length of nums is %d\n",length);
    int i;
    for(i=0;i<length;i++)
    {
        printf("%d ",nums[i]);
    }
}

  我們定義了一個printEach方法,其參數是一個指針,在方法內部通過sizeof計算數組長度。但是,運行結果並沒有同上面的結果一致:

  我們發現,雖然我們使用了指針,但由於sizeof是編譯器在編譯的時候計算的,無法動態計算。因此對於int *或者將數組傳遞給函數,那么就無法使用sizeof獲取大小了。即使函數聲明中寫着int[]也不行(為了避免誤解,不要在參數中聲明數組類型)。這里,sizeof(nums)只是計算了指針的字節數(這里指針指向了數組的首元素的地址,一個int占4個字節,所以最后length變成了1)。

  那么,為了避免出現無法計算長度的情況,我們一般都會在方法定義時增加一個長度的參數,讓調用者傳遞過來,函數內部不再計算長度。看看如下的代碼:

void printEachWithLen(int* nums,int length)
{
    int i;
    for(i=0;i<length;i++)
    {
        printf("%d ",nums[i]);
    }
}

  這時候,我們就可以在main函數中調用該printEachWithLen()函數:

int length = sizeof(nums)/sizeof(int);
printEachWithLen(nums,length);

  這下看看結果:

  因此,一般給函數傳遞數組/字符串的時候都要求額外傳遞“長度”參數,因為函數內部也不知道“有多長”。例如:memcpy(void * restrict, const void * restrict, size_t),第三個參數size_t就是長度。又例如在.NET中,要進行數組的復制,可以使用 Array.Copy 、Buffer.BlockCopy 、Array.ConstrainedCopy等方法,通過查看其方法定義,都要求傳遞了數組長度。

const int INT_SIZE = 4;
int[] arr = { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };
Buffer.BlockCopy(arr, 3 * INT_SIZE, arr, 0 * INT_SIZE, 4 * INT_SIZE);
foreach (int value in arr)
   Console.Write("{0}  ", value);
// The example displays the following output:
//  8  10  12  14  10  12  14  16  18  20    

二、strcpy的安全性問題

2.1 使用strcpy復制字符串

  一個簡單的場景,將一個字符串復制到另一個字符串中,在C語言課本中,最長出現的就是strcpy了。我們可以輕易地寫出下面的代碼來實現字符串復制:

char sourceStr[] = "hello edison";
char destStr[30];
strcpy(destStr,sourceStr);
printf("%s",destStr);

  運行結果如下圖所示:

  但是,我們常常聽人說strcpy是不安全的函數,為什么呢?先看看strcpy內部的循環判斷條件:

while ((*strDest++ = *strSrc++) != '\0')

  這個循環會一直執行,直到循環條件為空,即'\0',也就是說,如果strDest所指的存儲空間不夠大的話,這個函數會將strSrc中的部分內容拷貝到strDest所指內存空間后面的內存中。而strDest所指空間后面的內存卻是不可知的,有可能已經被其他資源占用了,這樣就會破壞原先存儲的內容,導致系統崩潰。

  因為strcpy在執行字符串拷貝的時候,會從strSrc所指位置開始,檢測當前內存單元中存儲的數據是否為'\0'。如果不為'\0',則將這個內存單元中的數據拷貝到strDest所指向的內存中。如果strSrc中存儲的字符串長度大於dst所申請的內存空間的話,就會產生越界,造成不可預知的后果。

PS:strlen根據'\0'判斷字符串結束,那么惡意攻擊者可以構造一個不包含'\0'的字符串,然后讓數據寫入數組之外的程序內存空間,從而進行破壞。

2.2 使用strncpy代替strcpy

  (1)strncpy函數定義:

char *strncpy(char *dest, const char *src,int count)

  將字符串src中的count個字符拷貝到字符串dest中去,最后返回指向dest的指針。

  (2)strncpy用法解析:

  這個函數和strcpy類似,當src的長度大於dst申請的空間的時候,情況和strcpy一樣;

  如果第3個參數count的值大於src中字符串的長度的話,就會將字符串src拷貝到dst中,返回函數。

  注意:如果源串長度大於n,則strncpy不復制最后的'\0'結束符,所以是不安全的,復制完后需要手動添加字符串的結束符才行。

  (3)strncpy用法實例:
char sourceStr[] = "hello edison";
char destStr[30];

int len = sizeof(sourceStr)/sizeof(char);
printf("%d\n",len);
strncpy(destStr,sourceStr,len-1);
// 保證安全的字符串復制
destStr[len-1]='\0';
printf("%s",destStr);

  運行結果如下圖所示:

參考資料

  如鵬網,《C語言也能干大事(第三版)》 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM