diff --git a/zh-cn/contribute/OpenHarmony-c-coding-style-guide.md b/zh-cn/contribute/OpenHarmony-c-coding-style-guide.md index 73012d71dde11cb7a11f129a7aa5c297ebd8f8a6..bd1e50054a320d1079afb57f38ffc7154153649f 100755 --- a/zh-cn/contribute/OpenHarmony-c-coding-style-guide.md +++ b/zh-cn/contribute/OpenHarmony-c-coding-style-guide.md @@ -42,6 +42,9 @@ 大小写字母混用,单词连在一起,不同单词间通过单词首字母大写来分开。 按连接后的首字母是否大写,又分: **大驼峰(UpperCamelCase)**和**小驼峰(lowerCamelCase)** +**内核风格(unix_like)** +内核风格又称蛇形风格。单词小写,用下划线分割。 +如:'test_result' ### 规则1.1 标识符命名使用驼峰风格 @@ -53,7 +56,10 @@ 注意: 上表中`常量`是指,全局作用域下,const 修饰的基本数据类型、枚举、字符串类型的变量,不包括数组、结构体和联合体。 -上表中`变量`是指除常量定义以外的其他变量,均使用小驼峰风格。 +上表中`变量`是指除常量定义以外的其他变量,均使用小驼峰风格。 +对于更亲和Linux/Unix的代码,可以使用内核风格。 +已使用内核命名风格的代码,可以选择继续使用内核风格。 +不管什么样的命名风格,都应该保证同一函数或结构体、联合体内的命名风格是一致的。 ### 建议1.1 作用域越大,命名应越精确 @@ -231,8 +237,12 @@ typedef struct tagNode { // Good: 使用 tag 前缀。这里也可以使用 ' 常量推荐采用全大写,下划线连接风格。作为全局变量,也可以保持与普通全局变量命名风格相同。 这里常量如前文定义,是指基本数据类型、枚举、字符串类型的全局 const 变量。 -函数式宏,如果功能上可以替代函数,也可以与函数的命名方式相同,使用大驼峰命名风格。 -这种做法会让宏与函数看起来一样,容易混淆,需要特别注意。 +函数式宏,命名风格,采用全大写,下划线连接风格。 +例外情况: +1、用宏实现泛型功能的函数。如:实现list,map等功能的宏。可以与函数的命名方式相同,使用大驼峰命名风格。 +2、函数接口发生变更为兼容老版本时,使用函数同宏进行替代。可以与函数的命名方式相同,使用大驼峰命名风格。 +3、日志打印宏。可以与函数的命名方式相同,使用大驼峰命名风格。 +注:使用大驼峰命名的函数式宏,需要在接口说明中标注为宏。 宏举例: ```c @@ -287,16 +297,16 @@ enum BaseColor { ### 建议1.5 避免函数式宏中的临时变量命名污染外部作用域 -首先,**尽量少的使用函数式宏。** +首先,**定义函数式宏前,应考虑能否定义为函数。如果可以则不要定义为函数式宏。** 当函数式宏需要定义局部变量时,为了防止跟外部函数中的局部变量有命名冲突。 -后置下划线,是一种解决方案。 例: +后置下双划线,是一种解决方案。 例: ```c #define SWAP_INT(a, b) do { \ - int tmp_ = a; \ + int tmp__ = a; \ a = b; \ - b = tmp_; \ + b = tmp__; \ } while (0) ``` @@ -304,11 +314,11 @@ enum BaseColor { ## 行宽 -### 建议2.1 行宽不超过 120 个字符 +### 规则2.1 行宽不超过 120 个字符 代码行宽不宜过长,否则不利于阅读。 控制行宽长度可以间接的引导开发去缩短函数、变量的命名,减少嵌套的层数,提升代码可读性。 -强烈建议和要求每行字符数不要超过 **120** 个;除非超过 **120** 能显著增加可读性,并且不会隐藏信息。 +要求每行字符数不要超过 **120** 个;除非超过 **120** 能显著增加可读性,并且不会隐藏信息。 虽然现代显示器分辨率已经很高,但是行宽过长,反而提高了阅读理解的难度;跟本规范提倡的“清晰”、“简洁”原则相背。 如下场景不宜换行,可以例外: @@ -323,14 +333,14 @@ enum BaseColor { ``` ## 缩进 -### 规则2.1 使用空格进行缩进,每次缩进4个空格 +### 规则2.2 使用空格进行缩进,每次缩进4个空格 只允许使用空格(space)进行缩进,每次缩进为 **4** 个空格。不允许使用Tab键进行缩进。 当前几乎所有的集成开发环境(IDE)和代码编辑器都支持配置将Tab键自动扩展为**4**空格输入,请配置你的代码编辑器支持使用空格进行缩进。 ## 大括号 -### 规则2.2 使用 K&R 缩进风格 +### 规则2.3 使用 K&R 缩进风格 **K&R风格** 换行时,函数左大括号另起一行放行首,并独占一行;其他左大括号跟随语句放行末。 @@ -354,7 +364,7 @@ int Foo(int a) ## 函数声明和定义 -### 规则2.3 函数声明、定义的返回类型和函数名在同一行;函数参数列表换行时应合理对齐 +### 规则2.4 函数声明、定义的返回类型和函数名在同一行;函数参数列表换行时应合理对齐 在声明和定义函数的时候,函数的返回值类型应该和函数名在同一行。 @@ -390,7 +400,7 @@ ReturnType ReallyReallyReallyReallyLongFunctionName( // 行宽不满 ## 函数调用 -### 规则2.4 函数调用参数列表换行时保持参数进行合理对齐 +### 规则2.5 函数调用参数列表换行时保持参数进行合理对齐 函数调用时,函数参数列表如果换行,应该进行合理的参数对齐。 左圆括号总是跟函数名,右圆括号总是跟最后一个参数。 @@ -419,7 +429,7 @@ int result = DealWithStructureLikeParams(left.x, left.y, // 表示一组相 ## 条件语句 -### 规则2.5 条件语句必须要使用大括号 +### 规则2.6 条件语句必须要使用大括号 我们要求条件语句都需要使用大括号,即便只有一条语句。 理由: @@ -433,7 +443,7 @@ if (objectIsNotExist) { // Good:单行条件语句也加大括号 } ``` -### 规则2.6 禁止 if/else/else if 写在同一行 +### 规则2.7 禁止 if/else/else if 写在同一行 条件语句中,若有多个分支,应该写在不同行。 @@ -453,7 +463,7 @@ if (someConditions) { ... } else { ... } // Bad: else 与 if 在同一行 ## 循环 -### 规则2.7 循环语句必须使用大括号 +### 规则2.8 循环语句必须使用大括号 和条件表达式类似,我们要求for/while循环语句必须加上大括号,即便循环体是空的,或循环语句只有一条。 ```c @@ -481,7 +491,7 @@ while (condition); // Bad:使用分号容易让人误解是while语句中 ## switch语句 -### 规则2.8 switch 语句的 case/default 要缩进一层 +### 规则2.9 switch 语句的 case/default 要缩进一层 switch 语句的缩进风格如下: ```c @@ -510,7 +520,7 @@ default: // Bad: default 未缩进 ## 表达式 -### 建议2.2 表达式换行要保持换行的一致性,操作符放行末 +### 建议2.1 表达式换行要保持换行的一致性,操作符放行末 较长的表达式,不满足行宽要求的时候,需要在适当的地方换行。一般在较低优先级操作符或连接符后面截断,操作符或连接符放在行末。 操作符、连接符放在行末,表示“未结束,后续还有”。 @@ -539,7 +549,7 @@ int sum = longVaribleName1 + longVaribleName2 + longVaribleName3 + ## 变量赋值 -### 规则2.9 多个变量定义和赋值语句不允许写在一行 +### 规则2.10 多个变量定义和赋值语句不允许写在一行 每行最好只有一个变量初始化的语句,更容易阅读和理解。 @@ -574,7 +584,7 @@ for (i = 0; i < row; i++) { 初始化包括结构体、联合体及数组的初始化 -### 规则2.10 初始化换行时要有缩进,或进行合理对齐 +### 规则2.11 初始化换行时要有缩进,或进行合理对齐 结构体或数组初始化时,如果换行应保持4空格缩进。 从可读性角度出发,选择换行点和对齐位置。 @@ -616,7 +626,7 @@ int c[][8] = { - 左大括号放行末时,对应的右大括号需另起一行 - 左大括号被内容跟随时,对应的右大括号也应跟随内容 -### 规则2.11 结构体和联合体在按成员初始化时,每个成员初始化单独一行 +### 规则2.12 结构体和联合体在按成员初始化时,每个成员初始化单独一行 C99标准支持结构体和联合体按照成员进行初始化,标准中叫"指定初始化"(designated initializer)。 如果按照这种方式进行初始化,每个成员的初始化单独一行。 ```c @@ -635,7 +645,7 @@ struct Date date = { // Good:使用指定初始化方式时,每行初始 ## 指针 -### 建议2.3 指针类型"\*"跟随变量名或者类型,不要两边都留有空格或都没有空格 +### 建议2.2 指针类型"\*"跟随变量名或者类型,不要两边都留有空格或都没有空格 声明或定义指针变量或者返回指针类型函数时,"\*" 靠左靠右都可以,但是不要两边都有或者都没有空格。 ```c @@ -663,13 +673,14 @@ int Foo(const char * restrict p); // OK. ## 编译预处理 -### 规则2.12 编译预处理的"#"默认放在行首,嵌套编译预处理语句时,"#"可以进行缩进 +### 规则2.13 编译预处理的"#"默认放在行首,嵌套编译预处理语句时,"#"可以进行缩进 -编译预处理的"#"统一放在行首;即便编译预处理的代码是嵌入在函数体中的,"#"也应该放在行首。 +编译预处理的"#"统一放在行首;即便编译预处理的代码是嵌入在函数体中的,"#"也应该放在行首。 +注意,开发过程尽量不要使用编译预处理宏。如果需使用,则应由专人进行统一管理。 ## 空格和空行 -### 规则2.13 水平空格应该突出关键字和重要信息,避免不必要的留白 +### 规则2.14 水平空格应该突出关键字和重要信息,避免不必要的留白 水平空格应该突出关键字和重要信息,每行代码尾部不要加空格。 总体规则如下: - if, switch, case, do, while, for 等关键字之后加空格; @@ -758,7 +769,7 @@ switch (var) { // Good: switch 关键字后面有1空格 注意:当前的集成开发环境(IDE)和代码编辑器都可以设置删除行尾的空格,请正确配置你的编辑器。 -### 建议2.4 合理安排空行,保持代码紧凑 +### 建议2.3 合理安排空行,保持代码紧凑 减少不必要的空行,可以显示更多的代码,方便代码阅读。下面有一些建议遵守的规则: - 根据上下内容的相关程度,合理安排空行; @@ -805,9 +816,15 @@ int Foo(void) **注释跟代码一样重要。** 写注释时要换位思考,用注释去表达此时读者真正需要的信息。在代码的功能、意图层次上进行注释,即注释解释代码难以表达的意图,不要重复代码信息。 -修改代码时,也要保证其相关注释的一致性。只改代码,不改注释是一种不文明行为,破坏了代码与注释的一致性,让阅读者迷惑、费解,甚至误解。 +修改代码时,也要保证其相关注释的一致性。只改代码,不改注释是一种不文明行为,破坏了代码与注释的一致性,让阅读者迷惑、费解,甚至误解。 -使用英文进行注释。 +使用**英文**进行注释。 + +必须要加注释说明场合如下(包含但不限于列举的场合): +1、模块对外提供的接口头文件必须对函数进行注释。 +2、定义全局变量必须加注释。 +3、核心算法必须加注释。 +4、超过50行的函数必须加注释。 ## 注释风格 @@ -995,7 +1012,7 @@ switch (var) { ## 头文件职责 头文件是模块或文件的对外接口。 -头文件中适合放置接口的声明,不适合放置实现(内联函数除外)。 +头文件中适合放置接口的声明,不允许放置实现(内联函数除外)。 头文件应当职责单一。头文件过于复杂,依赖过于复杂还是导致编译时间过长的主要原因。 ### 建议4.1 每一个.c文件都应该有相应的.h文件,用于声明需要对外公开的接口 @@ -1071,17 +1088,17 @@ static void Bar(void) 为防止头文件被多重包含,所有头文件都应当使用 #define 作为包含保护;不要使用 #pragma once 定义包含保护符时,应该遵守如下规则: -- 保护符使用唯一名称;建议考虑项目源代码树顶层以下的文件路径 +- 保护符使用唯一名称;统一使用子系统名_部件名_文件名的定义规则。 - 不要在受保护部分的前后放置代码或者注释,文件头注释除外。 -假定 timer 模块的 timer.h,其目录为 `timer/include/timer.h`。其保护符若使用 'TIME_H' 很容易不唯一,所以使用项目源代码树的全路径,如: +假定 util 子系统 timer 部件的 timer.h,其目录为 `timer/include/timer.h`。其保护符若使用 'TIME_H' 很容易不唯一,按规则定义如: ```c -#ifndef TIMER_INCLUDE_TIMER_H -#define TIMER_INCLUDE_TIMER_H +#ifndef UTIL_TIMER_TIMER_H +#define UTIL_TIMER_TIMER_H ... -#endif +#endif // UTIL_TIMER_TIMER_H ``` ### 规则4.3 禁止通过声明的方式引用外部函数接口、变量 @@ -1278,9 +1295,20 @@ DealWithFileHead(fileHead, sizeof(fileHead)); // 处理文件头 使用返回值而不是输出参数,可以提高可读性,并且通常提供相同或更好的性能。 -函数名为 GetXxx、FindXxx 或直接名词作函数名的函数,直接返回对应对象,可读性更好。 +函数名为 GetXxx、FindXxx、IsXxx、OnXxx或直接名词作函数名的函数,直接返回对应对象,可读性更好。 +例外: +1、多值返回时,可以设计为输出参数返回。 +2、涉及内存分配时,可以设计为输出参数返回。调用者将申请的内存做为出参传入,而函数内由不再分配内存。 + +### 建议5.3 设计函数的参数时,统一按输入、输出、出入的顺序定义参数。 -### 建议5.3 使用强类型参数,避免使用void\* +函数参数的定义统一按输入参数、输出参数、出入参的顺序进行定义。 + +### 规则5.3 设计函数的资源时,涉及内存、锁、队列等资源分配的,需要同时提供释放函数。 + +本着资源从那儿申请,向那儿释放的原则。如果函数申请了内存、锁、队列等资源,则模块需要同时提供资源的函数。 + +### 建议5.4 使用强类型参数,避免使用void\* 尽管不同的语言对待强类型和弱类型有自己的观点,但是一般认为c/c++是强类型语言,既然我们使用的语言是强类型的,就应该保持这样的风格。 好处是尽量让编译器在编译阶段就检查出类型不匹配的问题。 @@ -1325,8 +1353,7 @@ void FooListAddNode(FooNode *foo) 例外:某些通用泛型接口,需要传入不同类型指针的,可以用 `void *` 入参。 - -### 建议5.4 模块内部函数参数的合法性检查,由调用者负责 +### 建议5.5 模块内部函数参数的合法性检查,由调用者负责 对于模块外部传入的参数,必须进行合法性检查,保护程序免遭非法输入数据的破坏。 模块内部函数调用,缺省由调用者负责保证参数的合法性,如果都由被调用者来检查参数合法性,可能会出现同一个参数,被检查多次,产生冗余代码,很不简洁。 diff --git a/zh-cn/contribute/OpenHarmony-cpp-coding-style-guide.md b/zh-cn/contribute/OpenHarmony-cpp-coding-style-guide.md index 75f746a9e51fe9951674f650fd11dea842797ab5..7f0e600c0856ffe0606819d8dafba9b67b5bdfbf 100755 --- a/zh-cn/contribute/OpenHarmony-cpp-coding-style-guide.md +++ b/zh-cn/contribute/OpenHarmony-cpp-coding-style-guide.md @@ -15,7 +15,7 @@ 2. C++语言的模块化设计,如何设计头文件,类,接口和函数。 3. C++语言相关特性的优秀实践,比如常量,类型转换,资源管理,模板等。 4. 现代C++语言的优秀实践,包括C++11/14/17中可以提高代码可维护性,提高代码可靠性的相关约定。 - +5. 本规范优先适于用C++17版本。 ## 约定 **规则**:编程时必须遵守的约定(must) @@ -53,7 +53,7 @@ __驼峰风格(CamelCase)__ 上表中__变量__是指除常量定义以外的其他变量,均使用小驼峰风格。 ## 文件命名 -### 建议2.2.1 C++文件以.cpp结尾,头文件以.h结尾 +### 规则2.2.1 C++文件以.cpp结尾,头文件以.h结尾 我们推荐使用.h作为头文件的后缀,这样头文件可以直接兼容C和C++。 我们推荐使用.cpp作为实现文件的后缀,这样可以直接区分C++代码,而不是C代码。 @@ -66,7 +66,7 @@ __驼峰风格(CamelCase)__ 但是对于本文档,我们默认使用.h和.cpp作为后缀。 -### 建议2.2.2 C++文件名和类名保持一致 +### 规则2.2.2 C++文件名和类名保持一致 C++的头文件和cpp文件名和类名保持一致,使用下划线小写风格。 如果有一个类叫DatabaseConnection,那么对应的文件名: @@ -199,7 +199,7 @@ namespace Utils { ## 行宽 -### 建议3.1.1 行宽不超过 120 个字符 +### 规则3.1.1 行宽不超过 120 个字符 建议每行字符数不要超过 120 个。如果超过120个字符,请选择合理的方式进行换行。 例外: @@ -506,6 +506,41 @@ int&p = i; // Bad ### 规则3.13.1 编译预处理的"#"统一放在行首,嵌套编译预处理语句时,"#"可以进行缩进 编译预处理的"#"统一放在行首,即使编译预处理的代码是嵌入在函数体中的,"#"也应该放在行首。 +### 规则3.13.2 避免使用宏 +宏会忽略作用域,类型系统以及各种规则,容易引发问题。应尽量避免使用宏定义,如果必须使用宏,要保证证宏名的唯一性。 +在C++中,有许多方式来避免使用宏: +- 用const或enum定义易于理解的常量 +- 用namespace避免名字冲突 +- 用inline函数避免函数调用的开销 +- 用template函数来处理多种类型 +在文件头保护宏、条件编译、日志记录等必要场景中可以使用宏。 + +### 规则3.13.3 禁止使用宏来表示常量 +宏是简单的文本替换,在预处理阶段完成,运行报错时直接报相应的值;跟踪调试时也是显示值,而不是宏名; 宏没有类型检查,不宏全; 宏没有作用域。 + +### 规则3.13.4 禁止使用函数式宏 +宏义函数式宏前,应考虑能否用函数替代。对于可替代场景,建议用函数替代宏。 +函数式宏的缺点如下: +- 函数式宏缺乏类型检查,不如函数调用检查严格 +- 宏展开时宏参数不求值,可能会产生非预期结果 +- 宏没有独产的作用域 +- 宏的技巧性太强,例如#的用法和无处不在的括号,影响可读性 +- 在特定场景中必须用编译器对宏的扩展语法,如GCC的statement expression,影响可移植性 +- 宏在预编译阶段展开后,在期后编译、链接和调试时都不可见;而且包含多行的宏会展开为一行。函数式宏难以调试、难以打断点,不利于定位问题 +- 对于包含大量语句的宏,在每个调用点都要展开。如果调用点很多,会造成代码空间的膨胀 + +函数没有宏的上述缺点。但是,函数相比宏,最大的劣势是执行效率不高(增加函数调用的开销和编译器优化的难度)。 +为此,可以在必要时使用内联函数。内联函数跟宏类似,也是在调用点展开。不同之处在于内联函数是在编译时展开。 + +内联函数兼具函数和宏的优点: +- 内联函数执行严格的类型检查 +- 内联函数的参数求值只会进行一次 +- 内联函数就地展开,没有函数调用的开销 +- 内联函数比函数优化得更好 +对于性能要求高的产品代码,可以考虑用内联函数代替函数。 + +例外: +在日志记录场景中,需要通过函数式宏保持调用点的文件名(__FILE__)、行号(__LINE__)等信息。 ## 空格和空行 ### 规则3.14.1 水平空格应该突出关键字和重要信息,避免不必要的留白 @@ -552,10 +587,10 @@ x = r->y; // Good:通过->访问成员变量时不加空格 操作符: ```cpp -x = 0; // Good:赋值操作的=前后都要加空格 -x = -5; // Good:负数的符号和数值之前不要加空格 -++x; // Good:前置和后置的++/--和变量之间不要加空格 -x--; +x = 0; // Good:赋值操作的=前后都要加空格 +x = -5; // Good:负数的符号和数值之前不要加空格 +++x; // Good:前置和后置的++/--和变量之间不要加空格 +x--; if (x && !y) // Good:布尔操作符前后要加上空格,!操作和变量之间不要空格 v = w * x + y / z; // Good:二元操作符前后要加空格 @@ -780,7 +815,11 @@ MyClass::MyClass(int var) */ ## 函数头注释 -### 规则4.3.1 禁止空有格式的函数头注释 +### 规则4.3.1 公有(public)函数必须编写函数头注释 +公有函数属于类对外提供的接口,调用者需要了解函数的功能、参数的取值范围、返回的结果、注意事项等信息才能正常使用。 +特别是参数的取值范围、返回的结果、注意事项等都无法做到自注示,需要编写函数头注释辅助说明。 + +### 规则4.3.2 禁止空有格式的函数头注释 并不是所有的函数都需要函数头注释; 函数签名无法表达的信息,加函数头注释辅助说明; @@ -1375,7 +1414,30 @@ private: Foo& operator=(const Foo&); }; ``` -2. 使用C++11提供的delete, 请参见后面现代C++的相关章节。 +2. 使用C++11提供的delete, 请参见后面现代C++的相关章节。 + + +3. 推荐继承NoCopyable、NoMovable,禁止使用DISALLOW_COPY_AND_MOVE,DISALLOW_COPY,DISALLOW_MOVE等宏。 +```cpp +class Foo : public NoCopyable, public NoMovable { +}; +``` +NoCopyable和NoMovable的实现: +```cpp +class NoCopyable { +public: + NoCopyable() = default; + NoCopyable(const NoCopyable&) = delete; + NoCopyable& operator = (NoCopyable&) = delete; +}; + +class NoMovable { +public: + NoMovable() = default; + NoMovable(NoMovable&&) noexcept = delete; + NoMovable& operator = (NoMovable&&) noexcept = delete; +}; +``` ### 规则7.1.4 拷贝构造和拷贝赋值操作符应该是成对出现或者禁止 拷贝构造函数和拷贝赋值操作符都是具有拷贝语义的,应该同时出现或者禁止。 @@ -1462,10 +1524,40 @@ public: 会先执行Sub的构造函数,但首先调用Base的构造函数,由于Base的构造函数调用虚函数Log,此时Log还是基类的版本,只有基类构造完成后,才会完成派生类的构造,从而导致未实现多态的行为。 同样的道理也适用于析构函数。 +### 规则7.1.7 多态基类中的拷贝构造函数、拷贝赋值操作符、移动构造函数、移动赋值操作符必须为非public函数或者为delete函数 +如果报一个派生类对象直接赋值给基类对象,会发生切片,只拷贝或者移动了基类部分,损害了多态行为。 +【反例】 +如下代码中,基类没有定义拷贝构造函数或拷贝赋值操作符,编译器会自动生成这两个特殊成员函数, +如果派生类对象赋值给基类对象时就发生切片。可以将此例中的拷贝构造函数和拷贝赋值操作符声明为delete,编译器可检查出此类赋值行为。 +```cpp +class Base { +public: + Base() = default; + virtual ~Base() = default; + ... + virtual void Fun() { std::cout << "Base" << std::endl;} +}; + +class Derived : public Base { + ... + void Fun() override { std::cout << "Derived" << std::endl; } +}; + +void Foo(const Base &base) +{ + Base other = base; // 不符合:发生切片 + other.Fun(); // 调用的时Base类的Fun函数 +} +``` +```cpp +Derived d; +Foo(d); // 传入的是派生类对象 +``` +1. 将拷贝构造函数或者赋值操作符设置为private,并且不实现: ## 继承 -### 规则7.2.1 基类的析构函数应该声明为virtual +### 规则7.2.1 基类的析构函数应该声明为virtual,不准备被继承的类需要声明为final 说明:只有基类析构函数是virtual,通过多态调用的时候才能保证派生类的析构函数被调用。 示例:基类的析构函数没有声明为virtual导致了内存泄漏。 @@ -1524,7 +1616,8 @@ int main(int argc, char* args[]) } ``` 由于基类Base的析构函数没有声明为virtual,当对象被销毁时,只会调用基类的析构函数,不会调用派生类Sub的析构函数,导致内存泄漏。 - +例外: +NoCopyable、NoMovable这种没有任何行为,仅仅用来做标识符的类,可以不定义虚析构也不定义final。 ### 规则7.2.2 禁止虚函数使用缺省参数值 说明:在C++中,虚函数是动态绑定的,但函数的缺省参数却是在编译时就静态绑定的。这意味着你最终执行的函数是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。为了避免虚函数重载时,因参数声明不一致给使用者带来的困惑和由此导致的问题,规定所有虚函数均不允许声明缺省参数值。 @@ -2277,19 +2370,24 @@ a2.push_back(Foo2()); // 触发容器扩张,搬移已有元素时调用move c **注意** 默认构造函数、析构函数、`swap`函数,`move操作符`都不应该抛出异常。 -## 模板 +## 模板与泛型编程 -模板能够实现非常灵活简洁的类型安全的接口,实现类型不同但是行为相同的代码复用。 +### 规则9.8.1 禁止在OpenHarmony项目中进行泛型编程 +泛型编程和面向对象编程的思想、理念以及技巧完全不同,OpenHarmony项目主流使用面向对象的思想。 -模板编程的缺点: +C++提供了强大的泛型编程的机制,能够实现非常灵活简洁的类型安全的接口,实现类型不同但是行为相同的代码复用。 -1. 模板编程所使用的技巧对于使用c++不是很熟练的人是比较晦涩难懂的。在复杂的地方使用模板的代码让人更不容易读懂,并且debug 和维护起来都很麻烦。 -2. 模板编程经常会导致编译出错的信息非常不友好: 在代码出错的时候, 即使这个接口非常的简单, 模板内部复杂的实现细节也会在出错信息显示. 导致这个编译出错信息看起来非常难以理解。 -3. 模板如果使用不当,会导致运行时代码过度膨胀。 -4. 模板代码难以修改和重构。模板的代码会在很多上下文里面扩展开来, 所以很难确认重构对所有的这些展开的代码有用。 +但是C++泛型编程存在以下缺点: -所以, 建议__模板编程最好只用在少量的基础组件,基础数据结构上面__。并且使用模板编程的时候尽可能把__复杂度最小化__,尽量__不要让模板对外暴露__。最好只在实现里面使用模板, 然后给用户暴露的接口里面并不使用模板, 这样能提高你的接口的可读性。 并且你应该在这些使用模板的代码上写尽可能详细的注释。 +1. 对泛型编程不很熟练的人,常常会将面向对象的逻辑写成模板、将不依赖模板参数的成员写在模板中等等导致逻辑混乱代码膨胀诸多问题。 +2. 模板编程所使用的技巧对于使用c++不是很熟练的人是比较晦涩难懂的。在复杂的地方使用模板的代码让人更不容易读懂,并且debug 和维护起来都很麻烦。 +3. 模板编程经常会导致编译出错的信息非常不友好: 在代码出错的时候, 即使这个接口非常的简单, 模板内部复杂的实现细节也会在出错信息显示. 导致这个编译出错信息看起来非常难以理解。 +4. 模板如果使用不当,会导致运行时代码过度膨胀。 +5. 模板代码难以修改和重构。模板的代码会在很多上下文里面扩展开来, 所以很难确认重构对所有的这些展开的代码有用。 +所以,OpenHarmony大部分部件禁止模板编程,仅有 __少数部件__ 可以使用泛型编程,并且开发的模板要有详细的注释。 +例外: +1. stl适配层可以使用模板 ## 宏 在C++语言中,我们强烈建议尽可能少使用复杂的宏 @@ -2553,34 +2651,55 @@ void func() ``` ## 智能指针 -### 规则10.2.1 优先使用智能指针而不是原始指针管理资源 +### 规则10.2.1 单例、类的成员等所有机不会被多方持有的优先使用原始指针源而不是智能指针 **理由** -避免资源泄露。 +智能指针会自动释放对象资源避免资源泄露,但会带额外的资源开销。如:智能指针自动生成的类、构造和析构的开销、内存占用多等。 + +单例、类的成员等对象的所有权不会被多方持有的情况,仅在类析构中释放资源即可。不应该使用智能指针而增额外的开销。 **示例** ```cpp -void Use(int i) -{ - auto p = new int {7}; // 不好: 通过 new 初始化局部指针 - auto q = std::make_unique(9); // 好: 保证释放内存 - if (i > 0) { - return; // 可能 return,导致内存泄露 +class Foo; +class Base { +public: + Base() {} + virtual ~Base() + { + delete foo_; } - delete p; // 太晚了 -} +private: + Foo* foo_ = nullptr; +}; ``` **例外** -在性能敏感、兼容性等场景可以使用原始指针。 +1. 返回创建的对象时,需要指针销毁函数的可以使用智能指针。 +```cpp +class User; +class Foo { +public: + std::unique_ptr CreateUniqueUser() // 可使用unique_ptr保证对象的创建和释放在同一runtime + { + sptr ipcUser = iface_cast(remoter); + return std::unique_ptr(::new User(ipcUser), [](User *user) { + user->Close(); + ::delete user; + }); + } -### 规则10.2.2 优先使用`unique_ptr`而不是`shared_ptr` -**理由** -1. `shared_ptr`引用计数的原子操作存在可测量的开销,大量使用`shared_ptr`影响性能。 -2. 共享所有权在某些情况(如循环依赖)可能导致对象永远得不到释放。 -3. 相比于谨慎设计所有权,共享所有权是一种诱人的替代方案,但它可能使系统变得混乱。 + std::shared_ptr CreateSharedUser() // 可使用shared_ptr保证对象的创建和释放在同一runtime中 + { + sptr ipcUser = iface_cast(remoter); + return std::shared_ptr(ipcUser.GetRefPtr(), [ipcUser](User *user) mutable { + ipcUser = nullptr; + }); + } +}; +``` +2. 返回创建的对象且对象需要被多方引用时,可以使用shared_ptr。 -### 规则10.2.3 使用`std::make_unique`而不是`new`创建`unique_ptr` +### 规则10.2.2 使用`std::make_unique`而不是`new`创建`unique_ptr` **理由** 1. `make_unique`提供了更简洁的创建方式 2. 保证了复杂表达式的异常安全