Google C++代码规范
Google官方文档:https://google.github.io/styleguide/cppguide.html
一张图总结规范:https://blog.csdn.net/zyy617532750/article/details/81264648
为什么需要对代码进行规范?
- 提高可读性:通过规范化代码格式、命名方式和结构,可以让团队中的所有成员更容易理解和维护代码。
- 减少沟通成本:避免个人风格的冲突,注于逻辑和功能
- 减少潜在的错误:代码规范包含最佳实践(如命名规则、指针使用的注意事项等),遵守这些规范可以有效减少因风格不一致引起的潜在错误。
Google Style Guide
是业界广泛认可的代码风格指南,其权威性和普适性使其成为许多团队和个人学习和参考的对象以下是我个人的自我总结 Google Style Guide的相关知识(减少了一些模块(个人暂时未使用到的))
一、头文件
1.1 #define 保护
目的:通过
#define
来防止头文件被多重包含命名格式:_
__H
//示例
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
// 头文件相关声明...
#endif // FOO_BAR_BAZ_H_
1.2 避免前置声明
什么是前置声明?
定义:指在程序中提前声明一个类、函数或其他标识符,而不提供其完整定义。
class MyClass; // 前置声明
作用(优点):用于解决依赖问题或减少头文件的耦合,节约编译时间。
如果头文件只需要引用某个类的指针或引用类型,可以通过前置声明避免包含整个类的定义,从而减少编译依赖。
class MyClass; // 前置声明 class AnotherClass { MyClass* ptr; // 指针可以使用前置声明 };
为什么避免前置声明?(缺点)
代码的可维护性降低,隐藏了依赖关系(可能会让人忽略头文件变化后必要的重新编译过程)。
缺乏上下文信息:前置声明只能告诉编译器类的存在,无法检查类的具体内容(例如继承关系或成员函数)。如果代码中需要类的定义,就必须引入对应的头文件。
容易引发错误: 当前置声明的类被错误使用(如声明对象、调用成员函数等)时,可能导致编译错误或逻辑问题。
class MyClass; // 前置声明 MyClass obj; // 错误:无法实例化类
1.3 内联函数
只把 10 行以下的小函数定义为内联 (inline).(不要包含循环、switch、递归、析构、虚函数)
1.4 #include 路径及顺序
注意事项: 不能出现 UNIX 目录别名 (alias)
.
(当前目录) 或..
(上级目录)
//路径:google-awesome-project/src/base/logging.h #include "base/logging.h"
顺序:
- 源文件的头文件
- C头文件
- C++头文件
- 第三方库的头文件
- 本项目的头文件
//举例 #include "foo/server/fooserver.h" #include <sys/types.h> #include <unistd.h> #include <string> #include <vector> #include "base/basictypes.h" #include "foo/server/bar.h" #include "third_party/absl/flags/flag.h"
二 、作用域
2.1 命名空间
目的:命名空间将全局范围细分为不同的命名范围,因此有助于防止全局范围内的名称冲突。
命名:
_ 注意事项:应该在
namespace
内放置代码,禁止使用using
(例如:using namespace foo
),禁止内联 (inline) 命名空间// .h 文件 namespace mynamespace { // 所有声明都位于命名空间中. // 注意没有缩进. class MyClass { public: ... void Foo(); }; } // namespace mynamespace // .cc 文件 namespace mynamespace { // 函数定义位于命名空间中. void MyClass::Foo() { ... } } // namespace mynamespace
2.2 内部连接
什么是内部连接?
- 内部链接(Internal Linkage)用于描述标识符(如变量、函数)在编译单元(Translation Unit)内的可见性和作用范围。具有内部链接的标识符仅在定义它的编译单元中可见,其他编译单元无法直接访问这些标识符。
说明:若其他文件不需要使用
.cpp
文件中的定义, 可以将这些定义放入匿名命名空间 (unnamed namespace) 或声明为static
. 但是不要在.h
文件中使用这些手段.//实现内部链接的两种方法 // file1.cpp static int counter = 0; // 内部链接,仅在 file1.cpp 中可见 static void incrementCounter() { ++counter; } // file2.cpp namespace { int value = 42; // 内部链接,仅在 file2.cpp 中可见 void printValue() { std::cout << value << std::endl; } }
2.3 非成员函数、静态成员函数和全局函数
Nonmember, Static Member, and Global Functions
说明:建议非成员函数放入命名空间,不要使用全局函数,类的静态函数应当和类的实例或静态数据紧密相关.
结论:
定义一个和类的实例无关的函数 -> 非成员函数、静态成员函数
非成员函数 -> 位于命名空间内,且不应该依赖外部变量
2.4 局部变量
应该尽可能缩小函数变量的作用域 (scope),且声明离第一次使用的位置越近越好。并在声明的同时初始化。
注意:如果变量是一个对象, 那么它每次进入作用域时会调用构造函数, 每次退出作用域时都会调用析构函数.
//示例
int i = f(); // 良好: 声明时初始化。
vector<int> v = {1, 2}; // 良好: 立即初始化 v.
int jobs = NumJobs();
f(jobs); // 良好: 初始化以后立即 (或很快) 使用.
//如果变量是一个对象:循环的作用域外面声明这类变量更高效
Foo f; // 调用 1 次构造函数和析构函数.
for (int i = 0; i < 1000; ++i) {
f.DoSomething(i);
}
2.5 静态和全局变量
禁止使用该类变量(基本数据类型可以)
原因:
1. 破坏封装性
- 静态和全局变量可以被整个程序访问,使得数据暴露在外部,无形中增加了模块之间的耦合。
- 这违背了封装的设计原则,破坏了模块化代码的可维护性和灵活性。
2. 引发隐式依赖
- 静态和全局变量的修改可能影响程序中的多个模块,但这些依赖关系通常是隐式的,难以追踪和理解。
- 当一个模块修改了全局变量,其他模块的行为可能会受到影响,从而引发意料之外的错误。
3. 难以调试
- 静态和全局变量的值可以在程序的任何地方被修改,这种全局可变状态使得调试变得困难。
- 如果某个地方修改了全局变量,找到问题的根源可能非常耗时。
4. 线程安全问题
- 在多线程程序中,全局和静态变量需要显式加锁来防止数据竞争(race conditions)。
- 使用不当可能导致死锁、竞态条件或其他并发问题,增加代码的复杂性。
5. 降低代码的可测试性
- 单元测试通常要求代码模块是独立的,能够隔离测试其行为。静态和全局变量引入了共享状态,导致模块之间相互依赖,从而难以进行隔离测试。
6. 难以扩展
- 静态和全局变量使得模块的状态依赖外部数据,从而限制了代码的可扩展性。例如,在需要重构或扩展某个模块时,可能不得不修改全局变量的逻辑,导致大范围的代码调整。
三、类
3.1 构造函数的职责
构造函数 (constructor) 中不得调用虚函数 (virtual method)。不要在没有错误处理机制的情况下进行可能失败的初始化。
不能调用虚函数的原因:
对象的构造是按照类的继承层次从基类到派生类依次构造进行的
- 在构造基类对象时,派生类的部分尚未初始化完成。
- 直到派生类的构造函数被执行完毕,派生类的成员变量和虚表(vtable)才完全设置完成。
如果在基类构造函数中调用虚函数,由于派生类尚未完全初始化,调用的将是基类版本的虚函数,而不是派生类的重写版本。这可能会导致行为不符合预期。
//示例 #include <iostream> class Base { public: Base() { // 虚函数调用 show(); //此时构造调用的是基类的虚函数 } virtual void show() { std::cout << "Base::show()" << std::endl; } }; class Derived : public Base { public: Derived() : Base() {} void show() override { std::cout << "Derived::show()" << std::endl; } }; int main() { Derived obj;//明明我定义的是派生类的对象,但是调用的确实基类的虚函数 return 0; }
3.2 隐式类型转换
抑制隐式类型转换,定义类型转换运算符和单个参数的构造函数时, 请使用
explicit
关键字
3.3 可拷贝类型和可移动类型
必须明确指明该类是可拷贝的、仅可移动的、否则把隐式产生的拷贝和移动函数禁用。
//禁止拷贝、移动 class NotCopyableOrMovable { public: // 既不可复制也不可移动. NotCopyableOrMovable(const NotCopyableOrMovable&) = delete; NotCopyableOrMovable& operator=(NotCopyableOrMovable&) = delete; // 移动操作被隐式删除了, 但您也可以显式声明: NotCopyableOrMovable(NotCopyableOrMovable&&) = delete; NotCopyableOrMovable& operator=(NotCopyableOrMovable&&) = delete; };
什么时候要禁止拷贝、移动?
- 需要独占资源时:资源(动态内存、文件句柄、网络连接)不能被多个实例共享或复制,则应禁用拷贝。
- 逻辑上不应该被拷贝:如 单例模式。独占访问的资源(如线程、锁)。
- 成员变量本身不可拷贝或不可移动:类的成员包含 不可拷贝 或 不可移动 的对象(如
std::unique_ptr
、std::mutex
),需要明确禁用拷贝或移动。
3.4 组合 > 继承
组合 (composition) 比继承 (inheritance) 更合适,如需继承请定义为
public
什么是组合关系?
“has-a”(有一个)关系,一个对象由其他对象作为其成员变量组成。
被组合的类的对象随着宿主类的对象的创建和销毁而自动管理。
//举例:小汽车有一个发动机,小汽车启动调用发动机启动,小汽车销毁则发动机销毁 class Engine { public: void start() { std::cout << "Engine started" << std::endl; } }; class Car { private: Engine engine; // Car 组合了 Engine public: void start() { engine.start(); std::cout << "Car started" << std::endl; } }; int main() { Car car; car.start(); return 0; }
组合的优点:
代码复用
- 通过组合类可以直接复用已实现的类,而不需要重新编写代码。
灵活性
- 组合提供了更大的灵活性,可以自由选择被组合的类,而不会像继承那样被基类限制。
解耦性
- 组合优先于继承,因为它避免了子类和基类之间的强耦合,便于扩展和维护。
3.5 访问控制
类的 数据成员应该声明为私有 (private), 除非是常量. 这样做可以简化类的不变式 (invariant) 逻辑, 代价是需要增加一些冗余的访问器 (accessor) 代码 (通常是 const 方法).
3.6 声明顺序
类的定义通常以
public:
开头, 其次是protected:
, 最后以private:
结尾.在各个部分中, 应该将相似的声明分组, 并建议使用以下顺序:
- 类型和类型别名 (
typedef
,using
,enum
, 嵌套结构体和类, 友元类型)- (可选, 仅适用于结构体) 非静态数据成员
- 静态常量
- 工厂函数 (factory function)
- 构造函数和赋值运算符
- 析构函数
- 所有其他函数 (包括静态与非静态成员函数, 还有友元函数)
- 所有其他数据成员 (包括静态和非静态的)
class MyClass { public: // 类型和类型别名 typedef int Integer; using String = std::string; enum Color { Red, Green, Blue }; struct Point { }; friend void someFriendFunction(MyClass&); // (可选) 非静态数据成员 int id_; std::string name_; // 静态常量 static const int MAX_SIZE = 100; // 工厂函数 static MyClass createWithId(int id) { return MyClass(id); } // 构造函数和赋值运算符 MyClass(int id, const std::string& name) : id_(id), name_(name) {} MyClass& operator=(const MyClass& other) { if (this != &other) { id_ = other.id_; name_ = other.name_; } return *this; } // 析构函数 ~MyClass() {} // 所有其他函数 void display() const { std::cout << "ID: " << id_ << ", Name: " << name_ << std::endl; } private: // 所有其他数据成员 static int count_; // 静态数据成员 };
四、函数
4.1 输入和输出
函数的输出倾向于按值返回(而非输出参数), 否则按引用返回。 避免返回指针
输入参数为值或者为const引用
排序函数时,输入参数放在任何输出参数之前
4.2 简短函数
如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割.
4.3 函数重载
若要使用函数重载, 则必须能让读者一看调用点就胸有成竹, 而不用花心思猜测调用的重载函数到底是哪一种
4.4 缺省参数
不建议使用缺省参数,而是用函数重载
原因:
接口的二义性, 隐藏了调用意图
函数调用
print(5)
和print()
的意图不够直观,用户需要查阅函数定义才能确定行为。void print(int x = 0, int y = 10) { std::cout << "x: " << x << ", y: " << y << std::endl; } print(); // 输出: x: 0, y: 10 print(5); // 输出: x: 5, y: 10 print(5, 15); // 输出: x: 5, y: 15
五、其它C++特性
5.1 右值引用
只在定义移动构造函数与移动赋值操作时使用右值引用, 不要使用
std::forward
功能函数. 你可能会使用std::move
来表示将值从一个对象移动而不是复制到另一个对象.
5.2 友元
友元应该定义在同一文件内, 避免代码读者跑到其它文件查找使用该私有成员的类。
如果你只允许另一个类访问该类的私有成员时,使用友元是更好的选择
5.3 禁用运行时类型识别
禁止使用
typeid
或者dynamic_cast
5.4 类型转换
不要使用 C 风格类型转换. 而应该使用 C++ 风格.
- 用
static_cast
替代 C 风格的值转换, 或某个类指针需要明确的向上转换为父类指针时.- 用
const_cast
去掉const
限定符.- 用
reinterpret_cast
指针类型和整型或其它指针之间进行不安全的相互转换. 仅在你对所做一切了然于心时使用.
六、命名约定
类型 | 命名规则 |
---|---|
文件命名 | 全部小写,各单词用"_"连接(my_useful_class.cpp ) |
类型命名 类、结构体、typedef、enum | 首字母大写,不用下划线(MyExcitingClass ) |
变量命名 | 普通/结构体变量:全部小写,各单词用"“连接(string table_name; )类成员变量:在普通变量的末尾添加下划线”"( string table_name_; ) |
常量命名 | ”K“开头,后续所有单词首字母大写(const int kDaysInAWeek = 7; ) |
函数命名 | 驼峰命名,没有下划线(void AddTableEntry() ) |
命名空间命名 | 全部小写,各单词用"_"连接 |
枚举值命名 | ”K“开头,后续所有单词首字母大写(kOK = 0 ) |
宏命名 | 全部大写,单词之间下划线连接(#define PI_ROUNDED 3.0 ) |
七、注释
采用
Doxygen
注释风格
- 单行注释
///
- 行尾注释
///<
- 多行注释
/***/
7.1文件开头注释
在每一个文件开头加入版权公告.
文件注释描述了该文件的内容. 如果一个文件只声明, 或实现, 或测试了一个对象, 并且这个对象已经在它的声明处进行了详细的注释, 那么就没必要再加上文件注释. 除此之外的其他文件都需要文件注释.
/**
* @file <filename>.cpp
* @brief <简要描述文件的功能或用途>
*
* @author <作者姓名或团队名称>
* @date <创建日期,例如 YYYY-MM-DD>
* @version <版本号,例如 1.0.0>
*
* @details <详细描述文件的内容,包括主要功能、算法或实现逻辑>
* <可选: 对使用方法或特殊注意事项进行说明>
*/
7.2 类注释
每个类的定义都要附带一份注释, 描述类的功能和用法,
/**
* @brief <简要描述类的功能或用途>
* 类的详细介绍
*/
7.3 函数注释
每个函数声明处前都应当加上注释, 描述函数的功能和用途
函数声明处注释的内容:
- 函数的输入输出.
- 对类成员函数而言: 函数调用期间对象是否需要保持引用参数, 是否会释放这些参数.
- 函数是否分配了必须由调用者释放的空间.
- 参数是否可以为空指针.
函数定义处应介绍实现时的关键步骤
/**
* @brief <函数的简要描述>
*
* @details <函数的详细描述,包括逻辑、算法等>
*
* @param[in] <参数名> <参数的描述,标明输入>
* @param[out] <参数名> <参数的描述,标明输出>
* @param[in,out] <参数名> <既是输入也是输出的参数>
* @return <返回值的描述,如果是 void 可忽略>
*/
八、格式
- 行宽原则上不超过 80 列, 把 22 寸的显示屏都占完, 怎么也说不过去;
- 尽量不使用非 ASCII 字符, 如果使用的话, 参考 UTF-8 格式 (尤其是 UNIX/Linux 下, Windows 下可以考虑宽字符), 尽量不将字符串常量耦合到代码中, 比如独立出资源文件, 这不仅仅是风格问题了;
- UNIX/Linux 下无条件使用空格,(修改IDE配置);
- 函数参数, 逻辑条件, 初始化列表: 要么所有参数和函数名放在同一行, 要么所有参数并排分行;
- (个人习惯更喜欢Windows风格)除函数定义的左大括号可以置于行首外, 包括函数/类/结构体/枚举声明, 各种语句的左大括号置于行尾, 所有右大括号独立成行;
.
/->
操作符前后不留空格,*
/&
不要前后都留, 一个就可, 靠左靠右依各人喜好;- 预处理指令/命名空间不使用额外缩进, 类/结构体/枚举/函数/语句使用缩进;
- 初始化用
=
还是()
依个人喜好, 统一就好; return
不要加()
;- 水平/垂直留白不要滥用, 怎么易读怎么来.
- 关于 UNIX/Linux 风格为什么要把左大括号置于行尾 (
.cc
文件的函数实现处, 左大括号位于行首), 我的理解是代码看上去比较简约, 想想行首除了函数体被一对大括号封在一起之外, 只有右大括号的代码看上去确实也舒服; Windows 风格将左大括号置于行首的优点是匹配情况一目了然.