華為軟體編程規範學習(六)--函數、過程
6-1:對所調用函數的錯誤返回碼要仔細、全面地處理
6-2:明確函數功能,精確(而不是近似)地實現函數設計
6-3:編寫可重新進入函數時,應注意局部變數的使用(如編寫C/C++語言的可重新進入函數時,應使用auto即預設態局部變數或寄存器變數)
說明:編寫C/C++語言的可重新進入函數時,不應使用static局部變數,否則必須經過特殊處理,才能使函數具有可重新進入性。
6-4:編寫可重新進入函數時,若使用全域變數,則應通過關中斷、訊號量(即P、V操作)等手段對其加以保護
說明:若對所使用的全域變數不加以保護,則此函數就不具有可重新進入性,即當多個進程調用此函數時,很有可能使有關全域變數變為不可知狀態。
樣本:假設Exam是int型全域變數,函數Squre_Exam返回Exam平方值。那麼如下函數不具有可重新進入性。
unsigned int example(int para ){ unsigned int temp; Exam = para; // (**) temp = Square_Exam( ); return temp;}
此函數若被多個進程調用的話,其結果可能是未知的,因為當(**)語句剛執行完後,另外一個使用本函數的進程可能正好被啟用,那麼當新啟用的進程執行到此函數時,將使Exam賦與另一個不同的para值,所以當控制重新回到“temp = Square_Exam()”後,計算出的temp很可能不是預想中的結果。此函數應如下改進。
unsigned int example(int para ){ unsigned int temp; [申請訊號量操作] // 若申請不到“訊號量”,說明另外的進程正處於 Exam = para; // 給Exam賦值並計算其平方過程中(即正在使用此 temp = Square_Exam( ); // 訊號),本進程必須等待其釋放訊號後,才可繼 [釋放訊號量操作] // 續執行。若申請到訊號,則可繼續執行,但其 // 它進程必須等待本進程釋放訊號量後,才能再使 // 用本訊號。 return temp;}
6-5:在同一項目組應明確規定對介面函數參數的合法性檢查應由函數的調用者負責還是由介面函數本身負責,預設是由函數調用者負責
說明:對於模組間介面函數的參數的合法性檢查這一問題,往往有兩個極端現象,即:要麼是調用者和被調用者對參數均不作合法性檢查,結果就遺漏了合法性檢查這一必要的處理過程,造成問題隱患;要麼就是調用者和被調用者均對參數進行合法性檢查,這種情況雖不會造成問題,但產生了冗餘代碼,降低了效率。
其他
6-1:防止將函數的參數作為工作變數
說明:將函數的參數作為工作變數,有可能錯誤地改變參數內容,所以很危險。對必須改變的參數,最好先用局部變數代之,最後再將該局部變數的內容賦給該參數。
樣本:下函數的實現不太好。
void sum_data(unsigned int num, int *data, int *sum ){ unsigned int count; *sum = 0; for (count = 0; count < num; count++) { *sum += data[count]; // sum成了工作變數,不太好。 }}
若改為如下,則更好些。
void sum_data(unsigned int num, int *data, int *sum ){ unsigned int count ; int sum_temp; sum_temp = 0; for (count = 0; count < num; count ++) { sum_temp += data[count]; } *sum = sum_temp;}
6-2:函數的規模盡量限制在200行以內
說明:不包括注釋和空格行。
6-3:一個函數僅完成一件功能
6-4:為簡易功能編寫函數
說明:雖然為僅用一兩行就可完成的功能去編函數好象沒有必要,但用函數可使功能明確化,增加程式可讀性,亦可方便維護、測試。
樣本:如下語句的功能不很明顯。
value = ( a > b )? a : b ;
改為如下就很清晰了。
int max (int a, intb){ return ((a > b) ? a : b);}value = max (a, b);
或改為如下。
#define MAX (a, b)(((a) > (b)) ? (a) : (b))value = MAX (a, b);
6-5:不要設計多用途面面俱到的函數
說明:多功能集於一身的函數,很可能使函數的理解、測試、維護等變得困難。
6-6:函數的功能應該是可以預測的,也就是只要輸入資料相同就應產生同樣的輸出
說明:帶有內部“儲存空間”的函數的功能可能是不可預測的,因為它的輸出可能取決於內部儲存空間(如某標記)的狀態。這樣的函數既不易於理解又不利於測試和維護。在C/C++語言中,函數的static局部變數是函數的內部儲存空間,有可能使函數的功能不可預測,然而,當某函數的傳回值為指標類型時,則必須是STATIC的局部變數的地址作為傳回值,若為AUTO類,則返回為錯針。
樣本:如下函數,其傳回值(即功能)是不可預測的。
unsigned intinteger_sum( unsigned int base ){ unsigned int index; static unsigned int sum = 0; // 注意,是static類型的。 // 若改為auto類型,則函數即變為可預測。 for(index = 1; index <= base; index++) { sum += index; } return sum;}
6-7:盡量不要編寫依賴於其他函數內部實現的函數
說明:此條為函數獨立性的基本要求。由於目前大部分進階語言都是結構化的,所以通過具體語言的文法要求與編譯器功能,基本就可以防止這種情況發生。但在組合語言中,由於其靈活性,很可能使函數出現這種情況。
樣本:如下是在DOS下TASM的組譯工具例子。過程Print_Msg的實現依賴於Input_Msg的具體實現,這種程式是非結構化的,難以維護、修改。
... // 程式碼proc Print_Msg // 過程(函數)Print_Msg ... // 程式碼 jmp LABEL ... // 程式碼endpproc Input_Msg // 過程(函數)Input_Msg ... // 程式碼LABEL: ... // 程式碼endp
6-8:避免設計多參數函數,不使用的參數從介面中去掉
說明:目的減少函數間介面的複雜度。
6-9:非調度函數應減少或防止控制參數,盡量只使用資料參數
說明:本建議目的是防止函數間的控制耦合。調度函數是指根據輸入的訊息類型或控制命令,來啟動相應的功能實體(即函數或過程),而本身並不完成具體功能。控制參數是指改變函數功能行為的參數,即函數要根據此參數來決定具體怎樣工作。非調度函數的控制參數增加了函數間的控制耦合,很可能使函數間的耦合度增大,並使函數的功能不唯一。
樣本:如下函數構造不太合理。
int add_sub( int a,int b, unsigned char add_sub_flg ){ if(add_sub_flg == INTEGER_ADD) { return (a + b); } else { return (a b); }}
不如分為如下兩個函數清晰。
int add( int a, int b){ return (a + b);}int sub( int a, int b){ return (a b);}
6-10:檢查函數所有參數輸入的有效性
6-11:檢查函數所有非參數輸入的有效性,如資料檔案、公開變數等
說明:函數的輸入主要有兩種:一種是參數輸入;另一種是全域變數、資料檔案的輸入,即非參數輸入。函數在使用輸入之前,應進行必要的檢查。
6-12:函數名應準確描述函數的功能
6-13:使用動賓片語為執行某操作的函數命名。如果是OOP方法,可以只有動詞(名詞是對象本身)
樣本:參照如下方式命名函數。
void print_record(unsigned int rec_ind ) ;int input_record( void ) ;unsigned charget_current_color( void ) ;
建議6-14:避免使用無意義或含義不清的動詞為函數命名
說明:避免用含義不清的動詞如process、handle等為函數命名,因為這些動詞並沒有說明要具體做什麼。
建議6-15:函數的傳回值要清楚、明了,讓使用者不容易忽視錯誤情況
說明:函數的每種出錯傳回值的意義要清晰、明了、準確,防止使用者誤用、理解錯誤或忽視錯誤返回碼。
6-16:除非必要,最好不要把與函數傳回值類型不同的變數,以編譯系統預設的轉換方式或強制的轉換方式作為傳回值返回
6-17:讓函數在調用點顯得易懂、容易理解
6-18:在調用函數填寫參數時,應盡量減少沒有必要的預設資料類型轉換或強制資料類型轉換
說明:因為資料類型轉換或多或少存在危險。
6-19:避免函數中不必要語句,防止程式中的垃圾代碼
說明:程式中的垃圾代碼不僅佔用額外的空間,而且還常常影響程式的功能與效能,很可能給程式的測試、維護等造成不必要的麻煩。
6-20:防止把沒有關聯的語句放到一個函數中
說明:防止函數或過程內出現隨機內聚。隨機內聚是指將沒有關聯或關聯很弱的語句放到同一個函數或過程中。隨機內聚給函數或過程的維護、測試及以後的升級等造成了不便,同時也使函數或過程的功能不明確。使用隨機內聚函數,常常容易出現在一種應用場合需要改進此函數,而另一種應用場合又不允許這種改進,從而陷入困境。
在編程時,經常遇到在不同函數中使用相同的代碼,許多開發人員都願把這些代碼提出來,並構成一個新函數。若這些代碼關聯較大並且是完成一個功能的,那麼這種構造是合理的,否則這種構造將產生隨機內聚的函數。
樣本:如下函數就是一種隨機內聚。
void Init_Var( void ){ Rect.length = 0; Rect.width = 0; /* 初始化矩形的長與寬 */ Point.x = 10; Point.y = 10; /* 初始化“點”的座標 */}
矩形的長、寬與點的座標基本沒有任何關係,故以上函數是隨機內聚。
應如下分為兩個函數:
void Init_Rect( void){ Rect.length = 0; Rect.width = 0; /* 初始化矩形的長與寬 */}void Init_Point( void){ Point.x= 10; Point.y = 10; /* 初始化“點”的座標 */}
6-21:如果多段代碼重複做同一件事情,那麼在函數的劃分上可能存在問題
說明:若此段代碼各語句之間有實質性關聯並且是完成同一件功能的,那麼可考慮把此段代碼構造成一個新的函數。
6-22:功能不明確較小的函數,特別是僅有一個上級函數調用它時,應考慮把它合并到上級函數中,而不必單獨存在
說明:模組中函數劃分的過多,一般會使函數間的介面變得複雜。所以過小的函數,特別是扇入很低的或功能不明確的函數,不值得單獨存在。
6-23:設計高扇入、合理扇出(小於7)的函數
說明:扇出是指一個函數直接調用(控制)其它函數的數目,而扇入是指有多少上級函數調用它。
扇出過大,表明函數過分複雜,需要控制和協調過多的下級函數;而扇出過小,如總是1,表明函數的調用層次可能過多,這樣不利程式閱讀和函數結構的分析,並且程式運行時會對系統資源如堆棧空間等造成壓力。函數較合理的扇出(調度函數除外)通常是3-5。扇出太大,一般是由於缺乏中介層次,可適當增加中介層次的函數。扇出太小,可把下級函數進一步分解多個函數,或合并到上級函數中。當然分解或合并函數時,不能改變要實現的功能,也不能違背函數間的獨立性。
扇入越大,表明使用此函數的上級函數越多,這樣的函數使用效率高,但不能違背函數間的獨立性而單純地追求高扇入。公用模組中的函數及底層函數應該有較高的扇入。
較良好的軟體結構通常是頂層函數的扇出較高,中層函數的扇出較少,而底層函數則扇入到公用模組中。
6-24:減少函數本身或函數間的遞迴調用
說明:遞迴調用特別是函數間的遞迴調用(如A->B->C->A),影響程式的可理解性;遞迴調用一般都佔用較多的系統資源(如棧空間);遞迴調用對程式的測試有一定影響。故除非為某些演算法或功能的實現方便,應減少沒必要的遞迴調用。
6-25:仔細分析模組的功能及效能需求,並進一步細分,同時若有必要畫出有關資料流圖,據此來進行模組的函數劃分與組織
說明:函數的劃分與組織是模組的實現過程中很關鍵的步驟,如何劃分出合理的函數結構,關係到模組的最終效率和可維護性、可測性等。根據模組的功能圖或/及資料流圖映射出函數結構是常用方法之一。
6-26:改進模組中函數的結構,降低函數間的耦合度,並提高函數的獨立性以及代碼可讀性、效率和可維護性
最佳化函數結構時,要遵守以下原則:
(1)不能影響模組功能的實現。
(2)仔細考查模組或函數出錯處理及模組的效能要求並進行完善。
(3)通過分解或合并函數來改進軟體結構。
(4)考查函數的規模,過大的要進行分解。
(5)降低函數間介面的複雜度。
(6)不同層次的函數調用要有較合理的扇入、扇出。
(7)函數功能應可預測。
(8)提高函數內聚。(單一功能的函數內聚最高)
說明:對初步劃分後的函數結構應進行改進、最佳化,使之更為合理。
6-27:在多任務作業系統的環境下編程,要注意函數可重新進入性的構造
說明:可重新進入性是指函數可以被多個任務進程調用。在多任務作業系統中,函數是否具有可重新進入性是非常重要的,因為這是多個進程可以共用此函數的必要條件。另外,編譯器是否提供可重新進入函數庫,與它所服務的作業系統有關,只有作業系統是多任務時,編譯器才有可能提供可重新進入函數庫。如DOS下BC和MSC等就不具備可重新進入函數庫,因為DOS是單使用者單任務作業系統。
6-28:避免使用BOOL參數
說明:原因有二,其一是BOOL參數值無意義,TURE/FALSE的含義是非常模糊的,在調用時很難知道該參數到底傳達的是什麼意思;其二是BOOL參數值不利於擴充。還有NULL也是一個無意義的單詞。
6-29:對於提供了傳回值的函數,在引用時最好使用其傳回值
6-30:當一個過程(函數)中對較長變數(一般是結構的成員)有較多引用時,可以用一個意義相當的宏代替
說明:這樣可以增加編程效率和程式的可讀性。
樣本:在某過程中較多引用TheReceiveBuffer[FirstSocket].byDataPtr,
則可以通過以下宏定義來代替:
# define pSOCKDATAT TheReceiveBuffer[FirstScoket].byDataPtr