C++编码规范

本文档详细阐述了C++编程的规范和最佳实践,包括头文件的使用、命名空间、内联函数、作用域、类的设计、函数的规则、特性和命名约定等多个方面。旨在提高代码的一致性、可读性和维护性,减少潜在问题,如避免使用前置声明,限制内联函数的长度,以及明智地使用命名空间。同时,提倡在适当情况下限制或禁止某些C++特性,以保持代码简洁。

原文转自:  http://www.tanjp.com (即时修正和更新)

前言

使代码易于管理的方法之一是加强代码一致性。让任何程序员都可以快速读懂你的代码这点非常重要。保持统一编程风格并遵守约定意味着可以很容易根据“模式匹配”规则来推断各种标识符的含义.。创建通用,必需的习惯用语和模式可以使代码更容易理解。在一些情况下可能有充分的理由改变某些编程风格,但我们还是应该遵循一致性原则,尽量不这么做。

 

本指南的另一个观点是 C++ 特性的臃肿。C++ 是一门包含大量高级特性的庞大语言。某些情况下,我们会限制甚至禁止使用某些特性。这么做是为了保持代码清爽,避免这些特性可能导致的各种问题。指南中列举了这类特性,并解释为什么这些特性被限制使用。

 

 

1 头文件

通常每一个 .cpp 文件都有一个对应的 .h 文件,也有一些常见例外,如单元测试代码和只包含 main() 函数的 .cpp 文件。

正确使用头文件可令代码在可读性、文件大小和性能上大为改观。

下面的规则将引导你规避使用头文件时的各种陷阱。

 

1.1 自给自足

头文件应该能够自给自足(也就是可以作为第一个头文件被引入),以 .h 结尾。至于用来插入文本的文件,说到底它们并不是头文件,所以应以 .inc 结尾。不允许分离出 -inl.h 头文件的做法。

如果 .h 文件声明了一个模板或内联函数,同时也在该文件加以定义。凡是有用到这些的 .cpp 文件,就得统统包含该头文件,否则程序可能会在构建中链接失败。不要把这些定义放到分离的 -inl.h 文件里。

 

1.2 #define 保护

所有头文件都应该使用 #define 来防止头文件被多重包含,命名格式当是:<PROJECT>_<PATH>_<FILE>_H

为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径。

例如,项目 foo 中的头文件 foo/src/bar/baz.h 可按如下方式保护:

#ifndef FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ ... #endif

 

1.3 不使用前置声明

尽可能地避免使用前置声明。使用 #include 包含需要的头文件即可。

定义:所谓「前置声明」(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义.

优点:前置声明能够节省编译时间,多余的 #include 会迫使编译器展开更多的文件,处理更多的输入。

缺点:

1> 前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。

2> 前置声明可能会被库的后续更改所破坏。前置声明函数或模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。

3> 前置声明来自命名空间 std:: 的 symbol 时,其行为未定义。

4> 前置声明了不少来自头文件的 symbol 时,就会比单单一行的 include 冗长。

 

1.4 内联函数要精简

只有当函数少于10行时才将其定义为内联函数。虚函数和递归函数不使用内联。

 

1.5 #include 的路径及顺序

include路径中不使用相对路径 ./ 和 ../

头文件包含次序如下(每组之间用空行隔开):

1> 当前 .cpp 对应的 .h 文件

2> C系统文件

3> C++系统文件

4> 其他库的 .h 文件

5> 本项目内的 .h 文件

 

举例来说,project/src/foo/internal/fooserver.cpp 的包含次序如下:

#include "foo/public/fooserver.h" // 优先位置 #include <sys/types.h> #include <unistd.h> #include <hash_map> #include <vector> #include "base/basictypes.h" #include "base/commandlineflags.h" #include "foo/public/bar.h"

 

 

 

2 作用域

 

2.1 规范使用命名空间。

1> 要使用命名空间包装对外接口。

// .h 文件 namespace mynamespace { // 所有声明都置于命名空间中 // 注意不要使用缩进 class MyClass { public: ... void Foo(); }; } // namespace mynamespace // .cpp 文件 namespace mynamespace { // 函数定义都置于命名空间中 void MyClass::Foo() { ... } } // namespace mynamespace

 

 

2> 不应该使用 using 指示引入整个命名空间的标识符号。

特别在头文件禁止使用 using namespace

// 禁止 —— 污染命名空间 using namespace foo;

 

3> 不要在头文件中使用命名空间别名,除非显示标记内部命名空间使用。

// 在 .cpp 中使用别名缩短常用的命名空间 namespace baz = ::foo::bar::baz; // 在 .h 中使用别名缩短常用的命名空间 namespace librarian { namespace impl { // 仅限内部使用 namespace sidetable = ::pipeline_diagnostics::sidetable; } // namespace impl inline void my_inline_function() { // 限制在一个函数中的命名空间别名 namespace baz = ::foo::bar::baz; ... } } // namespace librarian

 

4> 禁止用内联命名空间。C++11引入内联命名空间新特性,但禁止使用。

 

2.2 合理使用匿名命名空间或static,限制变量或函数作用域。

在 .cpp 文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为 static 。但是不要在 .h 文件中这么做。

 

2.3 局部变量尽可能置于最小作用域内,并在变量声明时进行初始化。

“尽可能”是指有一个例外,如果变量是一个对象,每次进入作用域都要调用其构造函数,每次退出作用域都要调用其析构函数,这会导致效率降低。注意别在循环犯大量构造和析构的低级错误。

// 低效的实现 for (int i = 0; i < 1000000; ++i) { Foo f; // 构造函数和析构函数分别调用 1000000 次! f.DoSomething(i); } // 应该改为如下 Foo f; // 构造函数和析构函数只调用 1 次 for (int i = 0; i < 1000000; ++i) { f.DoSomething(i); }

 

2.4 禁止使用静态或全局非POD变量,特别是多线程环境下。

禁止定义静态储存周期非POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植。(POD: Plain Old Data, 原生数据类型,即int, char, float等,通俗的讲,一个类或结构体通过二进制拷贝后还能保持其数据不变,那么它就是一个POD类型)。

如果您确实需要一个 class 类型的静态或全局变量,可以考虑在 main() 函数或 pthread_once() 内初始化一个指针且永不回收。注意只能用 raw 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。

 

 

 

3 类

 

3.1 不要在构造函数总调用虚函数,也不要抛出异常导致初始化失败。

(不在构造函数中做太多逻辑相关的初始化)

 

3.2 使用 explicit 关键字禁止构造函数和转换运算符隐式类型转换。

 

3.3 如果类型需要就让它们支持拷贝/移动,否则就把隐式产生的拷贝/移动函数禁用。

 

3.4 仅当只有数据成员时使用 struct,其它一概使用 class。

 

3.5 除少数特定环境外,不要重载运算符,不要创建用户定义字面量。

 

3.6 类里面的声明顺序:将相似的声明放在一起,将 public 部分放在最前面。

1> 类定义一般以 public 开始,后跟 protected,最后是 private 。

2> 类型(包括 typedef, using 和嵌套的结构体与类)

3> 常量

4> 工厂函数

5> 构造函数

6> 赋值运算符

7> 析构函数

8> 其他函数

9> 数据成员

 

 

4 函数

 

4.1 输入参数(值或 const 引用)在先,后跟输出参数(非 const 指针)。

 

4.2 所有引用传递的参数必须加上 const ,也就说引用只用作输入参数。

void test_func(const std::string & in, std::string * out);

 

4.3 函数重载的参数类型必须能让读者胸有成竹,否则不应该使用函数重载。

 

4.4 非虚函数才能使用默认参数,并且必须保持默认值始终一致。

 

 

5 特性

 

5.1 只在定义移动构造函数与移动赋值操作时使用右值引用和 std::move。不要使用 std::forward 。

 

5.2 使用前置自增和自减。( ++i, --i)

 

5.3 使用统一的内建整型。(如: int32, uint32, uint8, 等等)

 

5.4 使用宏时要非常谨慎,尽量以内联函数、枚举类型或者常量代替。

 

5.5 整数用 0,实数用 0.0,指针用 nullptr 或 NULL,字符(串)用 ‘\0’ 。

 

5.6 尽可能用 sizeof(varname) 代替 sizeof(type) 。

Struct data; memset(&data, 0, sizeof(data));

 

5.7 auto 只在局部变量定义中使用,前提是可读性好。

 

5.8 尽量不用 lambda 表达式。

 

5.9 不要使用复杂的模版编程。

 

 

6 命名约定

 

6.1 文件名全部要小写,可以用下划线( _ )。

my_useful_class.h my_useful_class.cpp

 

6.2 类,结构体和 typedef 类型 或 using 别名每个单词首字母要大写,不能包含下划线。

class MyExcitingClass{...} //类名 struct UrlTableProperties{...} //结构体名 class ActorBase; typedef std::shared_ptr<ActorBase> ActorPtr; //typedef名 using ActorPtrMap = std::unordered_map<uint32, ActorPtr>; //using别名

 

6.3 枚举类型要以( en )开头,后面每个单词首字母大写,不能包含下划线。

6.4 枚举类型的枚举值要以 (en+类型缩写)开头,后跟大写字母,用下划线连接。

enum enActorID { enAID_MODB_BEGIN = 200, enAID_MODB_END = 249, enAID_CELL_BEGIN = 250, enAID_CELL_END = 399, }; enum enUrlTableErrors {...}

6.5 模版类型要以( tp )开头,后面每个单词首字母大写,不能包含下划线。

template<typename tpType> class SafeQueue { public: bool push(const tpType & po_val); bool pop(tpType & po_val); protected: std::queue< tpType > mo_queue; }

 

6.6 变量、函数参数和数据成员一律小写,单词之间用下划线( _ )连接。

1> 非公有成员变量要按以下前缀顺序拼接:

作用域前缀组成(在前):类或结构体非公有变量( m ),函数参数( p ),局部变量( z ),静态变量( s )

类型标识前缀组成:整型前缀( n ),字符串前缀( s ),布尔型前缀( b ),指针或智能指针前缀( p ),函数或仿函数( f ),STL集合容器( c ),其他类型( o )。

2> 公有成员变量无需前缀。

class TableInfo { //函数参数 void demo_func(uint32 pn_count, const std::string & ps_data, TableInfo * pp_next) { uint32 zn_count = pn_count; //局部整型 std::string zs_data = ps_data; //局部字符串 uint32 zp_next = pp_next; //局部指针 ... } public: int32 total; std::string name; //公有变量无需前缀 static uint32 global; //公有静态 private: uint32 mn_count; //非公有整型 TableInfo * mp_next; //非公有指针 std::string ms_data; //非公有字符串 std::list<int32> mc_queue; //非公有容器 TableInfo mo_parent; //非公有共对象 std::function<void(uint32, const std::string &, TableInfo *)> mf_func; //函数对象 static uint32 msn_static_data; //非公静态整型 }

 

6.7 常量命名以( k )开头,每个单词首字母大写,不能包含下划线。

const int kDaysInAWeek = 7;

 

6.8 函数命名一律小写,单词之间用下划线( _ )连接。

void my_test_func(); void set_count(int32 pn_count);

 

6.9 命名空间使用小写命名,单词之间用下划线( _ )连接。建议使用更独特的项目标识符

 

6.10 尽量不使用宏,如果必须使用,使用大写命名,单词之间用下划线( _ )连接。

 

 

7 注释

 

7.1 类定义附带一份注释,描述类的功能和用法,除非它功能相当明显。

7.2 函数声明处描述函数的功能和各个输入输出参数以及返回值。

7.3 如果变量名不能明显让读者明白,就给数据成员加上必要的注释说明。

7.4 代码实现中,给巧妙的,晦涩的,有趣的,重要的地方都加上注释。

7.5 不需描述显而易见的代码,不要用自然语言翻译代码作为注释,除非代码很难懂。

7.6 使用 TODO 注释暂时未完善的代码。

7.7 使用 DEPRECATED 注释标记某个接口已经弃用,它的存在只为了兼容性。

// 安全队列 // 描述: 通过互斥锁实现多线程安全访问。 // 示例: // SafeQueue<int32> sq(); // sq.push(123); // int32 zn_val = sq.pop(); // create by tjp 2019-4-2 使用互斥锁实现多线程同步访问。 template<typename tpType > class SafeQueue { public: // 将数据加入队列末尾。 // 注意: tpType 需类型支持拷贝。 // @po_val, 待加入的数据。 // @return, 成功返回true, 失败返回false。 bool push(const tpType & po_val) { { std::lock_guard<std::mutex> lock(mo_mutex); while ((mc_queue.size() >= mn_maxsize) && (!mb_noblock)) { //队列满且标记为阻塞,需要等待 ++mb_push_waiting; mo_cond_push.wait(mo_mutex); --mb_push_waiting; } if ((mc_queue.size() >= mn_maxsize) && mb_noblock) { //队列满且标记为不阻塞,马上返回 return false; } mc_queue.push(po_val); }//区域外互斥锁已经释放 if (mb_pop_waiting > 0) { mo_cond_pop.notify_one(); //通知其他线程可以取出 } return true; } bool pop(tpType & po_val) { // TODO(tjp): 待实现 return true; } // DEPRECATED, 接口不建议使用,可以使用 size() 替代。 bool empty() {...} private: std::list<tpType> mc_queue; //容器队列,默认大小为0. }

 

 

 

8 格式

 

8.1 每一行代码字符数不超过100。

8.2 使用制表符(4)作为缩进。

8.3 大括号开始和结束都独立一行。

if (condition) { ... // tab 4 缩进. } else if (...) { ... } else { ... }

8.4 增强可读性,简单语句可以写在同一行。

if (x == kFoo) return new Foo(); if (x == kBar) {return new Bar();} else{ return 0;}

 

8.5 预处理指令不要缩进,从行首开始

// 好 - 指令从行首开始 if (lopsided_score) { #if DISASTER_PENDING // 正确 - 从行首开始 DropEverything(); # if NOTIFY // 非必要 - # 后跟空格 NotifyClient(); # endif #endif BackToNormal(); }

 

8.6 类的访问控制 public:, protected:, private: 不要缩进,从行首开始。

8.7 构造函数初始化列表格式如下,参数的初始化次序按声明次序:

MyClass::MyClass(int var) : some_var_(var) // tab 4 indent , some_other_var_(var + 1) { DoSomething(); }

 

8.8 命名空间不缩进。

namespace { void foo() { // 正确. 命名空间内没有额外的缩进. ... } } // namespace

 

 

 

9 参考引用

Google 开源C++编码规范 https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值