代码风格指南
Google 开源项目风格指南——中文版
https://zh-google-styleguide.readthedocs.io/en/latest/contents.html
C++风格指南
1. 头文件
1.1 给需要被导入 (include) 但不属于头文件的文件设置为 .inc 扩展名, 并尽量避免使用.
.inc
文件:是 “include” 的缩写,不是标准的 .h
头文件,但也会在代码中通过 #include
导入。
它通常用于 代码片段,比如:
- 重复定义的结构体初始化代码
- 特定模块的一段配置或模板代码
- 宏实现
它 不应该包含函数声明或外部接口,而是被视为某种“嵌入性代码片段”。
// file: register_map.inc
REG(UART0, 0x1000)
REG(UART1, 0x1010)
REG(SPI0, 0x1020)// file: device_map.cpp
#define REG(name, addr) init_device(#name, addr);
#include "register_map.inc"
#undef REG
.inc
是为特殊 include 场景保留的临时文件扩展名,提示开发者“这是片段代码”,但为避免混乱和维护成本,应该慎用。
1.2 头文件应该自给自足 (self-contained, 也就是可以独立编译), 并以 .h 为扩展名.
一个 .h
头文件应该包含它自己需要的所有依赖,比如:
- 需要的其他头文件(如
<string>
、自定义类型的定义等) - 宏、前向声明等
也就是说,只 include 它本身,不依赖其他文件,也应该能单独通过编译(比如放进一个测试 cpp 文件里去编译)。
1.3 头文件
C++头文件的作用是声明接口,供其他源文件(.cpp
)使用。头文件不应该包含实现细节,而应该专注于对外公开的接口定义。
1.头文件里应放置的内容
内容 | 示例/说明 |
---|---|
类的声明与定义 | 包括成员函数声明、成员变量声明 |
结构体/枚举声明 | struct , enum class , enum |
函数声明(声明,不是实现) | void foo(); |
宏定义(只在必要时) | #define PI 3.14 (不推荐多用) |
常量声明(推荐 constexpr ) | constexpr int MAX = 100; |
模板声明或定义 | 模板必须定义在头文件中 |
内联函数定义(必要时) | inline int add(int a, int b) { return a + b; } |
命名空间 | namespace mylib { ... } |
类型别名(typedef , using ) | using String = std::string; |
头文件保护(Include Guard) | #ifndef HEADER_H ,或者 #pragma once |
2.不该放在头文件中的内容
内容 | 原因 |
---|---|
函数实现(除非是模板/内联) | 应放在 .cpp 文件中 |
变量定义(除非是 const 或 constexpr ) | 会导致重复定义,链接错误 |
复杂宏或调试宏 | 污染命名空间,难维护,建议放 .inc 或 .cpp 中 |
using namespace std; | 污染命名空间,影响所有包含它的文件 |
1.4 头文件防护符的格式是: < 项目>_< 路径>_< 文件名>_H_ .
1.5 不要依赖间接导入
在使用某个类型、函数或宏时,应该自己显式 #include
它所在的头文件,而不是依赖别的头文件帮你间接引入。
你用到什么,就自己 include 什么,不要假设别人帮你 include 了。
1.6 尽量避免使用前向声明. 应该导入你所需的头文件。
当你需要使用某个类型的完整定义时,不要用“前向声明”来省事,而是应该直接 #include
对应的头文件。
前向声明是告诉编译器“这个类型将会被定义,但现在我只告诉你它的名字”,而不包含其真正的定义。例如:
class B; // 前向声明
void FuncInB(); // 前向声明
extern int variable_in_b; // 前向声明
ABSL_DECLARE_FLAG(flag_in_b); // 前向声明
它通常用于:
- 指针或引用成员变量的声明(不需要知道类的全部内容)
- 减少头文件依赖,加快编译速度
1.7 不要内联超过 10 行的函数
1.8 #include 的路径及顺序:配套的头文件, C 语言系统库头文件, C++ 标准库头文件, 其他库的头文件, 本项目的头文件.
头文件的路径应相对于项目源码目录, 不能出现 UNIX 目录别名 (alias) . (当前目录) 或 … (上级目录).
假设你项目结构如下:
google-awesome-project/
└── src/└── base/│ └── logging.h└── util/└── foo.cc
✅ 推荐写法(规范):
在 src/util/foo.cc
中:
#include "base/logging.h"
❌ 不推荐写法(违反规范):
#include "../base/logging.h"
虽然文件物理上是这么相对布局的,但 Google C++ 风格明确指出:
路径应相对于项目源码根目录(如
src/
),不要使用.
或..
目录别名。
假设你是这样设置 include path 的:
如果你在 CMake 或 g++/clang++ 里设置了:
-Igoogle-awesome-project/src
那么这个头文件引用是合法且推荐的:
#include "base/logging.h"
假设你在写 dir/foo.cc
,它实现或测试的是 dir2/foo2.h
,应该这样引入头文件:
#include "dir2/foo2.h" // 1. 你实现或测试的头文件// 2. 空一行#include <unistd.h> // 3. C 系统头文件(如 POSIX 标准)
#include <stdlib.h> // 使用 .h 扩展名// 4. 空一行#include <algorithm> // 5. C++ 标准库头文件(无扩展名)
#include <vector>// 6. 空一行#include "gtest/gtest.h" // 7. 其他三方库的头文件// 8. 空一行#include "my_project/utils/log.h" // 9. 本项目其他头文件
每个分组内部的导入语句应该按字母序排列.
2.作用域
2.1 命名空间
-
将代码放入命名空间中:
-
除了极少数特殊情况(如
main()
函数、顶层宏、全局配置等),你应该始终使用命名空间来包裹代码,避免命名冲突。 -
比如:
namespace myproject {class Widget { public:void Draw(); };} // namespace myproject
-
-
不要使用
using namespace xxx;
:-
禁止写:
using namespace std; // ❌ 禁止
-
原因:容易引发命名冲突,污染全局命名空间,让代码维护困难、阅读不清晰,尤其在头文件中更危险。
-
-
不要使用内联命名空间 (
inline namespace
):-
内联命名空间用于版本控制,如:
inline namespace v1 {void foo(); }
-
禁止原因:这在大型项目中会让接口版本混淆、不透明,难以维护,不如手动明确命名。
-
允许其成员被自动“提升”到外部命名空间作用域中,让使用者无需明确指定版本号。如:
假设你维护一个库
mylib
,现在你发布了第一个版本:namespace mylib { inline namespace v1 {void greet(); // 函数 v1 版本 } }
用户使用:
mylib::greet(); // ✅ 不需要写 mylib::v1::greet()
它背后实际调用的是:
mylib::v1::greet();
之后你发布了第二个版本:
namespace mylib { inline namespace v2 {void greet(); // 新版本的 greet } }
旧代码不需要改动:
mylib::greet(); // ✅ 自动指向 v2 版本
而如果有人确实要用旧版本:
mylib::v1::greet(); // ✅ 仍然能用
-
2.2 内部链接:放入匿名命名空间 (unnamed namespace)或声明为 static
2.3 局部变量:应该尽可能缩小函数变量的作用域 (scope), 并在声明的同时初始化
2.4 禁止使用 静态储存周期 (static storage duration) 的变量, 除非它们可以 平凡地析构 (trivially destructible).
2.5 函数的局部静态变量可以动态地初始化 (dynamic initialization) . 除了少数情况外, 不推荐动态初始化静态类成员变量或命名空间内的变量.
类型 | 初始化时间 | 例子 | 特点 |
---|---|---|---|
静态初始化 | 编译期或加载期(编译器能决定) | int x = 0; | 不需要执行函数或复杂逻辑 |
动态初始化 | 程序运行时 | std::string s = getConfig(); | 需要执行函数、构造函数等逻辑 |
C++11 保证函数中 static
变量的动态初始化是线程安全的:
const std::string& get_config() {static std::string config = load_config(); // 安全、延迟初始化return config;
}
2.6 thread_local 变量
thread_local
是 C++11 引入的关键字,用于定义 每个线程拥有独立副本 的变量。
thread_local int counter = 0;
- 每个线程都维护自己的
counter
,互不影响。 - 常用于缓存、日志缓冲、统计计数器等场景。
“必须使用编译期常量初始化”
thread_local int x = 42; // ✅ OK,42 是编译期常量
thread_local std::string s = "abc"; // ❌ 不可以,涉及构造函数 → 动态初始化
- 原因是:动态初始化的 thread_local 变量存在初始化顺序和性能问题,容易出错。
- 多线程中,thread_local 的动态初始化可能发生在不确定的时间点、甚至多次初始化(不同线程),会导致逻辑错误或开销不可控。
“必须使用
ABSL_CONST_INIT
属性”
#include "absl/base/attributes.h"ABSL_CONST_INIT thread_local int x = 0; // ✅ 强制告知:x 使用编译期常量初始化
ABSL_CONST_INIT
是 Google Abseil 库的宏,标记变量应该使用静态常量初始化。- 编译器会帮助检查是否违反这一规则,防止误用动态初始化。
“优先采用
thread_local
,而非其他定义线程内局部数据的方法”
- 有人可能用
pthread_setspecific
、TLS API、线程 ID 映射等手动管理线程本地存储,代码复杂且容易错。 - 相比之下,
thread_local
更安全、更简洁,推荐优先使用。
3.类
3.1 构造函数 (constructor) 中不得调用虚函数 (virtual method). 不要在没有错误处理机制的情况下进行可能失败的初始化
推荐做法:使用工厂函数、Init 方法等替代构造逻辑
方法一:使用工厂函数返回 std::optional
/ StatusOr
class FileReader {
public:static std::optional<FileReader> Create(const std::string& filename) {FILE* f = fopen(filename.c_str(), "r");if (!f) return std::nullopt;return FileReader(f);}// ... 其他成员函数 ...private:explicit FileReader(FILE* f) : file_(f) {}FILE* file_;
};
方法二:用 Init 分离初始化逻辑
class FileReader {
public:FileReader() : file_(nullptr) {}bool Init(const std::string& filename) {file_ = fopen(filename.c_str(), "r");return file_ != nullptr;}private:FILE* file_;
};
3.2 不要定义隐式类型转换. 定义类型转换运算符和单个参数的构造函数时, 请使用 explicit 关键字
在构造函数前加 explicit
:
struct MyInt {explicit MyInt(int x) { value = x; }int value;
};void print(MyInt m);int main() {print(42); // ❌ 编译错误:不能隐式转换print(MyInt(42)); // ✅ 明确转换
}
同理,对于类型转换运算符:
struct A {explicit operator bool() const { return true; }
};
这样写可以避免在布尔上下文中被误用为 if (a)
这样的代码。
3.3 类的公有接口必须明确指明该类是可拷贝的、仅可移动的、还是既不可拷贝也不可移动的.
你定义一个类时,必须明确表明它是否支持拷贝和移动操作。如果你想让别人能复制或移动这个类的对象,就必须明确地支持;如果你不想支持,也应该明确禁止。不能模棱两可地“默认让编译器决定”。
设计意图 | 要做的事 |
---|---|
可拷贝 | 明确 = default 拷贝构造与赋值运算符 |
仅可移动 | 显式 = delete 拷贝操作,= default 移动操作 |
拷贝和移动都不允许 | 全部 = delete |
3.4 只能用 struct 定义那些用于储存数据的被动对象. 其他情况应该使用 class.
3.5 如果可以给成员起一个有意义的名字, 应该用结构体而不是数对 (pair) 或元组 (tuple).
3.6 通常情况下, 组合 (composition) 比继承 (inheritance) 更合适. 请使用 public 继承.
3.7 谨慎使用运算符重载 (overload). 禁止自定义字面量 (user-defined literal)
自定义字面量(User-defined Literals) 是 C++11 引入的一种语法扩展,允许你为自定义类型定义 带特殊后缀的常量值,看起来就像是对数字、字符串等原始数据加了“单位”或“标签”。
#include <iostream>constexpr long double operator"" _km(long double x) {return x * 1000; // 转换为米
}int main() {auto distance = 3.5_km; // 实际 distance = 3500std::cout << distance << " meters\n";
}
这个 _km
后缀就是你自己定义的字面量,允许你写出像 3.5_km
这样的代码,让数值变得更“人类友好”。
3.8 类的 所有数据成员应该声明为私有 (private), 除非是常量. 这样做可以简化类的不变式 (invariant) 逻辑, 代价是需要增加一些冗余的访问器 (accessor) 代码 (通常是 const 方法).
在 Google Test 中,有时例外:可以用 protected
.
Google Test 要求你的测试夹具类的测试函数(即 TEST_F(...)
内部)访问夹具的成员变量。如果成员变量是 private
,这些测试代码无法访问它。
所以,在测试类的声明和使用都在同一个 .cc
文件中时,Google 的规范允许你把数据成员设成 protected
,方便测试函数访问。
但如果测试类定义在 .h 文件中 ➜ 不允许这样做
如果你把测试夹具类放在头文件里(例如可能多个测试文件会用到这个类),那就要恢复正常封装规则,把成员设为 private
。
为什么?
- 因为
.h
文件是公共接口,对整个项目暴露; - 如果成员设为
protected
,别人可以在外部继承并访问; - 这打破了封装性,破坏了良好设计。
3.9 声明次序
类的定义通常以 public: 开头, 其次是 protected:, 最后以 private: 结尾.
在各个部分中, 应该将相似的声明分组, 并建议使用以下顺序:
-
类型和类型别名 (typedef, using, enum, 嵌套结构体和类, 友元类型)
-
(可选, 仅适用于结构体) 非静态数据成员
-
静态常量
-
工厂函数 (factory function)
-
构造函数和赋值运算符
-
析构函数
-
所有其他函数 (包括静态与非静态成员函数, 还有友元函数)
-
所有其他数据成员 (包括静态和非静态的)
4.函数
4.1 我们倾向于按值返回,否则按引用返回。避免返回指针,除非它可以为空.
4.2 我们倾向于编写简短, 凝练的函数.
4.3 使用函数重载时,必须确保“调用处”一眼就能看出调用的是哪个重载版本,而不需要读者反复查参数类型或翻代码去猜。否则就该避免重载,改为用不同的函数名(如 fromInt
、fromString
)来表达意图。
void Print(int count);
void Print(std::string_view message);Print(42); // 很明显是调用 Print(int)
Print("hello"); // 很明显是调用 Print(std::string_view)
这里调用点非常清晰,不会引起歧义。
4.4 只允许在非虚函数中使用缺省参数, 且必须保证缺省参数的值始终一致.一般推荐用函数重载代替缺省参数,除非默认参数真的能提升代码可读性
4.5 只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法.
-
常规写法:函数声明时,返回类型写在函数名前面,比如
int foo();
-
返回类型后置语法(主要是 C++11 引入的尾置返回类型写法)是把返回类型写在参数列表后面,用
->
指明返回类型,比如auto foo() -> int;
在大多数情况下,推荐用传统的写法(返回类型前置),因为它更常见,也更易懂。只有在那种情况下,比如返回类型特别复杂或依赖于模板参数,导致前置写法难写或难读时,才使用后置语法,这样写更清晰。
当返回类型依赖于模板参数时,用后置语法更直观:
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {return t + u;
}
5.来自 Google 的奇技
5.1 动态分配出的对象最好有单一且固定的所有主, 并通过智能指针传递所有权.
5.2 使用 cpplint.py 检查风格错误.
6.其他 C++ 特性
6.1 右值引用
1.只在定义移动构造函数和移动赋值操作符时使用右值引用
- 右值引用(
T&&
)主要用于实现 移动构造函数 和 移动赋值操作符,即当你想“搬走”一个对象内部资源而不是复制它时用。
class MyClass {
public:MyClass(MyClass&& other); // 移动构造函数,参数是右值引用MyClass& operator=(MyClass&& other); // 移动赋值操作符,参数是右值引用
};
- 右值引用最适合这两个地方,通常不要随意在其他函数参数里使用右值引用。
2. 不要使用 std::forward
函数
std::forward
是用在模板和完美转发场景里的,用来“完美转发”参数(保持参数的左值/右值属性)。- 在移动构造函数和移动赋值中,不需要用
std::forward
,因为你已经明确知道是右值引用了,直接用std::move
就可以。
3. 你可能会使用 std::move
来表示“移动而非复制”
std::move
是一个强制转换,表示“把这个左值变成右值引用”,从而触发移动语义。- 比如把资源从一个对象“搬走”而不是复制一份:
std::string s1 = "Hello";
std::string s2 = std::move(s1); // s1的内容被移动到s2,s1变为空或无效状态
6.2 若要用好函数重载,最好能让读者一看调用点(call site)就胸有成竹,不用花心思猜测调用的重载函数到底是哪一种。
6.3 我们不允许使用缺省函数参数,少数极端情况除外。尽可能改用函数重载。
6.4 我们不允许使用变长数组和 alloca().
特性 | 是否允许 | 原因 | 推荐替代 |
---|---|---|---|
VLA (int a[n] ) | ❌ 禁止 | 非标准、不安全、不可控栈使用 | std::vector<int> a(n); |
alloca() | ❌ 禁止 | 非标准、栈风险大、不可调试 | std::vector 或智能指针管理堆内存 |
6.5 我们允许合理的使用友元类及友元函数.
6.6 我们不使用 C++ 异常.
不是异常不好,而是:
- 现有代码大量不支持;
- 与老代码不兼容;
- 修改成本太高;
- 替代手段已经够用;
- 希望开源项目能内外复用。
Google 为了替代 C++ 中的异常机制,设计并广泛使用了 基于返回值的错误处理模式.
6.7 我们禁止使用 RTTI.
6.8 使用 C++ 的类型转换, 如 static_cast<>(). 不要使用 int y = (int)x 或 int y = int(x)等转换方式;
6.9 只在记录日志时使用流.其他地方一律不用流,优先使用格式化函数。
不推荐一般使用 C++ 流的原因:
- 效率低
- C++ 流是面向对象 + 虚函数机制的,性能远低于
printf
风格或absl::StrFormat
等现代库。
- C++ 流是面向对象 + 虚函数机制的,性能远低于
- 可读性差
- 流式拼接往往不如格式字符串直观清晰,尤其对于复杂格式化输出。
- 易错且难调试
- 类型推导和重载太灵活,容易出现隐式转换或意外格式。
- 国际化困难
- 流不支持本地化格式字符串,而格式函数更适合国际化(i18n)。
6.10 对于迭代器和其他模板对象使用前缀形式 (++i) 的自增, 自减运算符.
6.11 我们强烈建议你在任何可能的情况下都要使用 const. 此外有时改用 C++11 推出的 constexpr 更好。
1. 强烈建议尽可能使用 const
const
修饰符表示变量、函数参数或成员函数在使用过程中不会被修改。- 它可以帮助:
- 防止意外修改,增强代码安全性和可靠性。
- 提升代码可读性,让别人一眼看出哪些数据是只读的。
- 启用编译器更多优化。
- 帮助接口设计,明确哪些函数不修改对象状态(
const
成员函数)。
2. 适当使用 constexpr
constexpr
是 C++11 新引入的关键字,用于指示表达式或函数能在编译期计算。- 与
const
不同,constexpr
表示的是编译期常量,能带来更多的性能提升和更安全的代码。
6.12 在 C++11 里,用 constexpr 来定义真正的常量,或实现常量初始化。
6.13 整型
1. 默认只使用 int
- 在代码中默认使用
int
类型来表示整数。 int
类型通常是性能和可读性之间的良好平衡。
2. 需要特定大小时,使用 <stdint.h>
中的精确整型
- 如果程序中需要指定整数大小(比如网络协议、文件格式、跨平台兼容等),使用
<stdint.h>
里的类型:int16_t
(16位有符号整数)int32_t
(32位有符号整数)int64_t
(64位有符号整数)- 以及对应的无符号类型
uint16_t
,uint32_t
,uint64_t
。
3. 当变量可能超过 2³¹ (2GiB) 时,使用 64 位类型
- 如果你的变量可能存储超过 2^31 的值,就使用
int64_t
或者无符号的uint64_t
。
4. 注意计算过程中的溢出
- 即使变量的最终值不会超过
int
的范围,但在计算过程中(比如乘法、累加等),可能会产生溢出。 - 如果不确定,建议直接使用更大的类型(比如
int64_t
),以避免隐患。
6.14 代码应该对 64 位和 32 位系统友好.
方面 | 注意事项 | 推荐做法 |
---|---|---|
打印 | 避免格式符错误,使用 <inttypes.h> 宏 | 使用 PRId64 等标准宏 |
比较 | 避免有符号和无符号混用,避免隐式转换 | 明确类型,使用固定宽度类型 |
结构体对齐 | 跨平台可能差异,影响大小和字段偏移 | 明确对齐规则,避免依赖平台默认对齐 |
6.15 使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之.
宏用途 | 推荐替代 | 优点 |
---|---|---|
常量定义 | const 或 constexpr | 类型安全,调试友好 |
简单函数 | inline 函数 | 类型检查,避免多次求值,易维护 |
枚举常量 | enum 或 enum class | 类型安全,命名空间,易读 |
- 传统枚举 (
enum
)
enum Color {Red, // 默认值从0开始Green, // 1Blue // 2
};Color favorite = Green;
特点:
- 整型常量,默认从 0 开始递增。
- 所有枚举值共享同一作用域(全局),容易与其他符号冲突。
- 可隐式转换为
int
,不够类型安全。
- 枚举类 (
enum class
) — C++11 引入,推荐使用
enum class Shape {Circle,Square,Triangle
};Shape s = Shape::Circle;
特点:
- 枚举值作用域限制在枚举类型内部,使用时需加前缀
Shape::
,避免命名冲突。 - 不能隐式转换为整型,类型安全。
- 可指定底层类型:
enum class Status : uint8_t {OK = 0,Error = 1,Unknown = 255
};
- 宏替代示例
// 宏定义(不推荐)
#define STATUS_OK 0
#define STATUS_ERROR 1// 替代用 enum class
enum class Status {OK = 0,Error = 1
};
6.16 指针使用 nullptr,字符使用 ‘\0’ (而不是 0 字面值)。
6.17 尽可能用 sizeof(varname) 代替 sizeof(type).
变量类型如果发生变化,sizeof(varname)
会自动更新,无需额外修改。
6.18 用 auto 绕过烦琐的类型名,只要可读性好就继续用,别用在局部变量之外的地方。
6.19 你可以用列表初始化。
int x{5}; // 列表初始化基本类型
std::vector<int> v{1, 2, 3, 4}; // 初始化容器struct Point {int x;int y;
};Point p{10, 20}; // 初始化结构体
6.20 适当使用 lambda 表达式。当 lambda 将转移当前作用域时,首选显式捕获。
int x = 10;
int y = 20;// 隐式捕获(全部按值捕获)
auto lambda1 = [=]() {return x + y;
};// 显式捕获
auto lambda2 = [x, y]() {return x + y;
};
6.21 不要使用复杂的模板编程
6.22 只使用 Boost 中被认可的库
Boost 是一个非常知名且广泛使用的 C++ 开源库集合,涵盖了许多实用功能和工具。
6.23 适当用 C++11(前身是 C++0x)的库和语言扩展,在贵项目用 C++11 特性前三思可移植性。
7.命名约定
7.1 函数命名, 变量命名, 文件命名要有描述性; 少用缩写.
7.2 文件名要全部小写, 可以包含下划线 (_)
或连字符(-)
, 依照项目的约定. 如果没有约定, 那么“_”更好.
7.3 类型名称的每个单词首字母均大写, 不包含下划线: MyExcitingClass, MyExcitingEnum.
7.4 变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用, 如: a_local_variable, a_struct_data_member, a_class_data_member_.
7.5 声明为 constexpr 或 const 的变量, 或在程序运行期间其值始终保持不变的, 命名时以“k”开头, 大小写混合.
7.6 常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配: MyExcitingFunction(), MyExcitingMethod(), my_exciting_member_variable(), set_my_exciting_member_variable().
7.7 命名空间以小写字母命名. 最高级命名空间的名字取决于项目名称.
7.8 枚举的命名应当和常量 或宏 一致: kEnumName 或是 ENUM_NAME.
7.9 你并不打算使用宏, 对吧? 如果你一定要用, 像这样命名: MY_MACRO_THAT_SCARES_SMALL_CHILDREN
8.注释
8.1 使用 // 或 /* */, 统一就好.
8.2 在每一个文件开头加入版权公告。
1.法律公告和作者信息
2.文件内容