四種拷貝函數詳解與工業級拷貝實現
目錄
一、引言
內存拷貝我們在編程的過程中會經常遇到,調用C庫的函數也無非是memcpy()、memmove()、strcpy()、strncpy()這幾個函數,但是很少考慮過它們的實現原型和具體的差異;當遇到自己實現一個內存拷貝的原型時,卻發現細節考慮的不是很到位,如果問你的人是面試官,結果可想而知;
內存拷貝的實現問題非常具有代表性,原理也是比較簡單,但是能反映以下幾個重要問題:思維邏輯性、考慮問題的全面性、細節的把握能力。細思極恐吶!本文就和大家一起討論討論四種拷貝函數和工業級拷貝實現。
二、四種拷貝函數詳解
1、memcpy()函數
- 概覽
原型 |
|
功能 |
memcpy()會複製 src 所指的內存內容的前 num 個字節到 dest所指的內存地址上; memcpy()並不關心被複制的數據類型,只是逐字節地進行復制,這給函數的使用帶來了很大的靈活性,可以面向任何數據類型進行復制; |
注意 |
dest 指針要分配足夠的空間,也即大於等於 num字節的空間。如果沒有分配空間,會出現斷錯誤; 與 strcpy() 不同的是,memcpy() 會完整的複製 num個字節,不會因爲遇到“\0”而結束; |
返回值 | 返回指向 dest 的指針。注意返回的指針類型是void,使用時一般要進行強制類型轉換。 |
- 舉例
#include <string,h>
#include <stdio.h>
#include <stdlib.h>
#define N (15)
int main()
{
char *p1 = "qing zhu yi!";
char *p2 = (char *)malloc(sizeof(char) * N);
char *p3 = (char *)memcpy(p2, p1, N);
printf("p2 = %s\np3 = %s\n", p2, p3);
free(p2);
p2 = NULL;
p3 = NULL;
system("pause");
return 0;
}
運行結果 | p2 = qing zhu yi! p3 = qing zhu yi! |
代碼說明 |
1) 代碼首先定義p1,p2,p3三個指針,但略有不同,p1指向一個字符串字面值,給p2分配了10個字節的內存空間; 2) 指針p3通過函數memcpy直接指向了指針p2所指向的內存,也就是說指針p2、p3指向了同一塊內存。然後打印 p2,p3指向的內存值,結果是相同的; 3) 最後按照好的習慣釋放p2,並把p3也置爲NULL是爲了防止再次訪問p3指向的內存,導致野指針的發生; |
2、memmove()函數
- 概覽
原型 |
|
功能 |
memcpy()會複製 src 所指的內存內容的前 num 個字節到 dest所指的內存地址上; |
注意 | memmove() 更爲靈活,當src 和 dest所指的內存區域重疊時,memmove() 仍然可以正確的處理,不過執行效率上會比使用 memcpy()略慢些。 |
返回值 | 返回指向 dest 的指針。注意返回的指針類型是void,使用時一般要進行強制類型轉換。 |
- 舉例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main ()
{
char str[] = "memmove can be very useful......";
memmove (str+20,str+15,11);
puts (str);
system("pause");
return 0;
}
運行結果 |
memmove can be very very useful. |
代碼說明 |
處理內存重疊時的情況:先將內容複製到類似緩衝區的地方,再用緩衝區中的內容覆蓋 dest指向的內存 |
3、strcpy()函數
- 概覽
原型 |
|
功能 | strcpy() 把src所指的由NULL結束的字符串複製到dest 所指的數組中,返回指向dest 字符串的起始地址。 |
注意 |
如果參數 dest 所指的內存空間不夠大,可能會造成緩衝溢出(bufferOverflow)的錯誤情況,在編寫程序時需要特別留意,或者用strncpy()來取代 |
返回值 | 指向dest 字符串的起始地址 |
- 舉例
#include <stdio.h>
#include <string.h>
int main ()
{
char str1[]="Sample string";
char str2[40];
char str3[40];
strcpy (str2,str1);
strcpy (str3,"copy successful");
printf ("str1: %s\nstr2: %s\nstr3: %s\n",str1,str2,str3);
return 0;
}
運行結果 |
str1: Sample string str2: Sample string str3: copy successful |
代碼說明 |
無 |
4、strncpy()函數
- 概覽
原型 |
|
功能 | strncpy()會將字符串src前n個字符拷貝到字符串dest |
注意 |
不像strcpy(),strncpy()不會向dest追加結束標記'\0',這就引發了很多不合常理的問題,將在下面的示例中說明 |
返回值 | 指向dest 字符串的起始地址 |
- 舉例
#include <stdio.h>
#include <string.h>
int main(void)
{
char dest1[20];
char src1[] = "abc";
int n1 = 3;
char dest2[20] = "********************";
char src2[] = "abcxyz";
int n2 = strlen(src2) + 1;
char dest3[100] = "http://see.xidian.edu.cn/cpp/shell/";
char src3[6] = "abcxyz"; // 沒有'\0'
int n3 = 20;
char dest4[100] = "http://see.xidian.edu.cn/cpp/u/yuanma/";
char src4[] = "abc\0defghigk";
int n4 = strlen(src3);
strncpy(dest1, src1, n1); // n1小於strlen(str1)+1,不會追加'\0'
strncpy(dest2, src2, n2); // n2等於strlen(str2)+1,恰好可以把src2末尾的'\0'拷貝到dest2
strncpy(dest3, src3, n3); // n3大於strlen(str3)+1,循環拷貝str3
strncpy(dest4, src4, n4); // src4中間出現'\0'
printf("dest1=%s\n", dest1);
printf("dest2=%s, dest2[15]=%c\n", dest2, dest2[10]);
printf("dest3=%s\n", dest3);
printf("dest4=%s, dest4[6]=%d, dest4[20]=%d, dest4[90]=%d\n", dest4, dest4[6], dest4[20], dest4[90]);
return 0;
}
三、工業級拷貝函數實現
1、易出錯和忽略出現的幾個問題
- 輸入輸出普適性問題
需要拷貝的數據有不同的類型,爲了普適性函數將這樣定義原型:
void* memcpy(void* dst, const void* src, int count );
原因有二:
(1)、指針的類型爲什麼是void* 。首先我們需要明白一個概念上的區別即void 和 void* 。void表示空的意思,不可以用void修飾任何變量,void最常見的兩種用法:函數返回值爲空;函數參數爲空,表示不接受調用參數。這兩個用法相信大家很清楚,在此不再贅述。而void* 表示的是任意類型的指針,可通過任意類型的指針進行賦值,在此需要注意的是:不可以對void* 進行算術運算,不可以對void* 進行解引用,原因很明確,void*表示的是任意類型的指針,不同類型的指針進行算術運算結果顯然不同。
使用void* 指針,可以接受任意類型的指針的賦值,可以使用任何指針類型的參數來調用函數。在內部可以用char類型強制轉換就可以,具體參考【3】。
(2)、src爲什麼用const修飾。引用effective C++中03條款:儘可能使用const。
src爲要複製的原地址,僅僅是希望讀取他的內容,並不希望在操作的過程中改變它,因此const可以確保src的常量性。
- 目標地址和源地址所指內存區域重疊問題
(1) 地址不重疊 (dst src || dst (char*)src+count)
如果滿足上面條件,說明源目地址肯定不重疊或者即使重疊也不會影響數據的複製,這時候不需要擔心內容被覆蓋的問題。直接進行復制即可.
char* psrc=(char*)src;
char* pdst=(char*)dst;
if(pdst && psrc)
{
while(count--)
{
*pdst++ = *psrc++;
}
}
注:只有當dst > (char*)src+count*sizeof(*src)時,纔是真正的源目地址不重疊;
當dst (char*)src時,可以看出地址可能會重疊,但是不影響拷貝數據的準確性。
(2) 地址重疊 src dst src+count
這種情況下,和在有序數組中插入新元素一樣,在移動元素的時候,爲了防止覆蓋,從後面元素依次開始移動。這裏一樣的道理,爲了防止地址之間的重疊造成覆蓋,我們從高地址開始複製內容。具體實現如下:
if(dst>=src && (char*)dst<(char*)src+count)
{
char* psrc=(char*)src+count-1;
char* pdst=(char*)dst+count-1;
while(count--)
{
*pdst--=*psrc--;
}
}
- 實參地址是否有效問題
試想,如果有用戶這樣來調用你的memcpy函數,memcpy(null,p,5)或者memcpy(p,null,5)肯定編譯器不會報錯,但是執行結果卻是不可預料的。所以,我們還要對源目地址作是否爲空的判斷來確定是否可以正常使用。
使用assert可解決問題,同時避免了執行判斷語句 if( null==src || null==dst )帶來的執行開銷。
assert(src);
assert(dst);
2、流程圖
3、代碼實現
void * memMove(void *dest, const void * src, int count)
{
assert(dest);
assert(src);
void * result = dest;
char * p_src = src;
char * p_dest = dest;
if(dest <= src || (char *)dest >= (char *)src + count) // 地址不重疊,從前往後拷貝
{
while(count--)
{
*(char *)dest++ = *(char *)src++;
}
}
else // 地址重疊,從後往前拷貝
{
p_src = (char *)src + count - 1;
p_dest = (char *)dest + count -1;
while(count--)
{
*p_dest-- = *p_src--;
}
}
return result;
}
四、總結
(1)當源內存的首地址等於目標內存的首地址時,不進行任何拷貝
(2)當源內存的首地址大於目標內存的首地址時,實行正向拷貝
(3)當源內存的首地址小於目標內存的首地址時,實行反向拷貝
圖解:
當src內存區域和dst內存區域完全不重疊
當src內存區域和dest內存區域重疊時且dst所在區域在src所在區域前
當src內存區域和dst內存區域重疊時且src所在區域在dst所在區域前
五、參考文獻
[1] C語言的四種拷貝函數