软件架构设计(三)——基本编程规范

华为C语言编程规范(精华总结) - 知乎

C语言编程规范(一)(华为标准要求)

C语言编程规范(二)(华为标准要求) - 知乎

代码总体原则

清晰第一。清晰性是易于维护、易于重构的程序必需具备的特征。代码首先是给人读的,好的代码应当可以像文章一样发声朗诵出来。一般情况下,代码的可阅读性高于性能,只有确定性能是瓶颈时,才应该主动优化。

软件结构设计

文件应当职责单一/一个函数仅完成一件功能
一个模块通常包含多个 .c 文件,建议放在同一个目录下,目录名即为模块名。为方便外部使用者,建议每一个模块提供一个 .h ,文件名为目录名,需要注意的是,这个.h并不是简单的包含所有内部的.h,它是为了模块使用者的方便,对外整体提供的模块接口。如果一个模块包含多个子模块,则建议每一个子模块提供一个对外的 .h,文件名为子模块名。

头文件

对于C语言来说,头文件的设计体现了大部分的系统设计。不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上不合理的设计。在一个设计良好的系统中,修改一个文件,只需要重新编译数个,甚至是一个文件。因此,我们倾向于减少包含头文件,尤其是在头文件中包含头文件,以控制改动代码后的编译时间。

合理规划头文件的方法:

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

说明:头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。

内部使用的函数(相当于类的私有方法)声明不应放在头文件中。
内部使用的宏、枚举、结构定义不应放入头文件中。
变量定义不应放在头文件中,应放在.c文件中。
变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露,使用面向接口编程思想,通过 API 访问数据:如果本模块的数据需要对外部模块开放 ,应提供接口函数来设置、获取,同时注意全局数据的访问互斥。避免直接暴露内部数据给外部模型使用,是防止模块间耦合最简单有效的方法。即使必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的。

  • 头文件应当职责单一,禁止头文件循环依赖,禁止包含用不到的头文件

说明:头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。头文件循环依赖,指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的源代码重新编译。很多现有代码中头文件过大,职责过多,再加上循环依赖的问题,可能导致为了在.c中使用一个宏,而包含十几个头文件。

  • 头文件应向稳定的方向包含。

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

就我们的产品来说,依赖的方向应该是:产品依赖于平台,平台依赖于标准库。某产品线平台的代码中已经包含了产品的头文件,导致平台无法单独编译、发布和测试,是一个非常糟糕的反例。

除了不稳定的模块依赖于稳定的模块外,更好的方式是两个模块共同依赖于接口,这样任何一个模块的内部实现更改都不需要重新编译另外一个模块。在这里,我们假设接口本身是最稳定的。

编者推荐开发人员使用“依赖倒置”原则,即由使用者制定接口,服务提供者实现接口。

  • 头文件应当自包含

说明:简单的说,自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户增添不必要的负担。
即为了使用xx.c文件的接口,只要包含xx.h文件就可以了

示例:

如果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中多包含任何满足自包含之外的其他头文件。

  • 只能通过包含头文件的方式使用其他.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改变时可能导致声明和定义不一致。

  • 禁止在extern "C"中包含头文件

说明:在extern "C"中包含头文件,会导致extern "C"嵌套,Visual Studio对extern "C"嵌套层次有限制,嵌套层次太多会编译错误。

  • 包含头文件排列方式

建议以稳定度排序,将不稳定的头文件放在前面,如果有错误,不必编译稳定的头文件就可以发现修改较为频繁的头文件,可以减少编译时间。

函数

函数设计的精髓:编写整洁函数,同时把代码有效组织起来。

整洁函数要求:代码简单直接、不隐藏设计者的意图、用干净利落的抽象和直截了当的控制语句将函数有机组织起来。

代码的有效组织包括:逻辑层组织和物理层组织两个方面。逻辑层,主要是把不同功能的函数通过某种联系组织起来,主要关注模块间的接口,也就是模块的架构。物理层,无论使用什么样的目录或者名字空间等,需要把函数用一种标准的方法组织起来。例如:设计良好的目录结构、函数名字、文件组织等,这样可以方便查找。

  • 一个函数仅完成一件功能。

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

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

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

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

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

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

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

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

建议:模块内部由调用者检查,模块对外接口函数应该对参数进行合法性检查

  • 设计高扇入,合理扇出(小于7)的函数。

说明:扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。

扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,例如:总是1,表明函数的调用层次可能过多,这样不利于程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。通常函数比较合理的扇出(调度函数除外)通常是3~5。

扇出太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数进一步分解多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。

扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。

较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。

  • 函数不变参数使用const。

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

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

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

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

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

变量
  • 结构功能单一,不要设计面面俱到的数据结构。

说明:相关的一组信息才是构成一个结构体的基础,结构的定义应该可以明确的描述一个对象,而不是一组相关性不强的数据的集合。

设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。

  • 使用面向接口编程思想,通过API访问数据:如果本模块的数据需要对外部模块开放,应提供接口函数来设置、获取,同时注意全局数据的访问互斥。

说明:避免直接暴露内部数据给外部模型使用,是防止模块间耦合最简单有效的方法。

定义的接口应该有比较明确的意义,比如一个风扇管理功能模块,有自动和手动工作模式,那么设置、查询工作模块就可以定义接口为SetFanWorkMode,GetFanWorkMode;查询转速就可以定义为 GetFanSpeed;风扇支持节能功能开关,可以定义EnabletFanSavePower等等。

全局变量的缺点:
1、破坏函数的独立性和可移植性,使函数对全局变量产生依赖,存在耦合;
2、降低函数的代码可读性和可维护性。当多个函数读写全局变量时,某一时刻其取值可能不是确定的,对于代码的阅读和维护不利;

  • 建议将多次被调用的 “小函数”改为inline函数或者宏实现。

说明: 如果编译器支持inline,可以采用inline函数。否则可以采用宏。

在做这种优化的时候一定要注意下面inline函数的优点:其一编译时不用展开,代码SIZE小。其二可以加断点,易于定位问题,例如对于引用计数加减的时候。其三函数编译时,编译器会做语法检查。三思而后行

编程习惯

若是字符类型仍使用char告诉读者这是字符

sizeof后面跟类型而不是变量(习惯) 

宏、常量

  • 除非必要,应尽量少的使用函数式宏(函数式宏不超过十行),包含多条语句的函数式宏的实现语句可以使用以下三种:

目的:让宏有独立的作用域,并且跟分号能更好的结合而形成单条语句,从而规避此类问题
1、使用{}  尽量不用
优点:简单粗暴。
缺点:不能在无花括号且有分支的 if 语句中直接调用;能够不带 ; 直接调用。
2、do{...}while(0)  最常用
优点:支持在无花括号且有分支的 if 语句中直接调用;支持提前退出函数宏;强制调用时必须使用
缺点:无返回值,不能作为表达式的右值使用。
3、({})  需要返回值时用这个

  • 宏定义中尽量不使用 return 、 goto 、 continue 、 break等改变程序流程的语句
  • 不允许把带副作用的表达式作为参数传递给函数宏,使用宏时,不允许参数发生变化。如SQUARE(a++,b),参数包含函数调用等


 

  • 宏调试开关的使用
表达式
  • 用括号明确表达式的操作顺序,避免过分依赖默认优先级

使用括号强调所使用的操作符,防止因默认的优先级与设计思想不符而导致程序出错;同时使得代码更为清晰可读,然而过多的括号会分散代码使其降低了可读性。

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

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

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

重要的、复杂的函数,提供外部使用的接口函数应编写详细的注释。函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、设计约束等。

  • 全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明
  • 对于 switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释
质量(安全)保证
  • 所有的if ... else if结构应该由else子句结束 ;switch语句必须有default分支。
  • 函数中分配的内存,在函数退出之前要释放。

    说明:有很多函数申请内存,保存在数据结构中,要在申请处加上注释,说明在何处释放。

  • 赋值语句不要写在 if 等语句中,或者作为函数的参数使用

        因为if语句中,会根据条件依次判断,如果前一个条件已经可以判定整个条件,则后续条件语句不会再运行,所以可能导致期望的部分赋值没有得到运行。

  • 定义指针或者释放内存后应该显示地设置为NULL
  • 使用strncpy等较为安全的函数

运行效率

  • 创建资源库,以减少分配对象的开销。

    说明:例如,使用线程池机制,避免线程频繁创建、销毁的系统调用;使用内存池,对于频繁申请、释放的小块内存,一次性申请一个大块的内存,当系统申请内存时,从内存池获取小块内存,使用完毕再释放到内存池中,避免内存申请释放的频繁系统调用.

  • 函数形参量不能太大(比如巨大的结构体),因为要把参数复制到堆栈中。可以使用传递地址的方式解决(缺点:间接访问)
  • 函数中若局部变量需要大量的初始化,可以考虑用static修饰,把他变为静态变量。
  • 尽量用乘法或者其他方法代替除法,特别是浮点数除法
  • 不建议在子函数中初始化大规模数组,放入栈空间需要时间
  • 在多重循环中,应将最忙的循环放在最内层。原因:1、循环变量的操作次数 2、分支预测成功率
  • 变量若为大量数组和结构体嵌套,可使用临时变量指针,在访问该地址时可节省cpu资源

移植性

  • 在第三方库上再封装一层函数接口,以便于升级和替换
  • 不使用与硬件或操作系统关系很大的语句,而使用建议的标准语句,以提高软件的可移植 性和可重用性。

  • 使用标准的数据类型,有利于程序的移植。

    uint32_t fixed32;    // 总是32位
    uint64_t fixed64;    // 总是64位
    size_t size_var;     // 大小与平台相关,适合表示对象大小
    uintptr_t   uptr;    //可以用于指针操作,指定一个无符号整数类型,其属性是任何有效的指向 void 的指针都可以转换为这个类型,然后转换回指向 void 的指针,结果将与原始指针比较相等
    
    使用 size_t:当处理内存大小、对象计数、数组索引、循环计数器时
    使用 uintptr_t:当需要存储指针值、进行指针运算、地址操作、位操作时

可测性

  • 模块划分清晰,接口明确,耦合性小,有明确输入和输出,否则单元测试实施困难。

说明:单元测试实施依赖于:

  • 模块间的接口定义清楚、完整、稳定;
  • 模块功能的有明确的验收条件(包括:预置条件、输入和预期结果);
  • 模块内部的关键状态和关键数据可以查询,可以修改;
  • 模块原子功能的入口唯一;
  • 模块原子功能的出口唯一;
  • 依赖集中处理:和模块相关的全局变量尽量的少,或者采用某种封装形式。

  • 在同一项目组或产品组内,要有一套统一的为集成测试与系统联调准备的调测开关及相应打印函数,并且要有详细的说明。

    说明:本规则是针对项目组或产品组的。代码至始至终只有一份代码,不存在开发版本和测试版本的说法。测试与最终发行的版本是通过编译开关的不同来实现的。并且编译开关要规范统一。统一使用编译开关来实现测试版本与发行版本的区别,一般不允许再定义其它新的编译开关。

  • 在同一项目组或产品组内,调测打印的日志要有统一的规定。

    说明:统一的调测日志记录便于集成测试,具体包括:

    统一的日志分类以及日志级别;
    通过命令行、网管等方式可以配置和改变日志输出的内容和格式;
    在关键分支要记录日志,日志建议不要记录在原子函数中,否则难以定位;
    调试日志记录的内容需要包括文件名/模块名、代码行号、函数名、被调用函数名、错误码、错误发生的环境等。

  • 正确使用断言

    说明:断言是用来处理内部编程或设计是否符合假设,如果假设不成立,说明存在编程、设计错误。对于处理对于可能会发生的且必须处理的情况,要写防错程序,而不是断言。如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现。

        使用assert的核心原则是:用于处理绝不应该发生的情况,这就是为什么应该在程序Debug版本中使用,这是为了将主观上不应该发生的错误在程序Debug版本中就应该解决掉,从而在程序Release版本时不会产生这种不应该发生的类型的错误。断言的使用是有条件的。断言只能用于程序内部逻辑的条件判断,而不能用于对外部输入数据的判断, 因为在网上实际运行时,是完全有可能出现外部输入非法数据的情况。

  • 为单元测试和系统故障注入测试准备好方法和通道。

排版

  • 双目运算符的前后应当加空格。 单目运算符 !、~、++、--、-、*、& 等前后不加空格。

  • C语言中一行注释一般采用 // …,多行注释必须采用/* … */,注释符与注释内容间要有1空格,右置注释与前面代码至少1空格,在代码块的末尾(#endif 、大括号末尾)加上注释

  • 像 if、for、while 等关键字之后应留一个空格再跟左括号(,以突出关键字

  • 条件循环语句都需要使用大括号,即便只有一条语句。

  • “,”之后留空格, 双目运算符的前后应当加空格,不作结尾的;后面加空格,}后加空格

  • 有时候为了表达清晰如函数参数多的时候换行,函数左圆括号总是跟函数名,右圆括号总是跟最后一个参数。

  • 每行最好只有一个变量初始化的语句,更容易阅读和理解。

  • 编译预处理的"#"统一放在行首;即便编译预处理的代码是嵌入在函数体中的,"#"也应该放在行首。嵌套编译预处理语句时,"#"可以进行缩进。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值