开发规范

发布于:2025-07-31 ⋅ 阅读:(14) ⋅ 点赞:(0)

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. 线程安全

系统所有函数(特别是平台函数)需说明其被调用的安全等级,以免调用者误用,带来潜在安全风险,引起随机偶发的逻辑错误,导致系统整体不稳定,这样的错误很难定位和处理。函数的安全调用级别房内为线程安全、可重入和不可重入,平台提供的接口要尽量实现线程安全,方便应用任意调用,不能实现线程安全的要做特殊说明,并提供规避指南(程序用例),指导调用者正确使用;涉及信号中断处理的程序要考虑函数的可重入,以免引起内部逻辑混乱或死锁。

在多线程并行执行的程序中,多个线程调用同一个对象的类成员变量和函数时,类成员函数如果能通过内部同步机制实现对共享资源(成员变量、全局变量、静态变量、共享文件等)的正确访问、功能逻辑正确执行,类成员函数互不妨碍、互不影响,不出现数据污染等意外情况,这样的类成员函数就是线程安全的。

这样,函数调用方就不用考虑在多线程运行环境下调度和交替执行的影响,也不需要进行额外的同步,也不需在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,线程安全函数对于线程来说是原子操作。平台提供的公共基础接口均需实现线程安全,如:网络消息接口、数据库访问接口、安全控制访问接口等,以减少上层应用调用限制,简化应用调用过程;线程安全是由多线程对全局变量、静态变量等全局资源的访问冲突引起的,因此实现对象访问的线程安全,需考虑如下几点:

  • 无状态对象永远是线程安全;
  • 有多个线程同时执行对共享变量写操作,一般都需要考虑线程同步,否则可能影响线程安全。
  • 对共享变量读写分离,只有一个线程写,其它线程只有读操作,一般来说,这个共享变量是线程安全的,当然也要注意写操作的步骤,尽量保证在数据修改时读数据的正确性;

网站公告

今日签到

点亮在社区的每一天
去签到