非计算机专业C语言初学者编程规范(学生用)—函数与过程
6.1 函数的功能与规模设计
(1)函数应当短而精美,而且只做一件事。不要设计多用途面面俱到的函数,多功能集于一身的函数,很可能使函数的理解、测试、维护等变得困难。 一个函数应最多占满1或2个屏幕(就象我们知道的那样,ISO/ANSI的屏幕大小是80X24),只做一件事并且把它做好。
一个函数的最大长度与它的复杂度和缩进级别成反比。所以,如果如果你有一个概念上简单(案,“简单”是simple而不是easy)的函数,它恰恰包含着一个很长的case语句,这样你不得不为不同的情况准备不懂的处理,那么这样的长函数是没问题的。
然而,如果你有一个复杂的函数,你猜想一个并非天才的高一学生可能看不懂得这个函数,你就应当努力把它减缩得更接近前面提到的最大函数长度限制。可以使用一些辅助函数,给它们取描述性的名字(如果你认为这些辅助函数的调用是性能关键的,可以让编译器把它们内联进来,这比在单个函数内完成所有的事情通常要好些)。
对函数还存在另一个测量标准:局部变量的数目。这不该超过5到10个,否则你可能会弄错。应当重新考虑这个函数,把它分解成小片。人类的大脑一般能同时记住7个不同的东西,超过这个数目就会犯糊涂。或许你认为自己很聪明,那么请你理解一下从现在开始的2周时间你都做什么了。
(2)为简单功能编写函数。
虽然为仅用一两行就可完成的功能去编函数好象没有必要,但用函数可使功能明确化,增加程序可读性,亦可方便维护、测试。 示例:如下语句的功能不很明显。
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);
当一个过程(函数)中对较长变量(一般是结构的成员)有较多引用时,可以用一个意义相当的宏代替——这样可以增加编程效率和程序的可读性。 示例:在某过程中较多引用TheReceiveBuffer[FirstSocket].byDataPtr,则可以通过以下宏定义来代替:# define pSOCKDATA TheReceiveBuffer[FirstScoket].byDataPtr
(3)防止把没有关联的语句放到一个函数中,防止函数或过程内出现随机内聚。
随机内聚是指将没有关联或关联很弱的语句放到同一个函数或过程中。随机内聚给函数或过程的维护、测试及以后的升级等造成了不便,同时也使函数或过程的功能不明确。使用随机内聚函数,常常容易出现在一种应用场合需要改进此函数,而另一种应用场合又不允许这种改进,从而陷入困境。
在编程时,经常遇到在不同函数中使用相同的代码,许多开发人员都愿把这些代码提出来,并构成一个新函数。若这些代码关联较大并且是完成一个功能的,那么这种构造是合理的,否则这种构造将产生随机内聚的函数。
示例:如下函数就是一种随机内聚。
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; /* 初始化“点”的坐标 */
}
(4)如果多段代码重复做同一件事情,那么在函数的划分上可能存在问题。若此段代码各语句之间有实质性关联并且是完成同一件功能的,那么可考虑把此段代码构造成一个新的函数。
(5)减少函数本身或函数间的递归调用。递归调用特别是函数间的递归调用(如A->B->C->A),影响程序的可理解性;递归调用一般都占用较多的系统资源(如栈空间);递归调用对程序的测试有一定影响。故除非为某些算法或功能的实现方便,应减少没必要的递归调用,对于safe-related 系统不能用递归,因为超出堆栈空间很危险。
6.2 函数的返回值
(1)对于函数的返回位置,尽量保持单一性,即一个函数尽量做到只有一个返回位置。(单入口单出口)。要求大家统一函数的返回值,所有的函数的返回值都将以编码的方式返回。
例如编码定义如下:
#define CM_POINT_IS_NULL CMMAKEHR(0X200)
:
:
建议函数实现如下:
LONG 函数名(参数,……)
{
LONG lResult; //保持错误号
lResult=CM_OK;
//如果参数有错误则返回错误号
if(参数==NULL)
{
lResult=CM_POINT_IS_NULL;
goto END;
}
……
END:
return lResult;
}
(2)除非必要,最好不要把与函数返回值类型不同的变量,以编译系统默认的转换方式或强制的转换方式作为返回值返回。
(3)函数的返回值要清楚、明了,让使用者不容易忽视错误情况。函数的每种出错返回值的意义要清晰、明了、准确,防止使用者误用、理解错误或忽视错误返回码。
(4)函数的功能应该是可以预测的,也就是只要输入数据相同就应产生同样的输出。带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在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;
}
6.3 函数参数
(1)只当你确实需要时才用全局变量,函数间应尽可能使用参数、返回值传递消息。(2)防止将函数的参数作为工作变量。将函数的参数作为工作变量,有可能错误地改变参数内容,所以很危险。对必须改变的参数,最好先用局部变量代之,最后再将该局部变量的内容赋给该参数。
示例:下函数的实现不太好。
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;
}