FOR I=1 TO 100 FOR J=I+1 TO 100 IF A[I] > A[J] THEN T=A[J]: A[J]=A[I]: A[I]=T NEXT J ? A[I] NEXT I方法2:冒泡法排序,然后再輸出。
FOR I=1 TO 100 FOR J=I+1 TO 100 IF A[I] > A[J] THEN T=A[J]: A[J]=A[I]: A[I]=T NEXT NEXT FOR I=1 TO 100 ? A[I] NEXT顯然,“方法1”比“方法2”的效率要高,運行的更快。但是,從現在的程序設計角度來看,“方法2”更高級。原因很簡單:(1)功能模塊分割清晰——易讀;(2)也是最重要的——易維護。程序在設計階段的時候,就要考慮以后的維護問題。比如現在是實現了在屏幕上的輸出,也許將來某一天,你要修改程序,輸出到打印機上、輸出到繪圖儀上;也許將來某一天,你學習了一個新的高級的排序方法,由“冒泡法”改進為“快速排序”、“堆排序”。那么在“方法2”的基礎上進行修改,是不是就更簡單了,更容易了?!這種把功能模塊分離的程序設計方法,就叫“結構化程序設計”。
for(i=1; i<100; i++) { for(j=2; j< i; j++) if(i%j == 0) break; if(j >= i) printf("%d,", i); }然后,老師開始批判這個程序“這個叫什么呀?太慢了!因為我們都知道大偶數不可能是素數了,因此,要排除掉!” 于是,意尤未盡地寫出了第二個程序:
printf("2,"); for(i=3; i<100; i+=2) { for(j=2; j< i; j++) if(i%j == 0) break; if(j >= i) printf("%d,", i); }老師說:“看!我們只改動了一點點,程序運行的速度就提高了一倍多”。然后運用誘導式教學法繼續提問“程序的效率,還能再提高嗎?能!”,得意地寫出第三個程序:
printf("2,"); for(i=3; i<100; i+=2) ''不考慮大偶數 { for(j=3; j< i/2; j+=2) ''不考慮用偶數去測試,而且只驗算到一半就足夠了 if(i%j == 0) break; if(j >= i) printf("%d,", i); }“大家看,我們又只改動了一點點,運行速度又提高了一倍多??梢粤藛??不可以!我們還能再提高”。于是又高傲地寫出了第四個程序:
printf("2,"); for(i=3; i<100; i+=2) { int k = sqrt(i); for(j=3; j<= k; j+=2) if(i%j == 0) break; if(j >= k ) printf("%d", i); }然后,開始證明為什么我們判斷素數的時候,只需要驗算到平方根就足夠了:
計算范圍 |
100 | 1000 | 10000 | 100000 |
速度差 | 0.00秒 | 0.01秒 | 0.18秒 | 15秒 |
題外話:
多年以來,人們一直在尋找動態圖象(影視)的存儲和回放的算法,但效果都不理想。直到有人發現,原來在200多年前的數學家早就幫我們解決了這個問題——傅立葉(Fourier)級數展開。因此我要說,優秀的算法不是我們程序員要考慮的問題,我們的任務只要按照數學家給出的算法翻譯為計算機程序語言而已。(這句話恐怕要遭到大多數程序員拋出的板磚襲擊)再比如,計算一元多次方程解的問題。我們使用的就是牛頓的迭代算法。不要怪我瞧不起你,你能發明這個方法的話,那就是當代的牛頓了。
四、程序的易讀性與書寫方法
程序是否容易閱讀和維護,與怎么書寫有很大的關系。說實在的,C語言中為了方便程序員書寫,允許使用++,--,<<,&&,?......這些運算符號。但很多人經常亂用,以為自己寫的程序多么簡潔,效率多高。其實,當你分行書寫的話則更加容易閱讀和維護,效率也不會降低,因為編譯程序早就幫你優化為最快捷的代碼了。先看一個簡單的例子:
計算一個整數乘 255(C語言)
方法1:a *= 255;
方法2:因為移位運算比乘法運算要快很多倍,因此a*255的運算書寫為:
a =(a<<8)-a; //a*255 = a*256 - a = (a<<8) - a
方法1的書寫非常簡單,直截了當,顯然更容易維護。而方法2的書寫運用了移位的技巧,不容易閱讀,但效率最高。是不是真的是這樣那?把這兩個程序編譯為匯編代碼看看。原來無論是方法1還是方法2,它們的匯編代碼都是一樣的:
mov ecx, eax shl eax, 8 sub eax, ecx
也就是說,你認為非常技巧的書寫方法,其實編譯器的優化功能早就幫你想到了。那么方法2的方式就很值得批判了。下面是幾個有關C語言書寫方面的重要原則:
for(int a=1,s=0; a<=100 && (s+=a); a++);天呀,這樣寫是不會提高程序運行效率的,尤其是當運算表達式復雜的時候,就更不容易閱讀了,還是把運算寫到for的循環體中吧。
int s = 0; for(int a=1; a<=100; a++) s += a; //計算1+2+...+100 這不很好嗎?!再比如,if(a=b)這個寫法在語法上是允許的,但不要使用。要使用也要if(0!=(a=b))這樣的方式。 還有值得一提的是慎用“,”(逗號運算符)。
a = b++-(--c<<1+e&0x0f>>1); //這個人有病。出這個題目考試的老師,也有病。
file.Open(...); //當要打開文件的時候 char *lp=new char [100]; //當要申請內存的時候 ...... //先不要寫這段代碼 ...... //先不要寫這段代碼 file.Close(); //馬上寫關閉 delete [] lp; //馬上寫釋放 xxx.Loack(); //當某個對象需要鎖定的時候 for(....) ...... //先不要寫這段代碼 { //寫大括號的時候 xxx.Unlock(); //馬上寫解鎖 } //馬上寫大括號結束和這個道理相同,在C++的類中,如果需要申請內存,那么先在構造函數中給出 lp=NULL;然后馬上在析構函數中書寫 if(lp) delete []lp;
題目:合并2個文件到一個新文件中。(不要挑我的毛病呀~~~~~,我使用的是類C的方式書寫的。)
方法1:
FILE *f1,*f2,*f3; if(Open(f1)成功) { if(Open(f2)成功) { if(Open(f3)成功) { ...... //這里是真正干活的地方 Close(f1); Close(f2); Close(f3); } else //f3不成功 { Close(f1); Close(f2); ...... } } else //f2不成功 { Close(f1); ...... } } else //f1不成功 { ...... }==========================================================
FILE *f1=NULL,*f2=NULL,*f3=NULL; if(Open(f1)不成功) goto err; if(Open(f2)不成功) goto err; if(Open(f3)不成功) goto err; ...... //這里是真正干活的地方 err: if(f3) Close(f3); if(f2) Close(f2); if(f1) Close(f1);
方法1是最最標準的結構化設計,好嗎?不好!尤其是當{ }的層次比較深的時候,估計你尋找真正干活的代碼的地方都找不到。而使用方法2的程序,不但程序容易讀,而且沒有{ } 的深度。在C++中,又提供了異常try/catch的設計結構,而異常的結構則比 goto 的結構更好、更完善了。
五、面向對象的程序設計
隨著程序的設計的復雜性增加,結構化程序設計方法又不夠用了。不夠用的根本原因是“代碼重用”的時候不方便。面向對象的方法誕生了,它通過繼承來實現比較完善的代碼重用功能。很多學生在應聘工作,面試的時候,常被問及一個問題“你來談談什么是面向對象的程序設計”,學生無言,回來問我,這個問題應該怎么回答。我告訴他,你只要說一句話就夠了“面向對象程序設計是對數據的封裝;范式(模板)的程序設計是對算法的封裝?!焙髞碓儆袑W生遇到了這個問題,只簡單的一句對答,對方就對這個學生就刮目相看了(學生后來自豪地告訴我的)。為什么那?因為只有經過徹底的體會和實踐才能提煉出這個精華。
面向對象的設計方法和思想,其實早在70年代初就已經被提出來了。其目的就是:強制程序必須通過函數的方式來操縱數據。這樣實現了數據的封裝,就避免了以前設計方法中的,任何代碼都可以隨便操作數據而因起的BUG,而查找修改這個BUG是非常困難的。那么你可以說,即使我不使用面向對象,當我想訪問某個數據的時候,我就通過調用函數訪問不就可以了嗎?是的,的確可以,但并不是強制的。人都有惰性,當我想對 i 加1的時候,干嗎非要調用函數呀?算了,直接i++多省事呀。呵呵,正式由于這個懶惰,當程序出BUG的時候,可就不好捉啦。而面向對象是強制性的,從編譯階段就解決了你懶惰的問題。
巧合的是,面向對象的思想,其實和我們的日常生活中處理問題是吻合的。舉例來說,我打算丟掉一個茶杯,怎么扔那?太簡單了,拿起茶杯,走到垃圾桶,扔!注意分析這個過程,我們是先選一個“對象”------茶杯,然后向這個對象施加一個動作——扔。每個對象所能施加在它上面的動作是有一定限制的:茶杯,可以被扔,可以被砸,可以用來喝水,可以敲它發出聲音......;一張紙,可以被寫字,可以撕,可以燒......。也就是說,一旦確定了一個對象,則方法也就跟著確定了。我們的日常生活就是如此。但是,大家回想一下我們程序設計和對計算機的操作,卻不是這樣的。拿DOS的操作來說,我要刪除一個文件,方法是在DOS提示符下:c:> del 文件名<回車>。注意看這個過程,動作在前(del),對象在后(文件名),和面向對象的方法正好順序相反。那么只是一個順序的問題,會帶來什么影響那?呵呵,大家一定看到過這個現象:File not found. “啊~~~,我錯了,我錯了,文件名敲錯了一個字母”,于是重新輸入:c:> del 文件名2<回車>。不幸又發生了,計算機報告:File read only. 哈哈,痛苦吧:)。所以DOS的操作其實是違反我們日常生活中的習慣的(當然,以前誰也沒有提出過異議),而現在由于使用了面向對象的設計,那么這些問題,就在編譯的時候解決了,而不是在運行的時候。obj.fun(),對于這條語句,無論是對象,還是函數,如果你輸入有問題,那么都會在編譯的時候報告出來,方便你修改,而不是在執行的時候出錯,害的你到處去捉蟲子。
同時,面向對象又能解決代碼重用的問題——繼承。我以前寫了一個“狗”的類,屬性有(變量):有毛、4條腿、有翹著的尾巴(耷拉著尾巴的那是狼)、鼻子很靈敏、喜歡吃肉骨頭......方法有(函數):能跑、能聞、汪汪叫......如果它去抓耗子,人家叫它“多管閑事”。好了,狗這個類寫好了。但在我實際的生活中,我家養的這條狗和我以前寫的這個“狗類”非常相似,只有一點點的不同,就是我的這條狗,它是:卷毛而且長長的,鼻子小,嘴小......。于是,我派生一個新的類型,叫“哈巴狗類”在“狗類”的基礎上,加上新的特性。好了,程序寫完了,并且是重用了以前的正確的代碼——這就是面向對象程序設計的好處。我的成功只是站在了巨人的肩膀上。當然,如果你使用VC的話,重用最多的代碼就是MFC的類庫。
六、組件(COM)程序設計
有了面向對象程序設計方法,就徹底解決了代碼重用的問題了嗎?答案是:否!硬件越來越快,越來越小了,軟件的規模卻也越來越大了,集體合作越來越重要,代碼重用又出現的新的問題。
COM程序設計方法,就是解決以上問題的一個方式。有很多朋友覺得COM非常復雜難懂,不想學習了。你一定學習過程序設計的最基本的方法(非結構化設計:匯編、gwBasic......),然后,你又學習了結構化程序設計(C、Pascal......),然后,你又努力學習并熟練掌握了面向對象的程序設計方法(C++、Delphi、Java......),那么不要怕,要有信心去學習組件程序設計,它只是一個設計方法和思想,并且是目前較高級的方法,如果不掌握,就太可惜了。
學習了結構化程序設計,你就會“藐視”那些不遵守結構化設計思想而寫出的代碼; 學習了面向對象設計,你就會“嘲笑”那些為找BUG而暈頭轉向的程序員; 同樣,學習了組件程序設計,你就會站在更高的層次看待程序設計。
七、結束語
寫程序的目的是什么?養家糊口、興趣使然、我的事業......這些都對。但我要強調的是:寫程序的目的是為了修改程序。在這個觀點上,那么寫注釋、寫文檔、選擇語言、選擇結構......都是為這個服務的。本文從軟件設計方法的進化角度來反復闡述這個觀點,希望愛好者能有所體會和思考。
文中所討論的技術和觀點,適合于大多數情況下的程序設計,而對于特殊的應用的(驅動開發,嵌入式開發,網絡通訊,實時視頻......),這些領域中,由于硬件環境的限制和極限效率的要求,有些觀點就不合適了,需要具體情況具體分析。另外就是對于程序設計的初學者,可以先不考慮這么多問題,以掌握基本技巧方法和思想為要。
歡迎大家批評指正。文中一些調侃的部分,也請勿對號入坐^_^。