memcpy的完美實現--卓越的教練是如何訓練高手的

卓越的教練是如何訓練高手的?

(2009-08-06 12:44:53)

   編者按我們知道,優秀的運動員除了自身的天賦和努力之外,出色的教練必不可少。一個成功的企業除了擁有出類拔萃的員工之外,同樣需要一位出色的教練,那就是企業的CEO。由此可見,如果我們要想成爲一位優秀的程序員,毫無疑問卓越的教練是致關重要的。無數事實告訴我們,成功者之所以成功,是因爲成功地選擇導師的緣故。

   周立功公司之所以在一定程度上取得了一些成功,絕對不是運氣所爲,除了不斷努力選拔優秀人才之外,且不惜代價投入人力和資本培訓員工,更重要的是有效的方法,從而保證了初學者的投入與產出的最佳效果,讓具備一定潛質的應屆大學生脫穎而出。

    優秀與平庸全在一念之間,關鍵在於你的首次擇業而非就業,良好的習慣與平和的心態跟人們的第一份工作和第一位導師有很大的關係。當下的利益固然重要,但從某種程度上來看,機會、環境、遠景比眼前利益更重要,因爲機會、環境和遠景等因素會最大限度地將你鍛造成爲一個在未來有絕對競爭力的人才。同樣是賺錢,你是否參與了公司由默默無聞邁向卓越的過程?你是否收穫了更多的快樂?你是否找到了前所未有的成就感?而很多人僅僅賺到了一些錢,幸福而不快樂。

   你將希望成爲哪一類人,何去何從?最初的選擇尤其重要。

   本文是我們訓練程序員的系列文章之一,僅供參考!

 

   這幾年在全國巡迴招聘應屆畢業生的過程中,經常會遇到這樣的現象:有些同學對自己的筆試比較滿意,可是最後卻得不到面試的機會,心裏大爲不解,頗有“死不瞑目”的味道。那麼問題到底出現在哪裏呢?讓我們來看一個例子,這是我們招聘過程中一道常見的題目。

  寫一個函數,完成內存移動,併爲其寫一個簡單的測試用例來進行測試。

   夠簡單的吧?有的同學很快就寫出了答案,詳見程序清單1與程序清單2。

        

程序清單 1   V0.1版程序

void MyMemMove(char *dst,char *src,int count)

{

    while(count--)

    {

       *dst++ = *src++;

    }

}

 

程序清單 2   測試用例

void Test()

{

    charp1[256] = ”hello,world!”;

    charp2[256] = {0};

    MyMemMove(p2,p1,strlen(p1));

    printf(“%s”,p2);

}

   客觀地講,相比那些交白卷或者函數聲明都不會寫的同學來說,能夠寫出這段代碼的同學已經非常不錯了,至少在C語言這門課程上已經達到了現行高校的教育目標,但是離企業的用人要求還有一定的距離。我們不妨將上面的程序稱爲V0.1版本,看看還有沒有什麼地方可以改進。

   首先我們看看函數聲明是否合理,V0.1版的程序將源地址和目的地址都用char*來表示,這樣當然也沒有什麼問題,但是讓其他人使用起來卻很不方便,假如現在要將count個連續的結構體對象移動到另外一個地方去,如果要使用v0.1的程序的話,正確的寫法如下:

    MyMemMove((char *)dst,(char *)src,sizeof(TheStruct)*count)

   也就是說我們需要將結構體指針強制轉換成char *才能夠正常工作,這樣除了字符串以外其它的類型都不可避免地要進行指針強制轉換,否則編譯器就會呱呱叫,比如在VC++2008下就會出現這樣的錯誤:

    errorC2664: 'MyMemMove' : cannot convert parameter 1 from 'TheStruct *'to 'char *'

   那麼如何解決這個問題呢?其實很簡單,我們知道有一種特別的指針,任何類型的指針都可以對它賦值,那就是void*,所以應該將源地址和目的地址都用void*來表示。當然函數體的內容也要作相應的改變,這樣我們就得到了V0.2版的程序。

 

程序清單 3   V0.2版程序

void MyMemMove(void *dst,void *src,int count)

{

     while(count--)

     {

        *(char*)dst = *(char *)src;

        dst= (char *)dst + 1;

        src= (char *)src + 1;

     }

}

    有的同學可能會問,這裏面不是還有指針強制轉換嗎?只不過是換了地方。沒錯,強制指針轉換確實是從使用者的代碼轉移到了庫的代碼裏,但我們可以將MyMemMove理解爲庫,而將Test理解爲使用者,事實上通過調整之後的效果卻有天壤之別,V0.1是一逸永勞,而V0.2是一勞永逸!

    還有幾個細節需要注意,爲了實現鏈式表達式,我們應該將返回值也改爲void*。此外,如果我們不小心將“*(char *)dst = *(char *)src;”寫反了,寫成“*(char *)src =*(char*)dst;”編譯照樣通過,而爲了找出這個錯誤又得花費不少時間。注意到src所指向的內容在這個函數內不應該被改變,所有對src所指的內容賦值都應該被禁止,所以這個參數應該用const修飾,如果有類似的錯誤在編譯時就能夠被發現:

    errorC3892: 'src' : you cannot assign to a variable that is const

   作爲程序員犯錯誤在所難免,但是我們可以利用相對難犯錯誤的機器,也就是編譯器來降低犯錯誤的概率,這樣我們就得到了V0.3版的程序。

 

程序清單 4  V0.3版程序

void * MyMemMove(void *dst,const void *src,int count)

{

    void*ret=dst;

    while(count--)

    {

       *(char *)dst = *(char *)src;

       dst = (char *)dst + 1;

       src = (char *)src + 1;

    }

     returnret;

}

    現在再來考慮這樣一種情況,有使用者這樣調用庫:MyMemMove(NULL,src,count),這是完全可能的,因爲一般來說這些地址都是程序計算出來的,那就難免會算錯,出現零地址或者其它的非法地址也不足爲奇。可以預料的是,如果出現這種情況的話,則程序馬上就會down掉,更糟糕的是你不知道錯誤出在哪裏,於是不得不投入大量的精力在浩瀚的代碼中尋找bug。解決這類問題的通用辦法是對輸入參數作合法性檢查,也就是V0.4版程序。

 

程序清單 5  V0.4版程序

void * MyMemMove(void *dst,const void *src,int count)

{

    void*ret=dst;

    if(NULL==dst||NULL ==src)

    {

        returndst;

    }

    while(count--)

    {

        *(char*)dst = *(char *)src;

       dst = (char *)dst + 1;

        src= (char *)src + 1;

    }

     returnret;

}

    上面之所以寫成“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版程序

void * MyMemMove(void *dst,const void *src,int count)

{

     void*ret=dst;

#ifdef DEBUG

    if(NULL==dst||NULL ==src)

    {

         returndst;

    }

#endif

    while(count--)

    {

          *(char*)dst = *(char *)src;

          dst= (char *)dst + 1;

          src= (char *)src + 1;

     }

    return ret;

}

   如果在調試時我們加入“#define DEBUG”語句,增強程序的健壯性,那麼在調試通過後我們再改爲“#undefDEBUG”語句,提高程序的性能。事實上在標準庫裏已經存在類似功能的宏:assert,而且更加好用,它還可以在定義DEBUG時指出代碼在那一行檢查失敗,而在沒有定義DEBUG時完全可以把它當作不存在。assert(_expression_r_r)的使用非常簡單,當_expression_r_r爲0時,調試器就可以出現一個調試錯誤,有了這個好東西代碼就容易多了。

 

程序清單 7  V0.6版程序

void * MyMemMove(void *dst,const void *src,int count)

{

    assert(dst);

    assert(src);

    void*ret=dst;

    while(count--)

    {

        *(char*)dst = *(char *)src;

        dst= (char *)dst + 1;

        src= (char *)src + 1;

     }

     returnret;

}

   一旦調用者的兩個指針參數其中一個爲零,就會出現如圖1所示的錯誤,而且指示了哪一行非常容易查錯。

 

        卓越的教練是如何訓練高手的?

                          圖 1  assert(NULL)時,顯示錯誤

   到目前爲止,在語言層面上,我們的程序基本上沒有什麼問題了,那麼是否真的就沒有問題了呢?這就要求程序員從邏輯上考慮了,這也是優秀程序員必須具備的素質,那就是思維的嚴謹性,否則程序就會有非常隱藏的bug,就這個例子來說,如果用戶用下面的代碼來調用你的程序。

 

程序清單 8  重疊的內存測試

void Test()

{

    charp [256]= "hello,world!";

    MyMemMove(p+1,p,strlen(p)+1);

    printf("%s\n",p);

}

    如果你身邊有電腦,你可以試一下,你會發現輸出並不是我們期待的“hhello,world!”(在“helloworld!”前加個h),而是“hhhhhhhhhhhhhh”,這是什麼原因呢?原因出在源地址區間和目的地址區間有重疊的地方,V0.6版的程序無意之中將源地址區間的內容修改了!有些反映快的同學馬上會說我從高地址開始拷貝。粗略地看,似乎能解決這個問題,雖然區間是重疊了,但是在修改以前已經拷貝了,所以不影響結果。但是仔細一想,這其實是犯了和上面一樣的思維不嚴謹的錯誤,因爲用戶這樣調用還是會出錯:

   MyMemMove( p, p+1, strlen(p)+1);

   所以最完美的解決方案還是判斷源地址和目的地址的大小,才決定到底是從高地址開始拷貝還是低地址開始拷貝,所以V0.7順利成章地出來了。

 

程序清單 9  V0.7版程序

void * MyMemMove(void *dst,const void *src,int count)

{

     assert(dst);

     assert(src);

     void* ret = dst;

     if(dst <= src || (char *)dst >= ((char*)src + count)) {

        

        while (count--) {

            *(char *)dst = *(char *)src;

            dst = (char *)dst + 1;

            src = (char *)src + 1;

        }

     }

      else{

         

         dst = (char *)dst + count - 1;

         src = (char *)src + count - 1;

 

         while (count--) {

         *(char *)dst = *(char *)src;

         dst = (char *)dst - 1;

         src = (char *)src - 1;

       }

    }

    return(ret);

}

    經過以上7個版本的修改,我們的程序終於可以算是“工業級”了。回頭再來看看前面的測試用例,就會發現那根本就算不上是測試用例,因爲它只調用了最正常的一種情況,根本達不到測試的目的。有了上面的經歷,測試用例也就相應地出現了,我們不妨用字符數組來模擬內存。

 

程序清單 10  相對全面的測試用例

void Test()

{

    charp1[256] = "hello,world!";

    charp2[256] = {0};

   MyMemMove(p2,p1,strlen(p1)+1);

   printf("%s\n",p2);

   MyMemMove(NULL,p1,strlen(p1)+1);

   MyMemMove(p2,NULL,strlen(p1)+1);

   MyMemMove(p1+1,p1,strlen(p1)+1);

   printf("%s\n",p1);

   MyMemMove(p1,p1+1,strlen(p1)+1);

   printf("%s\n",p1);

}

    初寫代碼的時候,往往考慮的是程序正常工作的情況該怎麼處理。當你有了幾年經驗,寫了幾萬行代碼後就會發現,處理異常部分的分支代碼有時比正常的主幹線代碼還要多,而這也正是高質量程序和一般程序拉開差距的地方。如果把軟件產品當作一臺機器,那麼這樣一個個細小的函數和類就是零部件,只有當這些零部件質量都很高時,整個軟件產品的質量纔會高,不然就會像前幾年的國產轎車一樣,今天這個零件罷工明天那個零件休息。而作爲檢驗這些零部件的測試用例,一定要模擬各種惡劣的環境,將零部件隱藏的缺陷暴露出來,從這意義上說,編寫測試用例的程序員要比軟件設計的程序員思維要更嚴謹才行。

發佈了110 篇原創文章 · 獲贊 20 · 訪問量 32萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章