C/C++中四種拷貝函數詳解與工業級拷貝實現

                            四種拷貝函數詳解與工業級拷貝實現

 

目錄

一、引言

二、四種拷貝函數詳解

1、memcpy()函數

2、memmove()函數

3、strcpy()函數

4、strncpy()函數

三、工業級拷貝函數實現 

1、易出錯和忽略出現的幾個問題

2、流程圖

3、代碼實現

四、總結 

五、參考文獻


一、引言

內存拷貝我們在編程的過程中會經常遇到,調用C庫的函數也無非是memcpy()、memmove()、strcpy()、strncpy()這幾個函數,但是很少考慮過它們的實現原型和具體的差異;當遇到自己實現一個內存拷貝的原型時,卻發現細節考慮的不是很到位,如果問你的人是面試官,結果可想而知;

內存拷貝的實現問題非常具有代表性,原理也是比較簡單,但是能反映以下幾個重要問題:思維邏輯性、考慮問題的全面性、細節的把握能力。細思極恐吶!本文就和大家一起討論討論四種拷貝函數和工業級拷貝實現。


二、四種拷貝函數詳解

1、memcpy()函數

  • 概覽
原型
void *memcpy ( void * dest, const void * src, size_t num );

 

功能

memcpy()會複製 src 所指的內存內容的前 num 個字節到 dest所指的內存地址上;

memcpy()並不關心被複制的數據類型,只是逐字節地進行復制,這給函數的使用帶來了很大的靈活性,可以面向任何數據類型進行復制;

注意

dest 指針要分配足夠的空間,也即大於等於 num字節的空間。如果沒有分配空間,會出現斷錯誤;
dest 和 src所指的內存空間不能重疊(如果發生了重疊,使用 memmove() 會更加安全);

與 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()函數

  • 概覽
原型
void *memmove(void *dest, const void *src, size_t num);

 

功能

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()函數

  • 概覽
原型
 char*strcpy(char *dest, const char *src);

 

功能 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()函數

  • 概覽
原型
char *strncpy(char *dest, const char *src, size_t n);

 

功能  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 \leq 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  \leq (char*)src時,可以看出地址可能會重疊,但是不影響拷貝數據的準確性。

(2) 地址重疊    src \leq dst \leq 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語言的四種拷貝函數

[2] 內存拷貝函數memcpy的實現詳解

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