函数

编写整洁函数,同时把代码有效组织起来。

原则 2.1 一个函数仅完成一件功能。

一个函数实现多个功能给开发、使用、维护都带来很大的困难。 案例:realloc。在标准 C 语言中,realloc 是一个典型的不良设计。这个函数基本功能是重新分配内存,但它承担了太多的其他任务:如果传入的指针参数为 NULL 就分配内存,如果传入的大小参数为 0 就释放内存,如果可行则就地重新分配,如果不行则移到其他地方分配。如果没有足够可用的内存用来完成重新分配(扩大原来的内存块或者分配新的内存块),则返回 NULL,而原来的内存块保持不变。这个函数不易扩展,容易导致问题。例如下面代码容易导致内存泄漏:

char *buffer = (char *)malloc(XXX_SIZE);
.....
buffer = (char *)realloc(buffer, NEW_SIZE);

如果没有足够可用的内存用来完成重新分配,函数返回为 NULL,导致 buffer 原来指向的内存被丢失。

原则 2.2 重复代码应该尽可能提炼成函数。

重复代码提炼成函数可以带来维护成本的降低。

规则 2.1 避免函数过长,新增函数不超过 50 行(非空非注释行)。

本规则仅对新增函数做要求,对已有函数修改时,建议不增加代码行。 过长的函数往往意味着函数功能不单一,过于复杂(参见原则 2.1:一个函数只完成一个功能)。

函数的有效代码行数,即 NBNC(非空非注释行)应当在[1,50]区间。

例外:某些实现算法的函数,由于算法的聚合性与功能的全面性,可能会超过 50 行。

规则 2.2 避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过 4 层。

本规则仅对新增函数做要求,对已有的代码建议不增加嵌套层次。

函数的代码块嵌套深度指的是函数中的代码控制块(例如:if、for、while、switch 等)之间互相包含的深度。每级嵌套都会增加阅读代码时的脑力消耗,因为需要在脑子里维护一个“栈”(比如,进入条件语句、进入循环„„)。应该做进一步的功能分解,从而避免使代码的阅读者一次记住太多的上下文。

示例:如下代码嵌套深度为 5。

void serial (void)
{
 if (!Received)
 {
 TmoCount = 0;
 switch (Buff)
 {
 case AISGFLG:
 if ((TiBuff.Count > 3)
 && ((TiBuff.Buff[0] == 0xff) || (TiBuf.Buff[0] == CurPa.ADDR)))
 {
 Flg7E = false;
 Received = true;
 }
 else
 {
 TiBuff.Count = 0;
 Flg7D = false;
 Flg7E = true;
 }
 break;
 default:
 break;
 }
 }
}

规则 2.3 可重入函数应避免使用共享变量;若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护。

可重入函数是指可能被多个任务并发调用的函数。在多任务操作系统中,函数具有可重入性是 多个任务可以共用此函数的必要条件。共享变量指的全局变量和 static 变量。 编写 C 语言的可重入函数时,不应使用 static 局部变量,否则必须经过特殊处理,才能使函数具有可重 入性。

示例:函数 square_exam 返回 g_exam 平方值。那么如下函数不具有可重入性。

int g_exam;
unsigned int example( int para )
{
 unsigned int temp;
 g_exam = para; //(**)
 temp = square_exam ( );
 return temp;
}

此函数若被多个线程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的线程可能正好被激活,那么当新激活的线程执行到此函数时,将使 g_exam 赋于另一个不同的 para 值,所以当控制重新回到“temp =square_exam ( )”后,计算出的 temp 很可能不是预想中的结果。此函数应如下改进。

int g_exam;
unsigned int example( int para )
{
 unsigned int temp;
 [申请信号量操作] // 若申请不到“信号量”,说明另外的进程正处于
 g_exam = para; //给g_exam赋值并计算其平方过程中(即正在使用此
 temp = square_exam( ); // 信号),本进程必须等待其释放信号后,才可继
 [释放信号量操作] // 续执行。其它线程必须等待本线程释放信号量后
 // 才能再使用本信号。
 return temp;
}

规则 2.4 对参数的合法性检查,由调用者负责还是由接口函数负责,应在项目组/模块内应统一规定。 缺省由调用者负责。

对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率。

规则 2.5 对函数的错误返回码要全面处理。

一个函数(标准库中的函数/第三方库函数/用户定义的函数)能够提供一些指示错误发生的方法。这可以通过使用错误标记、特殊的返回数据或者其他手段,不管什么时候函数提供了这样的机制,调用程序应该在函数返回时立刻检查错误指示。 示例:下面的代码导致宕机

FILE _fp = fopen( "./writeAlarmLastTime.log","r");
char buff[128] = "";
fscanf(fp,“%s”, buff); /_ 读取最新的告警时间;由于文件 writeAlarmLastTime.log 为空,导
致 buff 为空 _/
fclose(fp);
long fileTime = getAlarmTime(buff); /_ 解析获取最新的告警时间;getAlarmTime 函数未检查
buff 指针,导致宕机 \*/

正确写法:

FILE \*fp = fopen( "./writeAlarmLastTime.log","r");
char buff[128] = "";
if (fscanf(fp,“%s”,buff) == EOF) //检查函数 fscanf 的返回值,确保读到数据
{
return ;
}
fclose(fp);
long fileTime = getAlarmTime(buff); //解析获取最新的告警时间;

规则 2.7 废弃代码(没有被调用的函数和变量)要及时清除。

程序中的废弃代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可能给程序的测试、维护等造成不必要的麻烦。

建议 2.1 函数不变参数使用 const。

不变的值更易于理解/跟踪和分析,把 const 作为默认选项,在编译时会对其进行检查,使代码更牢固/更安全。 示例:

int strncmp(const char *s1, const char *s2, register size_t n)
{
 register unsigned char u1, u2;
 while (n-- > 0)
 {
 u1 = (unsigned char) *s1++;
 u2 = (unsigned char) *s2++;
 if (u1 != u2)
 {
 return u1 - u2;
 }
 if (u1 == '\0')
 {
 return 0;
 }
 }
 return 0;
}

建议 2.2 函数应避免使用全局变量、静态局部变量和 I/O 操作,不可避免的地方应集中使用。

示例:如下函数,其返回值(即功能)是不可预测的。

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;
}

建议 2.3 检查函数所有非参数输入的有效性,如数据文件、公共变量等。

函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入参数之前,应进行有效性检查。

示例:下面的代码导致宕机

hr = root_node->get_first_child(&log_item); // list.xml 为空,导致读出log_item为空
…..
hr = log_item->get_next_sibling(&media_next_node); // log_item为空,导致宕机

正确写法:确保读出的内容非空。

hr = root_node->get_first_child(&log_item);
…..
if (log_item == NULL) //确保读出的内容非空
{
 return retValue;
}
hr = log_item->get_next_sibling(&media_next_node);

建议 2.4 函数的参数个数不超过 5 个。

函数的参数过多,会使得该函数易于受外部(其他部分的代码)变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。

函数的参数个数不要超过 5 个,如果超过了建议拆分为不同函数。

建议 2.5 除打印类函数外,不要使用可变长参函数。

可变长参函数的处理过程比较复杂容易引入错误,而且性能也比较低,使用过多的可变长参函数将导致函数的维护难度大大增加。

建议 2.6 在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加 static 关键字。

如果一个函数只是在同一文件中的其他地方调用,那么就用 static 声明。使用 static 确保只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能性。

建议定义一个 STATIC 宏,在调试阶段,将 STATIC 定义为 static,版本发布时,改为空,以便于后续的打热补丁等操作。

#ifdef _DEBUG
#define STATIC static
#else
#define STATIC
#endif