编写整洁函数,同时把代码有效组织起来。
一个函数实现多个功能给开发、使用、维护都带来很大的困难。 案例:realloc。在标准 C 语言中,realloc 是一个典型的不良设计。这个函数基本功能是重新分配内存,但它承担了太多的其他任务:如果传入的指针参数为 NULL 就分配内存,如果传入的大小参数为 0 就释放内存,如果可行则就地重新分配,如果不行则移到其他地方分配。如果没有足够可用的内存用来完成重新分配(扩大原来的内存块或者分配新的内存块),则返回 NULL,而原来的内存块保持不变。这个函数不易扩展,容易导致问题。例如下面代码容易导致内存泄漏:
char *buffer = (char *)malloc(XXX_SIZE);
.....
buffer = (char *)realloc(buffer, NEW_SIZE);
如果没有足够可用的内存用来完成重新分配,函数返回为 NULL,导致 buffer 原来指向的内存被丢失。
重复代码提炼成函数可以带来维护成本的降低。
本规则仅对新增函数做要求,对已有函数修改时,建议不增加代码行。 过长的函数往往意味着函数功能不单一,过于复杂(参见原则 2.1:一个函数只完成一个功能)。
函数的有效代码行数,即 NBNC(非空非注释行)应当在[1,50]区间。
例外:某些实现算法的函数,由于算法的聚合性与功能的全面性,可能会超过 50 行。
本规则仅对新增函数做要求,对已有的代码建议不增加嵌套层次。
函数的代码块嵌套深度指的是函数中的代码控制块(例如: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;
}
}
}
可重入函数是指可能被多个任务并发调用的函数。在多任务操作系统中,函数具有可重入性是 多个任务可以共用此函数的必要条件。共享变量指的全局变量和 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;
}
对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率。
一个函数(标准库中的函数/第三方库函数/用户定义的函数)能够提供一些指示错误发生的方法。这可以通过使用错误标记、特殊的返回数据或者其他手段,不管什么时候函数提供了这样的机制,调用程序应该在函数返回时立刻检查错误指示。 示例:下面的代码导致宕机
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); //解析获取最新的告警时间;
程序中的废弃代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可能给程序的测试、维护等造成不必要的麻烦。
不变的值更易于理解/跟踪和分析,把 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;
}
示例:如下函数,其返回值(即功能)是不可预测的。
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;
}
函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入参数之前,应进行有效性检查。
示例:下面的代码导致宕机
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);
函数的参数过多,会使得该函数易于受外部(其他部分的代码)变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。
函数的参数个数不要超过 5 个,如果超过了建议拆分为不同函数。
可变长参函数的处理过程比较复杂容易引入错误,而且性能也比较低,使用过多的可变长参函数将导致函数的维护难度大大增加。
如果一个函数只是在同一文件中的其他地方调用,那么就用 static 声明。使用 static 确保只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能性。
建议定义一个 STATIC 宏,在调试阶段,将 STATIC 定义为 static,版本发布时,改为空,以便于后续的打热补丁等操作。
#ifdef _DEBUG
#define STATIC static
#else
#define STATIC
#endif