Skip to main content

一般建议

以下是一些建议,而非硬性要求。 如果你在编辑代码,遵循现有代码的格式通常是合理的。 代码风格对于保持一致性很有必要。一致性能让代码更易于阅读,也更便于检索。 许多规则并没有严格的逻辑依据;它们更多是由约定俗成的实践决定的。

格式化

1. 大部分格式化工作由 clang-format 自动完成。 2. 缩进为 4 个空格。请配置您的开发环境,使 Tab 键输入四个空格。 3. 左右花括号必须各占一行。
4. 如果整个函数体只有一条 statement,可以将其写在同一行。在花括号两侧添加空格 (行尾空格除外) 。
5. 对于函数,括号两侧不要加空格。
6. 在 if、for、while 及其他表达式中,开括号前须加一个空格 (与函数调用不同) 。
7. 在二元运算符 (+、-、*、/、% 等) 和三元运算符 ?: 两侧添加空格。
8. 如果输入了换行符,请将运算符另起一行,并增加其前面的缩进。
9. 如有需要,可以在行内使用空格来对齐内容。
10. 不要在运算符 .、-> 两侧使用空格。 如有必要,operator 可以折行至下一行。此时,其前面的缩进量会相应增加。 11. 不要在一元运算符 (--、++、*、& 等) 与参数之间添加空格。 12. 逗号后加空格,逗号前不加空格。for 表达式中的分号同理。 13. 不要在 [] 运算符两侧使用空格。 14. 在 template <...> 表达式中,template 与 < 之间需加空格;< 之后和 > 之前不加空格。
15. 在类和结构体中,public、private 和 protected 与 class/struct 保持相同的缩进级别,其余代码向内缩进。
16. 如果整个文件使用相同的 namespace,且没有其他重要内容,则 namespace 内部无需缩进。 17. 如果 if、for、while 或其他表达式的块只包含单个 statement,则花括号是可选的。此时应将该 statement 单独置于一行。此规则同样适用于嵌套的 if、for、while …… 但如果内部 statement 包含花括号或 else,则外部块需要用花括号括起来。
18. 行尾不应有任何空格。 19. 源文件采用 UTF-8 编码。 20. 字符串字面量中可以使用非 ASCII 字符。
21. 不要在同一行中写多个表达式。 22. 将函数内的代码按段分组,各段之间最多只留一个空行。 23. 函数、类等内容之间用一到两个空行分隔。 24. A const (与某个值相关) 必须写在类型名称之前。
25. 声明指针或引用时,* 和 & 符号的左右两侧都应留空格。
26. 使用Template类型时,请用 using 关键字为其定义别名 (最简单的情况除外) 。 换句话说,Template参数只在 using 中指定,不会在代码里重复。 using 可以在局部声明,例如在函数内部。
27. 不要在同一条语句中声明多个不同类型的变量。
28. 不要使用 C 风格转换。
29. 在类和结构体中,应在每个可见性作用域内分别对成员和函数进行分组。 30. 对于较小的类和结构体,没有必要将方法声明与实现分开。 任何类或结构体中的较小方法也是如此。 对于Template类和结构体,不要将方法声明与实现分开 (否则它们必须定义在同一个翻译单元中) 。 31. 代码行宽可以放宽到 140 个字符,而不是 80 个字符。 32. 如果不需要后缀形式,始终使用前缀自增/自减运算符。

注释

1. 务必为代码中所有不那么简单的部分添加注释。 这一点非常重要。编写注释的过程可能会帮助你意识到,这段代码其实没有必要,或者它的设计有问题。
2. 注释可以根据需要写得详细一些。 3. 将注释放在其所说明的代码之前。少数情况下,也可以把注释放在同一行代码之后。
4. 注释只能使用英文书写。 5. 如果你在编写库,请在主头文件中添加详细注释加以说明。 6. 不要添加不能提供额外信息的注释。尤其不要留下像下面这样的空注释:
该示例引自 http://home.tamk.fi/~jaalto/course/coding-style/doc/unmaintainable-code/。 7. 不要在每个文件开头写无意义的注释 (如 author、创建日期等) 。 8. 单行注释以三个斜杠开头:///,多行注释以 /** 开头。这些注释被视为“文档注释”。 注意:你可以使用 Doxygen 根据这些注释生成文档。但通常并不使用 Doxygen,因为在 IDE 中浏览代码更方便。 9. 多行注释的开头和结尾不能有空行 (多行注释的结束行除外) 。 10. 如果要注释掉代码,请使用普通注释,不要使用“文档注释”。 11. 提交前,删除那些被注释掉的代码片段。 12. 不要在注释或代码中使用脏话。 13. 不要使用大写字母。不要使用过多的标点符号。
14. 不要使用注释作为分隔符。
15. 不要在注释中展开讨论。
16. 无需在代码块末尾添加说明其用途的注释。

名称

1. 变量和类成员的名称应使用带下划线的小写字母。
2. 对于函数 (方法) 名称,请使用首字母小写的驼峰命名法。
3. 类名 (struct) 应使用首字母大写的 CamelCase。interface 不使用 I 以外的前缀。
4. using 的命名方式与类名相同。 5. Template类型参数的命名:简单情况下,使用 T、T、U、T1、T2。 更复杂的情况下,要么遵循类名的命名规则,要么添加前缀 T。
6. Template 常量参数的名称:要么遵循变量命名规则,要么在简单情况下使用 N。
7. 对于抽象类 (接口) ,可以添加 I 前缀。
8. 如果变量只在局部使用,可以使用短名称。 其他情况下,请使用能体现其含义的名称。
9. define 和全局常量的名称使用带下划线的全大写形式。
10. 文件名应与其内容采用相同的命名风格。 如果文件只包含一个类,文件名应与类名一致 (CamelCase) 。 如果文件只包含一个函数,文件名应与函数名一致 (camelCase) 。 11. 如果名称中包含缩写,则:
  • 对于变量名,缩写应使用小写字母,如 mysql_connection (不要写成 mySQL_connection) 。
  • 对于类名和函数名,应保留缩写中的大写字母,如 MySQLConnection (不要写成 MySqlConnection) 。
12. 如果构造函数参数仅用于初始化类成员,其命名应与类成员相同,但末尾要加下划线。
如果该参数在构造函数体中未被使用,则可以省略下划线后缀。 13. 局部变量和类成员的命名没有区别 (无需前缀) 。
14. 对于 enum 中的常量,使用首字母大写的驼峰命名法。ALL_CAPS 也是可以接受的。如果 enum 不是局部的,请使用 enum class。
15. 所有名称都必须使用英文。不允许将希伯来语单词音译。 不要使用 T_PAAMAYIM_NEKUDOTAYIM 16. 如果缩写广为人知 (也就是说,你可以很容易在 Wikipedia 或搜索引擎中查到其含义) ,则可以接受。 AST、SQL。 不要使用 NVDH (一些随意拼凑的字母) 如果截短后的形式已被广泛使用,也可以接受不完整的单词。 如果在注释中同时给出了全称,也可以使用缩写。 17. 包含 C++ 源代码的文件名必须使用 .cpp 扩展名。头文件必须使用 .h 扩展名。

如何编写代码

1. 内存管理。 手动释放内存 (delete) 只能在库代码中使用。 在库代码中,delete 运算符只能在析构函数中使用。 在应用程序代码中,内存必须由其所属对象负责释放。 示例:
  • 最简单的方法是将对象分配在栈上,或作为另一个类的成员。
  • 对于大量小对象,请使用容器。
  • 对于少量位于堆上的对象,如需自动释放,请使用 shared_ptr/unique_ptr。
2. 资源管理。 使用 RAII,并参见上文。 3. 错误处理。 使用异常。在大多数情况下,你只需要抛出异常,不需要捕获它 (因为有 RAII) 。 在离线数据处理应用程序中,通常可以不捕获异常。 在处理用户请求的服务器中,通常只需在连接处理程序的顶层捕获异常即可。 在线程函数中,你应当捕获并保存所有异常,以便在 join 之后在主线程中重新抛出。
绝不要在未处理异常的情况下将其隐藏。也绝不要不加甄别地把所有异常都记入日志。
如果必须忽略某些异常,也只应忽略特定的异常,其余异常都应重新抛出。
使用带有响应代码或 errno 的函数时,务必检查返回结果,并在出错时抛出异常。
你可以使用 assert 来检查代码中的不变量。 4. 异常类型。 应用程序代码中没有必要使用复杂的异常层次结构。异常信息应当让系统管理员能够理解。 5. 从析构函数中抛出异常。 不建议这样做,但这是允许的。 可采用以下做法:
  • 创建一个函数 (done() 或 finalize()) ,提前完成所有可能导致异常的工作。如果该函数已经被调用,之后析构函数中就不应再出现异常。
  • 过于复杂的任务 (例如通过网络发送消息) 可以放到单独的方法中,由类的使用者在对象销毁前调用。
  • 如果析构函数中发生异常,最好将其记入日志,而不是悄悄忽略 (如果 logger 可用) 。
  • 在简单的应用程序中,可以接受依赖 std::terminate (针对 C++11 中默认 noexcept 的情况) 来处理异常。
6. 匿名代码块。 你可以在单个函数内部创建单独的代码块,以将某些变量限制在局部作用域内,这样在退出该代码块时就会调用析构函数。
7. 多线程。 在离线数据处理程序中:
  • 尽量先把单个 CPU 核心上的性能做到最好。然后在有必要时再对代码进行并行化。
在服务器应用中:
  • 使用线程池处理请求。到目前为止,我们还没有遇到任何需要 userspace 上下文切换的任务。
不要使用 fork 来做并行化。 8. 线程同步。 通常可以让不同线程使用不同的内存单元 (更好的是,不同的缓存行) ,从而避免使用任何线程同步机制 (joinAll 除外) 。 如果确实需要同步,大多数情况下,使用 lock_guard 保护的 mutex 就足够了。 其他情况下,使用系统同步原语。不要使用忙等待。 只有在最简单的情况下才应使用原子操作。 除非这正是你的主要专长领域,否则不要尝试实现无锁数据结构。 9. 指针与引用。 大多数情况下,优先使用引用。 10. const。 使用常量引用、指向常量的指针、const_iterator 和 const 方法。 将 const 视为默认选择,只有在必要时才使用非 const。 按值传递变量时,使用 const 通常没有意义。 11. unsigned。 必要时使用 unsigned。 12. 数值类型。 使用类型 UInt8、UInt16、UInt32、UInt64、Int8、Int16、Int32 和 Int64,以及 size_t、ssize_t 和 ptrdiff_t。 不要对数值使用这些类型:signed/unsigned long、long long、short、signed/unsigned char、char。 13. 传递参数。 如果复杂值后续需要被移动,则按值传递并使用 std::move;如果需要在循环中更新某个值,则按引用传递。 如果函数会接管在堆上创建的对象的所有权,应将参数类型设为 shared_ptr 或 unique_ptr。 14. 返回值。 大多数情况下,直接使用 return 即可。不要写 return std::move(res)。 如果函数在堆上分配对象并返回它,应使用 shared_ptr 或 unique_ptr。 在少数情况下 (例如在循环中更新某个值) ,你可能需要通过参数返回该值。这种情况下,该参数应为引用。
15. namespace。 没有必要为应用代码使用单独的 namespace。 小型库同样不需要这么做。 对于中大型库,应将所有内容都放在一个 namespace 中。 在库的 .h 文件中,可以使用 namespace detail 来隐藏应用代码不需要的实现细节。 在 .cpp 文件中,可以使用 static 或匿名 namespace 来隐藏符号。 此外,namespace 也可用于 enum,以防止相应名称进入外部 namespace (但更好的做法是使用 enum class) 。 16. 延迟初始化。 如果初始化需要参数,通常就不应该编写默认构造函数。 如果后续需要延迟初始化,可以添加一个默认构造函数来创建无效对象。或者,对于数量较少的对象,也可以使用 shared_ptr/unique_ptr。
17. 虚函数。 如果某个类不打算用于多态,就不需要将函数声明为 virtual。析构函数也一样。 18. 编码。 一律使用 UTF-8。使用 std::string 和 char *。不要使用 std::wstring 和 wchar_t。 19. 日志。 请参考代码中的各处示例。 提交前,删除所有无意义的日志、调试日志,以及其他任何类型的调试输出。 应避免在循环中记录日志,即使是 Trace 级别也不例外。 在任何日志级别下,日志都必须具有可读性。 在大多数情况下,日志只应在应用程序代码中使用。 日志消息必须用英文编写。 日志内容最好能让系统管理员看懂。 不要在日志中使用脏话。 日志应使用 UTF-8 编码。在极少数情况下,可以在日志中使用非 ASCII 字符。 20. 输入输出。 不要在对应用程序性能至关重要的内部循环中使用 iostreams (并且绝不要使用 stringstream) 。 请改用 DB/IO 库。 21. 日期和时间。 参见 DateLUT 库。 22. include。 始终使用 #pragma once,不要使用 include guard。 23. using。 不要使用 using namespace。可以对具体项使用 using,但要将其局部限制在类或函数内部。 24. 除非必要,否则不要对函数使用 trailing return type。
25. 变量的声明和初始化。
26. 对于虚函数,在基类中使用 virtual,但在派生类中应使用 override,而不是 virtual。

C++ 中未使用的功能特性

1. 不使用虚继承。 2. 现代 C++ 中那些提供了便捷语法糖的构造,例如

平台

1. 我们为特定平台编写代码。 但在其他条件相同的情况下,优先选择跨平台或可移植的代码。 2. 语言:C++20 (参见可用的 C++20 特性列表) 。 3. 编译器:clang。在撰写本文时 (2025 年 3 月) ,代码使用 >= 19 版本的 clang 编译。 使用标准库 (libc++) 。 4. 操作系统:Ubuntu Linux,不早于 Precise。 5. 代码面向 x86_64 CPU 架构编写。 CPU 指令集采用我们各台服务器均支持的最低指令集。目前为 SSE 4.2。 6. 使用 -Wall -Wextra -Werror -Weverything 编译选项,但有少数例外。 7. 除了那些难以静态链接的库之外,其他所有库都使用静态链接 (参见 ldd 命令的输出) 。 8. 代码在 release 配置下进行开发和调试。

工具

1. KDevelop 是一个不错的 IDE。 2. 调试时,使用 gdb、valgrind (memcheck) 、strace、-fsanitize=... 或 tcmalloc_minimal_debug。 3. 做性能分析时,使用 Linux Perf、valgrind (callgrind) 或 strace -cf。 4. 源代码存放在 Git 中。 5. 构建使用 CMake。 6. 程序通过 deb 包发布。 7. 提交到 master 的代码不得导致构建失败。 不过,只有选定的修订版本才被认为是可用的。 8. 尽可能频繁地提交,即使代码还只完成了一部分。 为此请使用分支。 如果你在 master 分支中的代码还无法构建,请在 push 之前将其从构建中排除。你需要在几天内完成它或将其删除。 9. 对于较复杂的更改,请使用分支并将其发布到服务器上。 10. 未使用的代码会从代码仓库中移除。

库

1. 使用 C++20 标准库 (允许使用实验性扩展) ,以及 boost 和 Poco 框架。 2. 不允许使用操作系统软件包中的库,也不允许使用预装库。所有库都应以源代码的形式放在 contrib 目录中,并随 ClickHouse 一起构建。详见添加新的第三方库指南。 3. 始终优先选择已经在使用的库。

一般建议

1. 尽量少写代码。 2. 优先尝试最简单的解决方案。 3. 在弄清楚代码将如何工作以及内部循环如何运作之前,不要动手写代码。 4. 在最简单的情况下,优先使用 using,而不是类或结构体。 5. 如果可以,尽量不要编写拷贝构造函数、赋值运算符、析构函数 (如果类中至少包含一个虚函数,则虚析构函数除外) 、移动构造函数或移动赋值运算符。换句话说,编译器生成的这些函数应当能够正常工作。你也可以使用 default。 6. 鼓励简化代码。在可能的情况下,尽量减少代码量。

其他建议

1. 对 stddef.h 中的类型显式加上 std:: 不推荐这样做。换句话说,我们建议写 size_t,而不是 std::size_t,因为前者更简洁。 当然,加上 std:: 也是可以接受的。 2. 对标准 C 库中的函数显式加上 std:: 不推荐这样做。换句话说,应该写 memcpy,而不是 std::memcpy。 原因是还存在类似的非标准函数,例如 memmem。我们确实偶尔会用到这些函数,而它们并不存在于 namespace std 中。 如果你到处都写 std::memcpy 而不是 memcpy,那么不带 std:: 的 memmem 看起来就会很奇怪。 不过,如果你更喜欢,也还是可以使用 std::。 3. 当标准 C++ 库中提供了相同函数时,使用 C 中的函数。 如果这样做效率更高,也是可以接受的。 例如,复制大块内存时,使用 memcpy 而不是 std::copy。 4. 多行函数参数。 以下任一种换行风格都可以:
最后修改于 2026年6月10日