本文来自肥宝游戏,引用必须保留文末二维码!
!
!
好几天没写文章了,周一整理自己刚修好的旧电脑,发现一本书《高质量C++编程指南》。由于近期在写游戏服务端的战斗。所以这个立马就吸引肥宝了。看了几天,深深感觉获益良多啊。
于是把笔记和自己的经验写下来,分享给大家。
一、写出美丽易读的程序
中学时代上课非常喜欢做笔记。可是肥宝差点儿没看过自己的笔记。不是肥宝懒。是由于肥宝字太丑了。
后来成为一个程序猿,以为这些代码都是电脑输出。统一字体。就不用这么纠结了。谁知道代码更须要写得美丽。由于需求是不断在变化的。bug也是不断地涌现。不管出于工作还是学习,你总会常常看回自己写的代码,比起中学时代看自己的笔记的概率高多了。
可能一个功能,在開始的时候会常常看到,不会认为难看。可是一周之后呢?一个月后呢?一年后呢?更离谱的是。有一次肥宝收到一个曾经的工作的人的电话。问肥宝代码的问题,那个时候肥宝都离职了几个月了。就算如今,旧的游戏依旧会有新的人维护,他们常常拿肥宝一两年前写的代码来问肥宝。肥宝认为维护一些写得丑的代码,跟自己每天被迫跟一个300斤的大肉參来个法式湿吻那么恐怖。
如何才算美丽的代码,如何才算丑的代码?以下给出一些小规则:
文档的结构:
【1】头文件用来声明。cpp文件用来定义。声明和定义分开。不但能够让代码更清晰、方便阅读,同一时候,假设在某些场合。代码不能公开,仅仅向用户提供头文件和二进制的库就可以。通常这种情况下。头文件和源文件是在不同文件夹的
【2】为了防止头文件被反复引用,应当用 ifndef/define/endif 结构产生预处理块。这个宏定义必须讲究。假设反复了,可能会出现莫名其妙的问题。
肥宝个人习惯是project名+文件夹+文件名称。
比如:
#ifndef _GateApp_Role_Player_H_ #define _GateApp_Role_Player_H_ //代码实如今这里。【3】用#include<file>引用标准库头文件(编译器从标准库文件夹開始搜索),用#include"file.h"引用其它文件(编译器从工作文件夹開始搜索)。。 #endif
【4】普通情况下h文件仅仅写声明,不写定义。写内联函数时不要包括for,递归等复杂操作。不要超过5行。由于消耗非常大,并且调试不方便。
【5】尽量少用局部变量。
【6】依照实际意义区分文件夹,一个文件夹内超过10个文件。要考虑是否拆分。
程序的风格:
良好的格式有助于阅读和维护。普通情况下,我们使用的IDE。入Visual Studio、Eclipse、XCode等,本身生成的代码都是符合良好的风格的,尽量使用代码提示功能,有助于形成良好风格。
肥宝平时用得比較多的是Visual Studio,所以以下的风格也是这个软件的。
【7】在每一个类声明之后加空行,每一个函数定义结束之后都要加空行。
【8】在一个函数体内。逻揖上密切相关的语句之间不加空行,其他地方应加空行分隔。
【9】一行代码仅仅做一件事情,仅仅定义一个变量,仅仅写一条语句。
这种代码easy阅读,而且方便于写凝视。
【10】if 、for 、while 、do 等语句自占一行。运行语句不得紧跟其后。
不论运行语句有多少都要加 {}。这样能够防止书写失误。
【11】尽可能在定义变量的同一时候初始化该变量(就近原则),放久了非常easy忘记。
不良风格的代码:
int attack, defense, life; // 攻击、防御、生命,同一时候声明多个变量,没有初始化 int hurt= attack - defense; life -= hurt; //运行多条语句 if (life < 0) onDeath(); for (int i = 0; i < 10; i++) findNext(); processNext();
良好风格的代码:
int attack = 0; // 攻击 int defense = 0; // 防御 --跟上下联系紧密,不加空行 int life = 0; // 生命 int hurt= attack - defense; life -= hurt。 if (0 >= life) { onDeath(); } for (int i = 0; i < 10; i++) { findNext(); } processNext();【12】keyword之后要留空格。 比如 const 、virtual 、inline 、case 等keyword之后至少要留一个空格。否则无法辨析keyword。
比如 if 、for 、while 等keyword之后应留一个空格再跟(,以突出keyword。
【13】函数名之后不要留空格,紧跟'(',以与keyword差别。'('向后紧跟,右括号、逗号、分号向前紧跟,紧跟处不留空格。
,之后要留空格,如Function(x, y, z) 。假设‘; ’不是一行的结束符号,其后要留空格,如 for (int i = 0; i < 10; i++)
【14】赋值操作符、比較操作符、算术操作符、逻辑操作符、位域操作符, 如“= ”、“+= ” “>= ”、“<= ”、“+ ”、“* ”、“% ”、“&& ”、“||”、“<< ”, “^ ”等二元操作符的前后应当加空格。
【15】一元操作符如“!”、“~ ”、“++ ”、“-- ”、“& ”(地址运算符)等后不加空格。
【16】“[]”、“.”、“->”这类操作符前后不加空格。
【17】对于表达式比較长的 for 语句和 if 语句。为了紧凑起见能够适当地去掉一些空格。
如 for (i=0; i<10; i++)和 if ((a<=b) && (c<=d)) ,或者太长的话能够适当换行。void Func1(int x, int y, int z); // 良好的风格 void Func1 (int x,int y,int z); // 不良的风格 if (year >= 2000) // 良好的风格 if(year>=2000) // 不良的风格 if ((a>=b) && (c<=d)) // 良好的风格 if(a>=b&&c<=d) // 不良的风格 for (i=0; i<10; i++) // 良好的风格 for(i=0;i<10;i++) // 不良的风格 for (i = 0; I < 10; i ++) // 过多的空格 x = a < b ?【18】程序的分界符‘ {’和‘}’应独占一行而且位于同一列,同一时候与引用它们的语句左对齐。a : b; // 良好的风格 x=a<b?a:b; // 不好的风格 int *x = &y; // 良好的风格 int * x = & y; // 不良的风格 array[5] = 0; // 不要写成 array [ 5 ] = 0; a.Function(); // 不要写成 a . Function(); b->Function(); // 不要写成 b -> Function();
这个是C++的风格,java的风格是{紧跟出现前的代码
【19】 { }之内的代码块在‘{’右边一个制表符处左对齐。
不良风格:
void Function(int x){ // program code } if (condition){ } else{ } for (initialization; condition; update){ // program code } while (condition){ }良好风格的代码
void Function(int x) { // program code } if (condition) { // program code } else { // program code } for (initialization; condition; update) { // program code } While (condition) { „// program code }假设出现嵌套的{}。则使用缩进(按Tab键)对齐,如:
{ { } }
【20】代码行最大长度宜控制在 70 至 80 个字符以内。
代码行不要过长,否则要拉来拉去非常麻烦
【21】长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。
表达式内尽量不要套多层表达式。
if ((crit >= 100) && (block <= 100) && (wreck <= 100)) { attack *= 2; } virtual CPoint changeToNewPoint(CPoint attackPoint, CPoint defensePoint); for (SeqCPlayer::iter iter = player.begin(); iter != player.end(); iter++) { check(); }
本文来自肥宝游戏,转载引用请加链接http://blog.csdn.net/u012175089/article/details/51078360
很多其它文章来自肥宝游戏
凝视
程序块的凝视常採用“/*…*/”,行凝视一般採用“ //…”。
【22】凝视是对代码的“提示”,而不是文档。凝视风格统一。
【23】 假设代码本来就是清楚的。则不必加凝视。
【24】边写代码边凝视。改代码后要同一时候改动凝视
【25】 凝视应当准确、易懂,防止凝视有二义性。 错误的凝视有害。
【26】 尽量避免在凝视中使用缩写。特别是不经常使用缩写。
【27】 凝视的位置应与被描写叙述的代码相邻,能够放在代码的上方或右方,不可放在下方。在VS中,在上方凝视后,以下全部代码的提示都受到影响的。所以优先放后方
【28】 当代码比較长。特别是有多重嵌套时,应当在一些段落的结束处加凝视,便于阅读。
【29】 对于思路复杂的代码。能够把思路写在凝视里面,否则别人看起来非常蛋疼。
另外一些设计思想也应该写下来。
对于一些业务多变,并且往往不合常理的需求,一定要多写凝视。比如游戏代码中,非常多策划特意要求做的特殊处理,有的是为了坑玩家。很多其它的是为了体验。
这东西看代码是看不到不论什么思路的。当你发现某个地方明明能够用非常简洁的代码写。却硬是要用一些非常傻比的方法的时候。非常可能不是那个人没经验或者偷懒。而是策划要求造成的。
所以像游戏这样的系统。一定要多谢凝视,但改的时候一定要跟着改,否则,呵呵呵呵呵呵呵呵呵呵呵。
类的风格
【30】 类的public函数写在前面,private数据写在后面。不要把什么都往一个类里面扔,要注意这个类本身的含义。
命名
命名并没有统一的标准!
非常多公司都有自己的一套标准。
【31】标识符应该能够望文生义,长度尽量小。信息尽量大
【32】 命名规则尽量跟系统风格保持一致
【33】 不要出现紧靠大写和小写区分的标识
【34】 尽量不要出现标识符全然同样的局部变量和全局变量
【35】 同一个项目的代码下,即使在不同project内,也不要出现全然同样的文件名,这是VS的一个bug。调试起来会在同名文件间跳来跳去。
【36】 变量的名字应使用名词或者形容词+名词。函数的名字应该使用动词或者动词+名词
【38】 使用反义词去命名相互排斥意义的变量或函数
【39】 尽量避免使用数字
以下是肥宝所在的项目的一些规范:
(1)类名开头大写,结构以S开头。类以C开头。数组用Seq开头,字典类型用Dict开头,指针用Ptr结尾。
(2)变量和參数用小写开头。之后每一个单词开头大写。变量名一般就不加类名这些前缀后缀了。
游戏代码的命名是个蛋疼的事情,作为游戏程序猿,碰到的非常多名词是无法翻译的。比如诛邪、神兽、涅槃、貔貅、修为、境地、法力,非常多非常多。有些时候你可能翻译了一个单词出来,过几天一个新的功能。你会发现意义好像差点儿相同的。这个时候我们常常会用拼音,通俗易懂啊。
还有个更蛋疼的事情,游戏的功能是多变的,策划的需求一天一个样。比如某个货币叫做荣誉,做好了功能,过几天这个荣誉要改名为修为。另外的地方突然有个货币叫做荣誉。这个时候你不得不跟着把名称也改掉。否则过一个星期,没几个人看得懂,并且跟client。PHP后台,运营等交流也非常蛋疼,大家无法同步。
所以,命名的时候不一定要按他们给出来的来命名,最好用一些跟中性的而又不违反语义的单词,另外自己弄一张汉语-英文-功能对比表。还有文档的功能名称通常跟上线后的功能名称全然不同的。
二、从习惯開始优化程序的性能
表达式和基本语句
【40】假设代码行中的运算符比較多,用括号确定表达式的操作顺序。避免 使用默认的优先级。
【41】 不要编写太复杂的复合表达式。
i = a >= b && c < d && c + f <= g + h ; // 复合表达式过于复杂【42】不要有多用途的复合表达式。
d = (a = b + c) + r ;【43】不要用数学表达式的方法来敲代码中的表达式
if (a < b < c) // a < b < c 是数学表达式而不是程序表达式 //并不表示 if (( a < b ) && ( b < c )) //而成了 if ( (a <b) < c))【44】不可将布尔变量直接与 true、false 、 1、0 进行比較。
bool isPlayer = ture; if (flag) // 表示 flag 为真 if (!flag) // 表示 flag 为假 //其他的使用方法都属于不良风格,比如: if (flag == TRUE) if (flag == 1 ) if (flag == FALSE) if (flag == 0)【45】应当将整型变量用“==”或“!
=”直接与 0 比較。
并且最好把0写在左边,其他数字也是
int life = 0; if ( 0 == life) if ( 0 != life ) if ( life == 0 )//easy写成 if ( life = 0 ),并且不会报错 //不可模仿布尔变量的风格而写成 if (value) // 会让人误解 value 是布尔变量 if (!value)【46】不可将浮点变量用“==”或“!=”与不论什么数字比較。 由于float和double都有精度限制。
double x = 0; if ( 0.0 == x )//隐含错误的比較 if ((x >= -EPSINON) && (x <= EPSINON)) //当中 EPSINON 是同意的误差(即精度)。【47】指针变量与Null比較,最好不直接写。(事实上肥宝一直习惯直接)
//推荐: if (p == NULL) // p 与 NULL 显式比較,强调 p 是指针变量 i if (p != NULL) //不推荐 if (p == 0) // easy让人误解 p 是整型变量 if (p != 0) if (p) // 事实上肥宝推荐这个,写这么久难道不知道这是指针么。 if (!p)
循环语句的使用
【48】长循环放里面,短循环放外面,由于会打断循环体,
#include "Test/TestManager.h" #include "Map/Point.h" #include <time.h> #include <iostream> #include <math.h> using namespace CppServer; void doSomething() { SPoint p; p.x = 1; p.y = 1; int dist = p.x * p.x + p.y * p.y; dist += p.x; dist += p.y; dist = (int)sqrt(dist); SPoint p2; } void CppServer::CTestManager::test1() { int n = 100; for (int m = 100; m <= 100000; m += 1000) { { clock_t startTime = clock();//開始时间 for (int i = 0; i <= m; i++) { for (int j = 0; j <= n; j++) { doSomething(); } } clock_t endTime = clock();//结束时间 std::cout << "多重循环1 i :" << m <<" j:"<<n << " time:" << (endTime - startTime) << std::endl; } { clock_t startTime = clock();//開始时间 for (int i = 0; i <= n; i++) { for (int j = 0; j <= m; j++) { doSomething(); } } clock_t endTime = clock();//结束时间 std::cout << "多重循环2 i :" << n << " j:" << m << " time:" << (endTime - startTime) << std::endl << std::endl; } } }为了验证这个效率详细区别到什么程度,写了一段測试代码。发现上万次循环的时候才有几毫秒的区别。可能dosomething函数太简单的原因。
只是肥宝认为有些时候比較难把短循环抽出去的话,就不是必需太刻意了。
本文来自肥宝游戏,转载引用请加链接http://blog.csdn.net/u012175089/article/details/51078360
很多其它文章来自肥宝游戏
【49】不可在 for 循环体内改动循环变量,防止 for 循环失去控制。尤其注意erase的使用方法
我们游戏服务端宕机有一半是这个引起的,每次新人来了都跟他们说,说了还是会犯错。
typedef std::vector<CPoint> SeqCPoint; typedef std::map<int, CPoint> MapCPoint; SeqCPoint points; MapCPoint pointMap; for (SeqCPoint::iter iter = points.begin(); iter != points.end(); ) { if(xxx) { points.erase(iter);//erase后,指针会跳到下一个 } else { iter ++; } } for (MapCPoint::iter iter = pointMap.begin(); iter != pointMap.end(); iter ++;) { if(xxx) { pointMap.erase(iter);//erase不须要特殊处理 } }【50】 switch语句中每一个case后面必须加break,必须加defualt。
【51】 少用慎用goto。
肥宝认为,业务层的代码,直接禁用吧!
常量
【52】尽量使用常量来表示程序中常常出现的数字或字符串
const float PI = 3.14159; // C++ 语言的 const 常量
【53】 须要对外公开的常量放在头文件。内部使用的放在源文件。
非常多人喜欢把常量放在同一个文件里,可是多了之后修改一下,多个地方要编译。我们游戏以前加一个错误码要编两个小时。后面改成配置了。这个自己把握吧。
【54】函数声明是,參数要写完整。命名要恰当。
【55】 參数最好加上&。能够大量节省对象构造和析构的时间。
SPoint CMapManager::changeAbsolute2Relative(SPoint& originPoint, SPoint& changePoint) { SPoint rePoint; rePoint.x = changePoint.x - originPoint.x; rePoint.y = changePoint.y - originPoint.y; return rePoint; }就这样一个函数,没加&的时候。运行时间是如今的三倍,能够看肥宝前一篇文章MMO游戏技能攻击区域的计算3--效率分析
【56】仅仅做传值的參数最好加上const
【57】不要使用太多參数,參数太多。能够弄一个结构包起来,让程序清晰。
【58】尽量不要使用类型和数量不确定參数
【59】默认參数要慎用,并且不要太多。
比如:
bool checkCanAttack( int life, int distance = 0 ) { if ( life > 0 ) { return false; } doOther(); return true; } checkCanAttack(100, 100);后来由于业务变化。函数改成
bool checkCanAttack( int life, bool isFighting = false, int distance = 0, int maxDistance = 0) { if ( life > 0 && !isFighting) { return false; } return true; } checkCanAttack(100, 100);//原来的使用并没有由于函数參数改了而出错。假设大量使用到,非常easy出错并且无法调试
由于调用地方太多。有的地方改漏了,在外网正式服出现了问题被人投诉才知道。
后来肥宝的解决的方法是把默认參数都去掉,然后一个个找错,是在没办法了。
返回值
【60】 函数返回和函数名字在语义上要一致
比如:
bool isWeekDay( int day )//推断是否是周末 { if( day == 6 || day == 0) { return false;//应该返回true,可是却返回false } return true; }写的人不会认为什么。可是用的人可能就会用错了。
【61】在函数体内,easy出现return或者throw的分支尽量写在前面,能够省掉剩下的推断
【62】假设返回值是一个对象,要考虑效率
//效率高。直接在函数外部构造了 return String(s1 + s2); //效率低,须要经过构造,传递,析构 String temp(s1 + s2); return temp; 当然。这个对基础类型没什么作用 return 1 + 1; int temp = 1 + 1; return temp;基础类型并没有构造这些步骤,省不了多少,看情况那种简洁用哪种。
【63】函数功能要单一,不要设计多用途的函数
【64】函数体规模要小,50行内,超过了最好拆分一下。
本文来自肥宝游戏。转载引用请加链接http://blog.csdn.net/u012175089/article/details/51078360
很多其它文章来自肥宝游戏
内存管理
【63】内存没分配成功,通经常使用指向这个内存的指针检測是否为空。
肥宝认为,内存分配不成功。后面的处理已经不重要了。整个系统内存都爆了,宕机什么的是必定的了。然后运营会收到各种投诉,查错,赔偿。维护一大堆事情陆续有来。
【64】内存后。要养成初始化的习惯。
不要以为检測一个数字为负数就能确定还没初始化,假设碰上反复使用的那块内存,真的不知道是什么。
【65】动态内存的申请与释放必须配对,防止内存泄漏。
【66】用 free 或 delete 释放了内存之后,马上将指针设置为 NULL,防止产 生“野指针”。
【67】指针消亡,并不代表指向的内存被释放,内存被释放,并不代表指针为null。
【68】指针声明的时候。要么指向详细内存,要么设置为null。否则是随机值,可能if(p == Null)不起作用的。
【69】new一个对象的代价大,还是初始化后重用的大呢?
struct SPoint { void init(); int x; int y; int z[100000]; }; void SPoint::init() { x = 0; y = 0; } int count = 10000000; { CppServer::SPoint p; clock_t startTime = clock();//開始时间 for (int i = 0; i <= count; i++) { //p.init(); p.x = 1; p.y = 1; int distance = sqrt(p.x * p.x + p.y * p.y); } clock_t endTime = clock();//结束时间 std::cout << "time:" << (endTime - startTime) << std::endl; } { clock_t startTime = clock();//開始时间 for (int i = 0; i <= count; i++) { CppServer::SPoint p; p.x = 1; p.y = 1; int distance = sqrt(p.x * p.x + p.y * p.y); } clock_t endTime = clock();//结束时间 std::cout << "time:" << (endTime - startTime) << std::endl; }
结果效率一样,可是假设运行了上面被凝视的init函数后。时间消耗大大提高。初始化可能更加占消耗
【70】内联函数是用于实现的,声明的时候不用写。
在头文件实现的函数自己主动成为内联函数
【71】 重载、内联、缺省參数、隐式转换等机制展现了非常多长处,可是这些 长处的背后都隐藏着一些隐患。不要总是想着用什么高级方法,不要为了用而用。非常多时候,先写出来,后面再优化就好了。当然,可能还没写好,需求就变了。
【78】类的构造函数和析构函数可能有非常多隐藏的操作。不一定适合做成构造函数
构造函数、析构函数、赋值函数
【79】赋值函数仅仅是位拷贝而不是值拷贝。假设当中有指针就非常可能出问题。并且会造成内存泄漏。
【80】构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表),初始化表在函数參数表之后,在函数体{}之前。
假设类存在构造关系,必须在初始化表里面初始化。
带有const的属性必须在初始化表里面初始化。
一个类初始化的时候,假设属性不是基础类型,放在初始化表来初始化,效率更高。
其它的建议
【81】不要一味提高效率,应该很多其它考虑程序的可读性,可维护性。假设你也是做游戏的话,非常可能你的游戏根本没几个人玩。永远碰不到这个瓶颈,哈哈哈哈哈!【82】优化程序的时候,要充分考虑游系统的特性,找出瓶颈所在。比如一个MMO游戏,在初期推广的时候,非常多新玩家,常常在某个时段某个功能非常多人同一时候玩。这个要注意。到了后期的功能。可能会非常少人接触,可是在后来往往有非常多跨服功能,差点儿全部玩家在同一个服玩,这些都是要考虑的问题。
【83】最好的优化应该是从业务级别開始。非常多东西计算量固定在那,怎么减都减不了的。须要考虑从业务级别入手。
【84】先优化数据结构。再优化算法
【85】有时候时间效率和空间效率对立,要做出折冲的方案,私人告诉你,如今内存非常廉价,硬盘更廉价。可是系统启动读数据,可能会非常慢非常慢。
【86】避免编写技巧性非常高代码。
【87】当心变量发生上溢或下溢,数组的下标越界。
【88】尽量使用标准库函数,不要“发明”已经存在的库函数。【89】尽量不要使用与详细硬件或软件环境关系密切的变量。
最后:
还有非常多非常多的优化方式。只是限于本人的水平,仅仅整理了这么多。路非常长。道无涯。苦练七十二变,才干笑对八十一难。努力吧,少年!