• C语言编程规范


    一、代码总体原则

    1)清晰第一
    ***清晰性是易于维护、易于重构的程序必需具备的特征 ***
    本规范通过后文中的原则
    如:
    优秀的代码可以自我解释,不通过注释即可轻易读懂
    头文件中适合放置接口的声明,不适合放置实现
    除了常见的通用缩写以外,不使用单词缩写,不得使用汉语拼音
    规则(如防止局部变量与全局变量同名)等说明清晰的重要性。
    一般情况下,代码的可阅读性高于性能,只有确定性能是瓶颈时,才应该主动优化。
    2)简洁为美
    ***简洁就是易于理解并且易于实现 ***
    废弃的代码(没有被调用的函数和全局变量)要及时清除,重复代码应该尽可能提炼成函数
    本规范通过后文中的原则
    如:
    文件应当职责单一
    一个函数仅完成一件功能)
    规则(重复代码应该尽可能提炼成函数/避免函数过长,新增函数不超过50行)等说明简洁的重要性
    3)选择合适的风格,与代码原有风格保持一致
    ——————————————————————————

    二、术语定义

    1)头文件

    原则:
    1、职责单一
    2、头文件应向稳定的方向包含
    原因:将不稳定的头文件放在最开始,一旦出错,就减少了时间,因为先编译稳定,再编译不稳定,如果不稳定出错就浪费了时间
    头文件include从抽象到具体
    次序:C标准库→C++标准库→OS相关→第三方库→工程头文件
    3、头文件适合放置接口的声明,不适合放置实现

    规则:
    1、每一个.c文件应有一个同名.h文件,用于声明需要对外公开的接口
    建议所有函数实现都按照调用顺序依次定义
    2、禁止头文件循环依赖
    嵌套,即a依赖b,b依赖c,c依赖a
    3、.c/.h文件禁止包含用不到的头文件
    4、头文件应当自包含
    自包含:任意一个头文件均可独立编译
    5、总是编写内部#include保护符(#define保护)
    不要在宏最前面加上" _ “,即使用 FILENAME_H 代替 _ FILENAME_H_
    因为一般以 " _ " 和” __ “开头的标识符为系统保留或者标准库使用,
    在有些静态检查工具中,若全局可见的标识符以”_"开头会给出告警
    6、禁止在头文件中定义变量
    7、只能通过包含头文件的方式使用其他.c提供的接口,禁止在.c中通过extern的方式使用外部函数接口、变量
    8、禁止在extern "C"中包含头文件

    建议:
    1)一个模块通常包含多个.c文件,建议放在同一个目录下,目录名即为模块名。
    为方便外部使用者,建议每一个模块提供一个.h,文件名为目录名
    2)如果一个模块包含多个子模块,则建议每一个子模块提供一个对外的.h,文件名为子模块名
    降低接口使用者的编写难度
    3)头文件不要使用非习惯用法的扩展名,如.inc
    4)同一产品统一包含头文件排列方式
    常见的包含头文件排列方式:功能块排序、文件名升序、稳定度排序
    ①以升序方式排列头文件可以避免头文件被重复包含

    #include 
    #include 
    #include 
    
    • 1
    • 2
    • 3

    ②以稳定度排序,建议将不稳定(修改频繁)的头文件放在前面
    ————————————————————————————

    2)函数

    精髓:编写整洁函数,同时把代码有效组织起来
    原则:
    1、一个函数仅完成一件功能
    2、重复代码应该尽可能提炼成函数
    3、一个函数的最大长度是和该函数的复杂度和缩进级数成反比的,如case
    4、避免函数过长,新增函数不超过50行(非空非注释行)
    5、避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层
    6、可重入函数应避免使用共享变量;若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护
    可重入函数是指可能被多个任务并发调用的函数
    编写C语言的可重入函数时,不应使用static局部变量,否则必须经过特殊处理,才能使函数具有可重入性
    7、对参数的合法性检查,由调用者负责还是由接口函数负责,应在项目组/模块内应统一规定。缺省(默认)由调用者负责
    8、对函数的错误返回码要全面处理
    9、设计高扇入,合理扇出(小于7)的函数
    扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它
    较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中
    10、废弃代码(没有被调用的函数和变量)要及时清除

    建议:
    1、函数不变参数使用const
    2、函数应避免使用全局变量、静态局部变量和I/O操作,不可避免的地方应集中使用
    3、检查函数所有非参数输入的有效性,如数据文件、公共变量等
    4、函数的参数个数不超过5个
    5、除打印类函数外,不要使用可变长参函数
    6、在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static关键字
    7、为了测试人员好看,避免将函数调用放在条件判断里面
    如:if( (a=2)>c )等等
    ——————————————————————————

    3)变量命名规则

    规则:
    1、全局变量应增加"g_"前缀
    2、静态变量应增加 “s_” 前缀
    3、禁止使用单字节命名变量,但允许定义i、j、k作为局部循环变量
    命名文件风格:
    unix :a_like_b
    window :ALikeB,不过遇见ReadRFCText会变得不清晰,前提:RFC是文件名

    建议:
    1、不建议使用匈牙利命名法
    2、使用名词或者形容词+名词方式命名变量
    3、函数命名应以函数要执行的动作命名,一般采用动词或者动词+名词的结构
    4、函数指针除了前缀,其他按照函数的命名规则命名
    5、相对独立的程序块之间、变量说明之后必须加空行

    符合规范——
    void *th_func(void *arg){
    int specific = (int)(int *)arg;
    //换行
    for (int i = 0; i <= specific; i++)
    {
    printf(“%lx run %d m\n”, pthread_self(), i);
    //
    int time = (int)(drand48() * 10000);
    usleep(time);
    }

    4)宏的命名规则

    规则:
    1、对于数值或者字符串等等常量的定义,建议采用全大写字母,单词之间加下划线’ _ ‘的方式命名(枚举同样建议使用此方式定义)
    2、除了头文件或编译开关等特殊标识定义,宏定义不能使用下划线’ _ ’ 开头和结尾(见上文)
    3、用宏定义表达式时,要有完备的括号
    如:
    #define RTC(a,b) ( (a)(b) )
    备注:因宏本质是替换,加上括号能更好得到预期的结果
    如:
    #define PP (2+2)
    当 PP * PP 时,由于加上了括号,即 (2+2) * (2+2) ,而不是 2+2
    2+2
    4、将宏所定义的多条表达式放在大括号中
    如:\ 是将其连接成一条语句,详见搜索宏函数的使用
    #define PP(x) {

    }
    5、少用魔鬼数字(及一个有含义的数字多次使用,一旦更改,后果难料,0除外)
    解决途径:加注释、加const等等

    建议:
    1、除非必要,应尽可能使用函数代替宏,如内联函数
    2、常量建议使用 const 定义代替宏
    3、宏定义中尽量不适用 return 、 goto 、continue 、 break 等改变程序流程的语句

    5)结构体的命名规则

    规则:
    1、不要使用类似“vps_t”之类的东西
    很多人认为typedef“能提高可读性”,实际不是。它们只在下列情况下有用
    a)完全不透明的对象(这种情况下要主动使用typedef来隐藏这个对象实际上是什么)
    b)清楚的整数类型
    c)当你使用sparse按字面的创建一个新类型来做类型检查的时候
    d)和标准C99类型相同的类型
    e)可以在用户空间安全使用的类型
    总的来说,如果一个指针或者一个结构体里的元素可以合理的被直接访问到,那么它们就不应该是一个 typedef

    6)变量的原则

    原则:
    1、一个变量只有一个功能,不能把一个变量用作多种用途
    2、结构功能单一;不要设计面面俱到的数据结构
    3、不用或者少用全局变量
    4、防止局部变量与全局变量同名
    5、通讯过程中使用的结构,必须注意字节序
    6、严禁使用未经初始化的变量作为右值(类似常量不能做左值)

    建议:
    1、构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,
    防止多个不同模块或函数都可以修改、创建同一全局变量的现象(降低全局变量耦合度)
    2、使用面向接口编程思想,通过API访问数据:
    如果本模块的数据需要对外部模块开放,应提供接口函数来设置、获取,同时注意全局数据的访问互斥
    3、在首次使用前初始化变量,初始化的地方离使用的地方越近越好
    4、明确全局变量的初始化顺序,避免跨模块的初始化依赖
    5、尽量减少没有必要的数据类型默认转换与强制转换
    ——————————————————————-——

    7)宏、常量的规则

    规则:
    1、用宏定义表达式时,要使用完备的括号
    2、将宏所定义的多条表达式放在大括号中
    3、使用宏时,不允许参数发生变化
    4、不允许直接使用魔鬼数字
    建议:
    1、除非必要,应尽可能使用函数代替宏
    2、常量建议使用const定义代替宏
    3、宏定义中尽量不使用return、goto、continue、break等改变程序流程的语句
    ——————————————————————————

    8)质量保证原则

    原则:
    1、代码质量保证优先原则
    2、要时刻注意易混淆的操作符
    3、必须了解编译系统的内存分配方式,特别是编译系统对不同类型的变量的内存分配规则,如局部变量在何处分配、静态变量在何处分配等
    4、不仅关注接口,同样要关注实现
    ——————————————————————————

    9)关于内存的原则与建议

    原则:
    1、禁止内存操作越界
    坚持下列措施可以避免内存越界
    ⚫ 数组的大小要考虑最大情况,避免数组分配空间不够。
    ⚫ 避免使用危险函数sprintf /vsprintf/strcpy/strcat/gets操作字符串,使用相对安全的函数
    snprintf/strncpy/strncat/fgets代替。
    ⚫ 使用memcpy/memset时一定要确保长度不要越界
    ⚫ 字符串考虑最后的’\0’,确保所有字符串是以’\0’结束
    ⚫ 指针加减操作时,考虑指针类型长度
    ⚫ 数组下标进行检查
    ⚫ 使用时sizeof或者strlen计算结构/字符串长度,避免手工计算
    延伸阅读材料: 《公司常见软件编程低级错误:内存越界.ppt》
    2、禁止内存泄漏
    说明:内存和资源(包括定时器/文件句柄/Socket/队列/信号量/GUI等各种资源)泄漏是常见的错误
    坚持下列措施可以避免内存泄漏
    ⚫ 异常出口处检查内存、定时器/文件句柄/Socket/队列/信号量/GUI等资源是否全部释放
    ⚫ 删除结构指针时,必须从底层向上层顺序删除
    ⚫ 使用指针数组时,确保在释放数组时,数组中的每个元素指针是否已经提前被释放了
    ⚫ 避免重复分配内存
    ⚫ 小心使用有return、break语句的宏,确保前面资源已经释放
    ⚫ 检查队列中每个成员是否释放
    3、禁止引用已经释放的内存空间
    ⚫ 内存释放后,把指针置为NULL;使用内存指针前进行非空判断。
    ⚫ 耦合度较强的模块互相调用时,一定要仔细考虑其调用关系,防止已经删除的对象被再次使用。
    ⚫ 避免操作已发送消息的内存。
    ⚫ 自动存储对象的地址不应赋值给其他的在第一个对象已经停止存在后仍然保持的对象(具有更大作用域的对象或者静态对象或者从一个函数返回的对象)
    4、编程时,要防止差1(范围溢出)错误
    5、所有的if … else if结构应该由else子句结束;switch语句必须default分支

    建议:
    1、函数中分配的内存,在函数退出之前要释放
    2、if语句尽量加上else分支,对没有else分支的语句要小心对待
    3、不要滥用goto语句
    4、时刻注意表达式是否会上溢、下溢

    10)程序效率愿原则与建议

    1、在保证软件系统的正确性、简洁、可维护性、可靠性及可测性的前提下,提高代码效率(概率较大的分支放在前面)
    记住:让一个正确的程序更快速,比让一个足够快的程序正确,要容易得太多。大多数时候,不要把注意力集中在如何使代码更快上,应首先关注让代码尽可能地清晰易读和更可靠
    2、通过对数据结构、程序算法的优化来提高效率

    建议:
    1、将不变条件的计算移到循环体外
    2、对于多维大数组,避免来回跳跃式访问数组成员(短-》长,cache命中)
    3、创建资源库,以减少分配对象的开销(进程池、线程池、内存池…)
    4、将多次被调用的“小函数”改为inline函数或者宏实现
    说明: 如果编译器支持inline,可以采用inline函数。否则可以采用宏。
    在做这种优化的时候一定要注意下面inline函数的优点:
    其一编译时不用展开,代码SIZE小。
    其二可以加断点,易于定位问题,例如对于引用计数加减的时候。
    其三函数编译时,编译器会语法检查。三思而后行。

    11)注释的原则与建议

    原则:
    1、优秀的代码可以自我解释,不通过注释即可轻易读懂
    2、注释的内容要清楚、明了,含义准确,防止注释二义性
    3、在代码的功能、意图层次上进行注释,即注释解释代码难以直接表达的意图,而不是重复描述代码
    说明:注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。
    对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释。注释不是为了名词解释(what),而是说明用途(why)
    规则:
    1、修改代码时,维护代码周边的所有注释,以保证注释与代码的一致性。不再有用的注释要删除

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

    /*************************************************
    Copyright © Huawei Technologies Co., Ltd. 1998-2011. All rights reserved.
    File name: 	// 文件名
    Author: ID: Version: Date: // 作者、工号、版本及完成日期
    Description: 	// 用于详细说明此程序文件完成的主要功能,与其他模块
    		// 或函数的接口,输出值、取值范围、含义及参数间的控
    		// 制、顺序、独立或依赖等关系
    Others: 	// 其它内容的说明
    History: 	// 修改历史记录列表,每条修改记录应含修改日期、修改
    		// 者及修改内容简述
    1. Date:
    Author: ID:
    Modification:
    2. ...
    *************************************************/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3、函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、设计约束等。
    4、全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明
    5、注释应放在其代码上方相邻位置或右方,不可放在下面。如放于上方则需与其上面的代码用空行隔开,且与下方代码缩进相同
    6、对于switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释
    7、避免在注释中使用缩写,除非是业界通用或子系统内标准化的缩写
    8、同一产品或项目组统一注释风格

    建议:
    1、避免在一行代码或表达式的中间插入注释
    2、注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达。对于有外籍员工的,由产品确定注释语言
    3、文件头、函数头、全局常量变量、类型定义的注释格式采用工具可识别的格式(如doxygcn格式,本文件文件名、功能概述、版权、作者、创建日期)


    12)排版与格式规则与建议

    规则:
    1、程序块采用缩进风格编写,每级缩进为4个空格
    2、相对独立的程序块之间、变量说明之后必须加空行
    3、一条语句不能过长,如不能拆分需要分行写。一行到底多少字符换行比较合适,产品可以自行确定
    说明:对于目前大多数的PC来说,132比较合适(80/132是VTY常见的行宽值);对于新PC宽屏显示器较多的产品来说,可以设置更大的值。
    换行时有如下建议:
    ⚫ 换行时要增加一级缩进,使代码可读性更好;
    ⚫ 低优先级操作符处划分新行;换行时操作符应该也放下来,放在新行首;
    ⚫ 换行时建议一个完整的语句放在一行,不要根据字符数断行
    示例:

    if ((temp_flag_var == TEST_FLAG)
    &&(((temp_counter_var - TEST_COUNT_BEGIN) % TEST_COUNT_MODULE)
    = TEST_COUNT_THRESHOLD)) {
    // process code
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4、多个短语句(包括赋值语句)不允许写在同一行内,即一行只写一条语句
    5、每一行的长度的限制是80列,我们强烈建议您遵守这个惯例
    6、if、for、do、while、case、switch、default等语句独占一行
    7、在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如->),后不应加空格
    8、空格使用方式(主要)取决于它是用于函数还是关键字

    建议:
    1、注释符(包括"/“、”//“、”/")与注释内容之间要用一个空格进行分隔
    2、源程序中关系较为紧密的代码应尽可能相邻

    13)表达式的规则与建议

    规则:
    1、表达式的值在标准所允许的任何运算次序下都应该是相同的
    建议:
    1、函数调用不要作为另一个函数的参数使用,否则对于代码的调试、阅读都不利
    2、赋值语句不要写在if等语句中,或者作为函数的参数使用
    3、用括号明确表达式的操作顺序,避免过分依赖默认优先级
    4、赋值操作符不能使用在产生布尔值的表达式上

    14)代码编辑、编译的规则

    规则:
    1、使用编译器的最高告警级别,理解所有的告警,通过修改代码而不是降低告警级别来消除所有告警
    2、在产品软件(项目组)中,要统一编译开关、静态检查选项以及相应告警清除策略
    3、本地构建工具(如PC-Lint)的配置应该和持续集成的一致
    4、使用版本控制(配置管理)系统,及时签入通过本地构建的代码,确保签入的代码不会影响构建成功
    5、要小心地使用编辑器提供的块拷贝功能编程

    15)可测性

    1、模块划分清晰,接口明确,耦合性小,有明确输入和输出,否则单元测试实施困难
    说明:单元测试实施依赖于:
    ⚫ 模块间的接口定义清楚、完整、稳定;
    ⚫ 模块功能的有明确的验收条件(包括:预置条件、输入和预期结果);
    ⚫ 模块内部的关键状态和关键数据可以查询,可以修改;
    ⚫ 模块原子功能的入口唯一;
    ⚫ 模块原子功能的出口唯一;
    ⚫ 依赖集中处理:和模块相关的全局变量尽量的少,或者采用某种封装形式
    2、 在同一项目组或产品组内,要有一套统一的为集成测试与系统联调准备的调测开关及相应打印函数,并且要有详细的说明
    3、在同一项目组或产品组内,调测打印的日志要有统一的规定
    说明:统一的调测日志记录便于集成测试,具体包括:
    ⚫ 统一的日志分类以及日志级别;
    ⚫ 通过命令行、网管等方式可以配置和改变日志输出的内容和格式;
    ⚫ 在关键分支要记录日志,日志建议不要记录在原子函数中,否则难以定位;
    ⚫ 调试日志记录的内容需要包括文件名/模块名、代码行号、函数名、被调用函数名、错误码、错误发生的环境等。
    4、使用断言记录内部假设
    断言是对某种内部模块的假设条件进行检查,如果假设不成立,说明存在编程、设计错误
    5、不能用断言来检查运行时错误
    断言是用来处理内部编程或设计是否符合假设;
    不能处理对于可能会发生的且必须处理的情况要写防错程序,而不是断言
    断言的使用是有条件的。
    断言只能用于程序内部逻辑的条件判断,而不能用于对外部输入数据的判断, 因为在网上实际运行时,是完全有可能出现外部输入非法数据的情况

    建议:
    1、为单元测试和系统故障注入测试准备好方法和通道

    16)安全性的原则与建议

    1、对用户输入进行检查
    场景:
    ⚫ 用户输入作为循环条件
    ⚫ 用户输入作为数组下标
    ⚫ 用户输入作为内存分配的尺寸参数
    ⚫ 用户输入作为格式化字符串
    ⚫ 用户输入作为业务数据(如作为命令执行参数、拼装sql语句、以特定格式持久化)
    这些情况下如果不对用户数据做合法性验证,很可能导致DOS、内存越界、格式化字符串漏洞、命令注入、SQL注入、缓冲区溢出、数据破坏等问题
    2、字符串
    ①确保所有字符串是以NULL结束
    ②不要将边界不明确的字符串写到固定长度的数组中
    3、整数安全
    C99标准定义了整型提升、整型转换级别以及普通算术转换的整型操作
    ①避免整数溢出
    ②避免符号错误
    说明:有时从带符号整型转换到无符号整型会发生符号错误,符号错误并不丢失数据,但数据失去了原来的含义
    ③避免截断错误(低位保留、高位丢失)
    4、格式化输出安全
    ①确保格式字符和参数匹配(一一对应)
    ②避免将用户输入作为格式化字符串的一部分或者全部(正则表达式的应用
    当用户输入的是“%s%s%s%s%s%s%s%s%s%s%s%s”,就可能触发无效指针或未映射的地址读取。
    5、文件I/O安全
    ①避免使用strlen()计算二进制数据的长度
    说明:strlen()函数用于计算字符串的长度,它返回字符串中第一个NULL结束符之前的字符的数量。因此用strlen()处理文件I/O函数读取的内容时要小心,因为这些内容可能是二进制也可能是文本
    ②使用int类型变量来接收字符I/O函数的返回值
    ③防止命令注入(更换函数)

    17)单元测试的规则与建议

    规则:
    1、在编写代码的同时,或者编写代码前,编写单元测试用例验证软件设计/编码的正确
    建议:
    1、单元测试关注单元的行为而不是实现,避免针对函数的测试

    18)可移植性的规则与建议

    规则:
    1、不能定义、重定义或取消定义标准库/平台中保留的标识符、宏和函数
    建议:
    1、不使用与硬件或操作系统关系很大的语句,而使用建议的标准语句,以提高软件的可移植性和可重用性
    2、除非为了满足特殊需求,避免使用嵌入式汇编

    19)业界编程规范

    1、《google C++编程指南》
    2、《汽车业C语言使用规范(MISRA)》

  • 相关阅读:
    MXNet-图像分类(symbol版本)【附源码】
    AI计算机视觉进阶项目(一)——带口罩识别检测(2)
    Lingo软硬件划分 实例
    微信小程序组件所在页面的生命周期
    spring之bean的生命周期
    力扣leetcode 670. 最大交换
    Java智能教育平台源码基于 SpringBoot + Mybatis + Shiro + mysql + redis构建,前后端分离。
    计算机中的第二个伟大发明(JMP/JMPR)
    springcloud----检索中间件 ElasticSearch 分布式场景的运用
    大数据题目的解题技巧
  • 原文地址:https://blog.csdn.net/hold_the_key/article/details/127736123