C語言之memcpy函數

昨天自己動手實現memcpy這個函數,用一個例程試了一下,結果正確,滿心歡心,可是有些地方想不明白,於是百度了一下,結果自己寫的函數簡直無法直視。

覺得還是寫個總結,以示教訓。

先貼上我自己的函數:


[cpp] view plain copy 在CODE上查看代碼片派生到我的代碼片

  1. char *mymemcpy(char *dest, const char * src, int n)  

  2. {  

  3.         char *pdest;  

  4.         char *psrc;  

  5.         pdest = dest;  

  6.         psrc = src;  

  7.   

  8.         for(n; n>0; n--)  

  9.         {  

  10.                 *pdest = *psrc;  

  11.                 pdest++;  

  12.                 psrc++;  

  13.         }  

  14. }  

我這個程序只能是在非常理想的條件下才能完成複製任務,一旦參數有誤,那麼就會運行出錯;另外沒有返回值,而是直接將dest指針作爲了返回型參數。

另一點需要注意:在函數體中我另外聲明瞭兩個指針,分別指向dest和src,我是這樣想的:由於循環中要移動指針,爲了不影響主程序中實參指針的位置,所以我認爲需要重新定義兩個指針。後來我發現,我這樣想是錯誤的,指針變量實質上也是變量,指針作爲形參,那麼這個指針也是實實在在存在的,那麼在函數中改變這個形參指針的位置並不會影響主程序中實參指針的位置。所以在這個函數中,沒必要重新聲明兩個指針,最好是在返回值中返回一個指向dest的指針就夠了。

我會犯上述這個錯誤,應該是受了“地址傳參”和“值傳參”的影響。由於地址傳參太過於強調函數可以改變主程序中的數據內容,以至於讓我把形參指針和實參指針等同起來了。實質上形參指針變量位置的改變並不會影響實參指針的位置。

重要的內容寫前面,

自己總結該函數的幾個要點:

1、參數判斷:對參數的合法性進行判斷

2、聲明中間變量:由於要返回目的指針,所以需要保留目的首地址;最好是不要破壞形參,設置臨時變量替換

3、void *類型:要注意dest和src的類型可能不同,進而造成dest++ src++不匹配的問題,先強制類型轉換

4、void 類型做右值:void類型變量或是返回值爲void類型的函數,一旦做右值編譯出錯

5、指針形參:即上面提到的,指針變量本質仍是指針,形參指針位置的改變不會影響實參指針的位置


下面是參考網友的一些總結,紅色字體爲本人批註:


memcpy實現內存拷貝,根據這個問題,我們可以提取出下面幾點:

1.可以拷貝任何數據,數據類型不能受限

2.源數據不能被改變

通過上面兩點可以確定函數原型爲void *memcpy(void *dest, const void *src),現在分析一下這些足夠了嗎?這個函數拷貝什麼時候結束,當時我就用了這個函數原型,由於是拷貝的任意數據,所以不能指定一個明確的結束標誌,既然這樣那麼只有明確的指定拷貝的大小纔可以.所以函數原型變成這樣void *memcpy(void *dest, void *src, size_t count);好吧,函數原型既然已經確認了,剩下的應該就是寫函數了,先等等,先別急着寫函數,實際上對於C語言的開發者來說,重要的不是函數功能的實現,重要的是函數出錯時的處理,如果你用的是Java或者C#大不了拋個異常出來,軟件崩潰一下,不會對其他造成任何影響;C這東西弄不好會把整個系統弄癱瘓,所謂”兵馬未動,糧草先行”,我麼還是先考慮考慮出錯的問題吧!我們根據函數原型來分析,

void *memcpy(void *dest, const void *src, size_t count);

1.空指針的問題,如果dest、src兩者或者兩者之一爲NULL,那麼自然能沒得完了;

2.拷貝大小count爲小於等於0的值,自然也是不正確的;

3.目標有沒有足夠的大小容納源數據,這個我們在函數內部似乎也無法進行保證,但是我們自己也要想到

4.內存地址有沒有重疊,這個我們暫時不考慮了。

有了上面的提示寫起來自然比較簡單了


[cpp] view plain copy 在CODE上查看代碼片派生到我的代碼片

  1. #include <stdio.h>  

  2. void *memcpy(void *dest, const void *src, size_t count)  

  3. {  

  4. <span style="white-space:pre">    </span>if (NULL == dest || NULL == src || count <= 0)  

  5. <span style="white-space:pre">        </span>return NULL;  

  6. <span style="white-space:pre">    </span>while (count--)  

  7. <span style="white-space:pre">        </span>*dest++ = *src++;  

  8. <span style="white-space:pre">    </span>return dest;  

  9. }  

上面這段代碼在Linux中使用gcc編譯是沒錯的,但是會有警告,所以改成這樣:
注意,上述代碼我在測試時,不僅有警告還有一個錯誤:error: invalid use of void expression,這是因爲void型的變量或者是函數返回值被使用了。使用下面這段代碼是可以通過編譯的:


[cpp] view plain copy 在CODE上查看代碼片派生到我的代碼片

  1. <pre name="code" class="cpp">#include <stdio.h>  

  2. void *memcpy(void *dest, const void *src, size_t count)  

  3. {  

  4.     if (NULL == dest || NULL == src || count <= 0)  

  5.         return NULL;  

  6.     while (count--)  

  7.         *(char *)dest++ = *(char *)src++;  

  8.     return dest;  

  9. }  



OK,也就這樣了,要是面試官再問起內存重疊的問題,你再和他侃侃.我的面試算是泡湯了.

總結:不要着急慢慢來,根據需求推出原型,根據原型推斷問題,這算是個教訓吧!!!

補充:

在這裏非常感謝博客園的求道於盲  這位好心的網友指出了我程序中的兩個錯誤,再次感謝.

1.返回了一個++過的指針

2.size_t是無符號類型的,size_t的定義爲:typedef unsigned int size_t;

所以count<=0,只會判斷==0的情況,如果傳入-1,會產生一個很大的無符號整型.

希望別人注意,改過的程序如下:


[cpp] view plain copy 在CODE上查看代碼片派生到我的代碼片

  1. void *memcpy(void *dest, const void *src, int count)  

  2. {  

  3. <span style="white-space:pre">    </span>void *ptr = dest;  

  4. <span style="white-space:pre">    </span>if (NULL == dest || NULL == src || count <= 0)  

  5. <span style="white-space:pre">    </span>return NULL;  

  6.    

  7. <span style="white-space:pre">    </span>while (count--)  

  8. <span style="white-space:pre">        </span>*(char *)dest++ = *(char *)src++;  

  9.    

  10. <span style="white-space:pre">    </span>return ptr;  

  11. }  





本文通過彙總一些網上蒐集到的資料,總結c語言中的memcpy實現

背景

想必大多數人在面試時被要求寫 memcpy的實現,很不幸,我也吃過這個虧(這種題要是寫的一塌糊塗後面完全沒戲),所以還是得提前準備一下,不然就只能呵呵了。先來看看一段錯誤的示範: 找茬:)


[cpp] view plain copy  在CODE上查看代碼片派生到我的代碼片

  1. void * memcpy(void *dest, const void *src, unsigned int count);  

  2. {  

  3.     if ((src == NULL) || (dest == NULL))  

  4.         return;  

  5.       

  6.     while (count--)  

  7.         *dest++ = *src++;  

  8.           

  9.     return dest;  

  10. }  

dest都指到哪裏去了?怎麼着也得備份一下dest的值,好讓函數返回的指針是從頭開始的

考慮一下指針類型,如果dest和src的指針類型不一樣,不能直接++賦值. 例如: int* p和 char*q, p++指針的值是4個4個加(0,4,8),q++是1個1個加(0,1,2,3,4)


第二版 - 定義兩個臨時變量,不要直接++ dest和src,並且指明指針類型char *


[cpp] view plain copy  在CODE上查看代碼片派生到我的代碼片

  1. void *memcpy(void *dest, const void *src, size_t count)  

  2. {  

  3.  char *tmp = dest;  

  4.  const char *s = src;  

  5.    

  6.  while (count--)  

  7.   *tmp++ = *s++ ;  

  8.     

  9.  return dest;  

  10. }  

能否改進? src和dest都強制轉換成char*類型的指針,那麼copy一定是一個字節一個字節的完成?那麼第三版來了



[cpp] view plain copy  在CODE上查看代碼片派生到我的代碼片

  1. void * memcpy(void *dst,const void *src,size_t num)  

  2. {  

  3.     int nchunks = num/sizeof(dst);   /*按CPU位寬拷貝*/  

  4.     int slice =   num%sizeof(dst);   /*剩餘的按字節拷貝*/  

  5.       

  6.     unsigned long * s = (unsigned long *)src;  

  7.     unsigned long * d = (unsigned long *)dst;  

  8.       

  9.     while(nchunks--)  

  10.         *d++ = *s++;  

  11.           

  12.     while (slice--)  

  13.         *((char *)d++) =*((char *)s++);  

  14.           

  15.     return dst;  

  16. }  



看着沒什麼問題了,可是如果dst和src地址不對齊,copy效率豈不降低? 是否需要先處理一下地址不對齊的情況?


再來看看dest和src地址有重疊的情況


內存重疊問題是指目的地址的內存空間的首地址,包含在源內存空間中,這兩段內存空間有了交集,因而在使用memcpy進行內存複製操作時,這段重疊的內存空間會被破壞.這種情況在應用程序級代碼中一般不會出現的,而在驅動或內核級代碼中要十分小心,儘量使用memmove函數.

memcpy對內存空間有要求的,dest和src所指向的內存空間不能重疊,否則複製的數據是錯誤的.下面具體講解一下這個錯誤是如何產生的.

如果內存空間佈局入下圖所示:

                                             

src所指向的內存空間後面部分數據被新拷貝的數據給覆蓋了(也就是dest<=src+size).所以拷貝到最後,原來的數據肯定不是原來的數據,拷貝的數據也不是想要的數據,使用memcpy函數可以得到錯誤的結果.


再者,如果內存空間佈局入下圖所示:

                                             

雖然原來的數據不再是原來的數據(dest+size>=src),但拷貝的數據是原來的數據,使用memcpy函數可以得到正確的結果.因此,在使用memcpy這個函數之前,還需要做一個判斷,如果dest<=src你才能使用這個函數不過完全沒有必要, 解決辦法,從高地址向地地址copy

實例


[cpp] view plain copy  在CODE上查看代碼片派生到我的代碼片

  1. void *memcpy(void *dest, const void *src, size_t count)  

  2. {  

  3.  char *d;  

  4.  const char *s;  

  5.    

  6.  if (dest > (src+size)) || (dest < src))  

  7.     {  

  8.     d = dest;  

  9.     s = src;  

  10.     while (count--)  

  11.         *d++ = *s++;          

  12.     }  

  13.  else /* overlap */  

  14.     {  

  15.     d = (char *)(dest + count - 1); /* offset of pointer is from 0 */  

  16.     s = (char *)(src + count -1);  

  17.     while (count --)  

  18.         *d-- = *s--;  

  19.     }  

  20.     

  21.  return dest;  

  22. }  


memcpy是把src指向的對象中的size個字符拷貝到dest所指向的對象中,返回指向結果對象的指針. 

memmove也是把src指向的對象中的size個字符拷貝到dest所指向的對象中,返回指向結果對象的指針,但這兩個函數在處理內存區域重疊的方式不同.

注意memmove這個函數名稱中有"move"這個單詞,而實際上src處的數據仍然還在,並沒有真的被"移動"了!這個函數名稱有它的歷史原因,是因爲有了memcpy函數後,發現這個函數有問題,又發明了另一個沒有問題的memcpy函數,但爲了保證兼容性依然保留了memcpy函數,而將新版本的memcpy函數改名爲memmove函數.


總結

1. 不要破壞傳進來的形參,定義新的臨時變量來操作

2.考慮指針的類型,不同類型的指針不能直接++賦值

3.overlap情況下需要從高地址處向前copy



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章