Google C++编码规范

文章目录

  • 一、 头文件
    • 1.1 Self-containned头文件
    • 1.2 #define 保护
    • 1.3 前置申明
    • 1.4 内联函数
    • 1.5 #include的路径与顺序
  • 二、作用域
    • 2.1 命名空间
    • 2.2 匿名命名空间和静态变量
    • 2.3 非成员函数、静态成员函数和全局函数
    • 2.4 局部变量
    • 2.5 静态和全局变量
  • 三、类
    • 3.1 构造函数的职责
    • 3.2 隐式类型转换
    • 3.3 可拷贝类型和可移动类型
    • 3.4 结构体VS类
    • 3.5继承
    • 3.6 多重继承
    • 3.7 接口
    • 3.8 运算符重载
    • 3.9 存取控制
    • 3.10 声明顺序
  • 4、函数
    • 4.1参数顺序
    • 4.2 编写简短函数
    • 4.3 引用参数
    • 4.4 函数重载
    • 4.5 缺省参数
    • 4.6 函数返回类型后置语法
  • 5、来自Google 的奇技

一、 头文件

  通常每一个.cpp文件都有一个与之对应的.h文件。不过也有一些例外。比如单元测试代码和只包含main()函数的.cpp文件。
正确使用头文件可令代码在可读性、文件大小和性能上大为改观。下面的规则将引导你规避使用头文件时的各种陷阱。

1.1 Self-containned头文件

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

  不过有一个例外,即一个文件并不是self-contained的,而是作为文本插入到代码某处。或者,文件内容实际上是其它头文件的特定平台(platform-specific)扩展部分。这些文件就要用.inc文件扩展名。
  如果.h文件声明了一个模板或内联函数,同时也在该文件加以定义。凡是有用到这些的.cc文件,就得统统包含该头文件,否则程序可能会在构建中链接失败。不要把这些定义放到分离的-inl.h文件里。
  有个例外:如果某函数模板为所有相关模板参数显式实例化,或本身就是某类的一个私有成员,那么它就只能定义在实例化该模板的.cc文件里。

1.2 #define 保护

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

  为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径.例如,项目foo中的头文件foo/src/bar/baz.h可按如下方式保护:

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_

1.3 前置申明

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

定义
  所谓「前置声明」是类、函数和模板的纯粹声明,没伴随着其定义.
优点

  • 前置声明能够节省编译时间,多余的#include会迫使编译器展开更多的文件,处理更多的输入
  • 前置声明能够节省不必要的重新编译的时间。#include代码因为头文件中无关的改动而被重新编译多次。

缺点

  • 前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
  • 前置声明可能会被库的后续更改所破坏。前置声明函数或模板有时会妨碍头文件开发者变动其 API.。例如扩大形参类型,加个自带默认参数的模板形参等等。
  • 前置声明来自命名空间 std::的symbol时,其行为未定义
  • 很难判断什么时候该用前置声明,什么时候该用#include。极端情况下,用前置声明代替includes甚至都会暗暗地改变代码的含义:
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_

// b.h:
struct B {}; struct D : B {}

// good_user.cc:
#include "b.h" void f(B*); void f(void*);
void test(D* x) { f(x); } // calls f(B*)

  如果#include被B和D的前置声明替代,test()就会调用f(void*)。

  • 前置声明了不少来自头文件的symbol时,就会比单单一行的include冗长
  • 仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂。

结论

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

  至于什么时候包含头文件,参见 1.5 #include 的路径及顺序

1.4 内联函数

只有当函数只有10行甚至更少时才将其定义为内联函数

定义
  当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用。
优点
  只要内联的函数体较小, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联。
缺点
  滥用内联将导致程序变得更慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
结论

  • 一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
  • 另一个实用的经验准则: 内联那些包含循环或switch语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或switch语句从不被执行).

  有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数(注: 递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数

1.5 #include的路径与顺序

使用标准的头文件包含顺序可增强可读性,避免隐藏依赖:相关头文件,C库,C++, 其他库的.h,本项目内的.h

  项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录"."(当前目录)或“…”(上级目录)。例如google-awesome-project/src/base/logging.h应该按如下方式包含:

#include "base/logging.h"

  又如dir/foo.cc或者dir/foo_test.cc的主要作用是实现或测试dir2/foo2.h的供能,foo.cc中包含头文件的次序如下:

  1. dir2/foo2.h(优先位置,详情如下)
  2. C系统文件
  3. C++系统文件
  4. 其他库的.h文件
  5. 本项目内.h文件

  这种优先的顺序排序保证当dir2/foo2.h遗漏某些必要的库时,dir/foo.cc或dir/foo_test.cc的构建会立刻中止。因此这一条规则保证维护这些文件的人们首先看到构建中止的消息而不是维护其他包的人们
dir/foo.cc和dir2/foo2.h通常在同一目录下(如base/basictypes_unittest.cc和base/basictypes.h),但也可以放在不同目录下。
  按字母顺序分别对每种类型的头文件进行二次排序是不错的主意。注意较老的代码可不符合这条规则,要在方便的时候改正它们。
&emsp 您所依赖的符号 (symbols) 被哪些头文件所定义,您就应该包含(include)哪些头文件,前置声明 (forward declarations) 情况除外。比如您要用到bar.h中的某个符号, 哪怕您所包含的foo.h已经包含了bar.h,也照样得包含bar.h,除非foo.h有明确说明它会自动向您提供bar.h中得symbol。不过凡是cc文件所对应得「相关头文件」已经包含的,就不用再重复包含进其cc文件里面了,就像foo.cc只包含foo.h就够了。不用再管后者所包含得其他内容。举例来说google-awesome-project/src/foo/internal/fooserver.cc得包含次序如下:

#include "foo/public/fooserver.h" // 优先位置
#include  
#include 
#include 
#include 
#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"

例外:
  有时,平台特定(system-specific)代码需要条件编译(conditional includes),这些代码可以放到其它 includes 之后。当然,您的平台特定代码也要够简练且独立,比如:

#include "foo/public/fooserver.h"
#include "base/port.h" // For LANG_CXX11. #ifdef LANG_CXX11
#include  #endif // LANG_CXX11

结论:

  1. 避免多重包含是学编程时最基本的要求
  2. 前置声明是为了降低编译依赖,防止修改一个头文件引发多米诺效应
  3. 内联函数的合理使用可提高代码执行效率
  4. -inl.h可提高代码可读性(一般很少用)
  5. 标准化函数参数顺序可以提高可读性和易维护性
  6. 包含文件的名称使用“.”和"…"虽然方便却易混乱, 使用比较完整的项目路径看上去很清晰, 很条理, 包含文件的次序除了美观之外, 最重要的是可以减少隐藏依赖, 使每个头文件在“最需要编译” (对应源文件处) 的地方编译, 有人提出库文件放在最后, 这样出错先是项目内的文件, 头文件都放在对应源文件的最前面, 这一点足以保证内部错误的及时发现了
  7. 类内部的函数一般会自动内联。所以某函数一旦不需要内联,其定义就不要再放在头文文里,而是放到对应的.cc文件里,这样可以保持头文件的类相当精炼,也很好地贯彻了声明与定义分离的原则
  8. 在include中中插入空行以分割相关头文件, C 库, C++ 库, 其他库的和本项目内的是个好习惯。

二、作用域

2.1 命名空间

鼓励在.cc文件内使用匿名命名空间或static声明使用具名的命名空间时, 其名称可基于项目名或相对路径禁止使用using指示。禁止使用内联命名空间

定义:
  命名空间将全局作用域细分为独立的, 具名的作用域, 可有效防止全局作用域的命名冲突.
优点:
  虽然类已经提供了(可嵌套的)命名轴线(注:将命名分割在不同类的作用域内),命名空间在这基础上又封装了一层。
  举例来说,两个不同项目的全局作用域都有一个类Foo,这样在编译或运行时造成冲突,如果每个项目将代码置于不同命名空间中,project1::Foo和project2::Foo作为不同符号自然不会冲突。
  内联命名空间会自动把内部的标识符放到外层作用域,比如:

namespace X {
	inline namespace Y {
		void foo();
	}  // namespace Y
}  // namespace X

X::Y::foo()与X::foo()彼此可代替。内联命名空间主要用来保持跨版本的 ABI 兼容性。
缺点:
  命名空间具有迷惑性, 因为它们使得区分两个相同命名所指代的定义更加困难。
  内联命名空间很容易令人迷惑,毕竟其内部的成员不再受其声明所在命名空间的限制。内联命名空间只在大型版本控制里有用
  有时候不得不多次引用某个定义在许多嵌套命名空间里的实体,使用完整的命名空间会导致代码的冗长
  在头文件中使用匿名空间导致违背 C++ 的唯一定义原则(One Definition Rule(ODR))。
结论:
  根据下文将要提到的策略合理使用命名空间

  • 遵守命名空间命名中的规则。
  • 像之前的几个例子中一样,在命名空间的最后注释出命名空间的名字
  • 用命名空间把文件包含,gflags的声明/定义,以及类的前置声明以外的整个源文件封装起来, 以区别于其它命名空间:
// .h 文 件
namespace mynamespace {
	// 所有声明都置于命名空间中
	// 注意不要使用缩进
	class MyClass {
	public:
		...
			void Foo();
	};
} // namespace mynamespace
// .cc 文 件
namespace mynamespace {
	// 函数定义都置于命名空间中
	void MyClass::Foo() {
		...
	}
} // namespace mynamespace

  更复杂的.cc文件包含更多, 更复杂的细节, 比如gflags或using声明。

#include "a.h"
DEFINE_FLAG(bool, someflag, false, "dummy flag"); 
namespace a {
	...code for a...
} // namespace a
  • 不要在命名空间std内声明任何东西, 包括标准库的类前置声明,在std命名空间声明实体是未定义的行为, 会导致如不可移植. 声明标准库下的实体, 需要包含对应的头文件。
  • 不应该使用using指示引入整个命名空间的标识符号
using namespace foo;
  • 不要在头文件中使用 命名空间别名 除非显式标记内部命名空间使用。因为任何在头文件中引入的命名空间都会成为公开API的一部分。
// 在 .cc 中使用别名缩短常用的命名空间
namespace baz = ::foo::bar::baz;
// 在 .h 中使用别名缩短常用的命名空间
namespace librarian {
	namespace impl { // 仅限内部使用
		namespace sidetable = ::pipeline_diagnostics::sidetable;
	} // namespace impl

	inline void my_inline_function() {
		// 限制在一个函数中的命名空间别名
		namespace baz = ::foo::bar::baz;
		...
	}
} // namespace librarian
  • 禁止用内联命名空间

2.2 匿名命名空间和静态变量

.cc文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为static。但是不要在.h文件中这么做。

定义:
  所有置于匿名命名空间的声明都具有内部链接性,函数和变量可以经由声明为static拥有内部链接性,这意味着你在这个文件中声明的这些标识符都不能在另一个文件中被访问。即使两个文件声明了完全一样名字的标识符,它们所指向的实体实际上是完全不同的。
结论:
  推荐、鼓励在.cc中对于不需要在其他地方引用的标识符使用内部链接性声明,但是不要在.h中使用。
  匿名命名空间的声明和具名的格式相同,在最后注释上namespace:

namespace {
	...
} // namespace

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

使用静态成员函数或命名空间内的非成员函数, 尽量不要用裸的全局函数. 将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关

优点:
  某些情况下, 非成员函数和静态成员函数是非常有用的, 将非成员函数放在命名空间内可避免污染全局作用域
缺点:
  将非成员函数和静态成员函数作为新类的成员或许更有意义, 当它们需要访问外部资源或具有重要的依赖关系时更是如此
结论:
  有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用 2.1命名空间 。举例而言,对于头文件myproject/foo_bar.h,应当使用:

namespace myproject {
	namespace foo_bar {
		void Function1(); void Function2();
	} // namespace foo_bar
} // namespace myproject

而非

namespace myproject {
	class FooBar {
	public:
		static void Function1(); static void Function2();
	};
} // namespace myproject

  定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的命名空间内
  如果你必须定义非成员函数, 又只是在.cc文件中使用它, 可使用匿名2.1命名空间或static链接关键字(如static int Foo(){…})限定其作用域。

2.4 局部变量

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

  C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好. 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值. 特别是,应使用初始化的方式替代声明再赋值, 比如:

int i;
i = f(); // 坏——初始化和声明分离
int j = g(); // 好——初始化时声明
vector<int> v;
v.push_back(1); // 用花括号初始化更好
v.push_back(2);
vector<int> v = {1, 2}; // 好——v开始就初始化

  属于if、while、和for语句的变量应当在这些语句中正常的声明,这样子这些变量的作用域就被限制在这些语句中了,举例说明:

while (const char* p = strchr(str, '/')) 
str = p + 1;

  有一个例外, 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低。

//低效的实现
for(int i = 0; i < 1000000; i++) 
{
	Foo f;	//构造函数和析构函数分别调用 1000000 次!
	f.DoSomething(i);
}

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

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

2.5 静态和全局变量

禁止定义静态储存周期非POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植

  禁止使用类的 静态储存周期 变量:由于构造和析构函数调用顺序的不确定性,它们会导致难以发现的 bug 。不过constexpr变量除外,毕竟它们又不涉及动态初始化或析构。
  静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型(POD:Planin Old Data):即int,char和float以及POD类型的指针、数组和结构体。
  静态变量的构造函数、析构函数和初始化的顺序在 C++ 中是只有部分明确的,甚至随着构建变化而变化,导致难以发现的 bug. 所以除了禁用类类型的全局变量,我们也不允许用函数返回值来初始化 POD 变量,除非该函数(比如getenv()或者getpid())不涉及任何全局变量。函数作用域里的静态变量除外,毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。
  需要注意的是:同一个编译单元内是明确的,静态初始化优先于动态初始化,初始化顺序按照声明顺序进行,销毁则逆序。不同的编译单元之间初始化和销毁顺序属于未明确行为 (unspecified
behaviour)。
  同理,全局和静态变量在程序中断时会被析构,无论所谓中断是从main()返回还是对exit()的调用。析构顺序正好与构造函数调用的顺序相反。但既然构造顺序未定义,那么析构顺序当然也就不定了。比如,在程序结束时某静态变量已经被析构了,但代码还在跑——比如其他线程——并试图访问它且失败;再比如,一个静态 string 变量也许会在一个引用了前者的其它变量析构之前被析构掉。
  改善以上析构问题的办法之一是用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 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。
说明:上文提及的静态变量泛指静态生存周期的对象, 包括: 全局变量, 静态变量, 静态类成员变量,以及函数静态变量
结论:

  • cc中的匿名命名空间可避免命名冲突, 限定作用域, 避免直接使用using关键字污染命名空间
  • 嵌套类符合局部使用原则, 只是不能在其他头文件中前置声明, 尽量不要public
  • 尽量不用全局函数和全局变量, 考虑作用域和命名空间限制, 尽量单独形成编译单元
  • 多线程中的全局变量 (含静态成员变量) 不要使用class类型(含STL容器),避免不明确行为导致的 bug
  • 作用域的使用, 除了考虑名称污染, 可读性之外, 主要是为降低耦合, 提高编译/执行效率
  • 匿名命名空间说白了就是文件作用域,就像 C static 声明的作用域一样,后者已经被 C++标准提倡弃用
  • 局部变量在声明的同时进行显式值初始化,比起隐式初始化再赋值的两步过程要高效,同时也贯彻了计算机体系结构重要的概念「局部性(locality)」
  • 注意别在循环犯大量构造和析构的低级错误

三、类

类是 C++ 中代码的基本单元. 显然, 它们被广泛使用. 下面列举了在写一个类时的主要注意事项。

3.1 构造函数的职责

不要在构造函数中调用虚函数, 也不要在无法报出错误时进行可能失败的初始化

定义
  在构造函数中可以进行各种初始化操作
优点

  • 无需考虑类是否被初始化
  • 经过构造函数完全初始化后的对象可以为const类型, 也能更方便地被标准容器或算法使用

缺点

  • 如果在构造函数内调用了自身的虚函数, 这类调用是不会重定向到子类的虚函数实现. 即使当前没有子类化实现, 将来仍是隐患
  • 在没有使程序崩溃 (因为并不是一个始终合适的方法) 或者使用异常 (因为已经被禁用了) 等方法的条件下, 构造函数很难上报错误
  • 如果执行失败, 会得到一个初始化失败的对象, 这个对象有可能进入不正常的状态, 必须使用bool isValid()或类似这样的机制才能检查出来, 然而这是一个十分容易被疏忽的方法.构造函数的地址是无法被取得的, 因此, 举例来说,由构造函数完成的工作是无法以简单的方式交给其他线程的。

结论

  • 构造函数不允许调用虚函数. 如果代码允许, 直接终止程序是一个合适的处理错误的方式. 否则,考虑用Init()方法或工厂函数
  • 构造函数不得调用虚函数, 或尝试报告一个非致命错误. 如果对象需要进行有意义的 (non-
    trivial) 初始化, 考虑使用明确的 Init() 方法或使用工厂模式。Avoid Init() methods on objects
    with no other states that affect which public methods may be called (此类形式的半构造对象有时无法正确工作)

3.2 隐式类型转换

不要定义隐式类型转换. 对于转换运算符和单参数构造函数, 请使用explicit关键字

定义
  隐式类型转换允许一个某种类型 (称作 源类型) 的对象被用于需要另一种类型 (称作 目的类型)的位置, 例如, 将一个int类型的参数传递给需要double类型的函数。
  除了语言所定义的隐式类型转换, 用户还可以通过在类定义中添加合适的成员定义自己需要的转换. 在源类型中定义隐式类型转换, 可以通过目的类型名的类型转换运算符实现 (例如operator bool()),在目的类型中定义隐式类型转换, 则通过以源类型作为其唯一参数 (或唯一无默认值的参数) 的构造函数实现。
  explicit关键字可以用于构造函数或 (在 C++11 引入) 类型转换运算符, 以保证只有当目的类型在调用点被显式写明时才能进行类型转换, 例如使用cast。这不仅作用于隐式类型转换, 还能作用于 C++11 的列表初始化语法:

class Foo {
	explicit Foo(int x, double y);
	...
};

void Func(Foo f);

  此时下面的代码是不允许的:

Func({ 42, 3.14 }); // 错误的

  这一代码从技术上说并非隐式类型转换, 但是语言标准认为这是explicit应当限制的行为。
优点

  • 有时目的类型名是一目了然的, 通过避免显式地写出类型名, 隐式类型转换可以让一个类型的可用性和表达性更强
  • 隐式类型转换可以简单地取代函数重载
  • 在初始化对象时, 列表初始化语法是一种简洁明了的写法

缺点

  • 隐式类型转换会隐藏类型不匹配的错误. 有时, 目的类型并不符合用户的期望, 甚至用户根本没有意识到发生了类型转换
  • 隐式类型转换会让代码难以阅读, 尤其是在有函数重载的时候, 因为这时很难判断到底是哪个函数被调用
  • 单参数构造函数有可能会被无意地用作隐式类型转换
  • 如果单参数构造函数没有加上explicit关键字, 读者无法判断这一函数究竟是要作为隐式类型转换, 还是编码者忘了加上explicit标记。
  • 并没有明确的方法用来判断哪个类应该提供类型转换, 这会使得代码变得含糊不清
  • 如果目的类型是隐式指定的, 那么列表初始化会出现和隐式类型转换一样的问题, 尤其是在列表中只有一个元素的时候

结论

  • 在类型定义中, 类型转换运算符和单参数构造函数都应当用explicit进行标记。一个例外是拷贝和移动构造函数不应当被标记为explicit,因为它们并不执行类型转换. 对于设计目的就是用于对其他类型进行透明包装的类来说, 隐式类型转换有时是必要且合适的. 这时应当联系项目组长并说明特殊情况
  • 不能以一个参数进行调用的构造函数不应当加上explicit。接受一个std::initializer_list作为参数的构造函数也应当省略explicit,以便支持拷贝初始化(例如 Mytype num = {1,2}

3.3 可拷贝类型和可移动类型

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

定义
  可拷贝类型允许对象在初始化时得到来自相同类型的另一对象的值, 或在赋值时被赋予相同类型的另一对象的值, 同时不改变源对象的值. 对于用户定义的类型, 拷贝操作一般通过拷贝构造函数与拷贝赋值操作符定义。string类型就是一个可拷贝类型的例子。
  可移动类型允许对象在初始化时得到来自相同类型的临时对象的值, 或在赋值时被赋予相同类型的临时对象的值 (因此所有可拷贝对象也是可移动的)。std::unique_ptr就是一个可移动但不可复制的对象的例子。对于用户定义的类型, 移动操作一般是通过移动构造函数和移动赋值操作符实现的。
  拷贝 / 移动构造函数在某些情况下会被编译器隐式调用. 例如, 通过传值的方式传递对象
优点

  • 可移动及可拷贝类型的对象可以通过传值的方式进行传递或者返回, 这使得 API 更简单, 更安全也更通用. 与传指针和引用不同, 这样的传递不会造成所有权, 生命周期, 可变性等方面的混乱, 也就没必要在协议中予以明确. 这同时也防止了客户端与实现在非作用域内的交互, 使得它们更容易被理解与维护. 这样的对象可以和需要传值操作的通用 API 一起使用, 例如大多数容器
  • 拷贝 / 移动构造函数与赋值操作一般来说要比它们的各种替代方案, 比如clone()、opyFrom()、Swap(),更容易定义, 因为它们能通过编译器产生, 无论是隐式的还是通过 = default . 这种方式很简洁, 也保证所有数据成员都会被复制. 拷贝与移动构造函数一般也更高效, 因为它们不需要堆的分配或者是单独的初始化和赋值步骤, 同时, 对于类似 省略不必要的拷贝 这样的优化它们也更加合适。
  • 移动操作允许隐式且高效地将源数据转移出右值对象. 这有时能让代码风格更加清晰

缺点

  • 许多类型都不需要拷贝, 为它们提供拷贝操作会让人迷惑, 也显得荒谬而不合理。单件类型(Registerer),与特定的作用域相关的类型(Cleanup),与其他对象实体紧耦合的类型(Mutex)从逻辑上来说都不应该提供拷贝操作。为基类提供拷贝 / 赋值操作是有害的, 因为在使用它们时会造成 对象切割。默认的或者随意的拷贝操作实现可能是不正确的, 这往往导致令人困惑并且难以诊断出的错误。
  • 拷贝构造函数是隐式调用的, 也就是说, 这些调用很容易被忽略. 这会让人迷惑, 尤其是对那些所用的语言约定或强制要求传引用的程序员来说更是如此. 同时, 这从一定程度上说会鼓励过度拷贝, 从而导致性能上的问题

结论

  • 如果需要就让你的类型可拷贝 / 可移动. 作为一个经验法则, 如果对于你的用户来说这个拷贝操作不是一眼就能看出来的, 那就不要把类型设置为可拷贝. 如果让类型可拷贝, 一定要同时给出拷贝构造函数和赋值操作的定义, 反之亦然. 如果让类型可拷贝, 同时移动操作的效率高于拷贝操作, 那么就把移动的两个操作 (移动构造函数和赋值操作) 也给出定义. 如果类型不可拷贝, 但是移动操作的正确性对用户显然可见, 那么把这个类型设置为只可移动并定义移动的两个操作
  • 如果定义了拷贝/移动操作, 则要保证这些操作的默认实现是正确的. 记得时刻检查默认操作的正确性, 并且在文档中说明类是可拷贝的且/或可移动的:
class Foo {
public:
	Foo(Foo&& other) : field_(other.field) {}
	// 差, 只定义了移动构造函数, 而没有定义对应的赋值运算符.

private:  Field field_;
  • 由于存在对象切割的风险, 不要为任何有可能有派生类的对象提供赋值操作或者拷贝 / 移动构造函数 (当然也不要继承有这样的成员函数的类). 如果你的基类需要可复制属性, 请提供一个public virtual Clone()和一个protected的拷贝构造函数以供派生类实现
  • 如果你的类不需要拷贝 / 移动操作, 请显式地通过在用之:
// MyClass is neither copyable nor movable. MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;

};

3.4 结构体VS类

仅当只有数据成员时使用struct,其他一概使用class

说明
  在C++中struct与class关键字几乎含义一样. 我们为这两个关键字添加我们自己的语义理解, 以便为定义的数据类型选择合适的关键字。
  struct用来定义包含数据的被动式对象, 也可以包含相关的常量, 但除了存取数据成员之外, 没有别的函数功能. 并且存取功能是通过直接访问位域, 而非函数调用。除了构造函数, 析构函数,Initialize(),Reset(),Validate()等类似的用于设定数据成员的函数外, 不能提供其它功能的函数。如果需要更多的函数功能,class更适合. 如果拿不准, 就用class。
  为了和 STL 保持一致, 对于仿函数等特性可以不用class,而用struct。
  注意: 类和结构体的成员变量使用不同的 命名规则。

3.5继承

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

定义
  当子类继承基类时, 子类包含了父基类所有数据及操作的定义. C++ 实践中, 继承主要用于两种场合: 实现继承, 子类继承父类的实现代码; 接口继承, 子类仅继承父类的方法名称
优点
  实现继承通过原封不动的复用基类代码减少了代码量. 由于继承是在编译时声明, 程序员和编译器都可以理解相应操作并发现错误。从编程角度而言, 接口继承是用来强制类输出特定的
API. 在类没有实现 API 中某个必须的方法时, 编译器同样会发现并报告错误
缺点
  对于实现继承, 由于子类的实现代码散布在父类和子类间之间, 要理解其实现变得更加困难. 子类不能重写父类的非虚函数, 当然也就不能修改其实现. 基类也可能定义了一些数据成员, 因此还必须区分基类的实际布局
结论

  • 所有继承必须是public的,如果你想使用私有继承, 你应该替换成把基类的实例作为成员对象的方式
  • 不要过度使用实现继承. 组合常常更合适一些. 尽量做到只在 “是一个” (“is-a”, 注: 其他“has-a” 情况下请使用组合) 的情况下使用继承: 如果Bar的确"是一种"Foo,Bar才能继承Foo
  • 必要的话, 析构函数声明为virtual, 如果你的类有虚函数, 则析构函数也应该为虚函数
  • 对于可能被子类访问的成员函数, 不要过度使用protected关键字。注意, 数据成员都必须是私有的。
  • 对于重载的虚函数或虚析构函数, 使用override,或(较不常用的)final关键字显式地进行标记。较早 (早于 C++11) 的代码可能会使用virtual关键字作为不得已的选项. 因此, 在声明重载时, 请使用override、final或virtual其中之一进行标记。标记为override或final的析构函数如果不是对基类虚函数的重载的话, 编译会报错, 这有助于捕获常见的错误。这些标记起到了文档的作用, 因为如果省略这些关键字, 代码阅读者不得不检查所有父类, 以判断该函数是否是虚函数。

3.6 多重继承

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

定义
  多重继承允许子类拥有多个基类. 要将作为 纯接口 的基类和具有 实现 的基类区别开来。
优点
  相比单继承 (见 继承), 多重实现继承可以复用更多的代码。
缺点
  真正需要用到多重 实现 继承的情况少之又少. 有时多重实现继承看上去是不错的解决方案, 但这时你通常也可以找到一个更明确, 更清晰的不同解决方案
结论
  只有当所有父类除第一个外都是 纯接口类 时, 才允许使用多重继承. 为确保它们是纯接口, 这些类必须以Interface为后缀

3.7 接口

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

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

  • 只有纯虚函数 (“ =0 ”) 和静态函数 (除了下文提到的析构函数)
  • 没有非静态数据成员
  • 没有定义任何构造函数. 如果有, 也不能带有参数, 并且必须为protected
  • 如果它是一个子类, 也只能从满足上述条件并以interface为后缀的类继承
  • 接口类不能被直接实例化, 因为它声明了纯虚函数. 为确保接口类的所有实现可被正确销毁, 必须为之声明虚析构函数。

优点
  Interface后缀增加了类名长度, 为阅读和理解带来不便. 同时, 接口属性作为实现细节不应暴露给用户。
结论
  只有在满足上述条件时, 类才以Interface结尾, 但反过来, 满足上述需要的类未必一定以Interface结尾。

3.8 运算符重载

除少数特定环境外,不要重载运算符。也不要创建用户定义字面量

定义
  C++ 允许用户通过使用operator关键字 对内建运算符进行重载定义, 只要其中一个参数是用户定义的类型。operator关键字还允许用户使用operator""定义新的字面运算符, 并且定义类型转换函数, 例如operator bool()。
优点

  • 重载运算符可以让代码更简洁易懂, 也使得用户定义的类型和内建类型拥有相似的行为. 重载运算符对于某些运算来说是符合符合语言习惯的名称 (例如 == , < , = , << ), 遵循这些语言约定可以让用户定义的类型更易读, 也能更好地和需要这些重载运算符的函数库进行交互操作。
  • 对于创建用户定义的类型的对象来说, 用户定义字面量是一种非常简洁的标记
    缺点
  • 要提供正确, 一致, 不出现异常行为的操作符运算需要花费不少精力, 而且如果达不到这些要求的话, 会导致令人迷惑的 Bug
  • 过度使用运算符会带来难以理解的代码, 尤其是在重载的操作符的语义与通常的约定不符合时
  • 函数重载有多少弊端, 运算符重载就至少有多少
  • 运算符重载会混淆视听, 让你误以为一些耗时的操作和操作内建类型一样轻巧
  • 对重载运算符的调用点的查找需要的可就不仅仅是像 grep 那样的程序了, 这时需要能够理解 C++ 语法的搜索工具
  • 如果重载运算符的参数写错, 此时得到的可能是一个完全不同的重载而非编译错误。 例如:foo < bar执行的是一个行为, 而&foo < &bar执行的就是完全不同的另一个行为了。
  • 重载某些运算符本身就是有害的. 例如, 重载一元运算符&会导致同样的代码有完全不同的含义, 这取决于重载的声明对某段代码而言是否是可见的。重载诸如&&,||和“,”会导致运算顺序和内建运算的顺序不一致
  • 运算符从通常定义在类的外部, 所以对于同一运算, 可能出现不同的文件引入了不同的定义的风险。如果两种定义都链接到同一二进制文件, 就会导致未定义的行为, 有可能表现为难以发现的运行时错误
  • 用户定义字面量所创建的语义形式对于某些有经验的C++ 程序员来说都是很陌生的

结论

  • 只有在意义明显, 不会出现奇怪的行为并且与对应的内建运算符的行为一致时才定义重载运算符,例如"|"要作为位或或逻辑或来使用, 而不是作为 shell 中的管道
  • 只有对用户自己定义的类型重载运算符. 更准确地说, 将它们和它们所操作的类型定义在同一个头文件中,.cc中和命名空间中。这样做无论类型在哪里都能够使用定义的运算符, 并且最大程度上避免了多重定义的风险. 如果可能的话, 请避免将运算符定义为模板, 因为此时它们必须对任何模板参数都能够作用. 如果你定义了一个运算符, 请将其相关且有意义的运算符都进行定义, 并且保证这些定义的语义是一致的. 例如, 如果你重载了 < , 那么请将所有的比较运算符都进行重载, 并且保证对于同一组参数, < 和 > 不会同时返回true
  • 建议不要将不进行修改的二元运算符定义为成员函数。如果一个二元运算符被定义为类成员,这时隐式转换会作用域右侧的参数却不会作用于左侧. 这时会出现a < b能够通过编译而b < a不能的情况, 这是很让人迷惑的
  • 不要为了避免重载操作符而走极端。比如说, 应当定义==,=和<<而不是Equals(),CopyForm()和PrintTo()。反过来说, 不要只是为了满足函数库需要而去定义运算符重载。比如说, 如果你的类型没有自然顺序, 而你要将它们存入std::set中, 最好还是定义一个自定义的比较运算符而不是重载<
  • 不要重载&&、||和",“,或者元运算符&,不要重载面operator”",也就是说, 不要引入用户定义字量

3.9 存取控制

将所有数据成员声明为private,除非石static const类型成员(遵循常量命名规则)。处于技术上的原因,在使用Google Test 时我们允许测试固件类中的数据成员为protected

3.10 声明顺序

将相似的声明放在一起,public部分放在最前

说明

  • 类定义一般应以public开始,后跟protected,最后是private。省略空部分
  • 在各个部分中, 建议将类似的声明放在一起, 并且建议以如下的顺序: 类型 (包括typedef,using和嵌套的结构体与类), 常量, 工厂函数, 构造函数, 赋值运算符, 析构函数, 其它函数, 数据成员
  • 不要将大段的函数定义内联在类定义中. 通常,只有那些普通的, 或性能关键且短小的函数可以内联在类定义中

4、函数

4.1参数顺序

函数的参数顺序为:输入参数在先,后跟输出参数

说明
  C/C++ 中的函数参数或者是函数的输入, 或者是函数的输出, 或兼而有之。 输入参数通常是值参或const引用,输出参数或输入/输出参数则一般为非const指针。在排列参数顺序时, 将所有的输入参数置于输出参数之前. 特别要注意, 在加入新参数时不要因为它们是新参数就置于参数列表最后, 而是仍然要按照前述的规则, 即将新的输入参数也置于输出参数之前。
  这并非一个硬性规定。 输入/输出参数 (通常是类或结构体) 让这个问题变得复杂。 并且, 有时候为了其他函数保持一致, 你可能不得不有所变通。

4.2 编写简短函数

我们倾向于编写简短,凝练的函数

说明
  我们承认长函数有时是合理的, 因此并不硬性限制函数的长度。如果函数超过40行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割。
  即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题, 甚至导致难以发现的 bug。 使函数尽量简短, 以便于他人阅读和修改代码。
  在处理代码时, 你可能会发现复杂的长函数. 不要害怕修改现有代码:如果证实这些代码使用 / 调试起来很困难, 或者你只需要使用其中的一小段代码, 考虑将其分割为更加简短并易于管理的若干函数。

4.3 引用参数

所有按引用传递的参数必须加上const

定义
  在 C 语言中, 如果函数需要修改变量的值, 参数必须为指针, 如int foo(int * pval)。在 C++ 中,函数还可以声明为引用参数int foo(int &pval)。
优点
  定义引用参数可以防止出现(*pval)++这样丑陋的代码。引用参数对于拷贝构造函数这样的应用也是必需的。 同时也更明确地不接受空指针。
缺点
  容易引起误解, 因为引用在语法上是值变量却拥有指针的语义。
结论

  1. 函数参数列表中, 所有引用参数都必须是const
void Foo(const string &in, string *out);

  事实上这在 Google Code 是一个硬性约定: 输入参数是值参或const引用,输出参数为指针。输入参数可以是const指针,但决不能是非const的引用参数, 除非特殊要求, 比如swap()。

  1. 在输入形参中用const T*比const T&更明智。比如:
  • 可能会传递空指针
  • 函数要把指针或对地址的引用赋值给输入形参

  总而言之, 大多时候输入形参往往是const T&,若用const T则说明输入另有处理。所以若要使用const T,则应给出相应的理由, 否则会使得读者感到迷惑。

4.4 函数重载

若要使用函数重载,则必须能让读者一看调用点就胸有成竹,而不用花心思猜测调用的重载函数到底是哪一种。这一规则也适用于构造函数

定义
  你可以编写一个参数类型为const string&的函数,然后用另一个参数类型为const char*的函数对其进行重载:

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

优点
  通过重载参数不同的同名函数, 可以令代码更加直观. 模板化代码需要重载, 这同时也能为使用者带来便利。
缺点
  如果函数单靠不同的参数类型而重载 (注:这意味着参数数量不变), 读者就得十分熟悉 C++ 五花八门的匹配规则, 以了解匹配过程具体到底如何。另外, 如果派生类只重载了某个函数的部分变体, 继承语义就容易令人困惑。
结论
  如果打算重载一个函数, 可以试试改在函数名里加上参数信息. 例如, 用AppendString()和AppendInt()等,而不是一口气重载多个Append()。如果重载函数的目的是为了支持不同数量的同一类型参数, 则优先考虑使用std::vector以便使用者可以用 列表初始化 指定参数。

4.5 缺省参数

只允许在非虚函数中使用缺省参数, 且必须保证缺省参数的值始终一致. 缺省参数与 函数重载遵循同样的规则. 一般情况下建议使用函数重载, 尤其是在缺省函数带来的可读性提升不能弥补下文中所提到的缺点的情况下

优点
  有些函数一般情况下使用默认参数, 但有时需要又使用非默认的参数. 缺省参数为这样的情形提供了便利, 使程序员不需要为了极少的例外情况编写大量的函数. 和函数重载相比, 缺省参数的语法更简洁明了, 减少了大量的样板代码, 也更好地区别了 “必要参数” 和 “可选参数”。
缺点

  • 缺省参数实际上是函数重载语义的另一种实现方式, 因此所有不应当使用函数重载的理由也都适用于缺省参数
  • 虚函数调用的缺省参数取决于目标对象的静态类型, 此时无法保证给定函数的所有重载声明的都是同样的缺省参数
  • 缺省参数是在每个调用点都要进行重新求值的, 这会造成生成的代码迅速膨胀. 作为读者, 一般来说也更希望缺省的参数在声明时就已经被固定了, 而不是在每次调用时都可能会有不同的取值
  • 缺省参数会干扰函数指针, 导致函数签名与调用点的签名不一致. 而函数重载不会导致这样的问题

结论

  • 对于虚函数, 不允许使用缺省参数, 因为在虚函数中缺省参数不一定能正常工作. 如果在每个调用点缺省参数的值都有可能不同, 在这种情况下缺省函数也不允许使用。(例如, 不要写像void f(int n = counter++);这样的代码)。
  • 在其他情况下, 如果缺省参数对可读性的提升远远超过了以上提及的缺点的话, 可以使用缺省参数。如果仍有疑惑, 就使用函数重载

4.6 函数返回类型后置语法

只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法

定义
  C++ 现在允许两种不同的函数声明方式.。以往的写法是将返回类型置于函数名之前。例如:

int foo(int x);

  C++11 引入了这一新的形式. 现在可以在函数名前使用auto关键字,在参数列表之后后置返回类型. 例如:

auto foo(int x) -> int;

  后置返回类型为函数作用域。对于像int这样简单的类型, 两种写法没有区别. 但对于复杂的情况, 例如类域中的类型声明或者以函数参数的形式书写的类型, 写法的不同会造成区别。
优点

  • 后置返回类型是显式地指定 Lambda 表达式 的返回值的唯一方式。某些情况下, 编译器可以自动推导出 Lambda 表达式的返回类型, 但并不是在所有的情况下都能实现。即使编译器能够自动推导, 显式地指定返回类型也能让读者更明了
  • 在已经出现了的函数参数列表之后指定返回类型, 能够让书写更简单, 也更易读, 尤其是在返回类型依赖于模板参数时。例如
template <class T, class U> auto add(T t, U u) -> decltype(t + u);

  对比下面的例子:

template <class T, class U> decltype(declval<T&>() + declval<U&>()) add(T t, U u);

缺点

  • 后置返回类型相对来说是非常新的语法, 而且在 C 和 Java 中都没有相似的写法, 因此可能对读者来说比较陌生
  • 在已有的代码中有大量的函数声明, 你不可能把它们都用新的语法重写一遍。因此实际的做法只能是使用旧的语法或者新旧混用。在这种情况下, 只使用一种版本是相对来说更规整的形式

结论
 &ems;在大部分情况下, 应当继续使用以往的函数声明写法, 即将返回类型置于函数名前。只有在必需的时候 (如 Lambda 表达式) 或者使用后置语法能够简化书写并且提高易读性的时候才使用新的返回类型后置语法。但是后一种情况一般来说是很少见的, 大部分时候都出现在相当复杂的模板代码中,而多数情况下不鼓励写这样复杂的模板代码。

5、来自Google 的奇技

  Google 用了很多自己实现的技巧 / 工具使 C++ 代码更加健壮, 我们使用 C++ 的方式可能和你在其它地方见到的有所不同。

你可能感兴趣的:(C++,c++,开发语言)