From b6b5388b3b21e4dc13876b54cdfc468185fb77dc Mon Sep 17 00:00:00 2001 From: Sven Wang Date: Fri, 26 Nov 2021 17:50:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E6=A3=80=E8=A7=86=E6=84=8F=E8=A7=81?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=BC=96=E7=A8=8B=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sven Wang --- .../OpenHarmony-c-coding-style-guide.md | 53 +++-- .../OpenHarmony-cpp-coding-style-guide.md | 189 ++++++++++++++---- 2 files changed, 194 insertions(+), 48 deletions(-) diff --git a/zh-cn/contribute/OpenHarmony-c-coding-style-guide.md b/zh-cn/contribute/OpenHarmony-c-coding-style-guide.md index 73012d71dd..0a13f276a4 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) ``` @@ -665,7 +675,8 @@ int Foo(const char * restrict p); // OK. ### 规则2.12 编译预处理的"#"默认放在行首,嵌套编译预处理语句时,"#"可以进行缩进 -编译预处理的"#"统一放在行首;即便编译预处理的代码是嵌入在函数体中的,"#"也应该放在行首。 +编译预处理的"#"统一放在行首;即便编译预处理的代码是嵌入在函数体中的,"#"也应该放在行首。 +注意,开发过程尽量不要使用编译预处理宏。如果需使用,则应由专人进行统一管理。 ## 空格和空行 @@ -805,10 +816,16 @@ int Foo(void) **注释跟代码一样重要。** 写注释时要换位思考,用注释去表达此时读者真正需要的信息。在代码的功能、意图层次上进行注释,即注释解释代码难以表达的意图,不要重复代码信息。 -修改代码时,也要保证其相关注释的一致性。只改代码,不改注释是一种不文明行为,破坏了代码与注释的一致性,让阅读者迷惑、费解,甚至误解。 +修改代码时,也要保证其相关注释的一致性。只改代码,不改注释是一种不文明行为,破坏了代码与注释的一致性,让阅读者迷惑、费解,甚至误解。 使用英文进行注释。 +必须要加注释说明场合如下(包含但不限于列举的场合): +1、模块对外提供的接口头文件必须对函数进行注释。 +2、定义全局变量必须加注释。 +3、核心算法必须加注释。 +4、超过50行的函数必须加注释。 + ## 注释风格 在 C 代码中,使用 `/*` `*/`和 `//` 都是可以的。 @@ -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 75f746a9e5..fc5fafe2cc 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) @@ -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 禁止在鸿蒙项目中进行泛型编程 +泛型编程和面向对象编程的思想、理念以及技巧完全不同,鸿蒙项目主流使用面向对象的思想。 -模板编程的缺点: +C++提供了强大的泛型编程的机制,能够实现非常灵活简洁的类型安全的接口,实现类型不同但是行为相同的代码复用。 -1. 模板编程所使用的技巧对于使用c++不是很熟练的人是比较晦涩难懂的。在复杂的地方使用模板的代码让人更不容易读懂,并且debug 和维护起来都很麻烦。 -2. 模板编程经常会导致编译出错的信息非常不友好: 在代码出错的时候, 即使这个接口非常的简单, 模板内部复杂的实现细节也会在出错信息显示. 导致这个编译出错信息看起来非常难以理解。 -3. 模板如果使用不当,会导致运行时代码过度膨胀。 -4. 模板代码难以修改和重构。模板的代码会在很多上下文里面扩展开来, 所以很难确认重构对所有的这些展开的代码有用。 +但是C++泛型编程存在以下缺点: -所以, 建议__模板编程最好只用在少量的基础组件,基础数据结构上面__。并且使用模板编程的时候尽可能把__复杂度最小化__,尽量__不要让模板对外暴露__。最好只在实现里面使用模板, 然后给用户暴露的接口里面并不使用模板, 这样能提高你的接口的可读性。 并且你应该在这些使用模板的代码上写尽可能详细的注释。 +1. 对泛型编程不很熟练的人,常常会将面向对象的逻辑写成模板、将不依赖模板参数的成员写在模板中等等导致逻辑混乱代码膨胀诸多问题。 +2. 模板编程所使用的技巧对于使用c++不是很熟练的人是比较晦涩难懂的。在复杂的地方使用模板的代码让人更不容易读懂,并且debug 和维护起来都很麻烦。 +3. 模板编程经常会导致编译出错的信息非常不友好: 在代码出错的时候, 即使这个接口非常的简单, 模板内部复杂的实现细节也会在出错信息显示. 导致这个编译出错信息看起来非常难以理解。 +4. 模板如果使用不当,会导致运行时代码过度膨胀。 +5. 模板代码难以修改和重构。模板的代码会在很多上下文里面扩展开来, 所以很难确认重构对所有的这些展开的代码有用。 +所以,鸿蒙大部分部件禁止模板编程,仅有 __少数部件__ 可以使用泛型编程,并且开发的模板要有详细的注释。 +例外: +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. 保证了复杂表达式的异常安全 -- GitLab