你真的會寫記憶體拷貝程式嗎

2021-10-23 08:49:33 字數 4801 閱讀 5162

寫乙個函式,完成記憶體移動(拷貝),並為其寫乙個簡單的測試用例來進行測試。

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

程式清單 1   v0.1版程式

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

}

程式清單 2   測試用例

void test()

; 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版程式

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

}

有的同學可能會問,這裡面不是還有指標強制轉換嗎?只不過是換了地方。沒錯,強制指標轉換確實是從使用者的**轉移到了庫的**裡,但我們可以將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版程式

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

return ret;

}

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

程式清單 5  v0.4版程式

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

while (count--)

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版程式

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

#endif

while (count--)

return ret;

}

如果在除錯時我們加入「#define debug」語句,增強程式的健壯性,那麼在除錯通過後我們再改為「#undef debug」語句,提高程式的效能。事實上在標準庫里已經存在類似功能的巨集:assert,而且更加好用,它還可以在定義debug時指出**在那一行檢查失敗,而在沒有定義debug時完全可以把它當作不存在。assert(_expression_r_r_r)的使用非常簡單,當_expression_r_r_r為0時,偵錯程式就可以出現乙個除錯錯誤,有了這個好東西**就容易多了。

程式清單 7  v0.6版程式

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

return ret;

}

一旦呼叫者的兩個指標引數其中乙個為零,就會出現如圖1所示的錯誤,而且指示了哪一行非常容易查錯。

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

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

程式清單 8  重疊的記憶體測試

void test()

如果你身邊有電腦,你可以試一下,你會發現輸出並不是我們期待的「hhello,world!」(在「hello world!」前加個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)

}else

}return(ret);

}

經過以上7個版本的修改,我們的程式終於可以算是「工業級」了。回頭再來看看前面的測試用例,就會發現那根本就算不上是測試用例,因為它只呼叫了最正常的一種情況,根本達不到測試的目的。有了上面的經歷,測試用例也就相應地出現了,我們不妨用字元陣列來模擬記憶體。

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

void test()

; 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);

}

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

你真的會寫週報嗎

初次踏入職場的人而言,寫週報是一件陌生的事情.乙份好的週報對自己的職場有著不可忽視的作用.對於老闆而言,收到乙份的週報,可以盡快了解員工的工作進度以及員工的所遇到的問題,如果有的時候乙份週報什麼問題什麼都沒有,反而會令上級覺的無法把控工作的進度,埋藏著 的炸彈.優秀的週報會令上級覺得此人做事穩重可靠...

你真的會寫二分查詢嗎?

二分查詢 二分查詢,找到該值在陣列中的下標,否則為 1 static int binaryserach int array,int key else if array mid key else return 1 每次移動left和right指標的時候,需要在mid的基礎上 1或者 1,防止出現死迴圈...

你真的懂程式設計師嗎?

人們常說程式設計師的生活枯燥為人刻板,其實這是你不懂程式設計師。程式設計師也是很懂得品味人生的,因為工作影響他門也許會對生活感慨,傷感自己沒足夠或是更多的時間去做工作之外的別的事。程式設計師在學習和工作期間幾乎天天和機器打交道,壓根就沒有受欺負或是欺負別人的機會,勤奮的程式設計師在除錯無窮多的程式b...