C++编程规范总结

引言

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 不要注释代码,代码不使用就直接删掉

有些人不习惯使用版本控制工具,某段代码不再使用了,他们会注释掉代码,而不是直接删除掉。他们的理由是,这段代码现在没有用,可能以后会有用,我注释了,以后真的再用的时候,就不用再写了。

不要这样做。

注释掉的代码,放在源文件里面,会将正常的代码搞混乱。有个破窗理论,说假如一个窗户破了,不去管它,路人就会倾向敲烂其它的窗户。同样,假如你看到代码某个地方乱了,会觉得再搞的更乱也没有关系,就会越来越乱。

而在现代的版本控制工具下,只要写好提交记录,找回从前的代码是很容易的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容