当前位置:   article > 正文

代码编写及阅读规范_.h必须放第一个吗

.h必须放第一个吗

阅读常识

1、C语言中在函数名或关键字前加下划线
        一般情况是标识该函数或关键字是自己内部使用的,与提供给外部的接口函数或关键字加以区分。

规范

综述

C++ 是一门十分复杂并且威力强大的语言,使用这门语言的时候我们应该有所节制,绝对的自由意味着混乱。

我十分清楚每个人对怎么编写代码都有自己的偏好。这里定下的规范,某些地方可能会跟个人原来熟悉的习惯相违背,并引起不满。但多人协作的时候,需要有一定规范。定下一些规范,当大家面对某些情况,有所分歧的时候,容易达成共识。另外通过一定规范,加强代码的一致性,从团队中某人的代码切换到另一个人的代码,会更为自然,让别人可以读懂你的代码是很重要的。

通常面对某种情况,会有两种或更多种做法,我们就挑选其中一种,共同遵守。这并不表示另一种做法就是错的,只是仅仅不同。

这里规范是死的,现实是多变的,当你觉得某些规范,对你需要解决问题反而有很大限制,可以违反,但要有理由,而不仅仅是借口。那到底是否在寻找借口,并没有很明确的判断标准。这就如同不能规定少于多少根头发为秃头,但当我们看到某个人的时候,自然能够判断他是否是秃头。同样,当我们碰到具体情况的时候,自然能够判断是否在寻找借口。

本规范编写过程中,大量参考了《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)
  2. CCNode *p = CCNode::create(); // (2)

也就说,上面两种写法。写成第(1)种。

我知道这个规定有很大的争议。指针符号到底靠近类型,还是靠近变量,这争论一直没有停过。其实两种写法都没有什么大问题,关键是统一。经考虑,感觉第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;

  • Xcode中,默认的语法提示,指针符号靠近变量,我再修改成靠近类型,比较麻烦。

这个有点道理。但我们也不能十分依赖工具。可以使用clang_format等美化工具去辅助调整代码。

1.4 花括号位置

采用Allman风格,if, for, while,namespace, 命名空间等等的花括号,另起一行。例子

  1. for (auto i = 0; i < 100; i++)
  2. {
  3. printf("%d\n", i);
  4. }

这条规定,很可能又引起争议。很多人采用 K&R 风格,将上面代码写成

  1. for (auto i = 0; i < 100; i++) {
  2. printf("%d\n", i);
  3. }

K&R风格在书籍印刷上会节省纸张。但在实际的代码中显得过于密集。Allman风格会更加清晰易读。当然,这理由带有很多主观因素。

1.5 if, for, while等语句就算只有一行,也强制使用花括号

永远不要省略花括号,不要写成:

  1. if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
  2. goto fail;

需要写成:

  1. if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
  2. {
  3. goto fail;
  4. }

省略花括号,以后修改代码,或者代码合并的时候,容易直接多写一行。如

  1. if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
  2. goto fail;
  3. goto fail;

就会引起错误。

------------------------------------------


2 命名约定

2.1 使用英文单词,不能夹着拼音

这条规则强制执行,不能有例外。

2.2 总体上采用骆驼命名法

单词与单词之间,使用大小写相隔的方式分开,中间不包含下划线。比如

  1. TimerManager // (1)
  2. playMusic // (2)

其中(1)为大写的骆驼命名法,(2)为小写的骆驼命名法。

不要使用

  1. timer_manager
  2. play_music

这种小写加下划线的方式在 boost 库,C++ 标准库中,用得很普遍。

接下来分别描述具体的命名方式。

2.3 名字不要加类型前缀

有些代码库,会在变量名字前面加上类型前缀。比如 b表示 bool, i 表示 int , arr 表示数组, sz 表示字符串等等。他们会命名为

  1. bool bEmpty;
  2. const char* szName;
  3. Array arrTeachers;

我们不提倡这种做法。变量名字应该关注用途,而不是它的类型。上面名字应该修改为

  1. bool isEmpty;
  2. const char* name;
  3. Array teachers;

注意,我们将 bool 类型添加上is。isEmpty, isOK, isDoorOpened,等等,读起来就是一个询问句。

2.4 类型命名

类型命名采用大写的骆驼命名法,每个单词以大写字母开头,不包含下划线。比如

  1. GameObject
  2. TextureSheet

类型的名字,应该带有描述性,是名词,而不要是动词。尽量避开Data, Info, Manager 这类的比较模糊的字眼。(但我知道有时也真的避免不了,看着办。)

所有的类型,class, struct, typedef, enum, 都使用相同的约定。例如

  1. class UrlTable
  2. struct UrlTableProperties
  3. typedef hash_map<UrlTableProperties*, std::string> PropertiesMap;
  4. enum UrlTableError

2.5 变量命名

2.5.1 普通变量名字

变量名字采用小写的骆驼命名法。比如

  1. std::string tableName;
  2. CCRect shapeBounds;

变量的名字,假如作用域越长,就越要描述详细。作用域越短,适当简短一点。比如

  1. for (auto& name : _studentNames)
  2. {
  3. std::cout << name << std::endl;
  4. }
  5. for (size_t i = 0; i < arraySize; i++)
  6. {
  7. array[i] = 1.0;
  8. }

名字清晰,并且尽可能简短。

2.5.2 类成员变量

成员变量,访问权限只分成两级,private 和 public,不要用 protected。 私有的成员变量,前面加下划线。比如:

  1. class Image
  2. {
  3. public:
  4. .....
  5. private:
  6. size_t _width;
  7. size_t _height;
  8. }

public 的成员变量,通常会出现在 C 风格的 struct 中,前面不用加下划线。比如:

  1. struct Color4f
  2. {
  3. float red;
  4. float green;
  5. float blue;
  6. float alpha;
  7. }

2.5.3 静态变量

类中尽量不要出现静态变量。类中的静态变量不用加任何前缀。文件中的静态变量统一加s_前缀,并尽可能的详细命名。比如

  1. static ColorTransformStack s_colorTransformStack; // 对
  2. static ColorTransformStack s_stack; // 错(太简略)

2.5.4 全局变量

不要使用全局变量。真的没有办法,加上前缀 g_,并尽可能的详细命名。比如

Document  g_currentDocument;

2.6 函数命名

变量名字采用小写的骆驼命名法。比如

  1. playMusic
  2. getSize
  3. isEmpty

函数名字。整体上,应该是个动词,或者是形容词(返回bool的函数),但不要是名词。

  1. teacherNames(); // 错(这个是总体是名词)
  2. getTeacherNames(); // 对

无论是全局函数,静态函数,私有的成员函数,都不强制加前缀。但有时静态函数,可以适当加s_前缀。

类的成员函数,假如类名已经出现了某种信息,就不用重复写了。比如

  1. class UserQueue
  2. {
  3. public:
  4. size_t getQueueSize(); // 错(类名已经为Queue了,
  5. // 这里再命名为getQueueSize就无意义)
  6. size_t getSize(); // 对
  7. }

2.7 命名空间

命令空间的名字,使用小写加下划线的形式,比如

namespace lua_wrapper;

使用小写加下划线,而不要使用骆驼命名法。可以方便跟类型名字区分开来。比如

  1. lua_wrapper::getField(); // getField是命令空间lua_wrapper的函数
  2. LuaWrapper::getField(); // getField是类型LuaWrapper的静态函数

2.8 宏命名

不建议使用宏,但真的需要使用。宏的名字,全部大写,中间加下划线相连接。这样可以让宏更显眼一些。比如

  1. #define PI_ROUNDED 3.0
  2. CLOVER_TEST
  3. MAX
  4. MIN

头文件出现的防御宏定义,也全部大写,比如:

  1. #ifndef __COCOS2D_FLASDK_H__
  2. #define __COCOS2D_FLASDK_H__
  3. ....
  4. #endif

不要写成这样:

  1. #ifndef __cocos2d_flashsdk_h__
  2. #define __cocos2d_flashsdk_h__
  3. ....
  4. #endif

2.9 枚举命名

尽量使用 0x11 风格 enum,例如:

  1. enum class ColorType : uint8_t
  2. {
  3. Black,
  4. While,
  5. Red,
  6. }

枚举里面的数值,全部采用大写的骆驼命名法。使用的时候,就为 ColorType::Black

有些时候,需要使用0x11之前的enum风格,这种情况下,每个枚举值,都需要带上类型信息,用下划线分割。比如

  1. enum HttpResult
  2. {
  3. HttpResult_OK = 0,
  4. HttpResult_Error = 1,
  5. HttpResult_Cancel = 2,
  6. }

2.10 纯 C 风格的接口

假如我们需要结构里面的内存布局精确可控,有可能需要编写一些纯C风格的结构和接口。这个时候,接口前面应该带有模块或者结构的名字,中间用下划线分割。比如

  1. struct HSBColor
  2. {
  3. float h;
  4. float s;
  5. float b;
  6. };
  7. struct RGBColor
  8. {
  9. float r;
  10. float g;
  11. float b;
  12. }
  13. RGBColor color_hsbToRgb(HSBColor hsb);
  14. HSBColor color_rgbToHsb(RGBColor rgb);

这里,color 就是模块的名字。这里的模块,充当 C++ 中命名空间的作用。

  1. struct Path
  2. {
  3. ....
  4. }
  5. Path* Path_new();
  6. void Path_destrory(Path* path);
  7. void Path_moveTo(Path* path, float x, float y);
  8. 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。代码文件名字,跟类型名字一样,采用大写的骆驼命名法。

路径名字,对于于模块的名字。跟上一章的命名规范一样,采用小写加下划线的形式。比如

  1. ui/home/HomeLayer.h
  2. ui/battle/BattleCell.h
  3. support/geo/Point.h
  4. support/easy_lua/Call.h

路径以及代码文件名,不能出现空格,中文,不能夹着拼音。假如随着代码的修改,引起模块名,类型名字的变化,应该同时修改文件名跟路径名。

2.12 命名避免带有个人标签

比如,不要将某个模块名字为

  1. HJCPoint
  2. hjc/Label.h

hjc为团队某人名字的缩写。

项目归全体成员所有,任何人都有权利跟义务整理修改工程代码。当某样东西打上个人标记,就倾向将其作为私有。其他人就会觉得那代码乱不关自己事情,自己就不情愿别人来动自己东西。

当然了,文件开始注释可以出现创建者的名字,信息。只是类型,模块,函数名字,等容易在工程中散开的东西不提倡。个人项目可以忽略这条。

再强调一下,任何人都有权利跟义务整理修改他人代码,只要你觉得你修改得合理,但不要自作聪明。我知道有些程序员,会觉得他人修改自己代码,就是入侵自己领土。

2.13 例外

有些时候,我们需要自己写的库跟C++的标准库结合。这时候可以采用跟C++标准库相类似的风格。比如

  1. class MyArray
  2. {
  3. public:
  4. typedef const char* const_iteator;
  5. ...
  6. const char* begin() const;
  7. const char* rbegin() const;
  8. }

----------------

3 代码文件

3.1 #define 保护

所有的头文件,都应该使用#define来防止头文件被重复包含。命名的格式为

__<模块>_<文件名>_H__

很多时候,模块名字都跟命名空间对应。比如

  1. #ifndef __GEO_POINT_H__
  2. #define __GEO_POINT_H__
  3. namespace geo
  4. {
  5. class Point
  6. {
  7. .....
  8. };
  9. }
  10. #endif

并且,#define宏,的名字全部都为大写。不要出现大小写混杂的形式。

3.2 #include 的顺序

C++代码使用#include来引入其它的模块的头文件。尽可能,按照模块的稳定性顺序来排列#include的顺序。按照稳定性从高到低排列。

比如

  1. #include <map>
  2. #include <vector>
  3. #include <boost/noncopyable.hpp>
  4. #include "cocos2d.h"
  5. #include "json.h"
  6. #include "FlaSDK.h"
  7. #include "support/TimeUtils.h"
  8. #include "Test.h"

上面例子中。#include的顺序,分别是C++标准库,boost库,第三方库,我们自己写的跟工程无关的库,工程中比较基础的库,应用层面的文件。

但有一个例外,就是 .cpp中,对应的.h文件放在第一位。比如geo模块中的, Point.h 跟 Point.cpp文件,Point.cpp中的包含

  1. #include "geo/Point.h"
  2. #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就需要重修编译。 假设

  1. B.cpp -> B.h
  2. 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中写成

  1. // 前面没有别的头文件包含
  2. #include "A.h"
  3. #include <string>
  4. #include .......

.... 包含其它头文件

之后可以尝试在 A.h 中去掉多余的头文件。当A.cpp可以顺利编译通过的时候,A.h包含的头文件就是过多或者刚刚好的。而不会是包含不够的。

3.3.2 前置声明

首先,只在头文件中使用引用或者指针,而不是使用值的,可以前置声明。而不是直接包含它的头文件。 比如

  1. class Test : public Base
  2. {
  3. public:
  4. void funA(const A& a);
  5. void funB(const B* b);
  6. void funC(const space::C& c);
  7. private:
  8. D _d;
  9. };

这里,我牵涉到几个其它类,Base, A, B, space::C(C 在命名空间space里面), D。Base和D需要知道值,A, B, space::C只是引用和指针。所以Base, C的头文件需要包含。A, B,space::C只需要前置声明。

  1. #include "Base.h"
  2. #include "D.h"
  3. namespace space
  4. {
  5. class C;
  6. }
  7. class A;
  8. class B;
  9. class Test : public Base
  10. {
  11. public:
  12. void funA(const A& a);
  13. void funB(const B* b);
  14. void funC(const space::C& c);
  15. private:
  16. D _d;
  17. };

注意命名空间里面的写法。

3.3.3 impl 手法

就是类里面包含实现类的指针。在cpp里面实现。

3.3.4 尽可能将代码拆分成相对独立的,粒度小的单元,放到不同的文件中

简单说,就是不要将所有东西都塞在一起。这样的代码组积相对清晰。头文件包含也相对较少。但现实中,或多或少会违反。

比如,工程用到一些常量字符串(或者消息定义,或者enum值,有多个变种)。一个似乎清晰的结构,是将字符串都放到同一个头文件中。不过这样一来,这个字符串文件,就几乎会被所有项目文件包含。当以后新加一个字符串时候,就算只加一行,工程几乎被全部编译。

更好的做法,是按照字符串的用途来分拆开。

又比如,有些支持库。有时贪图方便,不注意的,就会写一个 GlobalUtils.h 之类的头文件,包含所有支持库,因为这样可以不关心到底应该包含哪个,反正包含GlobalUtils.h就行,这样多省事。不过这样一来,需要加一个支持的函数,比如就只是角度转弧度的小函数,也会发生连锁编译。

更好的做法,是根据需要来包含必要的文件。就算你麻烦一点,写10行#include的代码,都比之后修改一行代码,就编译上10多分钟要好。

3.4 小结

减少编译时间,这点很重要。再啰嗦一下

  • 要减少头文件重复包含,需要团队的人所有人达成共识,认识到这是不好的。很多人对这问题认识不够,会被当成小题大作。
  • 不要贪方便。直接包含一个大的头文件,短期是很方便,长期会有麻烦。

3.5 #include中的头文件,尽量使用全路径,或者相对路径

路径的起始点,为工程文件代码文件的根目录。

比如

  1. #include "ui/home/HomeLayer.h"
  2. #include "ui/home/HomeCell.h"
  3. #include "support/MathUtils.h"

不要直接包含

  1. #include "HomeLayer.h"
  2. #include "HomeCell.h"
  3. #include "MathUtils.h"

这样可以防止头文件重名,比如一个第三方库文件有可能就叫 MathUtils.h。

并且移植到其它平台,配置起来会更容易。比如上述例子,在安卓平台上,就需要配置包含路径

  1. <Project_Root>/ui/home/
  2. <Project_Root>/support/

也可以使用相对路径。比如

  1. #include "../MathUtil.h"
  2. #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, 变量加 _前缀。比如

  1. class Data
  2. {
  3. private:
  4. const uint8_t* _bytes;
  5. size_t _size;
  6. }

公有的数据,通常出现在C风格的结构中,或者一些数据比较简单,并很常用的类,public数据不要加前缀。

  1. class Point
  2. {
  3. public:
  4. Point(float x_, float y_) : x(x_), y(y_)
  5. {
  6. }
  7. .....
  8. float x;
  9. float y;
  10. }

注意,我们在构造函数,使用 x_ 的方式表示传入的参数,防止跟 x 来重名。

4.3 局部变量

局部变量尽可能使它的作用范围最小。换句话说,就是需要使用的时候才定义,而不要在函数开始就全部定义。

从前C语言有个约束,需要将用到的全部变量都定义在函数最前面。之后这个习惯也被传到C++的代码当中。但这种习惯是很不好的。

  • 在函数最前面定义变量,变量就在整个函数都可见,作用域越大,就越容易被误修改。
  • C++ 中,定义类型的变量,需要调用构造函数,跟释放函数。很多时候函数中途就退出了,这时候调用构造函数和释放函数,就显得浪费。
  • 变量在最开始的时候,很难给变量一个合理的初始值,很难的话,也就很容易忘记。

我们的结论是,局部变量真正需要使用的时候才定义,一行定义一个变量,并且一开始就给它一个合适的初始值。

  1. int i;
  2. i = f(); // 错,初始化和定义分离
  3. int j = g(); // 对,定义时候给出始值

4.4 命名空间

C++中,尽量不要出现全局函数,应该放入某个命名空间当中。命名空间将全局的作用域细分,可有效防止全局作用域的名字冲突。

比如

  1. namespace json
  2. {
  3. class Value
  4. {
  5. ....
  6. }
  7. }
  8. namespace splite
  9. {
  10. class Value
  11. {
  12. ...
  13. }
  14. }

两个命名空间都出现了Value类。外部访问时候,使用 json::Value, splite::Value来区分。

4.5 文件作用域

假如,某个函数,或者类型,只在某个.cpp中使用,请将函数或者类放入匿名命名空间。来防止文件中的函数导出。比如

  1. // fileA.cpp
  2. namespace
  3. {
  4. void doSomething()
  5. {
  6. ....
  7. }
  8. }

上述例子,doSomething这个函数,放入了匿名空间。因此,此函数限制在fileA.cpp中使用。另外的文件定义相同名字的函数,也不会造成冲突。

另外传统C的做法,是在 doSomething 前面加 static, 比如

  1. // fileB.cpp
  2. static void doSomething()
  3. {
  4. ...
  5. }

doSomething也限制到文件fileB.cpp中。

同理,只在文件中出现的类型,也放到匿名空间中。比如

  1. // sqlite/Value.cpp
  2. namespace sqlite
  3. {
  4. namespace
  5. {
  6. class Record
  7. {
  8. ....
  9. }
  10. }
  11. }

上述例子,匿名空间嵌套到sqlite空间中。这样Record这个结构只可以在sqlite/Value.cpp中使用,就算是同属于空间sqlite的文件,也不知道 Record 的存在。

4.6 头文件不要出现 using namespace ....

头文件,很可能被多个文件包含。当某个头文件出现了 using namespace ... 的字样,所有包含这个头文件的文件,都简直看到此命令空间的全部内容,就有可能引起冲突。比如

  1. // Test.h
  2. #include <string>
  3. using namespace std;
  4. class Test
  5. {
  6. public:
  7. Test(const string& name);
  8. };

这个时候,只要包含了Test.h, 就都看到std的所有内容。正确的做法,是头文件中,将命令空间写全。将 string, 写成 std::string, 这里不要偷懒。

----------------

5 类

面向对象编程中,类是基本的代码单元。本节列举了在写一个类的时候,需要注意的事情。

5.1 让类的接口尽可能小

设计类的接口时,不要想着接口以后可能有用就先加上,而应该想着接口现在没有必要,就直接去掉。这里的接口,你可以当成类的成员函数。添加接口是很容易的,但是修改,去掉接口会会影响较大。

接口小,不单指成员函数的数量少,也指函数的作用域尽可能小。

比如,

  1. class Test
  2. {
  3. public:
  4. void funA();
  5. void funB();
  6. void funC();
  7. void funD();
  8. };

假如,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

比如

  1. // swf/Definition.h
  2. class Definition
  3. {
  4. public:
  5. virtual ~Definition() {}
  6. virtual void parse(const uint8_t* bytes, size_t len) = 0;
  7. };
  8. // swf/ShapeDefinition.h
  9. class ShapeDefinition : public Definition
  10. {
  11. public:
  12. ShapeDefinition() {}
  13. virtual void parse(const uint8_t* bytes, size_t len) override;
  14. private:
  15. Shape _shape;
  16. };


  1. Definition* p = new ShapeDefinition();
  2. ....
  3. delete p;

上面的例子,使用父类的指针指向子类,假如父类的析构函数不为virtual, 就只会调用父类的Definition的释放函数,引起子类独有的数据不能释放。所有需要加上virtual。

另外子类覆写的虚函数写上,override的时候,当父类修改了虚函数的名字,就会编译错误。从而防止,父类修改了虚函数接口,而忘记修改子类相应虚函数接口的情况。

--------------------

6 函数

6.1 编写短小的函数

函数尽可能的短小,凝聚,功能单一。

只要某段代码,可以用某句话来描述,尽可能将这代码抽取出来,作为独立的函数,就算那代码只有一行。最典型的就是C++中的max, 实现只有一句话。

  1. template <typename T>
  2. inline T max(T a, T b)
  3. {
  4. return a > b ? a : b;
  5. }
  • 将一段代码抽取出来,作为一个整体,一个抽象,就不用纠结在细节之中。
  • 将一个长函数,切割成多个短小的函数。每个函数中使用的局部变量,作用域也会变小。
  • 短小的函数,更容易复用,从一个文件搬到另一个文件也会更容易。
  • 短小的函数,因为内存局部性,运行起来通常会更快。
  • 短小的函数,也容易阅读,调试。

6.2 函数的参数可能少,原则上不超过5个

人脑短时记忆的数字是很有限的,大约可以记忆7个数字。有些人多些,有些人少些。我们这里取最少值,就是5个参数。

参数的个数,太多,就很容易混乱,记不住参数的意义。

同时参数的个数太多,很可能是因为这个函数做的事情有点多了。

可以通过很多手段来减少参数的个数。比如将函数分解,分解成多个短小的函数。或者将几个经常一起的参数,封装成一个类或者结构。比如,设计一个绘画贝塞尔曲线的接口

  1. void drawQuadBeizer(float startX, float startY,
  2. float controlX, float controlY,
  3. float endX, float endY);

这样的接口,就不够

  1. void drawQuadBeizer(const Point& start,
  2. const Point& control,
  3. const Point& end);

简洁易用。

当然,每个规则都会有例外。比如设置一个矩阵的数值,二维矩阵本来就需要6个数字来表示,设置接口自然需要6个参数。

6.3 函数参数顺序

参数顺序,按照传入参数,传出参数,的顺序排列。不要使用可传入可传出的参数。

  1. bool loadFile(const std::string& filePath, ErrorCode* code); // 对
  2. bool loadFile(ErrorCode* code, const std::string& filePath); // 错

保持统一的顺序,使得他人容易记忆。

6.4 函数的传出参数,使用指针,而不要使用引用

比如

  1. bool loadFile(const std::string& filePath, ErrorCode* code); // 对
  2. bool loadfile(const std::string& filePath, ErrorCode& code); // 错

因为当使用引用的时候,使用函数的时候会变成

  1. ErrorCode code;
  2. if (loadFile(filePath, code))
  3. {
  4. ...
  5. }

而使用指针,调用的时候,会是

  1. ErrorCode code;
  2. if (loadFile(filePath, &code))
  3. {
  4. ...
  5. }

这样从,&code的方式可以很明显的区分,传入,传出参数。试比较

  1. doFun(arg0, arg1, arg2); // 错
  2. 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的位置:

  1. const int* name; // 对(这样写,可读性更好)
  2. int const* name; // 错

7.2 不要注释代码,代码不使用就直接删掉

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

不要这样做。

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/400867
推荐阅读
相关标签
  

闽ICP备14008679号