1. 格式规范
1.1. 编码格式
以下场景使用不带BOM头的UTF-8编码格式:
- 所有内部文本文件,包括源代码、配置文件、画面文件、日志文件等。
- 数据库记录,包括内存库和关系库的所有字段。
- 操作系统,包括文件路径和终端输出。
- 业务系统有特殊需求的,在对外接口中实现局部编码转换。
- 换行符使用unix/linux格式的LF,禁止使用windows格式的CR+LF。
1.2. 代码版式
1.2.1. 空行
- 每个类声明和函数定义结束后,留两行空行。
- 在声明和实现中,逻辑关系紧密的语句中间不留空行,其余的加一行空行。
1.2.2. 缩进和对齐
- 缩进使用4空格,禁止使用tab。
- 左右花括号{}分别独立占一行,并且与引用语句行首列对齐。
1.2.3. 长行拆分
- 代码行最大长度宜控制在70至100个字符以内。代码行不要过长,否则眼睛看不过来,也不便于打印(随着GUI开发环境和高分宽屏的普及,此规则可以视情况适当放宽)。
- 长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。例如:
if ((very_longer_variable1 >= very_longer_variable2)
&& (very_longer_variable3 <= very_longer_variable4)
&& (very_longer_variable5 <= very_longer_variable6))
{
……
}
1.2.4. 语句与代码行
- 一行代码只做一件事情,尤其是变量定义,每行只允许定义一个变量。
- "if"、"for"、"while"、"do"、"try"、"catch" 等语句必须单独成行,执行语句另起一行,且必须使用左右花括号{}包含。
1.2.5. 空格的使用
- 关键字之后要留空格:象"const"、"virtual"、"inline"、"case" 等关键字之后至少要留一个空格,否则无法辨析关键字。象"if"、"for"、"while"、"catch" 等关键字之后应留一个空格再跟左括号"(",以突出关键字。
- 函数名之后不要留空格:紧跟左括号"(" ,以与关键字区别。"(" 向后紧跟。而")"、","、";" 向前紧跟,紧跟处不留空格。"," 之后要留空格,如Function(x, y, z)。如果";" 不是一行的结束符号,其后要留空格,如for (initialization; condition; update)。
- 二元操作符的前后应当加空格:赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如"="、"+=" ">="、"<="、"+"、"*"、"%"、"&&"、"||"、"<<", "^" 等二元操作符的前后应当加空格。
- 一元操作符的前后不加空格:一元操作符如"!"、"~"、"++"、"--"、"&"(地址运算符)等前后不加空格。象"[]"、"."、"->"这类操作符前后不加空格。
1.2.6. 修饰符的位置
- 为便于理解,应当将修饰符"*" 和"&" 紧靠数据类型。例如:
int* Function(void* p);
char* pName;
int* piX;
1.2.7. 与常量的比较
- 在与宏、常量进行"==", "!=", ">=", "<=" 等比较运算时,应当将常量写在运算符左边,而变量写在运算符右边。这样可以避免因为偶然写错把比较运算变成了赋值运算的问题。例如:
if (nullptr == p) // 如果把 "==" 错打成 "=",编译器就会报错
{
// ...
}
1.3. 命令规则
1.3.1. 通用规则
- 所有名称应使用有意义的英文词汇。
- 禁止使用非周知的缩写,尽量使用单词全拼。
- 能用英文单词描述,尽量减少使用拼音。
1.3.2. 文件名
- 文件名称由一个或多个单词构成,为实现Window(文件名不区分大小写)和Linux(文件名区分大小写)的通用,统一采用小写字母,以下划线分段。文件名称一定要能确切的表达文件的内容,禁止使用过于泛的名称,如用户管理对话框不应该命名为 User.h 而应该命名为 user_manage_dlg.h。
1.3.3. 命名空间名
- 一般使用本模块所在二级目录名,通常为单个小写单词。
- 命名空间不宜过多,一般一个二级目录用一个即可。
1.3.4. 类名/結构名
- 由多个有意义的单词组成,驼峰命名方式,每个单词首字母大写。
- 一般前面的单词为修饰性、限定性词汇,最后一个单词为名词。
- 推荐的组成形式:类的命名推荐用“前缀”+"名词"或"形容词+名词"的形式,例如:"GAnalyzer", "CFastVector" .... 前缀为了区分不同的工程或者模块。
1.3.5. 函数名
- 由多个有意义的单词组成,第一个单词首字母小写,其余单词首字母大写。
- 私有函数名称前加下划线,以做明显区分;
- 一般使用“动词”或者“动词+名词”的形式。示例如下:
getName();
getValue();
erase();
reserve();
- 获取和设置函数:返回值bool型函数使用isXxx形式,示例如下:
void setModified(bool bModified);
bool isModified();
- 针对QT的特别说明:
-
- Qt下slot函数统一采用slot作为前缀,示例如下:
slotMouseButtonClicked();
-
- Qt下signal函数统一采用signal作为前缀,示例如下:
signalMouseButtonClicked();
1.3.6. 变量名
- 变量的命名:变量名由“作用域前缀+类型前缀(可选)+一个或多个单词”组成。为便于界定,第一个单词首字母小写,其余单词首字母大写。变量名称含义明确,并有备注说明。对于某些用途简单明了的局部变量,也可以使用简化的方式,如:i,j,k,x,y,z....
- 作用域前缀:作用域前缀标明一个变量的可见范围。作用域可以有如下几种:
-
- m_ :类的成员变量(member)
- ms_ :类的静态成员变量(static member)
- s_ :静态变量(static)
- g_ :普通全局变量(global)
- gs_ :静态全局变量(static global)
- gg_ :进程或动态链接库中的全局变量(global global)
- 除非不得已,否则应该尽可能少使用全局变量。
- 一般使用“名词”或者“形容词+名词”的形式。
1.3.6. 常量名/枚举名/宏常量名
- 名称统一使用大写,标识符必须指示常量具体含义,单词间通过下划线来界定。
1.4. 头文件
1.4.1. 头文件名称
- C++的头文件名称必须和类名保持一致,后缀为.h。
- C++的头文件如果为纯模板头文件时,后缀名为.hpp。
- C的头文件名称一般和对应功能的子目录名称一致。
1.4.2. 头文件内容次序
- 头文件注释
- 预处理块
- 头文件引用
- 各类声明
1.4.3. 头文件注释
- 版权、作者声明。
- 功能摘要。
- 复杂数据结构和算法的设计说明。
1.4.4. 预处理块
- 尽量使用#pragma once方式。
- 少用#ifndef/#define/#endif方式,避免拷贝代码导致的错误。
1.4.5. 头文件引用
与前面目录规则配合,公开的头文件必须放在include/模块目录下,引用时应使用#include “xx/xxx.h”形式,禁止使用#include “xxx.h”形式。即INCLUDEPATH不应直接指向头文件所在目录,而应指向至少其上一层目录。
- 只引用必要的头文件,无效引用必须及时删除。
- 引用的头文件应严格遵循如下先特殊、后一般的次序:
- 自定义的内部模块。
- 第三方库模块。
- C++标准库。
- C标准库。
- 对自定义模块,原则上禁止定义仅包含头文件的头文件。
- 应遵循引用自洽,除了前向声明,对头文件中使用到的类型,应显式的对类型所在头文件进行引用,而不应依赖其它头文件的间接引用。
1.4.6. 各类声明
各类声明包括名字空间、类型的声明、常量的声明和函数的声明,其遵循如下原则:
- 最小暴露原则:头文件中暴露的信息越少越好,所在目录层次越浅的头文件中暴露的信息越少越好。可通过接口与实现分离的方式实现,参照impl类。
- 延迟暴露原则:在信息不确定是否需要暴露时,先将头文件放入较深层次的目录,直到有明确需求后,再移入较高层次的目录,提升信息的可见等级。
- 内容逻辑内聚:逻辑关系弱的避免放在一个头文件,不要仅仅因为包含方便就将一堆毫无关联的放在一起;内部逻辑关联紧密的尽量放在一起,比如函数内部需要用到枚举,那么函数声明最好与枚举定义放在一个头文件;在必要的时候,可以将枚举与函数声明分成两个头文件,减少链接依赖。
- 避免名字空间污染:不应直接使用using namespace xxx,避免名字空间污染。应尽量避免使用全局空间(不带范围)的声明,提倡使将声明放置在命名空间内。枚举和常量提倡放在关系紧密的类内部声明。
1.4.7. C 头文件
- C函数的引用必须在头文件中声明,调用方不得私自通过extern方式引入不在头文件中的函数。
1.4.8. C++头文件
- 一个C++头文件一般只和一个类对应,应尽量避免多个类共用一个头文件。
1.5. 实现文件
1.5.1. 实现文件名称
- C++的实现文件名称尽量和类名含义保持一致,后缀为.cpp。
- C的实现文件名称一般和对应功能的子目录名称一致,在文件过大时可按逻辑拆分,文件名加上子功能名称。
1.5.2. 实现文件内容次序
- 头文件引用。
- 各类局部声明,包括小类型、常量、静态函数、静态变量等,这些声明仅在本文件内有效。
- 函数体实现:按照头文件定义顺序进行实现。
1.5.3. 头文件引用
- 只引用必要的头文件,无效引用必须及时删除。
- 引用的头文件应严格遵循如下先特殊、后一般的次序:
-
- 自定义的内部模块。
- 第三方库模块。
- C++标准库。
- C标准库。
- 对自定义模块,原则上禁止定义仅包含头文件的头文件。
- 应遵循引用自洽,除了前向声明,对头文件中使用到的类型,应显式的对类型所在头文件进行引用,而不应依赖其它头文件的间接引用。
1.5.4. 注释
- 实现文件中复杂关键函数实现要求详细注释;
- 简单函数尽量少用注释,通过代码表述清楚,增加注释前,请优先考虑重构。
- 注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。
- 边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。失效注释要删除。
- 注释应当准确、易懂,防止注释有二义性。错误的注释不但无益反而有害。
- 当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。
1.6. 单元测试文件
单元测试文件与源代码文件同级管理,要求如下:
- 使用catch2为单元测试框架;
- 单元测试文件一般对应一个类或一个模块。
- 文件名以类名或模块名后加_test.cpp后缀。
单元测试必须覆盖所有公共接口,以及内部关键函数,并从逻辑上保障私有接口的代码覆盖率。
1.7. 源代码归档
- 任何编译过程中产生的中间文件不允许归档,如moc文件、qrc文件、obj文件等。
- 一般情况下,针对ide的工程文件不允许归档,如sln文件、.vscode目录等。
1.8. 生成物归档
2. 语法规范
2.1. 作用域限定
- 对一般类的成员变量和成员函数,加上基类作用域。
- 对模板类的成员变量和成员函数,必须使用this作用域或者基类作用域。
- 对于全局变量和全局函数,尽量使用::作用域。
2.2. 常量
- 代码中严禁使用非周知的字面量,如魔数或字符串,必须使用有名称含义的常量。
- 优先使用static const类型 形式定义常量,再次为宏。
2.3. 变量
- 正常情况禁止使用全局变量,如需使用,需要经过评审。
- 慎重的在类、文件和函数中使用静态变量。如非必要,避免使用静态变量。
- 类的成员变量默认应为私有变量。只有在明确有子类需要用的情况下,才能提升为保护变量。
- 尽量避免暴露类的成员变量,优先使用接口方式。
- 所有变量均必须初始化。类的成员变量通过{}方式在头文件中赋初始值。函数中的临时变量在定义时赋初始值。
- 动态分配内存的变量,尽量在初始化过程中预先分配好,或者在第一次使用时分配好。
2.4. 函数
- 公共函数的入参必须进行有效性检查。
- 函数的参数优先使用引用和指针,尽量减少值拷贝和拷贝构造。
- 在函数内部,入参不会被修改的,入参必须加const修饰。
- 类的成员函数,不修改成员变量的额,函数必须加const修饰。
- 函数的参数一般不超过5个。
- 尽量避免对操作符函数的重载。
2.5. 线程
- 应尽量减少线程的使用。
- 所有线程必须有明确的资源回收。
- 线程资源回收的次序必须清晰正确。
- 优先使用标准库的线程。
- 除了管理线程,其它现场尽量避免使用优先级调度。
- 对单进程中线程的数量,在方案设计时要预先考虑。
- 对单进程中可能会出现大量线程的情况,优先考虑使用协程代替。
2.6. 锁
- 应尽量减少锁的使用。
- 应尽量缩小锁的作用范围。
- 锁的申请和释放之间避免出现return、break、continue等跳转语句。
- 可通过defer或guard方式保障锁的最终释放。
- 尽量使用标准库的原子锁。
2.7. 编译
- 默认打开所有编译告警,少量不会产生错误后果的编译告警,在评审后才可关闭;
- 原则上发布版本的编译告警应全部消除,可采用高版本编译器进一步检查隐藏错误;
- 对所有未消除的编译告警,应作出分析,确保运行时不会产生问题。
2.8. 函数的返回值
对于可能出错的函数,我们需要规定统一使用 iasp::Long 作为函数的返回类型。
- 如果返回值大于或等于0,则表示函数运行无异常;
- 如果返回值小于0,则表示函数在执行过程中产生了异常。此时,返回的负值应能够准确反映出发生的具体错误类型。详细的错误类型参考:error_code.h
2.8. 错误处理
建立完善的错误处理机制,从异常发生的源头,将出错信息层层传递,递交最终使用者,实现异常快速定位,便于系统的不断完善;首先要确定错误是内部错误(函数内、线程内、进程内自己产生的)还是外部错误(外部参数、外部攻击、外部数据环境造成的),如果无法确定错误来自内部还是外部,那就是设计缺陷,需要修复;对内部错误和外部错误采取不同的处理策略:
- 内部错误:对于自身内部的错误越脆弱越好,这类错误往往是致命不可修复的,对内部错误采用断言和抛出异常的方式来处理;
- 外部错误:对外部的攻击和错误要足够强壮,对外部错误采用返回值来处理,如果对外部错误采用断言或异常处理就是BUG,需要修复;对每一个函数调用都要有返回值判断和处理,返回值<0、NULL、FALSE表示错误;
- 错误返回:通过函数返回值来描述函数执行成败(<0/false:表示失败的错误代码,>=0:表示执行成功的结果),尽量少用抛异常方式返回错误,以降低程序调用函数的复杂度;
- 除了平台,原则上不允许使用异常进行错误管理,错误状态优先通过返回的错误号体现;
- 原则上所有的错误返回值,都应该在日志中体现;
- 统一设计错误接口函数,统一规划错误编码策略。
2.9. 线程安全
系统所有函数(特别是平台函数)需说明其被调用的安全等级,以免调用者误用,带来潜在安全风险,引起随机偶发的逻辑错误,导致系统整体不稳定,这样的错误很难定位和处理。函数的安全调用级别房内为线程安全、可重入和不可重入,平台提供的接口要尽量实现线程安全,方便应用任意调用,不能实现线程安全的要做特殊说明,并提供规避指南(程序用例),指导调用者正确使用;涉及信号中断处理的程序要考虑函数的可重入,以免引起内部逻辑混乱或死锁。
在多线程并行执行的程序中,多个线程调用同一个对象的类成员变量和函数时,类成员函数如果能通过内部同步机制实现对共享资源(成员变量、全局变量、静态变量、共享文件等)的正确访问、功能逻辑正确执行,类成员函数互不妨碍、互不影响,不出现数据污染等意外情况,这样的类成员函数就是线程安全的。
这样,函数调用方就不用考虑在多线程运行环境下调度和交替执行的影响,也不需要进行额外的同步,也不需在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,线程安全函数对于线程来说是原子操作。平台提供的公共基础接口均需实现线程安全,如:网络消息接口、数据库访问接口、安全控制访问接口等,以减少上层应用调用限制,简化应用调用过程;线程安全是由多线程对全局变量、静态变量等全局资源的访问冲突引起的,因此实现对象访问的线程安全,需考虑如下几点:
- 无状态对象永远是线程安全;
- 有多个线程同时执行对共享变量写操作,一般都需要考虑线程同步,否则可能影响线程安全。
- 对共享变量读写分离,只有一个线程写,其它线程只有读操作,一般来说,这个共享变量是线程安全的,当然也要注意写操作的步骤,尽量保证在数据修改时读数据的正确性;