生活随笔
收集整理的這篇文章主要介紹了
内存拷贝函数的深入思考
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
本文是我們訓練程序員的系列文章之一,僅供參考!
?這幾年在全國巡回招聘應屆畢業生的過程中,經常會遇到這樣的現象:有些同學對自己的筆試比較滿意,可是最后卻得不到面試的機會,心里大為不解,頗有“死不瞑目”的味道。那么問題到底出現在哪里呢?讓我們來看一個例子,這是我們招聘過程中一道常見的題目。
?寫一個函數,完成內存移動,并為其寫一個簡單的測試用例來進行測試。
?夠簡單的吧?有的同學很快就寫出了答案,詳見程序清單1與程序清單2。
程序清單?1?V0.1版程序
[cpp]?view plaincopy
void?MyMemMove(char?*dst,char?*src,int?count)?? {?? ????while(count--)?? ????{?? ????????*dst++?=?*src++;?? ????}?? }?? 程序清單?2?測試用例
[cpp]?view plaincopy
void?Test()?? {?? ???char?p1[256]?=?”hello,world!”;?? ???char?p2[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下就會出現這樣的錯誤:
?error?C2664:?'MyMemMove'?:?cannot?convert?parameter?1?from?'TheStruct?*'?to?'char?*'
?那么如何解決這個問題呢?其實很簡單,我們知道有一種特別的指針,任何類型的指針都可以對它賦值,那就是void?*,所以應該將源地址和目的地址都用void*來表示。當然函數體的內容也要作相應的改變,這樣我們就得到了V0.2版的程序。
程序清單?3?V0.2版程序
[cpp]?view plaincopy
void?MyMemMove(void?*dst,void?*src,int?count)?? {?? ???while?(count--)?? ???{?? ??????*(char?*)dst?=?*(char?*)src;?? ??????dst?=?(char?*)dst?+?1;?? ??????src?=?(char?*)src?+?1;?? ???}?? [cpp]?view plaincopy
}?? [cpp]?view plaincopy
??
?
?有的同學可能會問,這里面不是還有指針強制轉換嗎?只不過是換了地方。沒錯,強制指針轉換確實是從使用者的代碼轉移到了庫的代碼里,但我們可以將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 plaincopy
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;?? ????}?? ????return?ret;?? }?? ?現在再來考慮這樣一種情況,有使用者這樣調用庫:
MyMemMove(NULL,src,?count),這是完全可能的,因為一般來說這些地址都是程序計算出來的,那就難免會算錯,出現零地址或者其它的非法地址也不足為奇。可以預料的是,如果出現這種情況的話,則程序馬上就會down掉,更糟糕的是你不知道錯誤出在哪里,于是不得不投入大量的精力在浩瀚的代碼中尋找bug。解決這類問題的通用辦法是對輸入參數作合法性檢查,也就是V0.4版程序。 程序清單?5?V0.4版程序
[cpp]?view plaincopy
void?*?MyMemMove(void?*dst,const?void?*src,int?count)?? {?? ????void?*ret=dst;?? ????if?(NULL==dst||NULL?==src)?? ????{?? ????????return?dst;?? ????}?? ????while?(count--)?? ????{?? ????????*(char?*)dst?=?*(char?*)src;?? ????????dst?=?(char?*)dst?+?1;?? ????????src?=?(char?*)src?+?1;?? ????}?? ????return?ret;?? }?? ?上面之所以寫成“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 plaincopy
void?*?MyMemMove(void?*dst,const?void?*src,int?count)?? {?? ????void?*ret=dst;?? ????#ifdef?DEBUG?? ????if?(NULL==dst||NULL?==src)?? ????{?? ????????return?dst;?? ????}?? ????#endif?? ????while?(count--)?? ????{?? ????????*(char?*)dst?=?*(char?*)src;?? ????????dst?=?(char?*)dst?+?1;?? ????????src?=?(char?*)src?+?1;?? ????}?? ????return?ret;?? }?? ?如果在調試時我們加入
“#define?DEBUG”語句,增強程序的健壯性,那么在調試通過后我們再改為“#undef?DEBUG”語句,提高程序的性能。事實上在標準庫里已經存在類似功能的宏:assert,而且更加好用,它還可以在定義DEBUG時指出代碼在那一行檢查失敗,而在沒有定義DEBUG時完全可以把它當作不存在。assert(_Expression)的使用非常簡單,當_Expression為0時,調試器就可以出現一個調試錯誤,有了這個好東西代碼就容易多了。 程序清單?7?V0.6版程序
[cpp]?view plaincopy
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;?? ????}?? ????return?ret;?? }?? ?一旦調用者的兩個指針參數其中一個為零,就會出現如圖1所示的錯誤,而且指示了哪一行非常容易查錯。
?
?圖?1?assert(NULL)時,顯示錯誤
?到目前為止,在語言層面上,我們的程序基本上沒有什么問題了,那么是否真的就沒有問題了呢?這就要求程序員從邏輯上考慮了,這也是優秀程序員必須具備的素質,那就是思維的嚴謹性,否則程序就會有非常隱藏的bug,就這個例子來說,如果用戶用下面的代碼來調用你的程序。
程序清單?8?重疊的內存測試
[cpp]?view plaincopy
void?Test()?? {?? ???char?p?[256]=?"hello,world!";?? ???MyMemMove(p+1,p,strlen(p)+1);?? ???printf("%s\n",p);?? }?? ?如果你身邊有電腦,你可以試一下,你會發現輸出并不是我們期待的
“hhello,world!”(在“hello?world!”前加個h),而是“hhhhhhhhhhhhhh”,這是什么原因呢?原因出在源地址區間和目的地址區間有重疊的地方,V0.6版的程序無意之中將源地址區間的內容修改了!有些反映快的同學馬上會說我從高地址開始拷貝。粗略地看,似乎能解決這個問題,雖然區間是重疊了,但是在修改以前已經拷貝了,所以不影響結果。但是仔細一想,這其實是犯了和上面一樣的思維不嚴謹的錯誤,因為用戶這樣調用還是會出錯: ?MyMemMove(?p,?p+1,?strlen(p)+1);
?所以最完美的解決方案還是判斷源地址和目的地址的大小,才決定到底是從高地址開始拷貝還是低地址開始拷貝,所以V0.7順利成章地出來了。
程序清單?9?V0.7版程序
[cpp]?view plaincopy
<pre?style="BACKGROUND-COLOR:?rgb(240,240,240);?MARGIN:?4px?0px"?class="cpp"?name="code">void?*?MyMemMove(void?*dst,const?void?*src,int?count)?? {?? ???assert(dst);?? ???assert(src);?? ???void?*?ret?=?dst;?? ???if?(dst?<=?src?||?(char?*)dst?>=?((char?*)src?+?count))?</pre><pre?style="BACKGROUND-COLOR:?rgb(240,240,240);?MARGIN:?4px?0px"?class="cpp"?name="code">???{?? ???????while?(count--)?</pre><pre?style="BACKGROUND-COLOR:?rgb(240,240,240);?MARGIN:?4px?0px"?class="cpp"?name="code">???????{?? ???????????*(char?*)dst?=?*(char?*)src;?? ???????????dst?=?(char?*)dst?+?1;?? ???????????src?=?(char?*)src?+?1;?? ???????}?? ????}?? ????else?</pre><pre?style="BACKGROUND-COLOR:?rgb(240,240,240);?MARGIN:?4px?0px"?class="cpp"?name="code">????{?? ????????dst?=?(char?*)dst?+?count?-?1;?? ????????src?=?(char?*)src?+?count?-?1;?? ????????while?(count--)?</pre><pre?style="BACKGROUND-COLOR:?rgb(240,240,240);?MARGIN:?4px?0px"?class="cpp"?name="code">????????{?? ????????????*(char?*)dst?=?*(char?*)src;?? ????????????dst?=?(char?*)dst?-?1;?? ????????????src?=?(char?*)src?-?1;?? ????????}?? ????}?? ????return(ret);?? }</pre><br>?? <pre></pre>?? ?經過以上<span?style="font-family:'Times?New?Roman'">7</span><span?style="font-family:宋體">個版本的修改,我們的程序終于可以算是</span><span?style="font-family:'Times?New?Roman'">“</span><span?style="font-family:宋體">工業級</span><span?style="font-family:'Times?New?Roman'">”</span><span?style="font-family:宋體">了。回頭再來看看前面的測試用例,就會發現那根本就算不上是測試用例,因為它只調用了最正常的一種情況,根本達不到測試的目的。有了上面的經歷,測試用例也就相應地出現了,我們不妨用字符數組來模擬內存。</span>?? <p></p>?? <p?style="padding-bottom:2px;?margin:4px?0px;?padding-left:0px;?padding-right:0px;?padding-top:2px">?? 程序清單?<span?style="font-family:'Times?New?Roman'">10?</span><span?style="font-family:宋體">相對全面的測試用例</span></p>?? <p?style="padding-bottom:2px;?margin:4px?0px;?padding-left:0px;?padding-right:0px;?padding-top:2px">?? </p>?? <pre?style="BACKGROUND-COLOR:?rgb(240,240,240);?MARGIN:?4px?0px"?class="cpp"?name="code">void?Test()?? {?? ???char?p1[256]?=?"hello,world!";?? ???char?p2[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);?? }</pre><br>?? <br>?? <p></p>?? <p?style="padding-bottom:2px;?margin:4px?0px;?padding-left:0px;?padding-right:0px;?padding-top:2px">?? ?初寫代碼的時候,往往考慮的是程序正常工作的情況該怎么處理。當你有了幾年經驗,寫了幾萬行代碼后就會發現,處理異常部分的分支代碼有時比正常的主干線代碼還要多,而這也正是高質量程序和一般程序拉開差距的地方。如果把軟件產品當作一臺機器,那么這樣一個個細小的函數和類就是零部件,只有當這些零部件質量都很高時,整個軟件產品的質量才會高,不然就會像前幾年的國產轎車一樣,今天這個零件罷工明天那個零件休息。而作為檢驗這些零部件的測試用例,一定要模擬各種惡劣的環境,將零部件隱藏的缺陷暴露出來,從這意義上說,編寫測試用例的程序員要比軟件設計的程序員思維要更嚴謹才行。</p>?? <pre></pre> ?
總結
以上是生活随笔為你收集整理的内存拷贝函数的深入思考的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。