C语言编程规范

1.代码总体原则

1.1 清晰第一

清晰性是易于维护、易于重构的程序必需具备的特征。代码首先是给人读的,好的代码应当可以像文章一样发声朗诵出来。
目前软件维护期成本占整个生命周期成本的40%~90%。根据业界经验,维护期变更代码的成本,小型系统是开发期的5倍,大型系统(100万行代码以上)可以达到100倍。业界的调查指出,开发组平均大约一半的人力用于弥补过去的错误,而不是添加新的功能来帮助公司提高竞争力。
一般情况下,代码的可阅读性高于性能,只有确定性能是瓶颈时,才应该主动优化。

1.2 简洁为美

简洁就是易于理解并且易于实现。代码越长越难以看懂,也就越容易在修改时引入错误。写的代码越多,意味着出错的地方越多,也就意味着代码的可靠性越低。因此,我们提倡大家通过编写简洁明了的代码来提升代码可靠性。
废弃的代码(没有被调用的函数和全局变量)要及时清除,重复代码应该尽可能提炼成函数。

1.3 选择合适的风格,与代码原有风格保持一致

产品所有人共同分享同一种风格所带来的好处,远远超出为了统一而付出的代价。在公司已有编码规范的指导下,审慎地编排代码以使代码尽可能清晰,是一项非常重要的技能。 如果重构/ / 修改其他风格的代码时,比较明智的做法是根据 现有 代码 的 现有风格继续编写代码,或者使用格式转换工具进行转换成公司内部风格。

2.头文件

对于C语言来说,头文件的设计体现了大部分的系统设计。 不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上反映了不合理的设计。

2.1 头文件中适合放置接口的声明,不适合放置实现

头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。
要求:
(1)内部使用的函数(相当于类的私有方法)声明不应放在头文件中。
(2)内部使用的宏、枚举、结构定义不应放入头文件中。
(3)变量定义不应放在头文件中,应放在.c文件中。
(4)变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。 即使必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的。

2.2 头文件应当职责单一,切忌依赖复杂

头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。很多现有代码中头文件过大,职责过多,再加上循环依赖的问题,可能导致为了在.c中使用一个宏,而包含十几个头文件。

错误示例:某平台定义WORD类型的头文件:

#include <VXWORKS.H>
#include <KERNELLIB.H>
#include <SEMLIB.H>
#include <INTLIB.H>
#include <TASKLIB.H>
#include <MSGQLIB.H>
#include <STDARG.H>
#include <FIOLIB.H>
#include <STDIO.H>
#include <STDLIB.H>
#include <CTYPE.H>
#include <STRING.H>
#include <ERRNOLIB.H>
#include <TIMERS.H>
#include <MEMLIB.H>
#include <TIME.H>
#include <WDLIB.H>
#include <SYSLIB.H>
#include <TASKHOOKLIB.H>
#include <REBOOTLIB.H>typedef unsigned short WORD;

这个头文件不但定义了基本数据类型WORD,还包含了stdio.h syslib.h等等不常用的头文件。如果工程中有10000个源文件,而其中100个源文件使用了stdio.h的printf,由于上述头文件的职责过于庞大,而WORD又是每一个文件必须包含的,从而导致stdio.h/syslib.h等可能被不必要的展开了9900次,大大增加了工程的编译时间。

2.3 头文件应向稳定的方向包含

头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。

就我们的产品来说,依赖的方向应该是: 产品依赖于平台,平台依赖于标准库。某产品线平台的代码中已经包含了产品的头文件,导致平台无法单独编译、发布和测试,是一个非常糟糕的反例。除了不稳定的模块依赖于稳定的模块外,更好的方式是两个模块共同依赖于接口,这样任何一个模块的内部实现更改都不需要重新编译另外一个模块。在这里,我们假设接口本身是最稳定的。

2.4 每一个 .c 文件应有一个同名 .h 文件,用于声明需要对外公开的接口

如果一个.c文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如main函数所在的文件。

现有某些产品中,习惯一个.c文件对应两个头文件,一个用于存放对外公开的接口,一个用于存放内部需要用到的定义、声明等,以控制.c文件的代码行数。编者不提倡这种风格。这种风格的根源在于源文件过大,应首先考虑拆分.c文件,使之不至于太大。另外,一旦把私有定义、声明放到独立的头文件中,就无法从技术上避免别人include之,难以保证这些定义最后真的只是私有的。

2.5 禁止头文件循环依赖

头文件循环依赖,指a.h包含b.h,b.h包含c.h,c.h包含a.h之类导致任何一个头文件修改,都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。而如果是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h/c.h的源代码重新编译。

2.6 .c/.h文件禁止包含用不到的头文件

很多系统中头文件包含关系复杂,开发人员为了省事起见,可能不会去一一钻研,直接包含一切想到的头文件,甚至有些产品干脆发布了一个god.h,其中包含了所有头文件,然后发布给各个项目组使用,这种只图一时省事的做法,导致整个系统的编译时间进一步恶化,并对后来人的维护造成了巨大的麻烦。

2.7 头文件应当自包含

简单的说,自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户增添不必要的负担。
示例:如果a.h不是自包含的,需要包含b.h才能编译,会带来的危害:每个使用a.h头文件的.c文件,为了让引入的a.h的内容编译通过,都要包含额外的头文件b.h。额外的头文件b.h必须在a.h之前进行包含,这在包含顺序上产生了依赖。
注意:该规则需要与“.c/.h文件禁止包含用不到的头文件”规则一起使用,不能为了让a.h自包含,而在a.h中包含不必要的头文件。a.h要刚刚可以自包含,不能在a.h中多包含任何满足自包含之外的其他头文件。

2.8 总是编写内部 #include 保护符( #define 保护)

多次包含一个头文件可以通过认真的设计来避免。如果不能做到这一点,就需要采取阻止头文件内容被包含多于一次的机制。通常的手段是为每个文件配置一个宏,当头文件第一次被包含时就定义这个宏,并在头文件被再次包含时使用它以排除文件内容。所有头文件都应当使用#define 防止头文件被多重包含,命名格式为FILENAME_H,为了保证唯一性,更好的命名是PROJECTNAME_PATH_FILENAME_H。

注:没有在宏最前面加上单下划线"“,是因为一般以单下划线”“和双下划线”__“开头的标识符为ANSIC等使用,在有些静态检查工具中,若全局可见的标识符以”_"开头会给出告警。

定义包含保护符时,应该遵守如下规则:保护符使用唯一名称,不要在受保护部分的前后放置代码或者注释。

正确示例:假定VOS工程的timer模块的timer.h,其目录为VOS/include/timer/timer.h,应按如下方式保护:

#ifndef VOS_INCLUDE_TIMER_TIMER_H
#define VOS_INCLUDE_TIMER_TIMER_H
...
#endif

也可以使用如下简单方式保护:

#ifndef TIMER_H
#define TIMER_H
...
#endif

例外情况:头文件的版权声明部分以及头文件的整体注释部分(如阐述此头文件的开发背景、使用注意事项等)可以放在保护符(#ifndef XX_H)前面。

2.9 禁止在头文件中定义变量

在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义。

2.10 只能通过包含头文件的方式使用其他 .c 提供的接口,禁止在.c 中通过 extern 的方式使用外部函数接口、变量

若a.c使用了b.c定义的foo()函数,则应当在b.h中声明extern int foo(int input);并在a.c中通过#include <b.h>来使用foo。禁止通过在a.c中直接写extern int foo(int input);来使用foo,后面这种写法容易在foo改变时可能导致声明和定义不一致。

3.函数

函数设计的精髓:编写整洁函数,同时把代码有效组织起来。
整洁函数要求:代码简单直接、不隐藏设计者的意图、用干净利落的抽象和直截了当的控制语句将函数有机组织起来。
代码的有效组织包括:逻辑层组织和物理层组织两个方面。逻辑层,主要是把不同功能的函数通过某种联系组织起来,主要关注模块间的接口,也就是模块的架构。物理层,无论使用什么样的目录或者名字空间等,需要把函数用一种标准的方法组织起来。例如:设计良好的目录结构、函数名字、文件组织等,这样可以方便查找。

3.1 一个函数仅完成一件功能

一个函数实现多个功能给开发、使用、维护都带来很大的困难。

将没有关联或者关联很弱的语句放到同一函数中,会导致函数职责不明确,难以理解,难以测试和改动。

3.2 重复代码应该尽可能提炼成函数

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

重复代码是我司不良代码最典型的特征之一。在“代码能用就不改”的指导原则之下,大量的烟囱式设计及其实现充斥着各产品代码之中。新需求增加带来的代码拷贝和修改,随着时间的迁移,产品中堆砌着许多类似或者重复的代码。

项目组应当使用代码重复度检查工具,在持续集成环境中持续检查代码重复度指标变化趋势,并对新增重复代码及时重构。当一段代码重复两次时,即应考虑消除重复,当代码重复超过三次时,应当立刻着手消除重复。

3.3 避免函数过长,新增函数不超过 50 行 (非空非注释行)

过长的函数往往意味着函数功能不单一,过于复杂。

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

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

延伸阅读材料: 业界普遍认为一个函数的代码行不要超过一个屏幕,避免来回翻页影响阅读;一般的代码度量工具建议都对此进行检查,例如Logiscope的函数度量:“Number of Statement” (函数中的可执行语句数)建议不超过20行,QA C建议一个函数中的所有行数(包括注释和空白行)不超过50行。

3.4 避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层

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

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

可重入函数是指可能被多个任务并发调用的函数。在多任务操作系统中,函数具有可重入性是多个任务可以共用此函数的必要条件。共享变量指的全局变量和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;
}

3.6 对函数的错误返回码要全面处理

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

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

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

3.8 函数不变参数使用const

不变的值更易于理解/跟踪和分析,把const作为默认选项,在编译时会对其进行检查,使代码更牢固/更安全。
正确示例:C99标准 7.21.4.4 中strncmp 的例子,不变参数声明为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;
}

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

带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在C语言中,函数的static局部变量是函数的内部存储器,有可能使函数的功能不可预测。

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

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

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

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

3.11 函数的参数个数不超过5个

函数的参数过多,会使得该函数易于受外部(其他部分的代码)变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。
函数的参数个数不要超过5个,如果超过了建议拆分为不同函数。

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

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

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

如果一个函数只是在同一文件中的其他地方调用,那么就用static声明。使用static确保只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能性。
正确示例:建议定义一个STATIC宏,在调试阶段,将STATIC定义为static,版本发布时,改为空,以便于后续的打热补丁等操作。

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

4.标识符命名与定义

标识符的命名规则历来是一个敏感话题,典型的命名风格如unix风格、windows风格等,从来无法达成共识。实际上,各种风格都有其优势也有其劣势,而且往往和个人的审美观有关。我们对标识符定义主要是为了让团队的代码看起来尽可能统一,有利于代码的后续阅读和修改,产品可以根据自己的实际需要指定命名风格,规范中不再做统一的规定。

4.1 标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解

尽可能给出描述性名称,不要节约空间,让别人很快理解你的代码更重要。
正确示例:

int error_number;
int number_of_completed_connection;

错误示例:

int n;
int nerr;
int n_comp_conns;

4.2 除了常见的通用缩写以外,不使用单词缩写,不得使用汉语拼音

较短的单词可通过去掉“元音”形成缩写,较长的单词可取单词的头几个字母形成缩写,一些单词有大家公认的缩写,常用单词的缩写必须统一。协议中的单词的缩写与协议保持一致。对于某个系统使用的专用缩写应该在注视或者某处做统一说明。

正确示例:一些常见可以缩写的例子:

argument 可缩写为 arg
buffer 可缩写为 buff
clock 可缩写为 clk
command 可缩写为 cmd
compare 可缩写为 cmp
configuration 可缩写为 cfg
device 可缩写为 dev
error 可缩写为 err
hexadecimal 可缩写为 hex
increment 可缩写为 inc
initialize 可缩写为 init
maximum 可缩写为 max
message 可缩写为 msg
minimum 可缩写为 min
parameter 可缩写为 para
previous 可缩写为 prev
register 可缩写为 reg
semaphore 可缩写为 sem
statistic 可缩写为 stat
synchronize 可缩写为 sync
temp 可缩写为 tmp

4.3 产品/项目组内部应保持统一的命名风格

Unix like和windows like风格均有其拥趸,产品应根据自己的部署平台,选择其中一种,并在产品内部保持一致。

4.4 用正确的反义词组命名具有互斥意义的变量或相反动作的函数等

正确示例:

add/remove begin/end create/destroy
insert/delete first/last get/release
increment/decrement put/get add/delete
lock/unlock open/close min/max
old/new start/stop  next/previous
source/target show/hide  send/receive
source/destination copy/paste up/down

4.5 尽量避免名字中出现数字编号,除非逻辑上的确需要编号

错误示例:如下命名,使人产生疑惑。
#define EXAMPLE_0_TEST_
#define EXAMPLE_1_TEST_
正确示例:应改为有意义的单词命名。
#define EXAMPLE_UNIT_TEST_
#define EXAMPLE_ASSERT_TEST_

4.6 重构/修改部分代码时,应保持和原有代码的命名风格一致

根据源代码现有的风格继续编写代码,有利于保持总体一致。

4.7 文件命名统一采用小写字符

因为不同系统对文件名大小写处理会不同(如MS的DOS、Windows系统不区分大小写,但是Linux系统则区分),所以代码文件命名建议统一采用全小写字母命名。

4.8 全局变量应增加“g_” 前缀,静态变量应增加“s_”

首先,全局变量十分危险,通过前缀使得全局变量更加醒目,促使开发人员对这些变量的使用更加小心。
其次,从根本上说,应当尽量不使用全局变量,增加g_和s_前缀,会使得全局变量的名字显得很丑陋,从而促使开发人员尽量少使用全局变量。

4.9 禁止使用单字节命名变量,但 允许 定义i 、j、k作为局部循环变量

5.注释

5.1 优秀的代码可 以自我解释,不通过注释即可轻易读懂。

优秀的代码不写注释也可轻易读懂,注释无法把糟糕的代码变好,需要很多注释来解释的代码往往存在坏味道,需要重构。
错误示例:注释不能消除代码的坏味道:

/* 判断m是否为素数*/
/* 返回值:: 是素数,: 不是素数*/
int p(int m)
{
    int k = sqrt(m);
    for (int i = 2; i <= k; i++)
        if (m % i == 0)
            break; /* 发现整除,表示m不为素数,结束遍历*/
    /* 遍历中没有发现整除的情况,返回*/
    if (i > k)
        return 1;
    /* 遍历中没有发现整除的情况,返回*/
    else
        return 0;
}

重构代码后,不需要注释:

int IsPrimeNumber(int num)
{
    int sqrt_of_num = sqrt (num);
    for (int i = 2; i <= sqrt_of_num; i++)
    {
        if (num % i == 0)
        {
            return FALSE;
        }
    }
    return TRUE;
}

5.2 注释的内容要清楚、明了,含义准确,防止注释二义性

有歧义的注释反而会导致维护者更难看懂代码,正如带两块表反而不知道准确时间。

5.3 在代码的功能、意图层次上进行注释,即注释解释 代码难以直接表达的意图 , 而不是重复描述代码

注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释。注释不是为了名词解释(what),而是说明用途(why)。

5.4 修改代码时,维护代码周边的所有注释,以保证注释与代码的一致性,不再有用的注释要删除

不要将无用的代码留在注释中,随时可以从源代码配置库中找回代码;即使只是想暂时排除代码,也要留个标注,不然可能会忘记处理它。

5.5 文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者姓名、工号、内容、功能说明、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明

正确示例:下面这段头文件的头注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。
在这里插入图片描述

5.6 函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、 设计约束等

重要的、复杂的函数,提供外部使用的接口函数应编写详细的注释。

5.7 全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明

/* The ErrorCode when SCCP translate /
/
Global Title failure, as follows / / 变量作用、含义*/
/* 0 -SUCCESS 1 -GT Table error /
/
2 -GT error Others -no use / / 变量取值范围*/
/* only function SCCPTranslate() in /
/
this modual can modify it, and other /
/
module can visit it through call /
/
the function GetGTTransErrorCode() / / 使用方法*/
BYTE g_GTTranErrorCode;

5.8 文件头、函数头、全局常量变量、类型定义的注释格式采用工具可识别的格式

采用工具可识别的注释格式,例如doxygen格式,方便工具导出注释形成帮助文档。以doxygen格式为例,文件头,函数和全部变量的注释的示例如下:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值