本文的代码风格主要是我本人在编写代码时所遵从的代码风格,以Google C++ 代码风格为基础,对一部分进行了修改。

Google C++代码风格可参考:https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/contents/

1 头文件

1.1 头文件中#define保护

所有的头文件都应该使用#define保护防止头文件被重复包含,命名格式应该为<PROJECT>_<PATH>_<FILE>_H_

为保证唯一性,头文件的命名应该基于所在项目源代码树的全路径,比如项目foo中的头文件foo/src/bar/baz.h使用以下宏。

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_

#endif // FOO_BAR_BAZ_H_

1.2 #include的路径以及顺序

#include的路径

项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录 . (当前目录) 或 .. (上级目录).

例如, google-awesome-project/src/base/logging.h 应该按如下方式包含:

#include "base/logging.h"

#include的顺序

使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖。

又如, dir/foo.ccdir/foo_test.cc 的主要作用是实现或测试 dir2/foo2.h 的功能, foo.cc 中包含头文件的次序如下:

1.dir2/foo2 // 比如如果当前文件为类的cpp文件,则优先包含该类的头文件
2.C标准库头文件
3.C++标准库头文件
4.第三方库头文件
5.本项目内的头文件

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

#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"

条件编译情况下的顺序

有时候,不同平台特定代码需要条件编译,这些代码可以放到上述的#include之后,比如,

#include "foo/public/fooserver.h"

#include "base/port.h"  // For LANG_CXX11.

#ifdef LANG_CXX11
#include <initializer_list>
#endif  // LANG_CXX11

尽可能将条件编译的代码写的简洁独立。

2 命名约定

代码风格中最重要的一致性原则就是命名约定,命名的风格能让我们在不需要查找类型声明的条件下快速明白某个名字代表的含义:比如类型、变量、函数、常量、宏等等。

2.1 通用命名规则

函数命名、变量命名、文件命名要有描述性,使人一眼就能明白其作用,建议不要使用缩写。

尽可能使用描述性的命名,尽可能让后人读者更易理解,不要使用只有开发才能明白的缩写,也不要通过砍掉几个字母来缩写单词。

正确的写法示例:

int price_count_reader;    // 无缩写
int num_errors;            // "num" 是一个常见的写法
int num_dns_connections;   // 人人都知道 "DNS" 是什么

错误的写法示例:

int n;                     // 毫无意义.
int nerr;                  // 含糊不清的缩写.
int n_comp_conns;          // 含糊不清的缩写.
int wgc_connections;       // 只有贵团队知道是什么意思.
int pc_reader;             // "pc" 有太多可能的解释了.
int cstmr_id;              // 删减了若干字母.

2.2 代码文件命名

本文参考的Google C++代码风格中对文件命名是进行以下约定的。

文件名最好全部小写,可以使用下划线_或者连字符-进行单词连接,建议使用下划线_

文件命名示例:

my_useful_class.cc
my-useful-class.cc
myusefulclass.cc

C++代码实现文件需要以.cc结尾,头文件需要以.h结尾。

不过本人喜欢喜欢首字母大写的命名规则,并且C++代码实现文件使用.cpp结尾,例如

MyUsefulClass.cpp

2.3 类型命名

类型命名的每个单词的首字母均大写,不包含下划线_,比如MyExcitingClassMyExcitingEnum

所有类型命名,比如类、结构体、类型定义typedef、枚举、类型模板参数等均使用此命名约定,即以每个单词首字母大写,不包含下划线。

类和结构体

class UrlTable{...};
class UrlTableTester{...};
struct UrlTableProperties{...};

类型定义

typedef hash_map<UrlTableProperties *, std::string> PropertiesMap;

枚举

enum UrlTableErrors {...}

using别名

using PropertiesMap = hash_map<UrlTableProperties *, string>;

2.4 变量命名

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

普通变量命名

std::string table_name

类数据成员

Google C++代码风格要求不管是静态类数据成员还是非静态类数据成员,其命名规则与普通变量一致,但需要在最后跟下划线_,比如

class TableInfo {
  ...
 private:
  std::string table_name_;
  static Pool<TableInfo>* pool_;
};

但是我在命名类数据成员时,喜欢使用m_做前缀,然后其他单词为首字母大写,对于指针型成员变量使用m_p前缀,对于bool型成员变量使用m_b前缀。m指英文单词memberppointer,至于为什么喜欢这种命名规则, 首先在类中看到m开头的成员变量就知道这是成员变量。

上述代码修改为我的风格之后的命名示例:

class TableInfo {
  ...
 private:
  std::string m_TableName;
  static Pool<TableInfo>* m_pPool;
};

结构体变量

不管是静态的还是非静态的, 结构体数据成员都可以和普通变量一样, 不用像类那样接下划线

struct UrlTableProperties {
  std::string name;
  int num_entries;
  static Pool<UrlTableProperties>* pool;
};

2.5 常量命名

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

const int kDaysInAWeek = 7;

所有具有静态存储类型的变量 (例如静态变量或全局变量) 都应当以此方式命名。

2.6 函数命名

常规函数使用单词首字母大写,取值函数使用Get前缀和设值函数使用Set前缀。

常规函数

void AddTableEntry();
bool DeleteUrl();

取值或者设值函数

void SetNum();
int GetNum();

2.7 命名空间namespace命名

命名空间namespace使用小写命名。

namespace database
{

}

注意命名空间namespace最好特别一点,避免与其它命名空间发生冲突。

2.8 枚举命名

枚举的命名与常量和宏一致,使用kEnumName或者ENUM_NAME

2009 年 1 月之前, 我们一直建议采用宏的方式命名枚举值。由于枚举值和宏之间的命名冲突,直接导致了很多问题. 由此, 这里改为优先选择常量风格的命名方式。

新代码应该尽可能优先使用常量风格,但是老代码没必要切换到常量风格,除非宏风格确实会产生编译期问题。

宏风格

enum AlternateUrlTableErrors {
    OK = 0,
    OUT_OF_MEMORY = 1,
    MALFORMED_INPUT = 2,
};

常量风格

enum UrlTableErrors {
    kOK = 0,
    kErrorOutOfMemory,
    kErrorMalformedInput,
};

这里统一使用常量风格,避免与宏冲突的问题。

2.9 宏命名

宏命名需要全部大写,并使用下划线_分隔。

#define PI_ROUNDED 3.0
#define PI 3.1415926

3 注释

注释不使用Google C++代码风格中介绍的注释,而是使用Doxygen的代码注释规范,许多开源项目都在使用,注释更加简单明了,并且按照注释格式可以直接生成代码说明文档。

Doxygen的说明文档请参考:https://doxygen.nl/manual/index.html

以下对常用的注释做简要说明。

3.1 注释

  • 单行注释:///或者//!
  • 多行注释:/**或者/*!

3.2 文件注释

文件注释通常放在整个文件开头。

/**
 * @file 文件名
 * @brief 简介
 * @details 细节
 * @mainpage 工程概览
 * @author 作者
 * @email 邮箱
 * @version 版本号
 * @date 年-月-日
 * @license 版权
 */

例如

/**
 * @file Test.h
 * @brief 测试头文件
 * @details 这个是测试Doxygen
 * @mainpage 工程概览
 * @author stubbornhuang
 * @email stubbornhuang@qq.com
 * @version 1.0.0
 * @date 2017-11-17
 */

3.3 类注释

类定义的注释方式非常简单,使用@brief后面填写类的概述,换行填写类的详细信息。

/**
 * @brief 类的简单概述
 * 类的详细概述
 */

示例

/**
 * @brief 测试类
 * 主要用来演示Doxygen类的注释方式
 */
 class Test{
 };

3.4 常量和变量的注释

常量/变量包括以下几种类型

  • 全局常量变量
  • 宏定义
  • 类/结构体/联合体的成员变量
  • 枚举类型的成员

注释分为两种方式,可根据具体情况自行选择

代码前注释

/// 注释
常量/变量

示例

/// 缓存大小
#define BUFSIZ 1024*4

代码后注释

常量/变量 ///< 注释

示例

#define BUFSIZ 1024*4 ///< 缓存大小

3.5 函数注释

简单注释

函数注释主要包含函数简介(@brief)、参数说明(@param)、返回说明(@return)和返回值说明(@retval)四部分。

/**
 * @brief 函数简介
 *
 * @param 形参 参数说明
 * @param 形参 参数说明
 * @return 返回说明
 *   @retval 返回值说明
 */

详细注释

除了简单注释中的5项之外,还可以根据需要添加详细说明(@detail)、注解(@note)、注意(@attention)、警告(@warning)或者异常(@exception)等。

/**
 * @brief 函数简介
 * @detail 详细说明
 * 
 * @param 形参 参数说明
 * @param 形参 参数说明
 * @return 返回说明
 *   @retval 返回值说明
 * @note 注解
 * @attention 注意
 * @warning 警告
 * @exception 异常
 */

示例

/**
* @brief 主函数
* @details 程序唯一入口
*
* @param argc 命令参数个数
* @param argv 命令参数指针数组
* @return 程序执行成功与否
*     @retval 0 程序执行成功
*     @retval 1 程序执行失败
* @note 这里只是一个简单的例子
*/
int main(int argc, char* argv[])
{
}

3.6 其他

还有一些其他的注释方式,可以根据需要使用。

注释命令 生成字段 说明
@see 参考
@class 引用类 用于文档生成链接
@var 引用变量 用于文档生成链接
@enum 引用枚举 用于文档生成链接
@code 代码块开始 @endcode成对使用
@endcode 代码块结束 @code成对使用
@bug 缺陷 链接到所有缺陷汇总的缺陷列表
@todo TODO 链接到所有TODO 汇总的TODO 列表
@example 使用例子说明
@remarks 备注说明
@pre 函数前置条件
@deprecated 函数过时说明

4 格式