引言
C++ 是一门十分复杂并且威力强大的语言,使用这门语言的时候我们应该有所节制,绝对的自由意味着混乱。
1.规范的作用:我十分清楚每个人对怎么编写代码都有自己的偏好。这里定下的规范,某些地方可能会跟个人原来熟悉的习惯相违背,并引起不满。但多人协作的时候,需要有一定规范。定下一些规范,当大家面对某些情况,有所分歧的时候,容易达成共识。另外通过一定规范,加强代码的一致性,从团队中某人的代码切换到另一个人的代码,会更为自然,让别人可以读懂你的代码是很重要的。
2.这里规范是死的,现实是多变的,当你觉得某些规范,对你需要解决问题反而有很大限制,可以违反,但要有理由,而不仅仅是借口。那到底是否在寻找借口,并没有很明确的判断标准。这就如同不能规定少于多少根头发为秃头,但当我们看到某个人的时候,自然能够判断他是否是秃头。同样,当我们碰到具体情况的时候,自然能够判断是否在寻找借口。
3.本规范编写过程中,大量参考了《Google C++ 编程规范》,Google那份规范十分好,建议大家对比着看。
1 格式
1.1 每行代码不多于 80 个字符
从前的电脑终端,每行只可以显示 80 个字符。现在有更大更宽的显示屏,很多人会认为这条规则已经没有必要。但我们有充分的理由:
版本控制软件,或者编码过程中,经常需要在同一显示屏幕上,左右并排对比新旧两个文件。80 个字符的限制,使得两个文件都不会折行,对比起来更清晰。
当代码超过 3 层嵌套,代码行就很容易超过 80 个字符。这条规则防止我们嵌套太多层级,层级嵌套太深会使得代码难以读懂。
规则总会有例外。比如当你有些代码行,是82个字符,假如我们强制规定少于80字符,人为将一行容易读的代码拆分成两行代码,就太不人性化了。
我们可以适当超过这个限制。
1.2 使用空格(Space),而不是制表符(Tab)来缩进,每次缩进4个字符
代码编辑器,基本都可以设置将Tab转为空格,请打开这个设置。
制表符在每个软件中的显示,都会有所不同。有些软件中每个Tab缩进8个字符,有些软件每个Tab缩进4个字符,随着个人的设置不同而不同。只使用空格来缩进,保证团队中每个人,看同一份代码,格式不会乱掉。
1.3 指针符号*,引用符号& 的位置,写在靠近类型的地方
对比两种写法, 写成第(1)种。
CCNode* p = CCNode::create(); // (1)
CCNode *p = CCNode::create(); // (2)
我知道这个规定有很大的争议。指针符号到底靠近类型,还是靠近变量,这争论一直没有停过。其实两种写法都没有什么大问题,关键是统一。经考虑,感觉第1种写法更统一更合理。理由:
在类中连续写多个变量,通常会用 Tab 将变量对齐。( Tab 会转化成空格)。比如
CCNode* _a;
CCNode _b;
int _c;
当星号靠近类型而不是变量。_a, _b, _c 等变量会很自然对齐。
而当星号靠近变量,如果不手动多按空格微调,会写成。
CCNode *_a;
CCNode _b;
int _c;
指针符号靠近类型,语法上更加统一。比如
const char* getTableName();
static_cast<CCLayer*>(node);
反对第一种写法的理由通常是:
假如某人连续定义多个变量,就会出错。
int* a, b, c;
上面写法本身就有问题。指针应该每行定义一个变量, 并初始化。
int* a = nullptr;
int* b = nullptr;
int* c = nullptr;
1.4 花括号位置
采用Allman风格,if, for, while,namespace, 命名空间等等的花括号,另起一行。例子
for (auto i = 0; i < 100; i++)
{
printf("%d\n", i);
}
这条规定,很可能又引起争议。很多人采用 K&R 风格,将上面代码写成
for (auto i = 0; i < 100; i++) {
printf("%d\n", i);
}
1.5 if, for, while等语句就算只有一行,也强制使用花括号
永远不要省略花括号,不要写成:
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
需要写成:
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
{
goto fail;
}
省略花括号,以后修改代码,或者代码合并的时候,容易直接多写一行。如
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail;
就会引起错误。
2 命名约定
2.1 使用英文单词,不能夹着拼音
这条规则强制执行,不能有例外。
2.2 总体上采用骆驼命名法
单词与单词之间,使用大小写相隔的方式分开,中间不包含下划线。比如
TimerManager // (1)
playMusic // (2)
其中(1)为大写的骆驼命名法,(2)为小写的骆驼命名法。
不要使用
timer_manager
play_music
这种小写加下划线的方式在 boost 库,C++ 标准库中,用得很普遍。
2.3 名字不要加类型前缀
有些代码库,会在变量名字前面加上类型前缀。比如 b表示 bool, i 表示 int , arr 表示数组, sz 表示字符串等等。他们会命名为
bool bEmpty;
const char* szName;
Array arrTeachers;
我们不提倡这种做法。变量名字应该关注用途,而不是它的类型。上面名字应该修改为
bool isEmpty;
const char* name;
Array teachers;
注意,我们将 bool 类型添加上is。isEmpty, isOK, isDoorOpened,等等,读起来就是一个询问句。
2.4 类型命名
类型命名采用大写的骆驼命名法,每个单词以大写字母开头,不包含下划线。比如
GameObject
TextureSheet
类型的名字,应该带有描述性,是名词,而不要是动词。尽量避开Data, Info, Manager 这类的比较模糊的字眼。(但我知道有时也真的避免不了,看着办。)
所有的类型,class, struct, typedef, enum, 都使用相同的约定。例如
class UrlTable
struct UrlTableProperties
typedef hash_map<UrlTableProperties*, std::string> PropertiesMap;
enum UrlTableError
2.5 变量命名
2.5.1 普通变量名字
变量名字采用小写的骆驼命名法。比如
std::string tableName;
CCRect shapeBounds;
变量的名字,假如作用域越长,就越要描述详细。作用域越短,适当简短一点。比如
for (auto& name : _studentNames)
{
std::cout << name << std::endl;
}
for (size_t i = 0; i < arraySize; i++)
{
array[i] = 1.0;
}
名字清晰,并且尽可能简短。
2.5.2 类成员变量
成员变量,访问权限只分成两级,private 和 public,不要用 protected。 私有的成员变量,前面加下划线。比如:
class Image
{
public:
.....
private:
size_t _width;
size_t _height;
}
public 的成员变量,通常会出现在 C 风格的 struct 中,前面不用加下划线。比如:
struct Color4f
{
float red;
float green;
float blue;
float alpha;
}
2.5.3 静态变量
类中尽量不要出现静态变量。类中的静态变量不用加任何前缀。文件中的静态变量统一加s_前缀,并尽可能的详细命名。比如
static ColorTransformStack s_colorTransformStack; // 对
static ColorTransformStack s_stack; // 错(太简略)
2.5.4 全局变量
不要使用全局变量。真的没有办法,加上前缀 g_,并尽可能的详细命名。比如
Document g_currentDocument;
2.6 函数命名
变量名字采用小写的骆驼命名法。比如
playMusic
getSize
isEmpty
函数名字。整体上,应该是个动词,或者是形容词(返回bool的函数),但不要是名词。
teacherNames(); // 错(这个是总体是名词)
getTeacherNames(); // 对
无论是全局函数,静态函数,私有的成员函数,都不强制加前缀。但有时静态函数,可以适当加s_前缀。
类的成员函数,假如类名已经出现了某种信息,就不用重复写了。比如
class UserQueue
{
public:
size_t getQueueSize(); // 错(类名已经为Queue了,
// 这里再命名为getQueueSize就无意义)
size_t getSize(); // 对
}
2.7 命名空间
命令空间的名字,使用小写加下划线的形式,比如
namespace lua_wrapper;
使用小写加下划线,而不要使用骆驼命名法。可以方便跟类型名字区分开来。比如
lua_wrapper::getField(); // getField是命令空间lua_wrapper的函数
LuaWrapper::getField(); // getField是类型LuaWrapper的静态函数
2.8 宏命名
不建议使用宏,但真的需要使用。宏的名字,全部大写,中间加下划线相连接。这样可以让宏更显眼一些。比如
#define PI_ROUNDED 3.0
CLOVER_TEST
MAX
MIN
头文件出现的防御宏定义,也全部大写,比如:
#ifndef __COCOS2D_FLASDK_H__
#define __COCOS2D_FLASDK_H__
....
#endif
不要写成这样:
#ifndef __cocos2d_flashsdk_h__
#define __cocos2d_flashsdk_h__
....
#endif
2.9 枚举命名
尽量使用 0x11 风格 enum,例如:
enum class ColorType : uint8_t
{
Black,
While,
Red,
}
枚举里面的数值,全部采用大写的骆驼命名法。使用的时候,就为 ColorType::Black
有些时候,需要使用0x11之前的enum风格,这种情况下,每个枚举值,都需要带上类型信息,用下划线分割。比如
enum HttpResult
{
HttpResult_OK = 0,
HttpResult_Error = 1,
HttpResult_Cancel = 2,
}
2.10 纯 C 风格的接口
假如我们需要结构里面的内存布局精确可控,有可能需要编写一些纯C风格的结构和接口。这个时候,接口前面应该带有模块或者结构的名字,中间用下划线分割。比如
struct HSBColor
{
float h;
float s;
float b;
};
struct RGBColor
{
float r;
float g;
float b;
}
RGBColor color_hsbToRgb(HSBColor hsb);
HSBColor color_rgbToHsb(RGBColor rgb);
这里,color 就是模块的名字。这里的模块,充当 C++ 中命名空间的作用。
struct Path
{
....
}
Path* Path_new();
void Path_destrory(Path* path);
void Path_moveTo(Path* path, float x, float y);
void Path_lineTo(Path* path, float x, float y);
这里,接口中Path出现的是类的名字。
2.11 代码文件,路径命名
代码文件的名字,应该反应出此代码单元的作用。
比如 Point.h, Point.cpp,实现了class Point;
当 class Point,的名字修改成,Point2d, 代码文件名字,就应该修改成 Point2d.h, Point2d.cpp。代码文件名字,跟类型名字一样,采用大写的骆驼命名法。
路径名字,对于于模块的名字。跟上一章的命名规范一样,采用小写加下划线的形式。比如
ui/home/HomeLayer.h
ui/battle/BattleCell.h
support/geo/Point.h
support/easy_lua/Call.h
路径以及代码文件名,不能出现空格,中文,不能夹着拼音。假如随着代码的修改,引起模块名,类型名字的变化,应该同时修改文件名跟路径名。
2.12 命名避免带有个人标签
比如,不要将某个模块名字为
HJCPoint
hjc/Label.h
hjc为团队某人名字的缩写。
项目归全体成员所有,任何人都有权利跟义务整理修改工程代码。当某样东西打上个人标记,就倾向将其作为私有。其他人就会觉得那代码乱不关自己事情,自己就不情愿别人来动自己东西。
当然了,文件开始注释可以出现创建者的名字,信息。只是类型,模块,函数名字,等容易在工程中散开的东西不提倡。个人项目可以忽略这条。
再强调一下,任何人都有权利跟义务整理修改他人代码,只要你觉得你修改得合理,但不要自作聪明。我知道有些程序员,会觉得他人修改自己代码,就是入侵自己领土。
2.13 例外
有些时候,我们需要自己写的库跟C++的标准库结合。这时候可以采用跟C++标准库相类似的风格。比如
class MyArray
{
public:
typedef const char* const_iteator;
...
const char* begin() const;
const char* rbegin() const;
}
3 代码文件
3.1 #define 保护
所有的头文件,都应该使用#define来防止头文件被重复包含。命名的格式为
__<模块>_<文件名>_H__
很多时候,模块名字都跟命名空间对应。比如
#ifndef __GEO_POINT_H__
#define __GEO_POINT_H__
namespace geo
{
class Point
{
.....
};
}
#endif
并且,#define宏,的名字全部都为大写。不要出现大小写混杂的形式。
3.2 #include 的顺序
C++代码使用#include来引入其它的模块的头文件。尽可能,按照模块的稳定性顺序来排列#include的顺序。按照稳定性从高到低排列。
比如
#include <map>
#include <vector>
#include <boost/noncopyable.hpp>
#include "cocos2d.h"
#include "json.h"
#include "FlaSDK.h"
#include "support/TimeUtils.h"
#include "Test.h"
上面例子中。#include的顺序,分别是C++标准库,boost库,第三方库,我们自己写的跟工程无关的库,工程中比较基础的库,应用层面的文件。
但有一个例外,就是 .cpp中,对应的.h文件放在第一位。比如geo模块中的, Point.h 跟 Point.cpp文件,Point.cpp中的包含
#include "geo/Point.h"
#include <cmath>
这里,将 #include "geo/Point.h",放到第一位,之后按照上述原则来排列#include顺序。理由下一条规范来描述。
3.3 尽可能减少头文件的依赖
代码文件中,每出现一次#include包含, 就会多一层依赖。比如,有A,B类型,各自有对应的.h文件和.cpp文件。
当A.cpp包含了A.h, A.cpp就依赖了A.h,我们表示为
A.cpp -> A.h
这样,当A.h被修改的时候,A.cpp就需要重修编译。 假设
B.cpp -> B.h
B.h -> A.h
这表示,B.cpp 包含了B.h, B.h包含了A.h, 这个时候。B.cpp虽然没有直接包含A.h, 但也间接依赖于A.h。当A.h修改了,B.cpp也需要重修编译。
当在头文件中,出现不必要的包含,就会生成不必要的依赖,引起连锁反应,使得编译时间大大被拉长。
使用前置声明,而不是直接#include,可以显著地减少依赖数量。实践方法:
3.3.1 头文件第一位包含
比如写类A,有文件 A.h, 和A.cpp 那么在A.cpp中,将A.h的包含写在第一位。在A.cpp中写成
// 前面没有别的头文件包含
#include "A.h"
#include <string>
#include .......
.... 包含其它头文件
之后可以尝试在 A.h 中去掉多余的头文件。当A.cpp可以顺利编译通过的时候,A.h包含的头文件就是过多或者刚刚好的。而不会是包含不够的。
3.3.2 前置声明
首先,只在头文件中使用引用或者指针,而不是使用值的,可以前置声明。而不是直接包含它的头文件。 比如
class Test : public Base
{
public:
void funA(const A& a);
void funB(const B* b);
void funC(const space::C& c);
private:
D _d;
};
这里,我牵涉到几个其它类,Base, A, B, space::C(C 在命名空间space里面), D。Base和D需要知道值,A, B, space::C只是引用和指针。所以Base, C的头文件需要包含。A, B,space::C只需要前置声明。
#include "Base.h"
#include "D.h"
namespace space
{
class C;
}
class A;
class B;
class Test : public Base
{
public:
void funA(const A& a);
void funB(const B* b);
void funC(const space::C& c);
private:
D _d;
};
注意命名空间里面的写法。
3.3.3 impl 手法
就是类里面包含实现类的指针。在cpp里面实现。
3.3.4 尽可能将代码拆分成相对独立的,粒度小的单元,放到不同的文件中
简单说,就是不要将所有东西都塞在一起。这样的代码组积相对清晰。头文件包含也相对较少。但现实中,或多或少会违反。
比如,工程用到一些常量字符串(或者消息定义,或者enum值,有多个变种)。一个似乎清晰的结构,是将字符串都放到同一个头文件中。不过这样一来,这个字符串文件,就几乎会被所有项目文件包含。当以后新加一个字符串时候,就算只加一行,工程几乎被全部编译。
更好的做法,是按照字符串的用途来分拆开。
又比如,有些支持库。有时贪图方便,不注意的,就会写一个 GlobalUtils.h 之类的头文件,包含所有支持库,因为这样可以不关心到底应该包含哪个,反正包含GlobalUtils.h就行,这样多省事。不过这样一来,需要加一个支持的函数,比如就只是角度转弧度的小函数,也会发生连锁编译。
更好的做法,是根据需要来包含必要的文件。就算你麻烦一点,写10行#include的代码,都比之后修改一行代码,就编译上10多分钟要好。
3.4 小结
减少编译时间,这点很重要。再啰嗦一下
要减少头文件重复包含,需要团队的人所有人达成共识,认识到这是不好的。很多人对这问题认识不够,会被当成小题大作。
不要贪方便。直接包含一个大的头文件,短期是很方便,长期会有麻烦。
3.5 #include中的头文件,尽量使用全路径,或者相对路径
路径的起始点,为工程文件代码文件的根目录。
比如
#include "ui/home/HomeLayer.h"
#include "ui/home/HomeCell.h"
#include "support/MathUtils.h"
不要直接包含
#include "HomeLayer.h"
#include "HomeCell.h"
#include "MathUtils.h"
这样可以防止头文件重名,比如一个第三方库文件有可能就叫 MathUtils.h。
并且移植到其它平台,配置起来会更容易。比如上述例子,在安卓平台上,就需要配置包含路径
<Project_Root>/ui/home/
<Project_Root>/support/
也可以使用相对路径。比如
#include "../MathUtil.h"
#include "./home/HomeCell.h"
这样做,还有个好处。就是只用一个简单脚本,或者一些简单工具。就可以分析出头文件的包含关系图,然后就很容易看出循环依赖。
4 作用域
作用域,表示某段代码或者数据的生效范围。作用域越大,修改代码时候影响区域也就越大,原则上,作用域越小越好。
4.1 全局变量
禁止使用全局变量。全局变量在项目的任何地方都可以访问。两个看起来没有关系的函数,一旦访问了全局变量,就会产生无形的依赖。使用全局变量,基本上都是怕麻烦,贪图方便。比如
funA -> funB -> funC -> funD
上图表示调用顺序。当funD需要用到funA中的某个数据。正确的方式,是将数据一层层往下传递。但因为这样做,需要修改几个地方,修改的人怕麻烦,直接定义出全局变量。这样做,当然是可以快速fix bug。但funA跟funD就引入无形的依赖,从接口处看不出来。
单件可以看做全局变量的变种。最优先的方式,应该将数据从接口中传递,其次封装单件,再次使用函数操作静态数据,最糟糕就是使用全局变量。
若真需要使用全局变量。变量使用g_开头。
4.2 类的成员变量
类的成员变量,只能够是private或者public, 不要设置成protected。protected的数据看似安全,实际只是一种错觉。
数据只能通过接口来修改访问,不要直接访问。这样的话,在接口中设置个断点就可以调试知道什么时候数据被修改。另外改变类的内部数据表示,也可以维持接口的不变,而不影响全局。
绝大多数情况,数据都应该设置成私有private, 变量加 _前缀。比如
class Data
{
private:
const uint8_t* _bytes;
size_t _size;
}
公有的数据,通常出现在C风格的结构中,或者一些数据比较简单,并很常用的类,public数据不要加前缀。
class Point
{
public:
Point(float x_, float y_) : x(x_), y(y_)
{
}
.....
float x;
float y;
}
注意,我们在构造函数,使用 x_ 的方式表示传入的参数,防止跟 x 来重名。
4.3 局部变量
局部变量尽可能使它的作用范围最小。换句话说,就是需要使用的时候才定义,而不要在函数开始就全部定义。
从前C语言有个约束,需要将用到的全部变量都定义在函数最前面。之后这个习惯也被传到C++的代码当中。但这种习惯是很不好的。
在函数最前面定义变量,变量就在整个函数都可见,作用域越大,就越容易被误修改。
C++ 中,定义类型的变量,需要调用构造函数,跟释放函数。很多时候函数中途就退出了,这时候调用构造函数和释放函数,就显得浪费。
变量在最开始的时候,很难给变量一个合理的初始值,很难的话,也就很容易忘记。
我们的结论是,局部变量真正需要使用的时候才定义,一行定义一个变量,并且一开始就给它一个合适的初始值。
int i;
i = f(); // 错,初始化和定义分离
int j = g(); // 对,定义时候给出始值
4.4 命名空间
C++中,尽量不要出现全局函数,应该放入某个命名空间当中。命名空间将全局的作用域细分,可有效防止全局作用域的名字冲突。
比如
namespace json
{
class Value
{
....
}
}
namespace splite
{
class Value
{
...
}
}
两个命名空间都出现了Value类。外部访问时候,使用 json::Value, splite::Value来区分。
4.5 文件作用域
假如,某个函数,或者类型,只在某个.cpp中使用,请将函数或者类放入匿名命名空间。来防止文件中的函数导出。比如
// fileA.cpp
namespace
{
void doSomething()
{
....
}
}
上述例子,doSomething这个函数,放入了匿名空间。因此,此函数限制在fileA.cpp中使用。另外的文件定义相同名字的函数,也不会造成冲突。
另外传统C的做法,是在 doSomething 前面加 static, 比如
// fileB.cpp
static void doSomething()
{
...
}
doSomething也限制到文件fileB.cpp中。
同理,只在文件中出现的类型,也放到匿名空间中。比如
// sqlite/Value.cpp
namespace sqlite
{
namespace
{
class Record
{
....
}
}
}
上述例子,匿名空间嵌套到sqlite空间中。这样Record这个结构只可以在sqlite/Value.cpp中使用,就算是同属于空间sqlite的文件,也不知道 Record 的存在。
4.6 头文件不要出现 using namespace …
头文件,很可能被多个文件包含。当某个头文件出现了 using namespace ... 的字样,所有包含这个头文件的文件,都简直看到此命令空间的全部内容,就有可能引起冲突。比如
// Test.h
#include <string>
using namespace std;
class Test
{
public:
Test(const string& name);
};
这个时候,只要包含了Test.h, 就都看到std的所有内容。正确的做法,是头文件中,将命令空间写全。将 string, 写成 std::string, 这里不要偷懒。
5 类
面向对象编程中,类是基本的代码单元。本节列举了在写一个类的时候,需要注意的事情。
5.1 让类的接口尽可能小
设计类的接口时,不要想着接口以后可能有用就先加上,而应该想着接口现在没有必要,就直接去掉。
这里的接口,你可以当成类的成员函数。添加接口是很容易的,但是修改,去掉接口会会影响较大。
接口小,不单指成员函数的数量少,也指函数的作用域尽可能小。
比如:
class Test
{
public:
void funA();
void funB();
void funC();
void funD();
};
假如,funD 其实是可以使用 funA, funB, funC 来实现的。
这个时候,funD,就不应该放到Test里面。可以将funD抽取出来。funD 只是一个封装函数,而不是最核心的。
void Test_funD(Test* test);
编写类的函数时候,一些辅助函数,优先采用 Test_funD 这样的方式,将其放到.cpp中,使用匿名空间保护起来,外界就就不用知道此函数的存在,那些都只是实现细节。
当不能抽取独立于类的辅助函数,先将函数,变成private, 有必要再慢慢将其提出到public。 不要觉得这函数可能有用,一下子就写上一堆共有接口。
再强调一次,如无必要,不要加接口。
从作用域大小,来看
独立于类的函数,比类的成员函数要好
私有函数,比共有函数要好
非虚函数,比虚函数要好
5.2 声明顺序
类的成员函数或者成员变量,按照使用的重要程度,从高到低来排列。
比如,使用类的时候,用户更关注函数,而不是数据,所以成员函数应该放到成员变量之前。 再比如,使用类的时候,用户更关注共有函数,而不是私有函数,所以public,应该放在private前面。
具体规范
按照 public, protected, private 的顺序分块。那一块没有,就直接忽略。
每一块中,按照下面顺序排列
typedef,enum,struct,class 定义的嵌套类型
常量
构造函数
析构函数
成员函数,含静态成员函数
数据成员,含静态数据成员
.cpp 文件中,函数的实现尽可能给声明次序一致。
5.3 继承
优先使用组合,而不是继承。
继承主要用于两种场合:实现继承,子类继承了父类的实现代码。接口继承,子类仅仅继承父类的方法名称。
我们不提倡实现继承,实现继承的代码分散在子类跟父亲当中,理解起来变得很困难。通常实现继承都可以采用组合来替代。
规则:
继承应该都是 public
假如父类有虚函数,父类的析构函数为 virtual
假如子类覆写了父类的虚函数,应该显式写上 override
比如:
// swf/Definition.h
class Definition
{
public:
virtual ~Definition() {}
virtual void parse(const uint8_t* bytes, size_t len) = 0;
};
// swf/ShapeDefinition.h
class ShapeDefinition : public Definition
{
public:
ShapeDefinition() {}
virtual void parse(const uint8_t* bytes, size_t len) override;
private:
Shape _shape;
};
Definition* p = new ShapeDefinition();
....
delete p;
上面的例子,使用父类的指针指向子类,假如父类的析构函数不为virtual, 就只会调用父类的Definition的释放函数,引起子类独有的数据不能释放。所有需要加上virtual。
另外子类覆写的虚函数写上,override的时候,当父类修改了虚函数的名字,就会编译错误。从而防止,父类修改了虚函数接口,而忘记修改子类相应虚函数接口的情况。
6 函数
6.1 编写短小的函数
函数尽可能的短小,凝聚,功能单一。
只要某段代码,可以用某句话来描述,尽可能将这代码抽取出来,作为独立的函数,就算那代码只有一行。最典型的就是C++中的max, 实现只有一句话。
template <typename T>
inline T max(T a, T b)
{
return a > b ? a : b;
}
将一段代码抽取出来,作为一个整体,一个抽象,就不用纠结在细节之中。
将一个长函数,切割成多个短小的函数。每个函数中使用的局部变量,作用域也会变小。
短小的函数,更容易复用,从一个文件搬到另一个文件也会更容易。
短小的函数,因为内存局部性,运行起来通常会更快。
短小的函数,也容易阅读,调试。
6.2 函数的参数可能少,原则上不超过5个
人脑短时记忆的数字是很有限的,大约可以记忆7个数字。有些人多些,有些人少些。我们这里取最少值,就是5个参数。
参数的个数,太多,就很容易混乱,记不住参数的意义。
同时参数的个数太多,很可能是因为这个函数做的事情有点多了。
可以通过很多手段来减少参数的个数。比如将函数分解,分解成多个短小的函数。或者将几个经常一起的参数,封装成一个类或者结构。比如,设计一个绘画贝塞尔曲线的接口
void drawQuadBeizer(float startX, float startY,
float controlX, float controlY,
float endX, float endY);
这样的接口,就不够
void drawQuadBeizer(const Point& start,
const Point& control,
const Point& end);
简洁易用。
当然,每个规则都会有例外。比如设置一个矩阵的数值,二维矩阵本来就需要6个数字来表示,设置接口自然需要6个参数。
6.3 函数参数顺序
参数顺序,按照传入参数,传出参数,的顺序排列。不要使用可传入可传出的参数。
bool loadFile(const std::string& filePath, ErrorCode* code); // 对
bool loadFile(ErrorCode* code, const std::string& filePath); // 错
保持统一的顺序,使得他人容易记忆。
6.4 函数的传出参数,使用指针,而不要使用引用
比如
bool loadFile(const std::string& filePath, ErrorCode* code); // 对
bool loadfile(const std::string& filePath, ErrorCode& code); // 错
因为当使用引用的时候,使用函数的时候会变成
ErrorCode code;
if (loadFile(filePath, code))
{
...
}
而使用指针,调用的时候,会是
ErrorCode code;
if (loadFile(filePath, &code))
{
...
}
这样从,&code的方式可以很明显的区分,传入,传出参数。试比较
doFun(arg0, arg1, arg2); // 错
doFun(arg0, &arg1, &arg2); // 对
6.5 不建议使用函数的缺省参数
我们经常会通过查看现有的代码来了解如何使用函数的接口。缺省参数使得某些参数难以从调用方就完全清楚,需要去查看函数的接口,也就是完全了解某个接口,需要查看两个地方。
另外,缺省参数那个数值,其实是实现的一部分,写在头文件是不适当的。
缺省参数,其实可以通过将一个函数拆分成两个函数。实现放到.cpp中。
7 其它
7.1 const的使用
我们建议,尽可能的多使用const。
C++中,const是个很重要的关键字,应用了const之后,就不可以随便改变变量的数值了,不小心改变了编译器会报错,就容易找到错误的地方。只要你觉得有不变的地方,就用const来修饰吧。比如:
想求圆的周长,需要用到Pi, Pi不会变的,加const,const double Pi = 3.1415926;
需要在函数中传引用,只读,不会变的,前面加const;
函数有个返回值,返回值是个引用,只读,不会变的,前面加const;
类中有个private数据,外界要以函数方式读取,不会变的,加const,这个时候const就是加在函数定义末尾。
const的位置:
const int* name; // 对(这样写,可读性更好)
int const* name; // 错
7.2 不要注释代码,代码不使用就直接删掉
有些人不习惯使用版本控制工具,某段代码不再使用了,他们会注释掉代码,而不是直接删除掉。他们的理由是,这段代码现在没有用,可能以后会有用,我注释了,以后真的再用的时候,就不用再写了。
不要这样做。
注释掉的代码,放在源文件里面,会将正常的代码搞混乱。有个破窗理论,说假如一个窗户破了,不去管它,路人就会倾向敲烂其它的窗户。同样,假如你看到代码某个地方乱了,会觉得再搞的更乱也没有关系,就会越来越乱。
而在现代的版本控制工具下,只要写好提交记录,找回从前的代码是很容易的。