http://jeremybai.github.io/blog/2014/09/05/memcpy/
記憶體複製的注意事項
05 September 2014
有道面試題是讓寫出memcpy的實現,memcpy是c和c++使用的記憶體複製函數,功能是從來源位址所指的記憶體位址的起始位置開 始複製n個位元組到目標位址所指的記憶體位址的起始位置中。與此類似的,在使用strcpy的時候,也應該需要注意這些問題。我們粗略的考慮下有哪些是值得注 意的。
1 需要對參數進行檢查,當傳入來源位址和目的地址參數有NULL時需要特殊處理;
2 來源位址在函數中是不應該被修改的,可以加上const進行修飾,防止代碼中錯誤的將來源位址進行修改,這樣在編譯的時候可以檢查出來;
3 傳入的源地址和目的地址的類型應該是任意的指標類型,而不是針對特定的指標類型,可以使用void*類型;
4 記憶體重疊的問題,一般進行複製時都是從來源位址的第一個位元組開始向後複製n個到目的地址,考慮這樣一種情況:`memcpy(p+1,p,N)`,這種情況下,目的地址會覆蓋來源位址的內容,此時有兩種方法,一種是用O(n)的空間來換取準確性,還有一種就是從後向前複製,這樣就算覆蓋也是覆蓋使用過的資料了。
考慮差不多就可以開始寫了:
void *mymemcpy(void *v_dst, const void *v_src, int c)
{
assert(v_dst);
assert(v_src);
const char *src = v_src;
char *dst = v_dst;
if (v_dst <= v_src)
{
while (c--)
*dst++ = *src++;
}
else
{
src = src + c - 1;
dst = dst + c - 1;
while (c--)
*dst-- = *src--;
}
return v_dst;
}
測試用例包括基本功能的測試,參數為空,記憶體覆蓋(目的地址大於或者小於來源位址都要考慮)。還有一點需要注意的是當調用這個函數之後,我們只能保證目的地址的資料的準確性,對於來源位址,在有記憶體重疊的情況下時,很有可能已經被改變了。比如:
int main()
{
char p1[] = "0123456";
char p2[] = "123";
mymemcpy(p2,p1,strlen(p1)+1);
printf("%s\n%s\n",p1,p2);
return 0;
}
執行完mymemcpy(庫函數memcpy也是一樣)之後,列印p1p2發現p2等於"0123456",但是p1等於"456"。這是因為 p1和p2都是區域變數,存取在棧中,因為兩個資料是連續申請的,所以位址也是連在一起的,我們傳入的第三個參數是p1的長度+1,也就是8,當複製到 '4'的時候,此時p2沒有足夠的空間去存放p1中的元素,於是就會佔用p1的空間。如下圖所示,於是列印p1發現p1變成了"456"。所以在調用記憶體 賦值的函數時,一定要保證目的地址有足夠的空間去容納你所要複製的資料。
assert是一個宏,定義在中,作用是先計算括弧內的運算式,如果其值為假(即為0),那麼它先向stderr列印一條出錯信 息,錯誤消息包括原始檔案,行號,和運算式本身,然後通過調用 abort 來終止程式運行。程式中通過比較目的地址和來源位址的大小來決定採用從前向後複製還是從後向前複製。其實,memcpy函數的使用場合是不需要考慮記憶體重疊 問題的,因為涉及到記憶體重疊時我們應該調用的是memmove函數,下面是Linux內核源碼中memcpy和memmove的代碼,可以看 出,memcpy在實現的時候就沒有考慮到記憶體重疊的問題。
void *memcpy(void *v_dst, const void *v_src, __kernel_size_t c)
{
const char *src = v_src;
char *dst = v_dst;
/* Simple, byte oriented memcpy. */
while (c--)
*dst++ = *src++;
return v_dst;
}
void *memmove(void *v_dst, const void *v_src, __kernel_size_t c)
{
const char *src = v_src;
char *dst = v_dst;
if (!c)
return v_dst;
/* Use memcpy when source is higher than dest */
if (v_dst <= v_src)
return memcpy(v_dst, v_src, c);
/* copy backwards, from end to beginning */
src += c;
dst += c;
/* Simple, byte oriented memmove. */
while (c--)
*--dst = *--src;
return v_dst;
}
_kernel_size_t是一個與機器相關的unsigned類型,功能與size_t類似,只不過是用在kernel中的,根據機器的位元數確定,64bits的機器上為typedef long unsigned int __kernel_size_t;,32bits機器上是typedef unsigned int __kernel_size_t;。 size_t也是如此,為了增強程式的可攜性(關於這一點理解的還是不夠明白),不同的系統上,定義size_t可能不一樣,size_t一般用來表示 一種計數,比如有多少東西被複製等。例如:sizeof操作符的結果類型是size_t,該類型保證能容納實現所建立的最大物件的位元組大小,經常用於迴圈 計數,陣列索引,還有位址運算等等。 還有一個原因就是sizet可以安全的容納指標,如果考慮應用程式的相容性和可攜性,指標的長度就是一 個問題,在大部分現代平臺上,資料指標的長度通常是一樣的,與指標類型無關,儘管C標準沒有規定所有類型指標的長度相同,但是通常實際情況就是這樣。但是 函數指標長度可能與資料指標的長度不同,比如類的函數指標。指針的長度取決於使用的機器和編譯器,一般是32位(32bits機器)或是64位長 (64bits機器),因為size_t是被定義為unsigned,所以可以使用size_t來進行存取。
留言列表