编码规范参考

C/C++编码规范参考

 

========================================================

来源 https://zhuanlan.zhihu.com/p/71782780

 

下面是一些广为采用的编码规范:

  • GNU Coding Standards

  • Guidelines for the Use of the C Language in Vehicle Based Software

  • C++ Coding Guidelines

  • SUN Code Conventions for Java

以下是一些介绍编码、编码规范的书籍:

  • C++编码规范,陈世忠,人民邮电出版社,2002

  • 高质量程序设计指南:C++/C语言,林锐等,电子工业出版社,2003

注:以下只是根据课题组已有的经验给出的总结,并非对所有场景均适用。

对于高质量的工程,一般会做到:

  1. 代码简洁精炼,美观,可读性好,高效率,高复用,可移植性好,高内聚,低耦合,没有冗余,不符合这些原则,必须特别说明。

  2. 规范性,代码有规可循。特殊排版、特殊语法、特殊指令,必须特别说明。

一、文件排版方面

1.包含头文件

1.1 先系统头文件,后用户头文件。

1.2 系统头文件,稳定的目录结构,应采用包含子路径方式。

1.3 自定义头文件,不稳定目录结构,应在dsp中指定包含路径。

1.4 系统头文件应用:#include <xxx.h>

1.5 自定义同文件应用:#include "xxx.h"

1.6 只引用需要的头文件。

2.h和cpp文件

2.1 头文件命名为*.h,内联文件命名为*.inl;C++文件命名为*.cpp

2.2 文件名用大小写混合,或者小写混合。例如DiyMainview.cppinfoview.cpp。不要用无意义的名称:例如         XImage.cppSView.cppxlog.cpp

2.3 头文件除了特殊情况,应使用#ifdef控制块。

2.4 头文件#endif应采用行尾注释。

2.5 头文件,首先是包含代码块,其次是宏定义代码块,然后是全局变量,全局常量,类型定义,类定义,内联部分。

2.6 CPP文件,包含指令,宏定义,全局变量,函数定义。

3.文件结构

3.1 文件应包含文件头注释和内容。

3.2 函数体类体之间原则上用2个空行,特殊情况下可用一个或者不需要空行。

4.空行

4.1 文件头、控制块,#include部分、宏定义部分、class部分、全局常量部分、全局变量部分、函数和函数之间,用两个 空 行。

二、注释方面

1.文件头注释

1.1 作者,文件名称,文件说明,生成日期(可选)

2.函数注释

2.1 关键函数必须写上注释,说明函数的用途。

2.2 特别函数参数,需要说明参数的目的,由谁负责释放等等。

2.3 除了特别情况,注释写在代码之前,不要放到代码行之后。

2.4 对每个#else#endif给出行末注释。

2.5 关键代码注释,包括但不限于:赋值,函数调用,表达式,分支等等。

2.6 善未实现完整的代码,或者需要进一步优化的代码,应加上 // TODO ...

2.7 调试的代码,加上注释 // only for DEBUG

2.8 需要引起关注的代码,加上注释 // NOTE ...

2.9 对于较大的代码块结尾,如for,while,do等,可加上 // end for|while|do

三、命名方面

1.原则

1.1 同一性:在编写一个子模块或派生类的时候,要遵循其基类或整体模块的命名风格,保持命名风格在整个模块中的同一性。

1.2 标识符组成:标识符采用英文单词或其组合,应当直观且可以拼读,可望文知意,用词应当准确,避免用拼音命名。

1.3 最小化长度 && 最大化信息量原则:在保持一个标识符意思明确的同时,应当尽量缩短其长度。

1.4 避免过于相似:不要出现仅靠大小写区分的相似的标识符,例如"i""I""function""Function"等等。

1.5 避免在不同级别的作用域中重名:程序中不要出现名字完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但容易使人误解。

1.6 正确命名具有互斥意义的标识符:用正确的反义词组命名具有互斥意义的标识符,如:"nMinValue" 和 "nMaxValue""GetName()" 和"SetName()" ….

1.7 避免名字中出现数字编号:尽量避免名字中出现数字编号,如Value1,Value2等,除非逻辑上的确需要编号。这是为了防止程序员偷懒,不肯为命名动脑筋而导致产生无意义的名字(因为用数字编号最省事)。

2.T,C,M,R类

2.1 T类表示简单数据类型,不对资源拥有控制权,在析构过程中没有释放资源动作。

2.2 C表示从CBase继承的类。该类不能从栈上定义变量,只能从堆上创建。

2.3 M表示接口类。

2.4 R是资源类,通常是系统固有类型。除了特殊情况,不应在开发代码中出现R类型。

3.函数名

3.1 M类的函数名称应采用HandleXXX命名,例如:HandleTimerEvent;不推荐采用java风格,例如 handleTimerEvent;除了标准c风格代码,不推荐用下划线,例如,handle_event

3.2 Leave函数,用后缀L。

3.3 Leave函数,且进清除栈,用后缀LC。

3.4 Leave函数,且删除对象,用后缀LD。

4.函数参数

4.1 函数参数用a作为前缀。

4.2 避免出现和匈牙利混合的命名规则如apBuffer名称。用aBuffer即可。

4.3 函数参数比较多时,应考虑用结构代替。

4.4 如果不能避免函数参数比较多,应在排版上可考虑每个参数占用一行,参数名竖向对齐。

5.成员变量

5.1 成员变量用m最为前缀。

5.2 避免出现和匈牙利混合的命名规则如mpBuffer名称。用mBuffer即可。

6.局部变量

6.1 循环变量和简单变量采用简单小写字符串即可。例如,int i;

6.2 指针变量用p打头,例如void* pBuffer;

7.全局变量

7.1 全局变量用g_最为前缀。

8.类名

8.1 类和对象名应是名词。

8.2 实现行为的类成员函数名应是动词。

8.3 类的存取和查询成员函数名应是名词或形容词。

9.风格兼容性

9.1 对于移植的或者开源的代码,可以沿用原有风格,不用C++的命名规范。

四、代码风格方面

1.Tab和空格

1.1 每一行开始处的缩进只能用Tab,不能用空格,输入内容之后统一用空格。除了最开始的缩进控制用Tab,其他部分为了对齐,需要使用空格进行缩进。这样可以避免在不同的编辑器下显示不对齐的情况。

1.2 在代码行的结尾部分不能出现多余的空格。

1.3 不要在"::","->","."前后加空格。

1.4 不要在",",";"之前加空格。

2.类型定义和{

2.1 类,结构,枚举,联合:大括号另起一行

3.函数

3.1 函数体的{需要新起一行,在{之前不能有缩进。

3.2 除了特别情况,函数体内不能出现两个空行。

3.3 除了特别情况,函数体内不能宏定义指令。

3.4 在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。

3.5 在头文件定义的inline函数,函数之间可以不用空行,推荐用一个空行。

4.代码块

4.1 "if"、"for"、"while"、"do"、"try"、"catch" 等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加 "{ }" 。这样可以防止书写和修改代码时出现失误。

4.2 "if"、"for"、"while"、"do"、"try"、"catch" 的括号和表达式,括号可紧挨关键字,这样强调的是表达式。

5.else

5.1 if语句如果有else语句,用 } else { 编写为一行,不推荐用 3 行代码的方式。

6.代码行

6.1 一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。

6.2 多行变量定义,为了追求代码排版美观,可将变量竖向对齐。

6.3 代码行最大长度宜控制在一定个字符以内,能在当前屏幕内全部可见为宜。

7.switch语句

7.1 case关键字应和switch对齐。

7.2 case子语句如果有变量,应用{}包含起来。

7.3 如果有并列的类似的简单case语句,可考虑将case代码块写为一行代码。

7.4 简单的case之间可不用空行,复杂的case之间应考虑用空行分割开。

7.5 case字语句的大括号另起一行,不要和case写到一行。

7.6 为所有switch语句提供default分支。

7.7 若某个case不需要break一定要加注释声明。

8.循环

8.1 空循环可用 for( ;; ) 或者 while( 1 ) 或者 while( true )

9.类

9.1 类继承应采用每个基类占据一行的方式。

9.2 单继承可将基类放在类定义的同一行。如果用多行,则应用Tab缩进。

9.3 多继承在基类比较多的情况下,应将基类分行,并采用Tab缩进对齐。

9.4 重载基类虚函数,应在该组虚函数前写注释 // implement XXX

9.5 友元声明放到类的末尾。

10.宏

10.1 不要用分号结束宏定义。

10.2 函数宏的每个参数都要括起来。

10.3 不带参数的宏函数也要定义成函数形式。

11.goto

11.1 尽量不要用goto。

五、类型

  1. 定义指针和引用时*&紧跟类型。

  2. 尽量避免使用浮点数,除非必须。

  3. typedef简化程序中的复杂语法。

  4. 避免定义无名称的类型。例如:typedef enum { EIdle, EActive } TState;

  5. 少用union,如果一定要用,则采用简单数据类型成员。

  6. enum取代(一组相关的)常量。

  7. 不要使用魔鬼数字。

  8. 尽量用引用取代指针。

  9. 定义变量完成后立即初始化,勿等到使用时才进行。

  10. 如果有更优雅的解决方案,不要使用强制类型转换。

六、表达式

  1. 避免在表达式中用赋值语句。
  2. 避免对浮点类型做等于或不等于判断。
  3. 不能将枚举类型进行运算后再赋给枚举变量。
  4. 在循环过程中不要修改循环计数器。
  5. 检测空指针,用 if( p )
  6. 检测非空指针,用 if( ! p )

七、函数

1.引用

1.1 引用类型作为返回值:函数必须返回一个存在的对象。

1.2 引用类型作为参数:调用者必须传递一个存在的对象。

2.常量成员函数

2.1 表示该函数只读取对象的内容,不会对对象进行修改。

3.返回值

3.1 除开void函数,构造函数,析构函数,其它函数必须要有返回值。

3.2 当函数返回引用或指针时,用文字描述其有效期。

4.内联函数

4.1 内联函数应将函数体放到类体外。

4.2 只有简单的函数才有必要设计为内联函数,复杂业务逻辑的函数不要这么做。

4.3 虚函数不要设计为内联函数。

5.函数参数

5.1 只读取该参数的内容,不对其内容做修改,用常量引用。

5.2 修改参数内容,或需要通过参数返回,用非常量应用。

5.3 简单数据类型用传值方式。

5.4 复杂数据类型用引用或指针方式。

八、类

1.构造函数

1.1 构造函数的初始化列表,应和类的顺序一致。

1.2 初始化列表中的每个项,应独占一行。

1.3 避免出现用一个成员初始化另一个成员。

1.4 构造函数应初始化所有成员,尤其是指针。

1.5 不要在构造函数和析构函数中抛出异常。

2.纯虚函数

2.1 M类的虚函数应设计为纯虚函数。

3.构造和析构函数

3.1 如果类可以继承,则应将类析构函数设计为虚函数。

3.2 如果类不允许继承,则应将类析构函数设计为非虚函数。

3.3 如果类不能被复制,则应将拷贝构造函数和赋值运算符设计为私有的。

3.4 如果为类设计了构造函数,则应有析构函数。

4.成员变量

4.1 尽量避免使用mutableVolatile

4.2 尽量避免使用公有成员变量。

5.成员函数

5.1 努力使类的接口少而完备。

5.2 尽量使用常成员函数代替非常成员函数,const函数

5.3 除非特别理由,绝不要重新定义(继承来的)非虚函数。(这样是覆盖,基类的某些属性无初始化)

6.继承

6.1 继承必须满足IS-A的关系,HAS-A应采用包含。

6.2 虚函数不要采用默认参数。

6.3 除非特别需要,应避免设计大而全的虚函数,虚函数功能要单一。

6.4 除非特别需要,避免将基类强制转换成派生类。

7.友元

7.1 尽量避免使用友元函数和友元类。

九、错误处理

  1. 申请内存用new操作符。
  2. 释放内存用delete操作符。
  3. newdeletenew[]delete[]成对使用。
  4. 申请内存完成之后,要检测指针是否申请成功,处理申请失败的情况。
  5. 谁申请谁释放。优先级:函数层面,类层面,模块层面。
  6. 释放内存完成后将指针赋空,避免出现野指针。
  7. 使用指针前进行判断合法性,应考虑到为空的情况的处理。
  8. 使用数组时,应先判断索引的有效性,处理无效的索引的情况。
  9. 代码不能出现编译警告。
  10. 使用错误传递的错误处理思想。
  11. 卫句风格:先处理所有可能发生错误的情况,再处理正常情况。
  12. 嵌套do-while(0)宏:目的是将一组语句变成一个语句,避免被其他if等中断。

十、性能

  1. 使用前向声明代替#include指令。Class M;
  2. 尽量用++i代替i++。即用前缀代替后缀运算。
  3. 尽量在for循环之前,先写计算估值表达式。
  4. 尽量避免在循环体内部定义对象。
  5. 避免对象拷贝,尤其是代价很高的对象拷贝。
  6. 避免生成临时对象,尤其是大的临时对象。
  7. 注意大尺寸对象数组。
  8. 80-20原则。

十一、兼容性

  1. 遵守ANSI C和ISO C++国际标准。
  2. 确保类型转换不会丢失信息。
  3. 注意双字节字符的兼容性。
  4. 注意运算溢出问题。
  5. 不要假设类型的存储尺寸。
  6. 不要假设表达式的运算顺序。
  7. 不要假设函数参数的计算顺序。
  8. 不要假设不同源文件中静态或全局变量的初始化顺序。
  9. 不要依赖编译器基于实现、未明确或未定义的功能。
  10. 将所有#include的文件名视为大小写敏感。
  11. 避免使用全局变量、静态变量、函数静态变量、类静态变量。在使用静态库,动态库,多线程环境时,会导致兼容性问题。
  12. 不要重新实现标准库函数,如STL已经存在的。

 

 

========================================================

来源 https://zhuanlan.zhihu.com/p/20326454

综述

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 指针符号*,引用符号& 的位置,写在靠近类型的地方

CCNode* p = CCNode::create(); // (1)
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, 命名空间等等的花括号,另起一行。例子

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

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

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

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

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, 都使用相同的约定。例如

classUrlTable
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。 私有的成员变量,前面加下划线。比如:

classImage
{
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_前缀。

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

classUserQueue
{
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 classColorType : 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++标准库相类似的风格。比如

classMyArray
{
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
{
    classPoint
    {
        .....
    };
}

#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 前置声明

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

classTest : 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
{
    classC;
}

classA;
classB;
classTest : 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, 变量加 _前缀。比如

classData
{
private:
    const uint8_t*  _bytes;
    size_t          _size;
}

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

classPoint
{
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
{
    classValue
    {
        ....
    }
}

namespace splite
{
    classValue
    {
        ...
    }
}

两个命名空间都出现了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
    {
        classRecord
        {
            ....
        }
    }
}

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

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

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

// Test.h
#include <string>using namespace std;

classTest
{
public:
    Test(const string& name);
};

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

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

5 类

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

5.1 让类的接口尽可能小

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

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

比如,

classTest
{
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
classDefinition
{
public:
    virtual ~Definition()   {}
    virtual void parse(const uint8_t* bytes, size_t len) = 0;
};

// swf/ShapeDefinition.h
classShapeDefinition : 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 不要注释代码,代码不使用就直接删掉

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

不要这样做。

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

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

 

================ End

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值