头文件
一般情况下,每.CPP
文件应该有一个相关的·h
文件。有一些常见的例外,如单元测试代码和只包含一个main
函数的cpp
文件。
正确使用头文件在可读性,文件大小和性能上有很大差异。
下面的规则将指导您绕过头文件使用中的各种陷阱。
#define用法
所有头文件应该由#define
防护,以避免多重包含。符号名称的格式应该是<PROJECT>_<PATH>_<FILE>_H_
。
为了保证唯一性,它们应根据在项目的源代码树的完整路径。例如,在文件中FOO项目cocos2dx/sprites_nodes/CCSprite.h
应具有以下防护:
#ifndef COCOS2DX_SPRITE_NODES_CCSPRITE_H_
#define COCOS2DX_SPRITE_NODES_CCSPRITE_H_
...
#endif // COCOS2DX_SPRITE_NODES_CCSPRITE_H_
// Pragma once is still open for debate
#pragma once
我们在考虑是是否使用#pragma once
,我们不确定他能支持所有平台。
前向声明
前向声明普通类可以避免不必要的#includes
。
定义:
“前向声明”是类、函数或者模版的声明,没有定义。用前向声明来替代#include
通常应用在客户端代码中。
优点:
- 不必要的
#includes
会强制编译器打开更多的文件并处理更多的输入。 - 不必要的
#includes
也会导致代码被更经常重新编译,因为头文件修改。
缺点:
- 不容易确定模版、typedefs、默认参数等的前向声明以及使用声明。
- 不容易判断对给定的代码该用前向声明还是
#include
,尤其是当有隐式转换时。极端情况下,用#include
代替前向声明会悄悄的改变代码的含义。 - 在头文件中多个前向声明比#include啰嗦。
- 前向声明函数或者模版会阻止头文件对APIs做“否则兼容”(otherwise-compatible)修改;例如,扩展参数类型或者添加带有默认值的模版参数。
- 前向声明std命名空间的符号通常会产生不确定的行为。
- 为了前向声明而结构化代码(例如,适用指针成员,而不是对象成员)会使代码更慢更复杂。
- 前向声明的实际效率提升未经证实。
结论:
- 使用头文件中声明的函数,总是
#include
该头文件。 - 使用类模版,优先使用
#include
。 - 使用普通类,可以用前向声明,但是注意前向声明可能不够或者不正确的情况;如果有疑问,就用
#include
。 - 不应只是为了避免
#include
而用指针成员代替数据成员。
总是#include
实际声明/定义的文件;不依赖非直接包含的头文件中间接引入的符号。例外是,Myfile.cpp
可以依赖Myfile.h
中的#include
和前向声明。
内联函数
只在函数体很小——10行代码以内——的时候将其定义为内联函数。
定义:
你可以在声明函数时允许编译器将其扩展内联,而不是通过常见的函数调用机制调用。
优点:
内联短小精悍的函数可以生成更高效的对象码。推荐内联取值函数、设值函数以及其余性能关键的短函数。
缺点:
滥用内联可能导致程序更慢。内联可能让代码尺寸增加或者减少,这取决于函数的尺寸。内联一个非常小的取值函数通常会减少代码尺寸,而内联一个非常大的函数会显著增加代码尺寸。在现代处理器架构下,更小尺寸的代码因为可以更好的利用指令缓存,通常跑得更快。
结论:
一个黄金法则是不要内联超过10行的函数。要小心析构函数,因为隐含成员和基类的析构函数,它们通常比看上去的要长。
另一个黄金法则:通常不建议内联带循环或者switch
语句的函数(除非,大部分情况下,循环或者switch
语句不会被执行)
需要注意的是,即便函数被声明为内联他们也不一定会真的内联;例如虚函数以及递归函数一般都不会被内联。通常递归函数不应该被内联。将虚函数内联的主要原因是为了方便或者文档需要,将其定义放在类中,例如取值函数以及设值函数。
-inl.h文件
如果有需要,可以用带-inl.h
后缀的文件来定义复杂内联函数。
内联函数的定义必须放在头文件中,这样编译器在函数调用处内联展开时才有函数定义可用。但实现代码通常还是放在.cpp
文件比较合适,因为除非会带来可读性或者性能上的好处,否则我们不希望在.h文件里堆放太多具体的代码。
如果一个内联函数的定义非常短,只含有少量逻辑,你可以把代码放在你的.h文件里。例如取值函数与设值函数都毫无疑问的应该放在类定义中。更复杂的内联函数为了实现者和调用者的方便,也要放在.h文件里,但是如果这样会让.h文件过于臃肿,你也可以将其放在一个单独的-inl.h
文件里。这样可以将具体实现与类定义分开,同时又确保了实现在需要用到的时候是被包含的。
-inl.h
文件还有一个用途是存放函数模板的定义。这样可以让你的模板定义更加易读。
不要忘记,就像其他的头文件一样,一个-inl.h
文件也是需要#define
防护的。
函数参数顺序
定义函数时,参数顺序应该为:输入,然后是输出。
C/C++函数的参数要么是对函数的输入,要么是函数给出的输出,要么两者兼是。输入参数通常是值或者常引用,而输出以及输入/输出参数是非const
指针。在给函数参数排序时,将所有仅输入用的参数放在一切输出参数的前面。特别需要注意的是,在加新参数时不要因为它们是新的就直接加到最后去;新的仅输入用参数仍然要放到输出参数前。
这不是一条不可动摇的铁律。那些既用于输入又用于输出的参数(通常是类/结构体)通常会把水搅浑,同时,为了保持相关函数的一致性,有时也会使你违背这条原则。
include的命名和顺序
使用以下标准顺序以增加可读性,同时避免隐藏的依赖关系:C库,C++库,其他库的.h文件,你自己项目的.h文件。
所有本项目的头文件都应该包含从源代码根目录开始的完整路径,而不要使用UNIX的目录快捷方式.(当前目录)或者..(上层目录)。例如google-awesome-project/src/base/logging.h应写为以下方式
#include "base/logging.h"
例如有文件dir/foo.cpp
或dir/foo_test.cpp
,他们的主要用途是实现或者测试dir2/foo2.h
头文件里的内容,那么include的顺序应该如下:
- dir2/foo2.h (推荐位置——理由见后)
- C system files.
- C++ system files.
- Other libraries' .h files.
- Your project's .h files.
按照这个推荐顺序,如果dir2/foo2.h
漏掉了什么必须的包含文件,dir/foo.cpp
或者dir/foo_test.cpp
就会编译失败。这样的规则就保证了工作在这些文件的人而不是在其他包工作的无辜的人最先发现问题。
dir/foo.cpp
和dir2/foo2.h
通常位于同一个目录(例如base/basictypes_test.cpp
和base/basictypes.h
),但是在不同目录也没问题。
在同一部分中包含文件应该按照字母顺序排列。注意如果老代码不符合这条规则,那就在方便的时候改过来。
例如cocos2dx/sprite_nodes/CCSprite.cpp
的include部分可能如下:
#include "sprite_nodes/CCSprite.h" // Preferred location.
#include <sys/types.h>
#include <unistd.h>
#include <hash_map>
#include <vector>
#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"
特例:有时候系统相关代码需要使用条件包含。这种情况下可以把条件包含放在最后。当然,要保持系统相关代码短小精悍并做好本地化。例如:
#include "foo/public/fooserver.h"
#include "base/port.h"
// For LANG_CXX11.
#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11