游戏客户端编码规范

游戏客户端编码规范
游戏客户端编码规范

游戏客户端C++高质量编码规范百纳信息技术有限公司游戏部门

百纳信息技术有限公司

版权所有侵权必究

修订记录

日期版本修改描述作者审核2014-5-22 V0.9 初稿Pchen

目录

1引言 (5)

1.1文档目的 (5)

1.2使用范围 (5)

2概述 (5)

3头文件 (5)

3.1文件头注释 (5)

3.2#define保护还是#pragma once (6)

3.3#include (6)

3.4using namespace (6)

3.5#define,全局常量定义 (6)

3.6inline函数 (6)

3.7继承 (7)

3.8布局 (7)

4命名规则 (7)

4.1变量名 (7)

4.2常量名 (8)

4.3类名 (8)

4.4函数名 (9)

4.5文件名 (9)

4.6类型名 (9)

5格式规范 (10)

5.1If,while (10)

5.2Switch (10)

6强制转换规范 (11)

7作用域 (11)

7.1Namespace (11)

7.2Extern (12)

7.3变量声明时机 (12)

8类规范 (12)

8.1继承 (12)

8.2构造函数 (12)

8.3析构函数 (12)

8.4拷贝构造函数 (12)

8.5单一职责原则 (13)

8.6组合优于继承 (13)

8.7轻量原则 (13)

8.8访问控制 (13)

9函数规范 (13)

9.1通用规则 (14)

10注释 (14)

11内存管理 (15)

11.1非cocos2d-x的对象 (15)

11.2Cocos2d-x对象 (15)

11.3其他规范 (15)

11.4Cocos2d-x优化内存技术 (16)

12其他杂项 (17)

12.1Map (17)

1引言

1.1 文档目的

为游戏客户端C/C++开发人员制定统一的编码规范,增进代码质量。

1.2 使用范围

游戏客户端C/C++开发人员。

2概述

我们的游戏客户端程序基于cocos2dx,目前大部分开发语言为C++。作为C/C++程序员都知道,C++具有很强大的特性,同时也使得使用C++编程比较复杂,容易出现BUG。特别是在内存管理这部分,不仅仅是新手,很多C++老手也会犯错误。

预防强于补救,一个程序员高手与新手的差别很大一部分就体现在编码风格的差距上,提前制定一些大家共同遵守的编码规范,对于提高代码的可读性,乃至于改进项目质量都很有意义。

本规范假定你对C/C++的语法已经十分熟悉。不会对此作出解释,建议开发人员理解每一个规范制定的原因,鼓励提出更好的见解。

3头文件

3.1 文件头注释

在文件的最开始要有文件头注释,目前暂定的文件头格式如下。后续会考虑加入重大修改者的信息。

/********************************************************************

Baina@copyright 2013

类名:

作用: 一个全局响应的sprite,setScale也会改变点击响应范围。

特性: 点击过久不响应,拖动不响应。

作者: peng chen

时间: 2014/4/10 15:00

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

备注:

*********************************************************************/

其他注释将不在此章节描述,参考专门的注释规范。

3.2 #define保护还是#pragma once

由于我们的客户端使用VS2010开发,并且目前打包脚本所使用的gcc已经支持#pragma once,因此头文件保护中统一使用#pragma once来进行头文件保护,防止重复引用。

#pragma once出现在文件头注释之后,其他所有代码之前(包括#include)。

3.3 #include

对于头文件包含的每一个其他的头文件必须给出理由。除cocos2d-x库的头文件外,所包含的头文件中只能包含:纯数据结构定义,接口定义,常量定义。

不允许直接包含其他非接口类定义的头文件,使用前置申明代替。如果前置申明不能解决问题,说明本类依赖于其他非接口类的实现,考虑下自己设计的问题(不要直接持有类对象,改为持有指针或引用,不要为了偷懒直接继承其他类,使用组合)。

#include应该出现在同一段,除预编译外,之间不允许插入其他代码。头文件不能依赖其包含顺序。使用脚本(待编写)来打乱所包含其他头文件包含顺序,重新编译也不会出错的头文件才算符合要求。默认的顺序如下:C库,C++库,其他库,项目内库。

#include中不允许出现“./xxx”,“../xxx”这样的相对路径,也不允许出现带盘符的绝对路径。合理的include 可以包含相对项目tree的路径,如下:

#include"cocoa/CCObject.h"

3.4 using namespace

头文件不允许使用using来污染命名空间。

3.5 #define,全局常量定义

原则上,除专门定义数据的头文件以外,头文件中不允许出现宏定义,枚举,常量等定义。你需要思考:为什么这些需要出现在我的头文件中,换到cpp中可以吗?别人也会需要我这个常量定义吗?如果别人需要,要不要放到一个专门的头文件中?

3.6 inline函数

我们一般只对get,set类的存取函数允许在头文件中直接定义(头文件里直接定义的函数不使用inline 也可能被编译器自动内联)。其他代码少于5行,逻辑简单明了的函数也可以使用inline声明,但是定义必须在cpp中。

对于get,set的方法,可以使用cocos2dx中常用的宏来简化代码编写。

3.7 继承

继承除第一个基类不做限制,从第二个往后必须只能是接口。不允许继承多个非接口类,即使是继承接口,也不宜过多。如果一定要继承多个可实例化类,使用组合,或修改自己的设计。

继承的写法如下:

class LocalClickSprite

: public cocos2d::CCSprite

, public cocos2d::CCTargetedTouchDelegate

{

……

3.8 布局

先public,再protected,再private。构造,析构在前,其次是static的变量,其他成员变量。其次是非虚的成员函数,然后是虚函数,然后是inline函数。从父类继承来的虚方法尽管不需要virtual关键字,也必须要加上,否则别人可能需要看到基类才知道这个是虚函数。

同一组逻辑的函数要放在一起,然后再遵守以上原则。

从同一个接口继承而来的成员函数要放在一起,然后再遵守以上原则。

4命名规则

4.1 变量名

命名规则:由作用域前缀+类型前缀+一个或多个单词组成。为便于界定,除前缀外,每个单词的首字母要大写。变量的名字应该是一个名词,或者是一个名词短语。变量名不限制长度,但是必须表达清楚意思,不允许出现非全球通用的缩写。

作用域前缀:作用域前缀标明一个变量的可见范围。作用域可以有如下几种:

前缀说明

无局部变量

m_ 类的成员变量(member)

sm_ 类的静态成员变量(static member)

s_ 静态变量(static)

g_ 外部全局变量(global)

sg_ 静态全局变量(static global)

gg_ 进程或动态链接库间共享的全局变量(global global)

类型前缀:类型前缀标明一个变量的类型,可以有如下几种:

前缀说明

n 整型和位域变量(number)

e 枚举型变量(enumeration)

c 字符型变量(char)

b 布尔型变量(bool)

f 浮点型变量(float)

st 结构体(struct)

p 指针型变量和迭代子(pointer)

pfn 指向函数的指针变量或指向函数对象的指针(pointer of

function)

pm 指向成员的指针(pointer of member)

r 引用(reference),此前缀对于常引用(const reference)来

说可以省略

g 数组(grid)

fo 函数对象(Function Object)

i 类的实例(instance)

sz 以0结尾的字符串(char *)

str string字符串(std::string)

map std::map

v std::vector

类型前缀可以组合使用,例如"gc"表示字符数组,"ppn"表示指向整型的指针的指针等等。

对于cocos2d-x的类型变量名,取消类型前缀,用最后一个单词来表明类型,如:

CCLabelTTF* m_nickNameLabel;

如有其他类型未出现在上述列表里,使用小写非缩写的类型名作为前缀。

4.2 常量名

#define宏定义的所有常量单词字母大写,单词之间必须用下划线隔开,不允许出现非全球通用的缩写。

Enum定义的常量,遵循cocos2d-x的命名规范,以k为前缀的匈牙利命名法,每个单词首字母大写即可。

其他const等方法定义的常量,目前在我们的项目中以变量命名的规则命名。

4.3 类名

类名首先必须是一个清晰表达出其作用名词,如果发现需要很复杂的命名,考虑自己的设计,类职责是否过于复杂?其次必须反映出其父类(CCObject除外)的类型。比如,继承了精灵的类,名字最后一个单词需要是Sprite,继承了CCNode,则最后一个单词必须是Node,继承了CClayer最后一个单词

要是Layer,以此类推。

我们的类名不包含CC前缀,所有单词首字母大写。

回调接口类:BalabalaDelegate

UI抽象接口类:BalabalaListener

线程安全类:BalaBala其他后缀ThreadSafe

单例管理类:BalaBalaManager

类名不限于以上。

4.4 函数名

函数名一般是一个动宾结构,从意义上来看是doSomething。第一个单词要小写,其他单词首字母大写。函数名不包含前缀。

全局或者static的成员函数的所有单词首字母大写。

Protected的成员函数要加_前缀。

Privated的成员函数要加__前缀。

响应函数:onBalabala

事件前响应:onBalabalaBegin

事件后响应:onBalabalaEnd

按钮点击响应函数:onBalabalaClicked

回调函数:balabalaCallBack

更新函数:updateBalabala

显示函数:showBalabala

函数名不限于以上。

4.5 文件名

文件名一般与其类名相同,.cpp和.h后缀。

同类文件放在同一个文件夹中。

4.6 类型名

自定义的类型名,最后一个单词要是其原始类型,常用的如MAP,VECTOR等等,函数指针FUNC。所有单词字母大写,单词之间需要用下划线隔开(类似于宏定义)。

有一个特例就是模板类,将模板类具化的typedef类名,命名规则与普通类命名规则一致。因为他本质上是一个类名。

5格式规范

所有缩进为四个空格。#开头的命令无论处在任何地方,不使用缩进。

所有的运算符号左右都要有空格。

变量申明时候即定义它。

所有的需要程序员理解优先级的表达式需要使用括号,要假设看代码的人不懂优先级。

不同参数之间使用逗号隔开,逗号挨着前一个参数,逗号后面有空格。For循环中的分号也是如此。

一行代码不得超出vs2010的屏幕外,由于现在分辨率提高,从常用的80字符限制提高为120字符限制。

5.1 If,while

If的格式如下。即使是单行也要使用大括号,复杂过长的表达式分行写,将连接的符号写在行末尾,这样一看就知道后面还有表达式。每行可以根据自己的逻辑添加注释。

如果是判断相等,常量写在左边。If后面有空格。

if ((m_nTouchBeginTime + m_nNoResponseTime > nCurTime) && //点击弹起是否足够快isTouchingMe(pTouch) && //点击是否在本精灵内

(m_touchBeginPos.fuzzyEquals(pTouch->getLocation(), 5)))

{

if (m_target)

{

(m_target->*m_handler)(this);

}

}

else if (0 == m_bClickFlag)

{

Some code;

}

While遵循与if同样的规则,总之花括号要独占一行。这样不会出现不同代码块之间紧挨在一起的情况。

5.2 Switch

对于switch,case语句不缩进。Case里面的代码块必须用花括号包围起来(这不仅仅是风格问题了,还涉及到变量作用域),并且缩进四个空格。break写在花括号外面,这样更容易看出该break是用来退出case 而不是什么for循环的。

必须要有default语句,你可以break,也可以assert

switch(opCode)

{

case SMSG_PLAYER_ENTER_CAISHEN_RESP:

{

SMSG_PlayerEnterCaiShenTable_Resp resp;

resp.PacketTo(*recvPacket);

onEnterCaiShenRoom(&resp);

if (m_pLoadingIndicator != NULL && m_pLoadingIndicator->getParent() != NULL)

{

this->removeChild(m_pLoadingIndicator);

m_pLoadingIndicator = NULL;

}

}

break;

case SMSG_PLAYER_CAISHEN_RESULT_INFO_RESP:

{

SMSG_CaiShenTableResult_Resp resp;

resp.PacketTo(*recvPacket);

onReceiveCaiShenResult(&resp);

}

break;

default:break;

}

6强制转换规范

禁止使用dynamiccast<>,一般说来如果你需要运行时判断类的类型,排除开性能原因以及某些c++库是否支持不说,这也说明你的设计出了问题。如果传入的参数是基类,那么就使用基类的方法。给你的类定一个标签来获取它的类型并不比dynamiccast<>高明,使用虚函数足矣。

除基本数据类型外,禁止使用裸露的c风格强制类型转换。类型转换使用static_cast<>。其他cast慎用,除非你知道你在做什么。

7作用域

7.1 Namespace

对于非公用类不强调使用namespace,但是对于要提供给别人使用的类要使用namespace。命名空间外包一层Baina命名空间。

对于cpp内的常量,使用匿名的命名空间包裹,防止外部访问和重名。

不允许在头文件中使用using 关键字。

7.2 Extern

不允许使用extern来引用cpp的全局变量给其他文件使用。改为使用static将全局变量限定在cpp文件内,再编写外部可访问的全局的存取函数来访问这个变量。

7.3 变量声明时机

变量要呆在它能呆在的最小范围里。除非是带有构造和析构的局部变量,并且需要处在一个循环里,而这个循环要被执行很多次,考虑到效率原因,可以将变量放在循环外部。

8类规范

8.1 继承

不允许继承一个以上的非接口类,不允许继承超过3个接口,否则考虑违背单一职责原则。

可以使用private继承来强制使用父类指针访问继承来的函数(你要知道你在做什么)。

8.2 构造函数

单参数构造函数要加explicit 前缀,防止非预期的隐式转换。

构造函数要对成员变量做初始化,尤其是所有的指针类型设置为NULL,将new放在其他地方。

构造函数中不允许带有逻辑含义的代码。

CCNode的派生类使用onEnter来执行其他需要初始化的逻辑。包括成员对象创建。

8.3 析构函数

一般说来,cocos2d-x风格的析构函数没有代码。

CCNODE的派生类将清除工作放在onExit.

8.4 拷贝构造函数

一般说来,客户端UI的类居多,它们不允许拷贝。

对于需要拷贝的对象,如果含有指针等需要深拷贝的成员,需要提供拷贝构造函数。

8.5 单一职责原则

类的职责应该单一,表现在类对外暴露的接口应该少而清晰。对外尽量隐藏类的实现,减少使用类的限制。

8.6 组合优于继承

能用组合,则不用继承。继承是一种比组合要深的多的耦合。原则上来说,特别对于cocos2dx,继承了Sprite,Layer这样的类的类,不允许再次被继承。

8.7 轻量原则

类暴露的接口不宜过多,否则考虑是否违背了单一职责原则。在我们的客户端中,一个layer可能会很复杂,有许多的逻辑,如果暴露的接口超过20个,或者代码超过1000行,重新考虑你的设计。改善的办法如下:

使用多个layer,将逻辑上相同的归类到一个layer。

将逻辑分离到一个其他的类,layer只负责显示。

将数据也分离到其他的类。

8.8 访问控制

能private的就private,不能的就protected,如果非要public,但是其实只需要暴露给某个特定的类,可以适当使用友元,友元不宜过多。

对于提供了create的典型cocos2dx对象,构造和析构函数为私有。Init函数为protected。

所有的成员变量不能是public的。

9函数规范

函数的行数不宜过多,一般说来不能超过一屏幕。

凡是能用const的地方,就要用const。

如果一段代码总是出现,写成函数。

不要使用宏写函数来偷懒。首先它没有增加可读性,其次它不可控。

不要编写递归的函数,除非它显著增加了可读性并且确保安全,任何递归实现都能使用非递归来替代。

函数的参数不能超过5个,否则考虑是否违背单一功能原则,对于同一逻辑的多个参数最好作为一个

整体传入。比如float posx,float posy将其修改为const CCPoint &pos。函数传入的参数如果是引用,必须为const,如果是需要修改的参数,则传入指针。对于传入的const引用,如果需要调用其非const this指针的成员函数,首先考虑将该函数修改为const this的函数。其次使用const_cast.

不允许使用默认参数的函数,默认参数的函数让程序员渐渐忽略该参数的意义。如果是一种fix的方案,使用一个新的函数。尽量避免函数重载的使用,除非他们执行的是完全一样的功能,比如分别用路径名和CCNode来创建一个控件。

9.1 通用规则

可重入性:对于所有依赖了非局部变量的成员函数,必须考虑被重入后的影响。

防御式编程:对传入的指针参数,要做非空判断,确定非空的,使用assert。对于值类的参数,要有正负性判断,大小判断等,做除数要有非0判断。

外部无关性:任何一个成员函数,不应该依赖于该类之外的的状态。全局函数应该保证在任何时候调用都能得出一样的结果。即使全局函数依赖某个标志变量,也要对外透明。

隐藏性:一个函数使用的时候者不需要知道它是怎么实现的。

10注释

记住:注释不是用来掩饰你拙劣的编码技巧的。注释也不是用来给其他程序员讲语法,或是翻译代码,或者是做其他的无用之功的。注释存在的意义,是对有可能存在误解,或者不那么容易理解,但是确实值得牺牲可读性的代码做解释的。评价一个注释是否写的好,就看他人在看了你的注释后,是否理解并且认可你写的代码。

除一些有争议的实现,或者是尽管复杂但是确实巧妙有效的实现(若仅仅是复杂,请重构),或者是其他有必要详细说明的地方外,注释都是简明扼要的。函数内的注释不超过一行,可以在代码前。短一些的可以在该行代码后。

函数或类前注释使用/**/, 函数内注释使用//

函数的注释应该包括函数的各个参数的意义,函数返回值的意义。或任何函数的一些使用时需要注意的地方,当然这种需要注意的越少,说明函数隐藏的越好。

如果你对一段代码做了注释,只是简短的说明他们做了什么,考虑把它们做成一个函数把。

我们不强求注释使用英文,但是我们欣赏使用英文注释的程序员。

11内存管理

11.1 非cocos2d-x的对象

对于使用new和delete的对象(客户端多使用cocos2d-x机制)。谁new,谁delete。如果是在构造中new,就在析构里delete,如果在函数里new并且传递给其他函数的,要在当前函数delete。

倘若在类A的函数里new的对象,在A当前函数不能delete,请重新考虑设计的问题。这个new的对象传给B,B为什么要一直持有这个对象?B本身的生命周期是否超过当A?如果B的生命周期超过了A,把new的过程移到B里面,可以通过A传入一些参数来控制对象的new的过程。如果B的生命周期完全被A控制,把这个new的过程移到构造函数。否则请确保这个new只会发生一次,而delete则放到类A的析构函数中。

总之,谁new,谁delete。

11.2 Cocos2d-x对象

谁创建了,谁稍后release或者立即autorelease。

谁使用,谁retain。

谁retain了,谁release。

某些cocos2dx的库方法是自带retain的,最常见的是addchild,要花时间搞清楚哪些会retain,掌握对象真正死亡的时机。

如果有类A和类B,类A持有类B,并且类A的生命周期完全覆盖了类B的,此时类A将一个与类A 同寿的对象传给B使用,B可以不必Retain。

对于类A创建的对象给B,但是B的生命周期不可控,如果类A不需要一直持有这个对象,在对象创建后立即使用autorelease,并且将对象传给B,将维护权转交给B。参考create函数的实现方法。

所有cocos2d的对象的创建和销毁不在构造和析构函数里,放到onEnter和onExit里面。

CCNode的子类的对象不要对自己的child执行release!重复release是很多crash的来源。生命周期的维护就交给CCNode来完成吧。如果确保所有的create函数都先autorelease再返回,对这样create出来的对象,没有在当前类里显示retain的,都不需要release。

Cocos2d-x中有很多观察者模式,需要把自己向观察者注册,比如触摸,定时回调等等。他们也会retain。保证在onExit的时候,向这些观察者取消自己的注册,否则有crash的风险。

11.3 其他规范

禁止使用malloc,alloca等系列函数,禁止使用memset,你总可以通过构造函数解决,memset也不比一个for循环的初始化更快。

禁止将一个指针赋值给另外一个指针。你总可以找到替代方法。

如果free,release不是发生在析构,立马将指针置为NULL(哪怕是在onExit)。

不要把一个局部变量以指针或是引用的形式return出去。除非你是new出来的。

使用数组必须考虑极限情况,防止越界,一般说来,对自己的计算没有信心的可以保留几个字节的余量。

请注意char *,char []表示的字符串末尾有个’/0’.

禁止在函数里直接声明大(超过10000)的局部数组,在低内存手机上可能爆栈,使用new xxx []。

同一个层次的对象,销毁顺序与创建顺序相反,好比一个栈,先进后出。

有必要去搞清楚,那些变量存储在堆,哪些是在栈,哪些是全局/静态存储区,哪些是常量区,他们分别有什么特点。

如果你不懂下面的函数有什么问题,你需要你的教科书。

void func(char *p)

{

p = new char[128];

}

string * func()

{

string str;

return &str;

}

内存是否会分配失败? 目前这种问题在pc上已经不多见,但是低内存手机存在crash的可能,因此需要对new加入非空判断。

11.4 Cocos2d-x优化内存技术

对象池:对于需要大量频繁new,delete或者创建,release的cocos2d-x的对象,考虑使用对象池技术,避免因为这些操作影响性能,并且频繁的申请释放内存会使得堆内存碎片化。

少用CCLabelTTF,cocos2d-x对于字体的支持是其最糟糕的部分之一,CCLabelTTF更是效率低下,如果一个页面上CCLabelTTF过多,会严重影响加载速度。

手动清除纹理:在cocos2d-x中,即使精灵被释放,它所使用的图片纹理其实依然保存在内存里,有时候在切换场景时,会产生内存消耗的高峰,容易导致加载缓慢甚至crash。可以在场景销毁前,手动清除某些不会被再次使用的纹理。

12其他杂项

12.1 Map

在我们项目里会常常使用map,对map使用方括号来赋值是十分方便好用的。但是!如果你不明白为什么方括号称为有副作用的调用。那么你也要谨记一点:除了赋值外,其他地方不得直接使用方括号来引用map里的元素。首先你必须要判断该map中是否有你要找的元素。

Bad:

m_mapPlayerAvtar[playerID]->setPokerNum(m_mapPokerNum[playerID]);

Good:

if (m_mapPlayerAvtar.find(playerID) != m_mapPlayerAvtar.end() &&

m_mapPokerNum.find(playerID) != m_mapPokerNum.end())

{

m_mapPlayerAvtar[playerID]->setPokerNum(m_mapPokerNum[playerID]);

}

关于map迭代内删除:

首先保存迭代器到一个命名类似iter_onlyForDelete的临时迭代器,删除只对这个临时迭代器操作,有人喜欢使用后缀++,效率上来说,两者都要生成临时的迭代器,差别不大。使用临时iter的可读性比后缀++要好(你不能排除有人不理解后缀++的可能)。

在确保了删除不会导致map迭代器失效,同时你还要保证这个函数重入不会改变map的结构,或者保证这个函数不会被重入。

12.2

相关主题
相关文档
最新文档