首页 > 学院 > 开发设计 > 正文

google c++ style 阅读笔记

2019-11-14 09:53:17
字体:
来源:转载
供稿:网友

头文件 1.1. Self-contained 头文件

头文件应该能够自给自足(self-contained,也就是可以作为第一个头文件被引入),以 .h 结尾。至于用来插入文本的文件,说到底它们并不是头文件,所以应以 .inc 结尾。不允许分离出 -inl.h 头文件的做法.

1.2. #define 保护

所有头文件都应该使用 #define 来防止头文件被多重包含, 命名格式当是: <PROJECT>_<PATH>_<FILE>_H_ .

1.3. 前置声明

尽可能地避免使用前置声明。使用 #include 包含需要的头文件即可。

结论:

尽量避免前置声明那些定义在其他项目中的实体.函数:总是使用 #include.类模板:优先使用 #include.

1.4. 内联函数

只有当函数只有 10 行甚至更少时才将其定义为内联函数. 另一个实用的经验准则: 内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行). 有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.

1.5. #include 的路径及顺序

使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖:dir2/foo2.h (优先位置, 详情如下)C 系统文件C++ 系统文件其他库的 .h 文件本项目内 .h 文件尽量避免前置声明那些定义在其他项目中的实体.函数:总是使用 #include.类模板:优先使用 #include. 2.1. 名字空间 鼓励在 .cc 文件内使用匿名名字空间. 使用具名的名字空间时, 其名称可基于项目名或相对路径. 禁止使用 using 指示(using-directive)。禁止使用内联命名空间(inline namespace)

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

2.1.1. 匿名名字空间

/ .h 文件namespace mynamespace {// 所有声明都置于命名空间中// 注意不要使用缩进class MyClass { public: … void Foo();};} // namespace mynamespace// .cc 文件namespace mynamespace {// 函数定义都置于命名空间中void MyClass::Foo() { …}} // namespace mynamespace

2.2. 嵌套类 不要将嵌套类定义成公有, 除非它们是接口的一部分, 比如, 嵌套类含有某些方法的一组选项

2.3. 非成员函数、静态成员函数和全局函数

有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个名字空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用 2.1. 名字空间。

定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的名字空间内.

2.4. 局部变量

将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.

如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数.

在循环作用域外面声明这类变量要高效的多:

Foo f; // 构造函数和析构函数只调用 1 次for (int i = 0; i < 1000000; ++i) { f.DoSomething(i);}

2.5. 静态和全局变量

禁止使用 class 类型的静态或全局变量:它们会导致难以发现的 bug 和不确定的构造和析构函数调用顺序。不过 constexpr 变量除外,毕竟它们又不涉及动态初始化或析构。

改善以上析构问题的办法之一是用 quick_exit() 来代替 exit() 并中断程序。它们的不同之处是前者不会执行任何析构,也不会执行 atexit() 所绑定的任何 handlers. 如果您想在执行 quick_exit() 来中断时执行某 handler(比如刷新 log),您可以把它绑定到 _at_quick_exit(). 如果您想在 exit() 和 quick_exit() 都用上该 handler, 都绑定上去。

综上所述,我们只允许 POD 类型的静态变量,即完全禁用 vector (使用 C 数组替代) 和 string (使用 const char [])。

如果您确实需要一个 class 类型的静态或全局变量,可以考虑在 main() 函数或 pthread_once() 内初始化一个指针且永不回收。注意只能用 raw 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。

注意「using 指示(using-directive)」和「using 声明(using-declaration)」的区别。 注意别在循环犯大量构造和析构的低级错误。

3.1. 构造函数的职责 不要在构造函数中进行复杂的初始化 (尤其是那些有可能失败或者需要调用虚函数的初始化).

在构造函数中执行操作引起的问题有:

构造函数很难上报错误的,关键是不能使用异常啊 操作失败会造成对象初始化失败,进入不确定状态。 如果在构造函数中内调用了自身的虚函数,这类调用是不会重定向到子类的虚函数实现,也就是不要在构造函数中调用自身的虚函数啦

当然了,如果对象需要进行有意义的 (non-trivial) 初始化, 考虑使用明确的init 方法或者工厂模式.

3.3. 显式构造函数 对单个参数的构造函数使用 C++ 关键字 explicit. 通常, 如果构造函数只有一个参数, 可看成是一种隐式转换

为避免构造函数被调用造成隐式转换, 可以将其声明为 explicit.

除单参数构造函数外, 这一规则也适用于除第一个参数以外的其他参数都具有默认参数的构造函数, 例如 Foo::Foo(string name, int id = 42).

最后, 只有 std::initializer_list 的构造函数可以是非 explicit, 以允许你的类型结构可以使用列表初始化的方式进行赋值. 例如:

```MyType m = {1, 2};MyType MakeMyType() { return {1, 2}; }TakeMyType({1, 2});```

3.4. 可拷贝类型和可移动类型 如果你的类型需要, 就让它们支持拷贝 / 移动. 否则, 就把隐式产生的拷贝和移动函数禁用

std::unique_ptr 就是一个可移动但不可复制的对象的例子

拷贝/ 移动构造函数在某些情况下会被编译器隐式调用,例如通过传值得方式传递对象。

优点呢:比他们的替代方案更加容易定义,简洁,能保证所有数据成员都会被复制,还有更好的是,不需要堆的分配和单独的初始化和赋值操作。一键式^_^

缺点:过度滥用拷贝,存在对象切割的风险,不要给任何有派生类的对象提过赋值操作或者拷贝、移动构造函数,当然也不要去继承这样的成员函数的类。如果你想copy类的属性,请提供public virtual Clone() 和一个protected 的拷贝构造函数以实现派生类的实现。

如果你的类不需要拷贝/移动操作,请显示的通过=delete 或其他手段禁用。

3.5. 委派和继承构造函数 在能够减少重复代码的情况下,使用委派和 继承构造函数。避免你干多余的活。

通过特殊的初始化列表的语法,委派构造函数允许类的一个构造函数调用其他类的构造函数。

x::x(const stting &name):name_(name){...}x::x():x(""){}

继承构造函数:允许派生类直接调用基类的构造函数,如继承基类的其他成员函数,而不需要重新声明。当基类拥有多个构造函数时,会特别的有用。

class Base{public: Base(); Base(int n); ...};class Derived:public Base{public: using Base::Base; // Base's constructors are redeclared here.}

如果派生类的构造函数只是调用基类的构造函数,而没有其他行为时,这一功能特别有用。

不好之处在于,如果你在派生类中引入了新的成员变量,而你的基类中却没有引入,那基类就不知道你添加了新的成员变量。

3.6. 结构体 VS. 类

3.7. 继承

使用组合 (composition, YuleFox 注: 这一点也是 GoF 在 <> 里反复强调的) 常常比使用继承更合理. 如果使用继承的话, 定义为 public 继承.

继承主要用于两种场合: 实现继承(implementation inheritance ),子类继承父类的实现代码;接口继承在(interface inheritance), 子类仅继承父类的方法名称。

优点:实现继承通过原封不动的复用基类代码减少代码量。由于继承是在编译器时声明。 缺点: 对于实现继承,由于子类的实现代码是分散在父类和子类之间,要理解其实实现变得更加困难。子类不能重写父类的非虚函数,当然也就不能修改实现,基类也可能定义了一些数据成员,还要区分基类的实际布局。

结论:所有继承必须public的,如果你想使用私有继承,你应该替换成把基类的实例作为对象的方式。

不要过度使用实现继承,组合常常更适合一些,尽量做到只在“是一个(is-a)(has-a)”情况下使用继承。

必要的话,析构函数声明为virtual。如果你的类有虚函数,则析构函数也应该为虚函数。注意在任何情况下,数据成员在任何情况下都必须是私有的。

当重载一个虚函数,在衍生类中把它明确的声明为virtual,理论依据:如果省略virtual关键字,代码阅读者不得不检查所有父类。

3.8. 多重继承 真正需要用到多重实现继承的情况少之又少. 只在以下情况我们才允许多重继承: 最多只有一个基类是非抽象类,其它基类都是以interface为后缀的纯接口类。

多重继承允许子类拥有多个基类. 要将作为 纯接口 的基类和具有 实现 的基类区别开来.

优点: 相比单继承,多重实现继承可以复用更多的代码

缺点: 真正需要用到多重,实现继承的情况少之又少,多重实现继承看上去是不错的解决方案,但你通常也可以找到一个更明确,更清晰的不同解决方案。

结论: 只有当所有父类除第一个外都是纯接口时,才允许使用多重继承,为确保它们是纯接口,这些类必须以interface为后缀。

3.9 接口

接口是指满足特定条件的类,这些类似Interface为后缀(不强制)

定义: 当一个满足一下要求时,称之为纯接口:

只有纯虚函数(“=0”)和静态函数(除了下文提到的析构函数) 没有非静态数据成员 没有定义任何构造函数。如果有,也不能带有参数,并且为protected 如果它是一个子类,也只能从满足上述条件并以Interface为后缀的类继承。

接口类不能被直接实例化,因为它声明了纯虚函数,为确保接口类的所有实现可被正确销毁,必须为之声明虚析构函数。

优点: 以interface为后缀可以提醒其他人不要为该接口类增加函数实现或非静态数据成员。这一点对于多重继承尤其重要。

缺点: interface后缀增加了类名长度,为了阅读和理解带来不便,同时,接口特性作为实现细节不应暴露给用户。

结论: 只有在满足上述需求时,类才以interface结尾,但反过来,满足上述需求的类未必以interface结尾。

3.10. 运算符重载 重载有个不好的地方,比如重载了Operator& 的类不能被前置声明

这里有极少数情况下还是可以使用的,operator<<(ostram&,const T&)

3.11. 存取控制

将 所有 数据成员声明为 private, 并根据需要提供相应的存取函数.

一般在头文件中把存取函数定义成内联函数.

3.11. 声明顺序

声明的顺序通常为 public protected private

每个区段的内声明顺序通常如下: typedefs 和枚举

常量 构造函数 析构函数 成员函数,含静态函数 数据成员,含静态成员

有元声明,应该放在private区段,如果宏定义了DISALLOW_COPY_AND_ASSIGN 禁用了拷贝和赋值,应当将其置于private 区段的末尾

3.12. 编写简短函数

小结:

1.避免构造函数的隐式转化,将构造函数声明为explicit 2.避免使用多重继承,除了一个基类实现外,其他类必须为纯接口 3.接口类类名以interface为后缀的,除提供带虚析构函数,和静态成员函数,其他的都为纯虚函数。如果想提过非静态成员, 构造函数的话,加上protected

4.来自Google的奇技

4.1 所有权和智能指针 动态内存分配的对象最好有单一且固定的所有主(owner),且通过智能指针传递所有权(ownership

智能指针可以看做是* 和 -> 的对象来看。 std::unique_ptr 是C++11 中新推出的一种智能指针, 用来表示动态分配出的对象(独一无二)的所有权。当std::unique_ptr 离开作用域就会被销毁。还挺智能的哈、 std::shared_ptr 同样可以表示动态分配对象(独一无二的所有权)还可以共享、复制给共同拥有者,当最后一个结束了,就销毁了。last game over, 销毁对象。

爽的地方: 传递对象的所有权,开销比copy还小,好吧,看来copy也是不很给力啊 传递所有权比【借用】指针或者引用简单,关键是省掉了两个用户一起协调生命周期的工作。

省事的完成了所有权的登记工作 对于const对象来说,智能指针简单易用,也比深度复制高效,这也就是是智能指针的存在。

烦恼: 智能指针,你当然必须的使用智能指针

所有权的登记工作在运行时进行,开销就不那么小了 某些时刻共享对象没有被销毁,看看是不是引用了(cycle reference) 这样就不太好了

当然,再怎么智能还是不能完全代替原生的指针的。

有些时候为了提高服务性能,并且发现操作对象是不可变的,比如说:

std::shared_ptr<const Foo>

这个时候用共享所有权,可以避免昂贵的拷贝代价,so,如果一定要用,总有这样的需求的时候,推荐用std::shared_ptr

4.2 cpplint cpplint.py 检查风格错误。 虽然说还不是那么的完美,不过也是很有用的工具,误报的时候,在行尾// NOLINT 或者在上一行加 // NOLINTNEXTLINE

5.其他C++ 特性

5.1 引用参数 所有引用参数都需要加上const 在c中要想修改变量的值,必须传递地址int foo(int *pval),但是C++中还可以用引用,int foo(const int &pval)

为啥要这样用呢? 引用参数时防止出现(*pval)++,目的明确,不接受空NULL指针

不好嘛,容易误导,引用是值变量,但是却有指针的语义。 参数列表中,记得加const void Foo(const string &in,sring *out) google code 硬性要求,传入参数前必须加const的引用或值参,输出参数为指针。除非用于交换。

有时候使用const T* 指针比const T& 更加明智, 如: 你要传入空指针NULL。 函数要把地址传递给输入参数。

5。2 右值引用

只在定义移动构造函数与移动赋值操作使用右值引用,不要使用std::forward

什么是右值引用呢? void foo(string &&s) 右值引用是一种只能绑定到临时对象的引用的一种,如上声明了一个其参数是string 的右值函数。

优点: 用于移动构造函数,只移动但是不发生拷贝,提高性能。 v1 是一个vector <string> ,则auto v2(std::move(1) 就能利用指针,而不用复制数据到v2了,效率提高了很多。

所以这个时候要高效率的使用STL库,如: std::unique_str std::move

什么时候可能使用呢? 只在定义一个移动构造函数或者是移动赋值操作时使用右值引用,记住不要使用std::forward 功能函数,可以使用std::move 来表示一个对象的移动而不是复制一个对象。

5.3. 函数重载

若要用好函数重载,最好能让读者看一调用点(call site)

编写一个参数类型为const string& ,用一个const char* 重载它

class MyClass{ public: void Analyze(const string & text ); void Analyze(const char *text,size_t textlen);};

这里有什么好处? 重载函数参数,令代码更加直观,模板化代码需要重载,同时为使用者带来便利。

缺点:当函数只重载了函数的部分体,这样会令人迷惑。

5.4 缺省参数 我们不允许使用缺省参数,还是尽量的使用函数重载。

5.5 变长数组和alloca() 不允许使用变长数组alloca();

最大的缺点是容易引起内存越界,这样就不好玩了。bug

应当改用更安全的allocater(分配器),就像std::vector 和std::unique_ptr

bool Base::Equal(Base *other) =0;bool Derived::Equal(Base *other){ Derived* that = danamic_cast<Derived*>(other); if(that == NULL){ return false; }}

基于类型的判断树是一个很强的暗示, 它说明你的代码已经偏离正轨了. 不要像下面这样:

if (typeid(*data) == typeid(D1)) { … } else if (typeid(*data) == typeid(D2)) { … } else if (typeid(*data) == typeid(D3)) { …

如果在类层级中加入新的子类,像这样的代码通常会崩溃,为啥,而且,因为某个子类改变了属性,而引发的问题,这样是很难排查的。

5.9 类型转换 在C++ 中使用类型转换,如static_cast<>() 不要使用int y=(int) x,或int y=int (x);

优点:相对于C来说,C有时候在做强制类型转换,如(int) 3.4 而有的时候是在做类型转换 如:(int)”hello”

static_cast 替代C风格进行转换 const_cast 去掉const 限定符 reinterpret_cast 指针类型和整型或其它指针之间进行不安全的相互转换。

5.10 流 只在记录时使用流

流用来代替printf() 和scanf()

优点:用流在打印时不用关心对象的类型。流的构造和析构函数会自动打开和关闭对应的文件。

缺点:流使得pread()等功能很难执行。如果不使用printf风格的格式化字符串,%.*s 用流处理性能很低,流不支持字符串操作符重载重新排序。而这一点对于软件国际化很有用。

cout<< this ; // 输出地址 cout<< *this; // 输出值 由于<<操作符会被重载,编译器不会报错,这样也是反对使用操作符重载的原因。

5.11 前置自增和自减

通常使用前置自增, 不考虑返回值的话,前置自增(++i)通常要比后自增(i++)效率高,why,因为前置增不会像后自增(或自减)要对表达式的值i进行拷贝。

如果是简单数值(非对象),两种都无所谓。对迭代器和模板类型,使用前置自增(自减)。

5.12 const用法

class Foo{ int Bar(char c) const;};

表示不能修改类成员变量的状态

优点:大家更容易理解如何使用变量,编译器也更好的检测类型。

缺点:const 是入侵性的:如果你向一个函数传入const变量,函数声明中 也必须对应const参数。否则需要const_cast 类型转换。

5.13 constexpr 用法

定义真正的常量,或实现常量初始化。 表示运行时,编译时都不可以改变。

5.14 整型 小心整型类型转换和整型提升 int 与unsigned int 运算时,前者被提升为unsigned int,而有可能 溢出。

5.15 64 位下的可移植性

代码应该对64和32位系统友好,处理打印,比较,结构体字节对齐注意 大多数编译器都允许调整结构体字节对齐,gcc中用attribute((packed)

创建64位常量时使用LL或ULL作为后缀 int64_t my_value = 0x123456789LL; uint64_t my_mask = 3ULL<<48; 5.16 预处理宏

当需要使用宏的时候,尽量使用内联函数、枚举、常量带之

宏有全局作用域,可能引发异常行为。测试时非常头疼。

宏的特性 在代码库底层 用# 字符串化,用##连接。

下面的规则,避免宏带来的一些问题 不要在.h后面定义宏。 在马上要使用时才进行#define,使用后立即#undef 不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称

5.17 0 ,nullptr 和NULL 整数用0 ,实数用0.0,指针用nullptr或NULL,字符串用’/0’

在C++11项目中使用nullptr, sizeof(NULL)就和sizeof(0)不一样

5.18 sizeof

尽可能用sizeof(varname)代替sizeof(type)


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表