1. 程式人生 > >C/c++中記憶體拷貝函式memcpy詳解

C/c++中記憶體拷貝函式memcpy詳解

原型:void*memcpy(void*dest, const void*src,unsigned int count); 

功能:由src所指記憶體區域複製count個位元組到dest所指記憶體區域。

說明:srcdest所指記憶體區域不能重疊,函式返回指向dest的指標。

舉例:

[cpp] view plaincopyprint?
  1. //   memcpy.c                       
  2. #include   <stdlib.h>           
  3. #include   <string.h>           
  4. main()             
  5. {                 
  6.     char *s= "Golden  Global   View ";  
  7.     char d[20];  
  8.     clrscr();  
  9.     memcpy(d,s,strlen(s));  
  10.     d[strlen(s)]=0;  
  11.     printf( "%s ",d);  
  12.     getchar();  
  13.     return   0;  
  14. }  

下面自行實現這個函式

程式清單 1 V0.1版程式 

[cpp] view plaincopyprint?
  1. void MyMemMove(char *dst,char *src,
    int count)   
  2. {   
  3.     while(count--)   
  4.         *dst++ = *src++;   
  5. }   
程式清單 2 測試V0.1用例 
[cpp] view plaincopyprint?
  1. void Test()   
  2. {   
  3.     char p1[256] = ”hello,world!”;   
  4.     char p2[256] = {0};   
  5.     MyMemMove(p2,p1,strlen(p1));   
  6.     printf(“%s”,p2);   
  7. }   
    客觀地講,相比那些交白卷或者函式宣告都不會寫的同學來說,能夠寫出這段程式碼的同學已經非常不錯了,至少在C語言這門課程上已經達到了現行高校的教育目標,但是離企業的用人要求還有一定的距離。我們不妨將上面的程式稱為V0.1版本,看看還有沒有什麼地方可以改進。 
   首先我們看看函式宣告是否合理,V0.1版的程式將源地址和目的地址都用char *來表示,這樣當然也沒有什麼問題,但是讓其他人使用起來卻很不方便,假如現在要將count個連續的結構體物件移動到另外一個地方去,如果要使用v0.1的程式的話,正確的寫法如下: 
    MyMemMove((char *)dst,(char *)src,sizeof(TheStruct)*count); 
也就是說我們需要將結構體指標強制轉換成char * 才能夠正常工作,這樣除了字串以外其它的型別都不可避免地要進行指標強制轉換,否則編譯器就會呱呱叫,比如在VC++2008下就會出現這樣的錯誤: 
error C2664: 'MyMemMove' : cannot convert parameter 1 from 'TheStruct *'to 'char *' ;那麼如何解決這個問題呢?其實很簡單,我們知道有一種特別的指標,任何型別的指標都可以對它賦值,那就是void *,所以應該將源地址和目的地址都用void*來表示。當然函式體的內容也要作相應的改變,這樣我們就得到了V0.2版的程式。 
程式清單 3 V0.2版程式 

[cpp] view plaincopyprint?
  1. void MyMemMove(void *dst,void *src,int count)   
  2. {   
  3.     while (count--)   
  4.     {   
  5.         *(char *)dst = *(char *)src;   
  6.         dst = (char *)dst + 1;   
  7.         src = (char *)src + 1;   
  8.     }   
  9. }   
有的同學可能會問,這裡面不是還有指標強制轉換嗎?只不過是換了地方。沒錯,強制指標轉換確實是從使用者的程式碼轉移到了庫的程式碼裡,但我們可以將 MyMemMove理解為庫,而將Test理解為使用者,事實上通過調整之後的效果卻有天壤之別,V0.1是一逸永勞,而V0.2是一勞永逸! 
     還有幾個細節需要注意,為了實現鏈式表示式,我們應該將返回值也改為void *。此外,如果我們不小心將“*(char *)dst = *(char *)src;”寫反了,寫成“*(char *)src =*(char *)dst;”編譯照樣通過,而為了找出這個錯誤又得花費不少時間。注意到src所指向的內容在這個函式內不應該被改變,所有對src所指的內容賦值都應該被禁止,所以這個引數應該用const修飾,如果有類似的錯誤在編譯時就能夠被發現: 
error C3892: 'src' : you cannot assign to a variable that is const ;作為程式設計師犯錯誤在所難免,但是我們可以利用相對難犯錯誤的機器,也就是編譯器來降低犯錯誤的概率,這樣我們就得到了V0.3版的程式。 
程式清單 4 V0.3版程式 

[cpp] view plaincopyprint?
  1. void * MyMemMove(void *dst,constvoid *src,int count)   
  2. {   
  3.     void *ret=dst;   
  4.     while (count--)   
  5.     {   
  6.         *(char *)dst = *(char *)src;   
  7.         dst = (char *)dst + 1;   
  8.         src = (char *)src + 1;   
  9.     }   
  10.     return ret;  
  11. }   
     現在再來考慮這樣一種情況,有使用者這樣呼叫庫:MyMemMove(NULL,src, count),這是完全可能的,因為一般來說這些地址都是程式計算出來的,那就難免會算錯,出現零地址或者其它的非法地址也不足為奇。可以預料的是,如果出現這種情況的話,則程式馬上就會down掉,更糟糕的是你不知道錯誤出在哪裡,於是不得不投入大量的精力在浩瀚的程式碼中尋找bug。解決這類問題的通用辦法是對輸入引數作合法性檢查,也就是V0.4版程式。 
程式清單 5 V0.4版程式 

[cpp] view plaincopyprint?
  1. void * MyMemMove(void *dst,constvoid *src,int count)   
  2. {   
  3.     void *ret=dst;   
  4.     if (NULL==dst||NULL ==src)   
  5.     {   
  6.         return dst;   
  7.     }   
  8.     while (count--)   
  9.     {   
  10.         *(char *)dst = *(char *)src;   
  11.         dst = (char *)dst + 1;   
  12.         src = (char *)src + 1;   
  13.     }   
  14.     return ret;   
  15. }   

     上面之所以寫成“if(NULL==dst||NULL ==src)”而不是寫成“if (dst == NULL || src == NULL)”,也是為了降低犯錯誤的概率。我們知道,在C語言裡面“==”和“=”都是合法的運算子,如果我們不小心寫成了“if (dst = NULL || src = NULL)”還是可以編譯通過,而意思卻完全不一樣了,但是如果寫成“if (NULL=dst||NULL =src)”,則編譯的時候就通不過了,所以我們要養成良好的程式設計習慣:常量與變數作條件判斷時應該把常量寫在前面。V0.4版的程式碼首先對引數進行合法性檢查,如果不合法就直接返回,這樣雖然程式dwon掉的可能性降低了,但是效能卻大打折扣了,因為每次呼叫都會進行一次判斷,特別是頻繁的呼叫和效能要求比較高的場合,它在效能上的損失就不可小覷。如果通過長期的嚴格測試,能夠保證使用者不會使用零地址作為引數呼叫MyMemMove函式,則希望有簡單的方法關掉引數合法性檢查。我們知道巨集就有這種開關的作用,所以V0.5版程式也就出來了。 

程式清單 6 V0.5版程式

[cpp] view plaincopyprint?
  1. void * MyMemMove(void *dst,constvoid *src,int count)   
  2. {   
  3.     void *ret=dst;   
  4. #ifdef DEBUG 
  5.     if (NULL==dst||NULL ==src)   
  6.     {   
  7.         return dst;   
  8.     }   
  9. #endif 
  10.     while (count--)   
  11.     {   
  12.         *(char *)dst = *(char *)src;   
  13.         dst = (char *)dst + 1;   
  14.         src = (char *)src + 1;   
  15.     }   
  16.     return ret;   
  17. }   
     如果在除錯時我們加入“#defineDEBUG”語句,增強程式的健壯性,那麼在除錯通過後我們再改為“#undef DEBUG”語句,提高程式的效能。事實上在標準庫裡已經存在類似功能的巨集:assert,而且更加好用,它還可以在定義DEBUG時指出程式碼在那一行檢查失敗,而在沒有定義DEBUG時完全可以把它當作不存在。assert(_Expression)的使用非常簡單,當_Expression為0時,偵錯程式就可以出現一個除錯錯誤,有了這個好東西程式碼就容易多了。 
程式清單 7 V0.6版程式 

[cpp] view plaincopyprint?
  1. void * MyMemMove(void *dst,constvoid *src,int count)   
  2. {   
  3.     assert(dst);   
  4.     assert(src);   
  5.     void *ret=dst;   
  6.     while (count--)   
  7.     {   
  8.         *(char *)dst = *(char *)src;   
  9.         dst = (char *)dst + 1;   
  10.         src = (char *)src + 1;   
  11.     }   
  12.     return ret;   
  13. }   
    到目前為止,在語言層面上,我們的程式基本上沒有什麼問題了,那麼是否真的就沒有問題了呢?這就要求程式設計師從邏輯上考慮了,這也是優秀程式設計師必須具備的素質,那就是思維的嚴謹性,否則程式就會有非常隱藏的bug,就這個例子來說,如果使用者用下面的程式碼來呼叫你的程式。 
程式清單 8 重疊的記憶體測試 

[cpp] view plaincopyprint?
  1. void Test()   
  2. {   
  3.     char p [256]= "hello,world!";   
  4.     MyMemMove(p+1,p,strlen(p)+1);   
  5.     printf("%s\n",p);   
  6. }   
    如果你身邊有電腦,你可以試一下,你會發現輸出並不是我們期待的“hhello,world!”(在“hello world!”前加個h),而是“hhhhhhhhhhhhhh”,這是什麼原因呢?原因出在源地址區間和目的地址區間有重疊的地方,V0.6版的程式無意之中將源地址區間的內容修改了!有些反映快的同學馬上會說我從高地址開始拷貝。粗略地看,似乎能解決這個問題,雖然區間是重疊了,但是在修改以前已經拷貝了,所以不影響結果。但是仔細一想,這其實是犯了和上面一樣的思維不嚴謹的錯誤,因為使用者這樣呼叫還是會出錯: 
MyMemMove( p, p+1, strlen(p)+1); 所以最完美的解決方案還是判斷源地址和目的地址的大小,才決定到底是從高地址開始拷貝還是低地址開始拷貝,所以V0.7順利成章地出來了。 
程式清單 9 V0.7版程式 

[cpp] view plaincopyprint?
  1. 相關推薦

    C/c++記憶體拷貝函式memcpy

    原型:void*memcpy(void*dest, const void*src,unsigned int count);  功能:由src所指記憶體區域複製count個位元組到dest所指記憶體區域。 說明:src和dest所指記憶體區域不能重疊,函式返回指向des

    c++記憶體拷貝函式(C++ memcpy)

    原型:void*memcpy(void*dest, const void*src,unsigned int count);  功能:由src所指記憶體區域複製count個位元組到dest所指記憶體區域。 說明:src和dest所指記憶體區域不能重疊,函式返回指向dest的指

    C++,getline函式

    C++中本質上有兩種getline函式,一種在標頭檔案<istream>中,是istream類的成員函式。一種在標頭檔案<string>中,是普通函式。 在<istream>中的getline函式有兩種過載形式: istream&am

    C++的freopen()函式使用

    所謂重定向輸出,就是可以把原本只是輸出在控制檯的字元,輸出到你指定的路徑檔案中。(輸入類似,就是從指定的檔案中讀取,而不是讀取在控制檯中的輸入。)重定向函式可以在任何時候開啟、關閉。   函式名:freopen  標準宣告:FILE *freopen( const c

    C/C++的freopen()函式使用

    剛剛看到了一個比較有意思的C/C++重定向 標準輸入輸出 的庫函式。在此總結。 所謂重定向輸出,就是可以把原本只是輸出在控制檯的字元,輸出到你指定的路徑檔案中。(輸入類似,就是從指定的檔案中讀取,而不是讀取在控制檯中的輸入。)重定向函式可以在任何時候開啟、關閉。函式名:fr

    C++ STL的map容器用法

    Map是STL的一個關聯容器,它提供一對一(其中第一個可以稱為關鍵字,每個關鍵字只能在map中出現一次,第二個可能稱為該關鍵字的值)的資料 處理能力,由於這個特性,它完成有可能在我們處理一對一資料的時候,在程式設計上提供快速通道。這裡說下map內部資料的組織,m

    記憶體拷貝函式 memcpy

    windows下實現: void* __cdecl memcpy(void* dst,const void* src,size_t count) { void*ret=dst;   #if defined(_M_MRX000)||defined(_M_ALPHA)||defined(_M_PP

    記憶體拷貝函式memcpy

    memcpy memcpy是C/C++記憶體拷貝函式,函式原型void*memcpy(void *dest, const void *src, size_t n);功能是從源src所指的記憶體地址的起始位置開始拷貝n個位元組到目標dest所指的記憶體地址的起始位置中。 【m

    C語言中的system函式引數

    http://blog.csdn.net/pipisorry/article/details/33024727 函式名: system   功   能: 發出一個DOS命令   用   法: int system(char *command);   system函式已

    c++11enum class的用法

    要了解enum class的出現,則需要首先了解enum,方才知道為何有這東西。那麼Meyers首先舉出一個例子來闡述: enum Color {black, white, red}; auto white = false; // error 其緣由在於black, white, red等並沒有屬於C

    C 語言二維陣列指標

    C語言中,指標是一個複雜但又靈活多變的知識點,我們知道,在一維陣列中,對於一個數組a[],*a,a,&a,都表示a的首地址,但如果與二維陣列混合使用,就顯得更為複雜了。例如對於一個二維陣列  a[2][4]={{1,2.3},{4,5,6}}  a+i,&a

    JavaScript立即執行函式例項 轉載 作者:李牧羊

    javascript和其他程式語言相比比較隨意,所以javascript程式碼中充滿各種奇葩的寫法,有時霧裡看花,當然,能理解各型各色的寫法也是對javascript語言特性更進一步的深入理解。這篇文章主要給大家介紹了關於JavaScript中立即執行函式的相關資料,需要的朋友可以參考下。 前言

    Erlang的fun函式使用

       先看一個Erlang的規定:在Eralng中,同一個模組中的兩個函式,如果她們同名但是它們的目(arity)不同,這樣的兩個函式被認為是完全不同的兩個函式。通常情況下,這樣的函式被用作輔助函式。   fun函式就是一個匿名函式(因為他自己沒有名字),但就這個匿名函式,用

    Android記憶體洩漏超級精煉

    一、前期基礎知識儲備 (1)什麼是記憶體? JAVA是在JVM所虛擬出的記憶體環境中執行的,JVM的記憶體可分為三個區:堆(heap)、棧(stack)和方法區(method)。 棧(stack):是簡單的資料結構,但在計算機中使用廣泛。棧最顯著的特徵是:LIF

    GCD的dispatch_group函式

    1、引入dispatch_group函式的目的 在追加到dispatch_Queue中的多個處理全部結束後想要執行結束的處理,這種需求經常會在我們的程式中出現。 (第一種情況)只使用一個Serial Dispatch Queue時,只要將想要執行的操

    C++發聲函式Beep

    以前,我聽過一個神犇用C++函式做的音樂,當時的心裡就十分激動:哇,好厲害啊,好神啊。 這次,我終於通過自己無助的盲目的摸索、研究,寫出了這篇文章(此時我的內心是雞凍的233) 下面是正文: 其實啊,Windows API 就提供了一個這樣奇妙的發音函式,它就是Bee

    c++記憶體位元組對齊問題

    struct MyStruct  {  double dda1;  char dda;  int type  };  對結構MyStruct採用sizeof會出現什麼結果呢?sizeof(MyStruct)為多少呢?也許你會這樣求:  sizeof(MyStruct)=sizeof(double)+sizeo

    c++拷貝函數(轉)

    light clu 默認 fun 編譯 存在 自動生成 pri 指針成員 一. 什麽是拷貝構造函數 首先對於普通類型的對象來說,它們之間的復制是很簡單的,例如 int a = 100; int b = a; 而類對象與普通對象不同,類對象內部結構一般

    C#的IDisposable模式用法

    數據庫 nor 是否 entry block 記錄日誌 自定義 技術分享 ssa 本文實例講述了C#中IDisposable模式的用法,針對垃圾資源的回收進行了較為詳細的講解。分享給大家供大家參考之用。具體方法如下: 首先,對於垃圾回收而言,在C#中,托管資源的垃圾回收是

    C# Entity Framework的IQueryable和IQueryProvider

    oid display provide 分析 當前 負責 nbsp enum share 前言 相信大家對 Entity Framework 一定不陌生,我相信其中Linq To Sql是其最大的亮點之一,但是我們一直使用到現在卻不曾明白內部是如何實現的,今天我們