抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Google C++ 代码规范

Google C++ Style Guide

Google提出的一套代码规范,能提高代码的可读性,减少出错。建议配合Effective C++阅读

头文件

所有头文件都应该自给自足(self-contained)

self-contained : compile on their own

确保你的header files包含了你需要的所有东西,而不是假设你#include进来的某个(些)headers帮你包含了你需要的东西。

A.h包含了B.h和C.h,而B.h也包含了C.h,那么在A.h中使用C.h时,会难以判断到底该用哪一个

#define 保护

每个头文件都要使用#pragma once保护

前置声明

我个人理解的前置声明,是指在一个.h文件中开头声明另一个.h文件中的类,以此来实现类似#include某个文件的某一部分

与之对应的是直接使用#include引入文件,但这样实际会增加编译工作量,降低编译速度,于是我们非必要不#include

Google建议要避免使用前置声明,因为使用前置声明会影响类的修改,也容易产生错误

但使用前置声明可以提高编译速度,因此需要按情况选择

优点

  • 节省编译时间,避免很多不必要的(因为头文件改动而导致的)重新编译

缺点

  • 隐藏了依赖关系
  • 头文件改动时会破坏后续,影响开发者改动API(比如当你要添加一个形参时)
    • 但这个是不是可以通过宏的方法避免?#define Func Func

两个类互相引用

前置声明(Forward Declarations)基本仅用于两个类互相引用

// A.h
#include "B.h"
class A
{
B b; //A中使用了B,因此
...
}
// B.h
class A;
class B
{
A* a;
...
}

失去依赖关系

前置声明最大的问题是失去依赖关系

// B.h
struct B {};
struct D : B {}; //这里的D继承自B
#include "b.h"
// 如果使用前置声明替换掉#include,就会出现错误
void f(B*);
void f(void*);
void test(D* x) { f(x); } // Calls f(B*)

内联函数

除非是极其高频调用,确定是性能瓶颈的地方,不然不用内联函数

不要内敛超过十行的函数

析构函数远比表面要长,因为他隐式调用着成员和基类的析构函数

#include路径

按照源码目录树结构排列,避免使用UNIX的快捷目录,比如.(当前目录)和..(上级目录)

#include顺序

  1. C头文件
  2. C++头文件
  3. 第三方库头文件
  4. 本项目头文件

作用域

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

#include "a.h"

namespace MyNamespace
{
class MyClass
{
...
};
} //namespace MyNamespace

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

不要污染命名空间

  • 不要在std命名空间中声明任何东西
  • 不要使用using namespace xxx
  • 不要在头文件中使用命名空间别名,如namespace bbb = ::Foo::Bar::Baz

静态变量

不要在.h文件中声明静态变量,至少不能用全裸的静态变量

不要定义静态存储周期POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中静态变量的构造和析构顺序不确定,会导致bug(除非你使用constexpr)

原生数据类型(POD,Olain Old Data),如int、char、float,int*、int[]、结构体

在同一编译单元,静态初始化会优先于动态初始化,并按照声明顺序初始化,以逆序销毁

但是不同编译单元,初始化、销毁顺序是未定义行为

构造函数不要调用虚函数

在类被完全初始化前,这个类其实是基类+其他部分,此时调用虚函数不一定会指向子类的重载,可能会报错、出错

优点

  • 不需要考虑类是否被完全初始化
  • 初始化的对象可以为const类型

如果你想进行有意义的non-trivial初始化,可以使用明确的Init()方法,或者工厂模式

与之对应,析构函数要设为虚函数

不要隐式类型转换

不要使用隐式类型转换

  • 隐式转换会降低代码可读性,由于是存在函数重载时,让人难以判断到底在用哪一个函数
  • 隐式转化可能会导致类型不匹配的错误

使用explict关键词(常用于单参数构造函数和类型转化操作符),可以使当前类型只能被显示类型转换,如果使用隐式转化,会报错

class Foo
{
explicit Foo(int x, int y); //这个类型不能被隐式转化,调用Func({42, 3.14})会报错
...
}
void Func(Foo f);

拷贝和移动

如果你的类型需要拷贝和支持,就请实现它,否则禁用它(=delete

std::unique_ptr<int> 可移动,但是不能复制

  • 可拷贝类型,可以使用相同类型的另一对象进行初始化,当前对象会得到相同的值,同时不改变源对象。
    • 用户通过定义拷贝构造函数、拷贝赋值操作符实现
  • 可移动对象,可以使用相同类型的临时对象进行初始化,当前对象会得到相同的值
    • 用户通过定义移动构造函数、移动赋值操作符实现

不要为任何有可能有子类的类型提供拷贝和移动,可能会导致对象割裂

除了数据成员,其他一律用class

C++的class和struct很像,大部分功能相同,不过

  • struct可以拥有成员变量,但不能拥有成员函数(你可以写非成员函数+引用)
  • struct成员为public,而class可以拥有private成员
  • struct的继承方式为public,比class少(尽管全世界C++项目99%的继承都是public继承)

struct与pairs、tuples

struct的字段名更具可读性,当数据有意义时,尽量使用struct

不过pairs和tuples更适用于泛型编程

组合与继承

组合很好用,能用接口用接口,如果非要用继承,请public继承

对于虚函数进行重载时,用override、final标记,尽管这个关键字没有什么作用,但能提高代码可读性

全世界C++项目99%的继承都是public继承

public、protected、private继承你可以理解为设限

如果是public继承,那么子类访问父类成员的存取类型都不会超高public(废话),父类的public、protected、private成员,对于子类为public、protected、private

如果是protected继承,那么子类访问父类成员的存取类型都不会超高protected,父类的public、protected、private成员,对于子类为protected、protected、private

请不要使用多重继承,不过你能继承多个接口

关于接口

  • 接口是用interface标记的类,只有纯虚数和静态函数,没有非静态成员
  • 接口不能被直接实例化,也不需要定义构造函数
  • 请为接口实现虚析构函数
  • 请不要为其添加函数实现或非静态成员数据

操作符重载

尽量别重载

访问控制

类的数据成员应该为private,除非是一个常量

数据成员设为private,然后编写public的访问函数,不过感觉不如C#的属性优雅

声明顺序

随便找个.h文件,看看类的声明是怎么写的

  • 相似的声明放在一起,并按以下顺序
    • 类型及类型别名(typedef, using, enum ,嵌套的结构体和类)
    • 静态常量
    • 工厂函数
    • 构造函数和赋值运算符
    • 析构函数
    • 其他函数
    • 数据成员
  • 先写public,再写protected、private

不要将大段的函数定义写在类定义中,建议.h声明,.cpp定义

函数

参数

见名知意,知行合一

看到函数名就能知道这是干什么的,比如Get、Set,如果一个函数是Get,那就做Get的事

  • 优先使用返回值而非输出参数,这能提高可读性和性能
  • 不要返回空指针(除非你是这样设计的)
  • 输入参数、不需要改变的参数可以用const引用
  • 输出参数、可以被修改的参数可以用指针
  • 参数需要排序,输入先于输出,无默认值先于有默认值
void foo(const std::string& in_str, std::string* out_str);

简短的函数

个人感觉完全没必要,尤其是对于C#、Java,40行好干什么

简短的函数能提高代码可读性,提高调试效率,在函数式编程很常用

函数重载

函数重载能够使同一作用域中,有一组相同函数名、不同参数列表的函数,常用于命名一组功能相似的函数

不过如果函数仅靠参数类型进行重载,就会涉及匹配、派生类之类的问题,让人感受困惑

如果要重载Append(),其实可以在函数名上添加类型信息,比如AppendInt()、AppendString()

缺省参数

写缺省不如写重载

缺省本质上就是一种函数重载,所有不适合使用重载的地方,都不适合使用缺省

由于缺省参数会干扰函数指针,因此一定不要在虚函数中使用缺省参数

函数返回值的后置写法

后置写法本身没问题,swift、js等语言都是这样写的,但是对于C++来说是一种“很新的东西”,容易让人感到困惑

C++11后,C++的函数可以使用后置返回类型,不过除了lambda表达式,一般不这样写

// 前置写法
int foo(int x);

// 后置写法
auto foo(int x) -> int;

// lambda只能后置写法
sort(vec.begin(), vec.end(), [](int a, int b) -> bool { return a < b; });

在泛型编程中,当返回类型依赖于模版参数时,也可以使用后置写法,能提高可读性

// 前置写法
template<class T, class U>
decltype(declval<T&>() + declval<U&>()) add(T t, U u);

// 后置写法
template<class T, class U>
auto add(T t, U u) -> decltype(t + u);

所有权和智能指针

关于所有权和智能指针,可以参考垃圾回收中引用计数法

C++标准鼓励我们使用智能指针管理资源

对于一个动态的对象,我们更倾向于让其拥有单一、固定所有权(ownership),并使用指南指针做所有权转换

  • std::unique_ptr,独占资源
    • 当指针离开作用域,资源就会被销毁
    • 无法复制(copy),但能转移(move)所有权
  • std::shared_ptr,共享资源
    • 当资源失去所有引用时,资源被销毁
    • 可以复制,共享所有权(无需转移)

好处

  • 有的对象甚至没法复制,只能转移
  • 转移通常比复制更高效,尤其是一些const对象,转移比深拷贝高效得多
  • 使用智能指针能提高可读性,也减少了内存泄漏

其他

右值引用

关于左右值,建议阅读C++11

只在定义移动构造函数和移动赋值操作时使用右值引用

推荐使用std::move,不要使用std::forward(除非你在模版编程)

// copy
void set(const string & var1, const string & var2){
m_var1 = var1;
m_var2 = var2;
}
// move
void set(string && var1, string && var2){
m_var1 = std::move(var1);
m_var2 = std::move(var2);
}
// forward
template<typename T1, typename T2>
void set(T1 && var1, T2 && var2){
m_var1 = std::forward<T1>(var1);
m_var2 = std::forward<T2>(var2);
}

forward的作用是什么?

如果不使用forward,我们需要同时定义copy和move函数,在处理左值时调用copy,处理右值调用move,这增加了代码量,如果使用forward,我们只需要写一份

但forward让一个函数能干两种事,这不符合“知行合一”,为了代码可读性,我们通常不会使用。如果我们明确这个函数要move,那就用move

友元

友元:在定义一个类时,可以将一些(定义在外部的)函数声明为友元,这些友元函数可以访问该类的private、protected成员

  • 友元扩大了类的封装,在OOP中很忌讳,但只要合理,还是可以用的
    • 原本是private的成员,对于友元类、函数,都是public的
    • 友元类似一种许可,当一个类设置友元后,相当于给友元开了管理员权限,能随意访问原本受保护的成员
  • 如果你要用,请写在同一个文件中
class Child{
public:
Child() { name = "default name"; }
string getName() { return name; }
friend class Mother; // 母亲有权利修改孩子的名字,尽管setName是private的
private:
string name;
void setName(string nn) { name = nn; }
};

class Mother{
public:
Child child;
void renameChild(string nn){ child.setName(nn); }
};

int main(){
Mother mother;
Child child;
mother.child = child;
// child.setName()的访问控制为private,你没法在这里调用
cout << mother.child.getName() << endl; // default name
mother.renameChild("Tom");
cout << mother.child.getName() << endl; // Tom
return 0;
}

异常

谷歌不使用C++异常,让异常Let it crash。不过异常在C#、Java、Swift中很常用,

抛异常可能会导致一些未定义行为,比如不要在析构函数中使用异常

不使用异常,如果出错常用方法为:

  • 直接abort()
  • 吞异常,Let it crash

RTTI

不使用运行时类型识别,不使用typeiddynamic_cast

运行时类型识别会使代码难以维护,如果你需要用RTTI(除了单元测试),说明你的类设计的不好

类型转化

使用C++风格的类型转化(而非C风格的)

double double_value;
// C++
float f = static_cast<float>(double_value);
// C
float f2 = (float)double_value;
  • 使用static_cast做值转换、子类指针转父类指针
  • 使用const_cast去掉const限定符
  • 使用reinterpret_cast做指针和整型、指针和指针间的转换(仅在你会用时使用)
  • 不使用dynamic_cast

严格别名

C++的reinterpret_cast不会编译为任何CPU指令,会编译为纯汇编,于是你可以像汇编一样操作指向内存的指针地址,破坏了C++对汇编对抽象

严格别名(strict aliasing)规定:只有同一种类型的指针,才可以出现别名,总之没事别做不相干类型的指针转化

float* f;
int* i;
// 编译器优化时会假定 f != i
// 若你的代码让 f == i (比如使用reinterpret_cast),那就是未定义行为,违反严格别名规则

当你通过指针,将一个struct塞到一个buffer中,或者将一个buffer塞到struct中,在这个过程中,指针的类型发生转换,从buffer转化为struct

typedef struct Msg {
int a;
int b;
} Msg;

int main() {
int x[2] = {1, 2};
int* p = x;

Msg* msg = (Msg*)p; // Msg* msg = reinterpret_cast<Msg*>(p);
cout << msg->a << endl; // 1
cout << msg->b << endl; // 2
return 0;
}

上面这个过程干了什么?你知道这个buffer的内存地址、长度,你想直接用这些信息访问内存,这是对的,但是某些情况下这就是未定义行为

谷歌认为除了日志接口需要,不要使用流

说实话我感觉流挺方便的,只要能保持输出的一致性就行,而且C++的Stream类型安全,用起来十分方便,尤其是输出字符串和对象时

自增与自减

谷歌建议一律使用前置自增自减,尤其是迭代器

一般而言,使用前置自增++i能提高性能,后置会生成临时对象和拷贝复制

不过,如果自增自减后变量并没有被用到,仅仅是用于记录迭代次数,编译器会对后置自增自减进行优化,性能一样,而且后置更像自然语言,可读性更强

const

在任何能使用const的地方使用const或constexpr

整型

C++内置的整型只,只使用int

如果需要明确长度,那就用<cstdint中的int16_tint64_t

如果你不确定用何种大小的int,那么用最大的

可移植性

代码应该对32位和64位系统友好,在处理打印、比较、结构体对齐时要注意

不是所有人都在用64位系统(不过iOS这种封闭平台确实做到了完全禁用32位)

预处理宏

尽管在图形Shader中,用宏进行条件变异很常用,但这其实是因为GPU对分支的处理不好,因此我们通过编译多份代码来避免运行时使用分支。不过这对CPU代码纯属是提高包体,降低可读性,增加调试难度,不推荐使用

  • 宏具有全局作用域,使你看到的和编译器看到的内容不同,尽量用内联、const进行替换

  • 不要使用宏做条件编译

  • 不要在.h文件中定义宏

  • 使用完#define后要立刻#undef

  • 不要使用展开后让C++构造不稳定的宏

#define max(a,b) (a > b ? a : b)

int x = 5, y = 6;
int n = max(++x, ++y); // 本质为 int n = (++x > ++y ? ++x : ++y),和预期不一致
  • 不要在宏后面写单行注释
  • 不要用##处理函数、类、变量的名字,可读性很差
// 用##将两个宏拼起来,很trick的写法
#define CONS(a,b) int(a##e##b)
cout << CONS(2, 3) << endl; // 输出2000,因为CONS宏将2和3拼成了2e3

nullptr

空指针用nullptr,数值用0(或者0.0),std::string\0

绝对不要用NULL

sizeof

sizeof用于获取类型的大小,不过不建议对类型使用,而是对varname使用

MyStruct data;
memset(&data, 0, sizeof(MyStruct)); // 对类型使用,不推荐
memset(&data, 0, sizeof(data)); // 对varname使用,推荐
// 如果有人改动了data的类型,比如改成了MyStruct2,第二种方法不会出错

类型推断

函数模版参数类型推断

template<typename T>
void f(T t);

f(0); // f<int>(0)

类模版类型推断

Class Template Argument Deduction(CTAD)

std::array a = {1, 2, 3};		// a is a std::array<int, 3>

auto变量类型推断

若一个变量被声明为auto,那么它的类型会自动匹配成初始化表达式的类型

auto a = 42;	// a is an int
auto& b = a; // b is an int&
auto d {42}; // d is an int, not a std::initializer_list<int>
auto il = {1,2,3,4}; // il is a std::initializer_list<int>

有的变量类型巨长(尤其是使用模版和命名空间时),而且一次初始化要写两遍,使用auto能提高可读性

  • 仅在局部变量中使用auto,比如for循环
  • 仅在一眼能看出类型的地方使用auto
  • 不要列表初始化auto变量
for(auto& i: list){...}

函数返回值类型推断

不要用于函数返回值,除非你是lambda使用后置返回值

auto f() -> int { return 1; }

指定的初始化

C++20才加入的新东西,感觉大家的项目基本都没法用吧

可以方便的生成高可读性高聚合的初始化表达式,尤其对一些字段顺序不明显的结构

struct Point {
float x = 0.0;
float y = 0.0;
float z = 0.0;
};

Point p = {
.x = 1.0,
.y = 2.0,
// z will be 0.0
};

Lambda表达式

  • 将所有的捕获显示写出来
  • 只有lambda声明周期很短时,才使用[&]
  • 使用[=]显式捕获
[=](int x) { return x + n; }	// 默认捕获方式,不好
[n](int x) { return x + n; } // 显式捕获方式,好

[&] { foo.doSomething(); } // 不好
[&foo] { foo.doSomething(); } // 好

泛型编程

避免复杂的泛型编程,这东西可读性相当差,调试难度相当高

别名

别名(Aliases)能让复杂类型的名字变简单,尤其是那些带有命名空间的类型

typedef Foo Bar;	// 传统方法
using Bar = Foo; // C++11后推荐的用法
  • 不要在公共区域使用别名,除非你写对其写详细注释
  • 不要在公共区域引入命名空间别名

命名规范

命名规范因项目而异,只要保持一致性即可,谷歌的命名规范我个人不习惯,可能我C#、Swift写的比较多,感觉好奇怪

通用规范

命名别嫌长,要见名知意

你看看人家Java的方法名长度

不要用汉语拼音

你要记住,英语是表音文字,因此可以靠读音/26个字母排列明确意义,而汉字是象形文字,形状才能明确意义,只靠汉语拼音无法明确区分同音字词。

而且汉语拼音是新中国扫盲、普及普通话的工具,不仅外国人看不懂,甚至一些说方言的人也看不懂

慎用缩写

日本人用假名翻译“龙”,还省去了几个词,结果/ˈdræɡən/硬是读成了“多拉贡”

注释

见名知意,自文档的代码确实好,但有时你还是需要写注释,以提高可读性

  • 每个文件开头写入版权公告(不要轻易删除原作者的信息)
  • 代码段行前注释,描述类行后注释,行后注释要对齐
  • 假设读代码的人水平比你高,有些过于明显的API解释,不要写(不会有人给i++写注释吧)
  • 标点、换行(对于python,yml很重要)建议依赖IDE,统一即可
  • 写TODO(比如Xcode就支持TODO:MARK:,很多IDE都能检查)
  • 如果一个接口被弃用,可以写弃用注释DEPRECATED

格式

个人建议格式依赖IDE,如果你有自己的坚持,那么先配置IDE

  • 一行不能太长,不然别人要缩放屏幕或者拖动水平滚动条,#include语句除外
  • 使用UTF-8编码(GB-2312的中文注释用UTF-8打开就是乱码)
  • 只使用空格而非制表符,IDE都支持将制表符替换为空格,建议一个制表符=4个空格
  • 返回类型,函数名,前几个参数都放在头一行
  • 空行不要超过两行

评论