天天看點

第十章、函數與過程

第十章、函數與過程

10.1 參數規則

規則 10.1.1 如果函數沒有參數,則用 void 填充

說明:函數在說明的時候,可以省略參數名。但是為了提高代碼的可讀性,要求不能省略。

正例:
void SetValue(int iWidth, int iHeight); 
float GetValue(void);

反例:
void SetValue(int, int);
float GetValue();
           

規則 10.1.2 如果參數是指針,且僅作輸入用,則應在類型前加 const

說明:防止該指針在函數體内被意外修改。

正例:
int GetStrLen(const char *pcString);
           

規則 10.1.3 當結構變量作為參數時,應傳送結構的指針而不傳送整個結構體,并且不得修改結構中的元素,用作輸出時除外

說明:一個函數被調用的時候,形參會被一個個壓入被調函數的堆棧中,在函數調用結束以後再彈出。一個結構所包含的變量往往比較多,直接以一個結構為參數,壓棧出棧的内容就會太多,不但占用堆棧空間,而且影響代碼執行效率,如果使用不當還可能導緻堆棧的溢出。如果使用結構的指針作為參數,因為指針的長度是固定不變的,結構的大小就不會影響代碼執行的效率,也不會過多地占用堆棧空間。

規則 10.1.4 避免函數有太多的參數,參數個數盡量控制在 5 個以内。

說明:如果參數太多,在使用時容易将參數類型或順序搞錯,而且調用的時候也不友善。如 果參數的确比較多,而且輸入的參數互相之間的關系比較緊密,不妨把這些參數定義成一個結構,然後把結構的指針當成參數輸入。

規則 10.1.5 盡量不要使用類型和數目不确定的參數

說明:對于參數個數可變的函數調用,編譯器不作類型檢查和參數檢查。例如指針的指針,這種風格的函數在編譯時喪失了嚴格的類型安全檢查。

規則 10.1.6 避免使用 BOOLEAN 參數

說明:一方面因為 BOOLEAN 的 TRUE/FALSE 的含義是有些模糊的(不同程式設計語言的定義不完全相同),在調用時很難知道該參數到底傳達的是什麼意思;其次 BOOLEAN 參數值不利于擴充。

規則 10.1.7 如果輸入參數以值傳遞的方式傳遞對象,則宜改用“const &”方式來傳遞,這樣可以省去臨時對象的構造和析構過程,進而提高效率;

說明:引用實際上是傳址,不用調用拷貝構造函數,是以效率高。

10.2 傳回值的規則

規則 10.2.1 不要省略傳回值的類型,如果函數沒有傳回值,那麼應聲明為 void 類型

說明:C 語言中,凡不加類型說明的函數,一律自動按整型處理。如果不注明類型,容易被誤解為 void 類型,産生不必要的麻煩。

C++語言有很嚴格的類型安全檢查,不允許上述情況發生。由于 C++程式可以調用 C 函數,為了避免混亂,規定任何 C/ C++函數都必須有類型。

規則 10.2.2 函數名字與傳回值類型在語義上不可沖突。

違反這條規則的典型代表是 C 标準庫函數 getchar。 例如:
  char c;
  c = getchar(); 
  if (c == EOF) ...
按照 getchar 名字的意思,将變量 c 聲明為 char 類型是很自然的事情。但不幸的是 getchar 的确不是 char 類型,而是 int 類型,其原型如下:
  int getchar(void);
  由于 c 是 char 類型,取值範圍是[-128,127],如果宏 EOF 的值在 char 的取值範圍
之外,那麼 if 語句将總是失敗,這種“危險”人們一般哪裡料得到!導緻本例錯誤的責 任并不在使用者,是函數 getchar 誤導了使用者。
           

規則 10.2.3 對于有傳回值的函數,每一個分支都必須有傳回值

說明:為了保證對被調用函數傳回值的判斷,有傳回值的函數中的每一個退出點都需要有傳回值。

建議 10.2.1 如果傳回值表示函數運作是否正常,規定 0 為正常退出,不同非 0 值辨別不同異常退出。盡量減少使用 TRUE 或 FALSE 作為傳回值

正例:
int RelRadioChan(const T_RelRadioChanReq *ptReq,T_RelRadioChanAck *ptAck);
int SubFunction(void);
           

僅供參考:如果函數的傳回值是一個對象,有些場合用“引用傳遞”替換“值傳遞”可以提高效率,而有些場合隻能用“值傳遞”而不能用“引用傳遞”,否則會出錯。

例如:
class String
{
	…
    // 指派函數
    String & operate=(const String &other);
    
	// 相加函數,如果沒有friend修飾則隻許有一個右側參數
	friend  String   operate+( const String &s1, const String &s2);

private:
    char *m_data;
};

 
String的指派函數operate = 的實作如下:
String & String::operate=(const String &other)
{
        if (this == &other)
            return *this;
    	delete m_data;
    	m_data = new char[strlen(other.data)+1];
        strcpy(m_data, other.data);
    	return *this;   // 傳回的是 *this的引用,無需拷貝過程
}

對于指派函數,應當用“引用傳遞”的方式傳回String對象。如果用“值傳遞”的方式,雖然功能仍然正确,但由于return語句要把 *this拷貝到儲存傳回值的外部存儲單元之中,增加了不必要的開銷,降低了指派函數的效率。
例如:
    String a,b,c;
    …
    a = b;      // 如果用“值傳遞”,将産生一次 *this 拷貝
    a = b = c;  // 如果用“值傳遞”,将産生兩次 *this 拷貝
 
String的相加函數 operate + 的實作如下:
String  operate+(const String &s1, const String &s2) 
{
	String temp;
    delete temp.data;   // temp.data是僅含‘\0’的字元串
    temp.data = new char[strlen(s1.data) + strlen(s2.data) +1];
    strcpy(temp.data, s1.data);
    strcat(temp.data, s2.data);
    return temp;
}
對于相加函數,應當用“值傳遞”的方式傳回String對象。如果改用“引用傳遞”,那麼函數傳回值是一個指向局部對象temp的“引用”。由于temp在函數結束時被自動銷毀,将導緻傳回的“引用”無效。
例如:
    c = a + b;
此時 a + b 并不傳回期望值,c什麼也得不到,流下了隐患。
           
拓展:值傳遞、指針傳遞和引用傳遞

傳指針和傳引用的差別以及指針和引用的差別

C++語言中,函數的參數和傳回值的傳遞方式有三種:值傳遞、指針傳遞和引用傳遞。
//
以下是“值傳遞”的示例程式。由于Func1函數體内的x是外部變量n的一份拷貝,改變x的值不會影響n, 是以n的值仍然是0。
void Func1(int x)
{
	x = x + 10;
}
…
int n = 0;
Func1(n);
cout << “n = ” << n << endl;  // n = 0

//
以下是“指針傳遞”的示例程式。由于Func2函數體内的x是指向外部變量n的指針,改變該指針的内容将導緻n的值改變,是以n的值成為10。

void Func2(int *x)
{
	(* x) = (* x) + 10;
}
…
int n = 0;
Func2(&n);
cout << “n = ” << n << endl;      // n = 10

//
以下是“引用傳遞”的示例程式。由于Func3函數體内的x是外部變量n的引用,x和n是同一個東西,改變x等于改變n,是以n的值成為10。
void Func3(int &x)
{
    x = x + 10;
}
…
int n = 0;
Func3(n);
cout << “n = ” << n << endl;      // n = 10

對比上述三個示例程式,會發現“引用傳遞”的性質象“指針傳遞”,而書寫方式象“值傳遞”。
           

10.3 函數内部實作的規則

函數體的實作并不是随心所欲,而是有一定的規矩可循。不但要仔細檢查入口參數的有效性和精心設計傳回值,還要保證函數的功能單一,具有很高的功能内聚性,盡量減少函數之間的耦合,友善調試和維護。

規則 10.3.1 對輸入參數的正确性和有效性進行檢查

說明:很多程式錯誤是由非法參數引起的,我們應該充分了解并正确處理來防止此類錯誤。

規則 10.3.2 防止将函數的參數作為工作變量

說明:将函數的參數作為工作變量,有可能錯誤地改變參數内容,是以很危險。對必須改變的參數,最好先用局部變量代之,最後再将該局部變量的内容賦給該參數。

正例:
void SumData(int iNum, int *piData, int *piSum ) 
{
	int iCount ;
	int iSumTmp; // 存儲“和”的臨時變量
	iSumTmp = 0;
	for (iCount = 0; iCount < iNum; iCount++) 
	{
		iSumTmp += piData[iCount]; 
	}
	*piSum = iSumTmp; 
}

反例:
void SumData(int iNum, int *piData, int *piSum ) 
{
	int iCount;
	*piSum = 0;
	for (iCount = 0; iCount < iNum; iCount++ ) 
	{
		*piSum += piData[iCount]; // piSum 成了工作變量,不好。 
	}
}
           

規則 10.3.3 盡量避免函數帶有“記憶”功能。函數的輸出應該具有可預測性,即相同的輸入應當産生相同的輸出

說明:帶有“記憶”功能的函數,其行為可能是不可預測的,因為它的行為可能取決于某種 “記憶狀态”。這樣的函數既不易了解又不利于測試和維護。在 C/C++語言中,函數 的 static 局部變量是函數的“記憶”存儲器。建議盡量少用 static 局部變量,除非必需。

規則 10.3.4 函數的功能要單一,不要設計多用途的函數

說明:多用途的函數往往通過在輸入參數中有一個控制參數,根據不同的控制參數産生不同 的功能。這種方式增加了函數之間的控制耦合性,而且在函數調用的時候,調用相同的一個函數卻産生不同的效果,降低了代碼的可讀性,也不利于代碼調試和維護。

正例:
以下兩個函數功能清晰:
int Add(int iParaOne, int iParaTwo) 
{
	return (iParaOne + iParaTwo); 
}
int Sub(int iParaOne, int iParaTwo) 
{
	return (iParaOne – iParaTwo); 
}

反例:
如果把這兩個函數合并在一個函數中,通過控制參數決定結果,不可取。
int AddOrSub(int iParaOne, int iParaTwo, unsigned char ucAddOrSubFlg) 
{
	if (INTEGER_ADD == ucAddOrSubFlg) // 參數标記為“求和”
	{
		return (iParaOne + iParaTwo); 
	}
	else 
	{
		return (iParaOne –iParaTwo); 
	}
}
           

規則 10.3.5 函數功能明确,防止把沒有關聯的語句放到一個函數中

說明:防止函數或過程内出現随機内聚。随機内聚是指将沒有關聯或關聯很弱的語句放到同 一個函數或過程中。随機内聚給函數或過程的維護、測試及以後的更新等造成了不便, 同時也使函數或過程的功能不明确。使用随機内聚函數,常常容易出現在一種應用場合需要改進此函數,而另一種應用場合又不允許這種改進,進而陷入困境。

正例:
矩形的長、寬與點的坐标基本沒有任何關系,應該在不同的函數中實作。
void InitRect(void) 
{
	// 初始化矩形的長與寬 
	tRect.wLength = 0; 
	tRect.wWidth = 0;
}

void InitPoint(void) 
{
	// 初始化“點”的坐标 
	tPoint.wX = 10; 
	tPoint.wY = 10;
}

反例:
矩形的長、寬與點的坐标基本沒有任何關系,故以下函數是随機内聚。
void InitVar(void) {
	// 初始化矩形的長與寬 
	tRect.wLength = 0; 
	tRect.wWidth = 0;
	// 初始化“點”的坐标 
	tPoint.wX = 10; 
	tPoint.wY = 10;
}
           

規則 10.3.6 函數體的規模不能太大,盡量控制在 200 行代碼之内。

說明:冗長的函數不利于調試,可讀性差。

規則 10.3.7 return語句不可傳回指向“棧記憶體”的“指針”或者“引用”,因為該記憶體在函數體結束時被自動銷毀。

例如:
char * Func(void)
{
	char str[] = “hello world”; // str的記憶體位于棧上
	…
	return str;     			// 将導緻錯誤
}
           

建議10.3.1 如果函數傳回值是一個對象,要考慮return語句的效率。

例如:
return String(s1 + s2);
	這是臨時對象的文法,表示“建立一個臨時對象并傳回它”,不要以為它與“先建立一個局部對象temp并傳回它的結果”是等價的,如	

String temp(s1 + s2);
return temp;
	實質不然,上述代碼将發生三件事。
    首先,temp對象被建立,同時完成初始化;
    然後拷貝構造函數把temp拷貝到儲存傳回值的外部存儲單元中;
    最後,temp在函數結束時被銷毀(調用析構函數)。
    
	然而“建立一個臨時對象并傳回它”的過程是不同的,編譯器直接把臨時對象建立并初始化在外部存儲單元中,省去了拷貝和析構的化費,提高了效率。

類似地,我們不要将
return int(x + y);  // 建立一個臨時變量并傳回它
	寫成
int temp = x + y;
return temp;
	由于内部資料類型如int,float,double的變量不存在構造函數與析構函數,雖然該“臨時變量的文法”不會提高多少效率,但是程式更加簡潔易讀。
           

10.4 函數調用的規則

規則 10.4.1 必須對所調用函數的錯誤傳回值進行處理

說明:函數傳回錯誤,往往是因為輸入的參數不合法,或者此時系統已經出現了異常。如果 不對錯誤傳回值進行必要的處理,會導緻錯誤的擴大,甚至導緻系統的崩潰。

正例: 
在程式中定義了一個函數:
int DbAccess(WORD wEventNo, T_InPara *ptInParam, T_OutPara *ptOutParam);
在引用該函數的時候應該如下處理:
int iResult;
iResult = DbAccess(EV_GETRADIOCHANNEL, ptReq, ptAck); 
switch (iResult)
{
	case NO_CHANNEL: 
	{
		[異常處理]
		break; 
	}
	case CELL_NOTFOUND: 
	{
		[異常處理]
		break; 
	}
	default: 
	{
		[其它處理] 
	}
} [正常處理]

反例:
對上面的正例中定義的函數進行如下的處理就不合适。
DbAccess(EV_GETRADIOCHANNEL, ptReq, ptAck); 
[正常處理]
           

規則 10.4.2 減少函數本身或函數間的遞歸調用。

說明:遞歸調用特别是函數間的遞歸調用(如 A->B->C->A),影響程式的可了解性;遞歸調用一般都占用較多的系統資源(如棧空間);遞歸調用對程式的測試有一定影響。故除非為某些算法或功能的實作友善,應減少沒必要的遞歸調用。

規則 10.4.3 避免函數的代碼塊嵌套過深,新增函數的代碼塊嵌套不超過4層。

函數的代碼塊嵌套深度指的是函數中的代碼控制塊(例如:if、for、while, switch等)之間互相包含的深度。每級嵌套都會增加閱讀代碼時的腦力消耗,因為需要在腦裡維護一個“棧”(比如,進入條件語句、進入循環…)。應該做進一步的功能分解,進而避免使代碼的閱讀者一次記住太多的上下文。

原則 10.4.1 設計高扇入、合理扇出的函數

說明:扇出是指一個函數直接調用(控制)其它函數的數目,而扇入是指有多少上級函數調用它。

扇出過大,表明函數過分複雜,需要控制和協調過多的下級函數;而扇出過小,如總是1,表明函數的調用層次可能過多,這樣不利于程式閱讀和函數結構的分析,并且程式運作時會對系統資源如堆棧空間等造成壓力。函數較合理的扇出(排程函數除外)通常是3-5。扇出太大,一般是由于缺乏中間層次,可适當增加中間層次的函數。扇出太小,可把下級函數進一步分解成多個函數,或合并到上級函數中。當然分解或合并函數時,不能改變要實作的功能,也不能違背函數間的獨立性。

扇入越大,表明使用此函數的上級函數越多,這樣的函數使用效率高,但不能違背函數間的獨立性而單純地追求高扇入。公共子產品中的函數及底層函數應該有較高的扇入。較好的軟體結構通常是頂層函數的扇出較高,中層函數的扇出較少,而底層函數則扇入到公共子產品中。

原則10.4.2 重複代碼應該盡可能提煉成函數。

說明:重複代碼提煉成函數可以帶來維護成本的降低。

重複代碼是我司不良代碼最典型的特征之一。在“代碼能用就不改”的指導原則之下,大量的煙囪式設計及其實作充斥着各産品代碼之中。新需求增加帶來的代碼拷貝和修改,随着時間的遷移,産品中堆砌着許多類似或者重複的代碼。

項目組應當使用代碼重複度檢査工具,在持續內建環境中持續檢查代碼重複度名額變化趨勢,并對新增重複代碼進行及時重構。當一段代碼重複兩次時,即應考慮消除重複,當代碼重複超過3次時,應當立刻着手消除重複。

繼續閱讀