一、可重入函式
1)什麼是可重入性?
可重入(reentrant)函式可以由多于一個任務並發使用,而不必擔心數據錯誤。
相反, 不可重入(non-reentrant)函式不能由超過一個任務所共享,除非能確保函式的互斥(或者使用信號量,或者在程式碼的關鍵部分禁用中斷)。
可重入函式可以在任意時刻被中斷,稍后再繼續運作,不會丟失數據。可重入函式要么使用本地變數,要么在使用全域變數時保護自己的數據。


2)可重入函式︰
不為連續的呼叫持有靜態數據。
不返回指向靜態數據的指標;所有數據都由函式的呼叫者提供。
使用本地數據,或者透過製作全域數據的本地拷貝來保護全域數據。
如果必須訪問全域變數,記住利用互斥信號量來保護全域變數。
絕不呼叫任何不可重入函式。


3)不可重入函式︰
函式中使用了靜態變數,無論是全域靜態變數還是區域靜態變數。
函式返回靜態變數。
函式中呼叫了不可重入函式。
函式體內使用了靜態的數據架構;
函式體內呼叫了malloc()或者free()函式;
函式體內呼叫了其他標準I/O函式。
函式是singleton中的成員函式而且使用了不使用線程獨立存儲的成員變數 。
總的來說,如果一個函式在重入條件下使用了未受保護的共享的資源,那么它是不可重入的。


4)示例
在多線程條件下,函式應當是線程安全的,進一步,更強的條件是可重入的。可重入函式保證了在多線程條件下,函式的狀態不會出現錯誤。
以下分別是一個不可重入和可重入函式的示例︰
//c code
static int tmp;
void func1(int* x, int* y) {
    tmp=*x;
    *x=*y;
    *y=tmp;
}



void func2(int* x, int* y) {
    int tmp;
    tmp=*x;
    *x=*y;
    *y=tmp;
}
func1是不可重入的,func2是可重入的。
因為在多線程條件下,作業系統會在func1還沒有執行完的情況下,切換到另一個線程中,那個線程可能再次呼叫func1,這樣狀態就錯了。


二、函式編寫規範
1 ︰對所呼叫函式的錯誤返回碼要仔細、全面地處理
 
2 ︰明確函式功能,精確(而不是近似)地實現函式設計
 
3 ︰編寫可重入函式時,應注意區域變數的使用(如編寫C/C++ 語言的可重入函式時,應使用auto 即預設態區域變數或暫存器變數)
說明︰編寫C/C++語言的可重入函式時,不應使用static區域變數,否則必須經過特殊處理,才能使函式具有可重入性。
 
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;
}
 
5 ︰在同一項目組應明確規定對界面函式參數的合法性檢查應由函式的呼叫者負責還是由界面函式本身負責,預設是由函式呼叫者負責說明︰
對于模組間界面函式的參數的合法性檢查這一問題,往往有兩個極端現象,

︰要么是呼叫者和被呼叫者對參數均不作合法性檢查,結果就遺漏了合法性檢查這一必要的處理過程,造成問題隱患
;要么就是呼叫者和被呼叫者均對參數進行合法性檢查,這種情況雖不會造成問題,但產生了冗餘程式碼,降低了效率。
 
6 ︰防止將函式的參數作為工作變數
說明︰將函式的參數作為工作變數,有可能錯誤地改變參數內容,所以很危險。對必須改變的參數,最好先用區域變數代之,最後再將該區域變數的內容賦給該參數。
示例︰如下函式的實現就不太好。
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;
}
 
7 ︰函式的規模盡量限制在200 行以內
說明︰不包括註釋和空格行。


8 ︰一個函式僅完成一件功能


9 ︰為簡單功能編寫函式
說明︰雖然為僅用一兩行就可完成的功能去編函式好像沒有必要,但用函式可使功能明確化,增加程式可讀性,亦可方便維護、測試。
示例︰如下語句的功能不很明顯。
value = ( a > b ) ? a : b ;
改為如下就很清晰了。
 
int max (int a, int b)
{
    return ((a > b) ? a : b);
}
 
value = max (a, b);
 
或改為如下。
 
#define MAX (a, b) (((a) > (b)) ? (a) : (b))
 
value = MAX (a, b);
 
10︰不要設計多用途面面俱到的函式
說明︰多功能集于一身的函式,很可能使函式的理解、測試、維護等變得困難。
 
11︰函式的功能應該是可以預測的,也就是只要輸入數據相同就應產生同樣的輸出
說明︰帶有內部“存儲器”的函式的功能可能是不可預測的,因為它的輸出可能取決于內部存儲器(如某標記)的狀態。
這樣的函式既不易于理解又不利于測試和維護。在C/C++語言中,函式的static區域變數是函式的內部存儲器,有可能使函式的功能不可預測,然而,
當某函式的返回值為指標類型時,則必須是STATIC的區域變數的位址作為返回值,若為AUTO類,則返回為錯針。
示例︰如下函式,其返回值(即功能)是不可預測的。
 
unsigned int integer_sum( unsigned int base )
{
    unsigned int index;
    static unsigned int sum = 0; // 注意,是static類型的。
                                 // 若改為auto類型,則函式即變為可預測。
    for (index = 1; index <= base; index++)
    {
        sum += index;
    }
    return sum;
}
 
12 ︰盡量不要編寫倚賴于其他函式內部實現的函式
說明︰此條為函式獨立性的基本要求。由於目前大部分高階語言都是架構化的,所以透過具體語言的語法要求與編譯器功能,基本就可以防止這種情況發生。
但在組合語言中,由於其靈活性,很可能使函式出現這種情況。


示例︰如下是在DOS下TASM的組合程式例子。過程Print_Msg的實現倚賴于Input_Msg的具體實現,這種程式是非架構化的,難以維護、修改。
 
...  // 程式程式碼
proc Print_Msg // 過程(函式)Print_Msg
    ...  // 程式程式碼
    jmp  LABEL
    ...  // 程式程式碼
endp
 
proc Input_Msg // 過程(函式)Input_Msg
    ...  // 程式程式碼
LABEL:
    ...  // 程式程式碼
endp


13 ︰避免設計多參數函式,不使用的參數從界面中去掉
說明︰目的減少函式間界面的複雜度。
 
14 ︰非調度函式應減少或防止控制參數,盡量只使用數據參數
說明︰本建議目的是防止函式間的控制耦合。調度函式是指根據輸入的消息類型或控制命令,來啟動相應的功能實體(即函式或過程),而本身並不完成具體功能。
控制參數是指改變函式功能行為的參數,即函式要根據此參數來決定具體怎樣工作。非調度函式的控制參數增加了函式間的控制耦合,很可能使函式間的耦合度增大,
並使函式的功能不唯一。


示例︰如下函式構造不太合理。
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);
}


15 ︰檢查函式所有參數輸入的有效性
 
16 ︰檢查函式所有非參數輸入的有效性,如數據文件、公共變數等
說明︰函式的輸入主要有兩種︰一種是參數輸入;另一種是全域變數、數據文件的輸入,即非參數輸入。函式在使用輸入之前,應進行必要的檢查。
 
17 ︰函式名應準確描述函式的功能
 
18 ︰使用動賓詞組為執行某操作的函式命名。如果是OOP 方法,可以只有動詞(名詞是對象本身)
示例︰參照如下模式命名函式。
void print_record( unsigned int rec_ind ) ;
int  input_record( void ) ;
unsigned char get_current_color( void ) ;
 
19 ︰避免使用無意義或含義不清的動詞為函式命名
說明︰避免用含義不清的動詞如process、handle等為函式命名,因為這些動詞並沒有說明要具體做什麼。
 
20 ︰函式的返回值要清楚、明了,讓使用者不容易忽視錯誤情況
說明︰函式的每種出錯返回值的意義要清晰、明了、準確,防止使用者誤用、理解錯誤或忽視錯誤返回碼。
 
21 ︰除非必要,最好不要把與函式返回值類型不同的變數,以編譯系統默認的轉換模式或強製的轉換模式作為返回值返回
 
22 ︰讓函式在呼叫點顯得易懂、容易理解
 
23 ︰在呼叫函式填寫參數時,應盡量減少沒有必要的默認數據類型轉換或強製數據類型轉換
說明︰因為數據類型轉換或多或少存在危險。
 
24 ︰避免函式中不必要語句,防止程式中的垃圾程式碼
說明︰程式中的垃圾程式碼不僅佔用額外的空間,而且還常常影響程式的功能與性能,很可能給程式的測試、維護等造成不必要的麻煩。
 
25 ︰防止把沒有關聯的語句放到一個函式中
說明︰防止函式或過程內出現隨機內聚。隨機內聚是指將沒有關聯或關聯很弱的語句放到同一個函式或過程中。
隨機內聚給函式或過程的維護、測試及以後的升級等造成了不便,同時也使函式或過程的功能不明確。
使用隨機內聚函式,常常容易出現下一種應用場合需要改進此函式,而另一種應用場合又不允許這種改進,從而陷入困境。
在程式設計時,經常遇到在不同函式中使用相同的程式碼,許多開發人員都愿把這些程式碼提出來,並構成一個新函式。
若這些程式碼關聯較大並且是完成一個功能的,那么這種構造是合理的,否則這種構造將產生隨機內聚的函式。


示例︰如下函式就是一種隨機內聚。
 
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;   /* 初始化“點”的坐標 */
}
 
26︰如果多段程式碼重複做同一件事情,那么在函式的劃分上可能存在問題
說明︰若此段程式碼各語句之間有實質性關聯並且是完成同一件功能的,那么可考慮把此段程式碼構造成一個新的函式。
 
27︰功能不明確較小的函式,特別是僅有一個上級函式呼叫它時,應考慮把它合併到上級函式中,而不必單獨存在
說明︰模組中函式劃分的過多,一般會使函式間的界面變得複雜。所以過小的函式,特別是扇入很低的或功能不明確的函式,不值得單獨存在。
 
28 ︰設計高扇入、合理扇出(小于7 )的函式
說明︰扇出是指一個函式直接呼叫(控制)其它函式的數目,而扇入是指有多少上級函式呼叫它。
扇出過大,表明函式過分複雜,需要控制和協調過多的下級函式;
而扇出過小,如總是1,表明函式的呼叫層次可能過多,這樣不利程式閱讀和函式架構的分析,並且程式運作時會對系統資源如堆棧空間等造成壓力。
函式較合理的扇出(調度函式除外)通常是3-5。扇出太大,一般是由於缺乏中間層次,可適當增加中間層次的函式。
扇出太小,可把下級函式進一步分解多個函式,或合併到上級函式中。當然分解或合併函式時,不能改變要實現的功能,也不能違背函式間的獨立性。


扇入越大,表明使用此函式的上級函式越多,這樣的函式使用效率高,但不能違背函式間的獨立性而單純地追求高扇入。公共模組中的函式及底層函式應該有較高的扇入。
較良好的軟體架構通常是頂層函式的扇出較高,中層函式的扇出較少,而底層函式則扇入到公共模組中。
 
29 ︰減少函式本身或函式間的遞歸呼叫
說明︰遞歸呼叫特別是函式間的遞歸呼叫(如A->B->C->A),影響程式的可理解性;遞歸呼叫一般都佔用較多的系統資源(如棧空間);遞歸呼叫對程式的測試有一定影響。
故除非為某些算法或功能的實現方便,應減少沒必要的遞歸呼叫。
 
30 ︰仔細分析模組的功能及性能需求,並進一步細分,同時若有必要畫出有關數據流圖,據此來進行模組的函式劃分與組織
說明︰函式的劃分與組織是模組的實現過程中很關鍵的步驟,如何劃分出合理的函式架構,關係到模組的最終效率和可維護性、可測性等。
根據模組的功能圖或/及數據流圖映射出函式架構是常用方法之一。
 
31 ︰改進模組中函式的架構,降低函式間的耦合度,並提升函式的獨立性以及程式碼可讀性、效率和可維護性
最佳化函式架構時,要遵守以下原則︰
(1)不能影響模組功能的實現。
(2)仔細考查模組或函式出錯處理及模組的性能要求並進行完善。
(3)透過分解或合併函式來改進軟體架構。
(4)考查函式的規模,過大的要進行分解。
(5)降低函式間界面的複雜度。
(6)不同層次的函式呼叫要有較合理的扇入、扇出。
(7)函式功能應可預測。
(8)提升函式內聚。(單一功能的函式內聚最高)
說明︰對初步劃分后的函式架構應進行改進、最佳化,使之更為合理。
 
32 ︰在多任務作業系統的環境下程式設計,要注意函式可重入性的構造
說明︰可重入性是指函式可以被多個任務行程呼叫。在多任務作業系統中,函式是否具有可重入性是非常重要的,因為這是多個行程可以共用此函式的必要條件。
另外,編譯器是否提供可重入函式庫,與它所服務的作業系統有關,只有作業系統是多任務時,編譯器才有可能提供可重入函式庫。
如DOS下BC和MSC等就不具備可重入函式庫,因為DOS是單用戶單任務作業系統。
 
33 ︰避免使用BOOL 參數
說明︰原因有二,
其一是BOOL參數值無意義,TURE/FALSE的含義是非常模糊的,在呼叫時很難知道該參數到底傳達的是什麼意思;
其二是BOOL參數值不利于擴充。還有NULL也是一個無意義的單詞。
 
34 ︰ 對于提供了返回值的函式,在引用時最好使用其返回值
 
35 ︰當一個過程(函式)中對較長變數(一般是架構的成員)有較多引用時,可以用一個意義相當的巨集代替
說明︰這樣可以增加程式設計效率和程式的可讀性。
示例︰在某過程中較多引用TheReceiveBuffer[FirstSocket].byDataPtr,
則可以透過以下巨集定義來代替︰
# define pSOCKDATA TheReceiveBuffer[FirstScoket].byDataPtr

arrow
arrow
    全站熱搜

    立你斯 發表在 痞客邦 留言(0) 人氣()