首页 > 技术文章 > C++风格指南

xi-jiajia 2020-12-07 11:17 原文

头文件

Self-contained头文件

头文件应该能够自给自足,即可以作为第一个头文件被引入。换句话说,头文件不应依赖于与其并列引入的其他头文件。

#define保护

所有的头文件都应该使用#define包含来防止被多重包含,其格式为:

#ifndef PROJECT_PATH_FILE_H_
#define PROJECT_PATH_FILE_H_
...
#endif

前置声明

前置声明指类、函数和模板的纯粹声明而未伴随其定义。尽可能避免使用前置声明,即不要通过声明的方式引入类、函数或模板,而是通过#include包含这些声明所在的头文件。

内联函数

仅当函数少于10行时才将其定义为内联函数。

#include路径及顺序

推荐的头文件包含顺序为:相关头文件(指被测试的头文件),C库,C++库,其他库的头文件,本项目内的头文件。如此,便可在放在首位的被测试的头文件遗漏必要的库时,终止构建测试程序,从而避免隐藏依赖。

在#include中插入空行以分割不同种类的头文件。

作用域

命名空间

命名空间将全局作用域细分为独立的、具名的作用域,可有效防止全局作用域的命名冲突。

在命名空间的最后注释出命名空间的名字。

使用using引入整个命名空间的标识符号会导致命名空间污染。

不要在头文件中使用命名空间别名,除非将此别名限制在其他命名空间或函数内部使用。

匿名命名空间和静态变量

在.cpp文件中定义一个不需要被外部引用的变量时,可以将其置于匿名名称空间中,但在.h文件中不推荐此做法。

置于匿名名称空间的声明具有内部连接性,这与利用static进行声明相同,这意味着在此文件中声明的标识符不能在另一个文件中被访问。

非成员函数、静态成员函数和全局函数

将函数直接置于命名空间中而不要使用类的静态方法模拟处命名空间的效果。类的静态方法应当和类的实例或静态数据紧密相关。

局部变量

将函数变量尽可能置于最小作用域内,并再变量声明时进行初始化。虽然C++允许在函数 的任何位置声明变量,但是提倡在尽可能小的作用域中声明变量,离第一次使用越近越好,这使代码浏览者更容易定位变量声明的位置,了解变量的类型和初始值。除此之外,还应使用初始化的方式替代声明再赋值。

对于if、while和for语句的变量,应该在这些语句中正常的声明,以使变量的作用域被限制在这些语句中。但变量若为对象,则每次循环都要调用其构造函数与析构函数,将导致效率降低。此时可将变量置于循环作用域外面。

静态和全局变量

静态变量包括全局变量、静态变量、静态类成员变量和函数静态变量,这些变量都应是原生数据类型(POD:Plain Old Data,包括int、char、float及以上类型的指针、数组与结构体),而不应是自定义的类类型。因为构造函数和析构函数的调用顺序不确定性,会导致难以发现的问题。总的来说,就是需要禁用类类型的全局变量。

构造函数的职责

在构造函数中可以进行各种初始化操作,但不要在构造函数中调用虚函数。

隐式类型转换

不要定义隐式类型转换,对于转换运算符和单参数构造函数,请使用explicit关键字。

可拷贝类型和可移动类型

如果自定义的类型不需要拷贝或移动操作,则应该显式的对其进行禁用。

结构体与类

仅当只有数据成员时使用结构体,其他情况使用类。

继承

使用组合通常比使用继承更合理。如果使用继承,尽量定义为public继承。

当子类继承基类时,子类包含了父基类所有的数据及操作的定义。继承主要用于两种场合:实现继承——子类继承父类的实现代码;接口继承——子类继承父类的方法名称。

继承尽量是public的,若想使用私有继承,可替换为将基类实例作为对象成员的方式。

如果定义的类有虚函数,则析构函数也应为虚函数。

数据成员都应该是私有的。

多重继承

真正需要用到多重继承的情况较少,通常在仅有一个基类是非抽象类且其他基类都是纯接口类时才使用多重继承。

接口

当类满足如下条件时,称之为纯接口:

  • 只有纯虚函数和静态函数(析构函数除外)
  • 没有非静态数据成员
  • 没有定义任何构造函数(若有也需要不带参数且为protected)
  • 如果作为子类,也只能从满足以上条件的类继承而来

接口类不能被直接实例化,因为其中声明了纯虚函数。为了确保接口类的所有实现可以被正确销毁,必须为之声明虚析构函数。

运算符重载

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

存取控制

将所有数据成员声明为private,除非是static const类型成员。

声明顺序

将相似的声明放在一起,将public部分放在最前。

函数

参数顺序

编写简短函数

引用参数

函数重载

缺省参数

函数返回类型后置语法

C++特性

引用参数

右值引用

函数重载

缺省参数

边长数组和alloca()

友元

异常

运行时类型识别

类型转换

前置自增和自减

const用法

constexpr用法

整型

64位下的可移植性

预处理宏

0,nullptr和NULL

sizeof

auto

列表初始化

Lambda表达式

模板编程

Boost库

C++11

命名约定

文件命名

文件命名要全部小写,可以包含下划线。

类型命名

类型名称每个单词首字母均大写,不包含下划线。

所有的类型命名——类,结构体,类型定义(typedef),枚举,类型模板参数——均使用相同约定,即以大写字母开始,每个单词首字母均大写,不包含下划线。

变量命名

变量(包括函数参数)和数据成员名一律小写,单词之间用下划线连接起来,类的成员变量以下划结尾,但结构体的成员变量不需要下划线。

常量命名

声明为constexpr或const的变量,或在程序运行期间其值始终保持不变的,命名以k开头,大小写混合。

函数命名

常规函数使用大小写混合,取值和设值函数则要求与变量名匹配。

一般来说,函数名的每个单词首字母大写,没有下划线。对于首字母缩写的单词,则更倾向于将它们视作一个单词进行首字母大写而非原来的全部大写。

命名空间命名

命名空间以小写字母命名,要注意避免嵌套命名空间的名字之间发生冲突。

不使用缩写作为名称的规则同样适用于命名空间。

枚举命名

枚举的命名应当和常量一致,以k开头,大小写混合;或和宏一致,全为大写并使用下划线。

宏命名

全部大写,使用下划线。

命名规则的特例

如果命名的实体与已有C/C++实体相似,可参考现有命名策略。

注释

文件注释

文件注释描述了该文件的内容,如果一个文件只声明、实现或测试了一个对象,并且这个对象已经在它的声明处进行了详细的注释,那么就没有必要再加上文件注释。

如果一个.h文件声明了多个概念,则文件注释应当对文件的内容做一个大致的说明,同时说明各个概念之间的连续。对于每个概念的详细文档应当放在各个概念中,而不是文件注释中。

类注释

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

类注释应当为理解和使用类提供足够的信息,同时应当提醒使用类时应当考虑的因素。如果类由任何同步前提,请用文档说明。如果类的实例可被多线程访问,要特别注意说明多线程环境下相关的规则和常量使用。

如果类的声明和定义分开了,则描述类用法的注释应当和接口定义放在一起,描述类的操作和实现的注释应当和实现放在一起。

函数注释

函数声明处的注释描述函数功能;定义出的注释描述函数实现。

注释使用叙述式而非指令式,注释只是为了描述函数,而不是命令函数做什么。

函数声明处注释的内容:

  • 函数的输入输出
  • 对类成员函数而言,函数调用期间对象是否需要保持引用参数,是否会释放这些参数
  • 函数是否分配了必须由调用者释放的空间
  • 参数是否可以为空指针
  • 是否存在函数使用上的性能隐患
  • 如果函数是可以重入的,其同步前提是什么

避免对显而易见的内容进行说明。

注释函数重载时,注释的重点内容应该是函数中被重载的部分,而不是简单的重复被重载函数的注释。多数情况下,函数重载不需要额外的文档,因此也没有必要加上注释。

注释构造、析构函数时,应当注明构造函数做了什么及析构函数清理了什么。如果无关紧要,可以直接省略掉注释。

如果函数的实现过程中用到了很巧妙的方式,那么在函数定义处应当加上解释性的注释。如所使用的编程技巧、实现的大致步骤及如此实现的理由。不要从.h文件或其他地方的函数声明处直接复制注释,简要重述函数功能是可以的,但重点要放在如何实现上。

变量注释

通常变量名本身足以很好说明变量用途,某些情况下,也需要额外的注释说明。

每个类数据成员(即成员变量)都应该用注释说明用途。如果有非变量的参数不能用类型与变量名明确表达,则应当加上注释。

如果变量类型与变量名已经足以描述一个变量,那么就不再需要加上注释。

如果变量可以接收NULL或-1等特殊值,需加以说明。

全局变量也要注释说明含义及用途,以及作为全局变量的原因。

实现注释

巧妙或复杂的代码段前要加注释。

比较晦涩的地方要在行尾加入注释。如果需要连续进行多行注释,可以使之对其获得更好的可读性。

如果函数参数的意义不明显,考虑用下面的方式进行弥补:

  • 如果参数是一个字面常量,并且这一常量在多处函数调用中被使用,用以推断它们一致,则应当用一个常量名让这一约定变得更明显
  • 考虑更改函数的签名,让某个bool类型的参数变为enmu类型,这样可以让这个参数的值表达其意义
  • 如果某个函数有多个配置选项,可以考虑定义一个类或结构体保存所有的选项,并传入类或结构体的实例。这样的选项可以在调用处使用变量名引用,就能清晰地表明其意义,同时减少了函数参数的数量,使得函数调用更易读写。当使用其他的选项时,亦不需要对调用点进行修改
  • 用具名变量代替大段而负载的嵌套表达式

不要用自然语言翻译代码作为注释。

注释的通常写法是包含正确大小写和结尾句号的完成叙述性语句。大多情况下,完整的句子比片段可读性更高。

对临时的、短期的解决方案,或已经够好但仍不完美的代码使用TODO注释。

格式

行长度

非ASCII字符

空格还是制表位

函数声明与定义

Lambda表达式

函数调用

列表初始化格式

条件语句

循环和开关选择语句

指针和引用表达式

布尔表达式

函数返回值

变量及数组初始化

预处理指令

类格式

构造函数和初始值列表

命名空间格式化

水平留白

垂直留白

规则特例

现有不合规范的代码

Windows代码

推荐阅读