天天看点

第十章、函数与过程

第十章、函数与过程

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次时,应当立刻着手消除重复。

继续阅读