程序编写规则是附加在程序编译规则基础之上的一套计算机源程序编写方法。它建立在软件工程理论基础之上,经过程序员长时间经验的积累而产生出来的。不按照程序编写规则来写代码虽然不会发生程序编译错误,但是如果按照规则编写,对于程序的开发,调试和维护,程序员之间的交流有很大的好处。尤其在开发大型程序时,确定统一合理的程序编写规则更是保证程序编写质量的重要保证。
程序编写规则使得程序代码风格更加统一,更加易于理解,更加易于使用,修改和维护,而且还能帮助程序员避开一些程序中的陷阱(编译正确但极易出错)。虽然有些要求相当严格,但这是提高编程水平,提高产品质量的必要条件。
本文主要涉及到两个部分的内容,代码编写格式和对错误的防范。前者是为了让代码风格更加清晰简单,易于别人阅读;后者则是告诉我们如何尽量小心错误的发生,以及如何处理这些错误。当然,前者也是后者的基础,一段易于阅读,接口简单的程序也必然能减少错误的发生。
本规则要求目标软件的程序员必须认真学习和严格遵守,不按照本规则编写的程序将不被视为合格的程序。这里要特别说明,这里的要求都是最基本的,力求实用,所以,如果有哪些规则大家认为不合适,或根本做不到,应该进行沟通,我们一起来进行改进。更高的要求,相信会在各位进入项目开发的时候,会得到更加明确的指示。随着编程技术的不断发展,我们编写程序已经越来越容易了。但是这并不能说明大家已经可以随心所欲的写代码,相反,养成良好的编程习惯,对于我们未来的工作将会有莫大的益处。
命名规则和代码格式共同组建了程序员之间进行沟通的语言和语法。
通常一个名字由两个部分组成:模块名和功能名。其中,模块名可以省略,或者有多个。命名时应该本着能够最真实的反映代码模块层次结构的原则来决定。
ERFunc.cpp
DD_CreateSurface()
CAnimal::Move()
gpWorldPosition
模块名和功能名之间的连接可以使用下划线,或者采用大小写间隔的方法。模块名最好用该模块的缩写,一般是英文单词或汉语拼音的第一个大写字母。功能名则必须采用英文单词或者汉语拼音全称,不要用缩写。
文件名中要尽量体现程序的层次和模块和划分。如:main_startapp.cpp。层次之间用下划线连接或者大小写区分都可以。
函数的名字一般为1-n个词组成,每个词的第一个字母大写,其它小写,连续拼写,最终为动词。
WORD MAP_GetGroundData( int nLayer, int nCol, int nRow );
和 void MAP_SetGroundData( int nLayer, int nCol, int nRow, WORD codeG );
正意
反意
Open
Close
Create
Destroy
New
Delete
Get(或Peek)
Set
Draw
Erase(或Hide)
Init
Quit(或Fini)
Show
Hide
Send
Receive
Play
Stop
In
Out
Up
Down
Front
Back
Build(或Load)
Release(或Unload)
New
Delete
Next
Prev
Attach
Detach
Push
Pop
Buy
Sell
Take, Accept
Give, Giveup
Show( BOOL bShow=TRUE ); 只要使用一个参数就可以区分显示或隐藏,以减少函数的个数,而不必要建立一个Hide()函数。
SetBlood(), GetBlood()
SetLength, Length()
Cobj::IsStaticObj()
我们使用匈牙利命名法来给标识符命名。变量的名字一般为1-n个词组成,每个词的第一个字母大写,其它小写,连续拼写,最终为名词。
MAP_ptSavePos
SzUserName
m_bRead
MAP_DATA_NONE
GAME_PLAYER_MAX
由两个部分组成,前缀和功能名。前缀要求用小写字母见下表:
数据类型
前缀
整型数(int, short, long, char)
n
长整型(long)
l
无符号的整型(unsigned int)
u
无符号长整型(unsigned long)
ul*
字节型(byte)
by*
浮点型(float)
f
字符型(char)
c
字(WORD, unsigned short)
w
双字(DWORD, unsigned long)
dw
布尔型数(BOOL,int)
b
ANSI字符串(char*,LPSTR, LPCSTR)
sz,str
宽字节字符集字符串(WCHAR*,LPCWSTR,LPWSTR)
wcs
多字节字符集字符串(MBCS String)
mbcs*
点(POINT)
pt
矩形(RECT)
rc
大小(SIZE)
sz*
句柄(HANDEL)
h
窗口句柄(HWND)
hwnd*
指针(void*)
p
外部指针(void*)
lp,gp
表示错误代码的变量/对象
e
if( nFootballGameID != 1
&& nFootballGameID != 2
&& nFootballGameID != 3
&& nFootballGameID != 4 )
{…}
while ( i<100 )
{
i++;
};
while ( a
在现阶段,注释几乎是程序代码唯一的文档,所以必须给予足够的重视。在写注释的方面,绝对不能吝啬笔墨。如果可能,我们应该统一注释的格式,这样便于从代码中通过程序的手段提取文档。请记住,注释是良好的编程风格的重要方面!
在代码的旁边写注释作为说明文档,其重要性有两点:第一,它距离代码最近,别人阅读起来最方便,快捷;第二,因为它的第一点优势,所以它的修改也是非常方便的,所以它往往也最能正确反映代码的意图。
除非是大量的注释,不要使用C语言的注释方法。如果使用的话,则“/*”和“*/”必须单独占一行。这样做的原因是:使用汉语的注释,如果在其它文字的平台上编译,而且注释标识在同一行,有时会出现汉语文字和后面的“*/”相混淆的情况,造成大面积代码被误认为屏蔽掉的错误。
在注释的编写过程中,设想你在阅读别人的代码,你最需要了解哪些内容,你是否能看得明白,一定要慢慢养成这个习惯。
在所有文件的开始部分要求必须用注释方式写出文件的名字,版本号,撰写者名字,修改者名字,开始制作时的时间,各版本的修改时间及所做的主要修改,文件的主要功能,程序需要的外部函数库,版权信息等其它要说明的文字。具体格式和顺序可以自定。
撰写者的名字尤为重要,这是让你名垂青史的最重要的第一个地方。但是,只有你创建的这个文件才可以写上你的名字,或者有超过一半的修改才能作为修改者写上你的名字。有个别不道德的程序员会以此将别人的代码据为己有。
//////////////////////////////
// DDAPI.h : v0039
// Written by : Liu Gang
// Compiler : Microsoft Visual C++ 6.0 & DirectX 6.1
// Library : DDraw.Lib Dxguid.lib
// Copyright (C) : 1996-1999 Overmax Studio, All rights reserved
// v0010 : Aug.26.1996
// v0020 : Nov.8.1996
// v0030 : Dec.11.1996, upgrade DirectDraw from 1.0 to 2.0
// v0039 : May.12.1997, changed the global palette to object
//////////////////////////////
// header file
// This file provides basic interfaces for DirectDraw with C++
// extension, so that someone can use more easily.
//////////////////////////////
// BUG FIX : Apr.14.2003, Liu Gang, 对中毒以后的人,必须记住是谁下的毒。
//中毒时间结束以后,要删除这个变量
#include // fopen(), fclose()
#include "DMStruct.h" // CDMList
extern gMapData; // Declared in mapdata.cpp,时间紧迫不得已临时用一下。
extern MAP_GetmyProfile() // Defined in omprofile.cpp,为了减少头文件包含。
////////////
BOOL ReadLine( const char *name, char *value, int nSize );
BOOL ReadLine( const char *name, int *value );
BOOL ReadLine( const char *name, float *value );
////////////
int m_nType; // 类型,用来区分子类
int m_nID; // 物体的ID,在物体表数组中的序号,唯一的标识
CERString m_strIndex; // 物体的索引,主要用于编程和编辑,存放的是目录结构
CERString m_strName; // 物体的名称,主要用于游戏的显示,存放的是名字
CERString m_strAlias; // 物体的别名,主要用于命令识别。
要尽量减少暴露在头文件中的函数。一个函数如果只在模块内部使用,尽量定义为局部函数。局部函数的声明不要在头文件中,而放在源文件的开始部分。这样可以降低别人阅读代码的难度,也不会被其他程序员无意调用。在命名时,把它的功能名的第一个词汇的第一个字母定为小写。
局部函数的定义要使用"static"关键字。
如果一个局部函数需要被人引用,则在引用的地方要用注释说明原因(详见上面“注释”的内容)。
// V1.16 Oct.31.2002,客户端角色头顶必须顶对话, ERFunc.cpp
static int makeSaying( CERObject* pObj, LPCTSTR szMsgOld, char* szMsgNew, int nSize );
BOOL SetValue( const mystruct &MyData ); // 参数不可以修改
BOOL SetValue( const mystruct * pStruct ); // 参数可以修改
const mystruct& GetValue( int nIndex ); // 返回值不可以修改
BOOL ReadLine( const char *name, char *value, int nSize ); // nSize就是缓冲区的大小
void GetInitString( char* szInit, int nSize );
int USER_GetSetting( const CUserInfo& user, char* szSetting, int nSize );
如果inline函数过多,把它们放到专门的*.inl文件中,在头文件最后包含进去。比如模版文件的定义部分就非常适合这样使用。这样可以让真正的头文件看起来更加简洁干净。
例如:
#include “test.inl”
char* str = new char[10];
delete [] str;
程序中的错误千差万别,多种多样,程序员必须正确面对这些BUG,才能做到胸有成竹,写出的程序才能完美无缺。
对于大多数非正常错误,都可以通过增加ASSERT检测,在ASSERT阶段发现并排除,对于有些只在release版才出现的错误,也可以通过重载系统的ASSERT而在release版中定位。
在发布阶段,可以通过在程序中插入写Log的检查点的方式来确定程序流程,这也是有效地定位非正常错误的方式。
预定义 _CHECK_POINT宏
ifdef CHECKPOINT
SaveLog(_FILE,_LINE); //检查点位置存盘
#else
{;} //空操作
#endif
在输出错误信息时,错误信息应该能够帮助程序员以最快的速度找到出现错误的地方,错误的性质,类型,和出错原因。所以错误信息应该至少包含以下的内容:
WriteError( "SendMsg() Msg Type error, Send failed! <%d, %d>\r\n",pMsg->m_Type ,pMsg->m_Scope );
WriteLogFile( “error.log”, “Database Error(%d) : File not found<%s>!”, 205, szFileName );
在编写模块时,要自己建立一套错误处理机制。要建立统一的错误序号及说明文字,有统一的错误出口函数。
程序调试的手段是程序员用来发现BUG,查找BUG,解决BUG最重要的手段。定位一个BUG,往往占据修改BUG的绝大部分时间。我们应该把BUG尽可能的消灭在编写代码的时刻,而不是游戏发布的时刻,这样才能达到最高的编码效率。
Assert( nIndex >= 0 && nIndex < MAX_NUM );
Assert( pChild != NULL );
if( error != 0 )
{
char szError[256];
sprintf( szError, “DDAPI CDDSurface::LoadBitmap() Error (0) : File not found <%s>\n”, filename );
OutputDebugString( szError );
}
#ifdef LOG
WriteLogFile( "easyrpg.log", "Here cannot be a room!\n" );
#endif
int aaa = 0;
#ifndef _DEBUG
try
{
#endif
…(实际的代码1)
aaa = 1;
…(实际的代码2)
aaa = 2;
…(实际的代码3)
aaa = 3;
…
#ifndef _DEBUG
}
catch(...)
{
char szMsg[ER_MESSAGE_SIZE];
sprintf( szMsg, "newpersonex() Error : %d <%s %d %d>\n", aaa, szType, nLevel, nSLevel );
WriteLogFile( "easyrpg.log", szMsg );
}
#endif
使用DbgHelp.dll或者minidump提供的方法,可以更简单和方便的定位到错误。因为这些技术目前来看还比较前卫,暂时不再这里讨论。有兴趣的人可以自行研究。
n 必须要对指针进行初始化,也就是赋予NULL值。
n 给指针分配内存前必须要确保指针为NULL值。
n 对分配成功与否要进行判定。
n 使用指针之前要确保指针不为NULL值。
n 删除指针后,必须给指针赋NULL值。
CERMessage * pMsg = NULL; // 给指针初始化
……
Assert( pMsg == NULL ); // 给指针分配内存前要确保指针为空
pMsg = new CERMessage;
if( !pMsg ) // 给指针分配内存以后要确保指针不为空,注意这里是“正常错误”。
return 0;
……
BOOL SendMessage( CERMessage* pMsg )
{
Assert(pMsg); // 使用之前要确保指针不为空。
SendMsg( pMsg );
}
……
Assert( pMsg ); // 删除之前要确保指针不为空。
delete pMsg;
pMsg = NULL; // 删除之后要确保指针为空。
assert( nIndex >= MN_TYPE_FIRST && nIndex <= MN_TYPE_LAST );
int nData = m_MyData[nIndex];
char szDest[256], szSrc[256];
……
strcpy( szDest, szSrc );
assert( strlen(szDest) < sizeof(szDest) );
注意:如果使用sizeof()函数,szDest必须是字符串数组,而不能是指针,否则sizeof(pChar)只能是4。
ASSERT(lpSolider->IsHuman());
((Chuman *)lpSolider)->Die();
这行代码是《烈火文明》游戏中发现的致命死机错误之一,在该游戏发行两个月后找到。原因:除数为零。
m_pTimeSlider->SetCurrentPercent ( pFighter->GetTime()*100/pFighter->GetTimeMax() );
我们应该这样写:
assert(pFighter->GetTimeMax() != 0 );
m_pTimeSlider->SetCurrentPercent ( pFighter->GetTime()*100 / pFighter->GetTimeMax() );
void TellFriend(LPCTSTR szMsg, CERObject* pObj)
{
assert(!IsEmpty(szMsg)); // 检测第一个参数合法
assert(pObj); // 检测第二个参数也合法
…
}
if( nGeneral ),写成if( nGeneral > 0 )会保险一些。
注意,如果按照前者写法,该变量是负值的话也会返回真值的。
程序的逻辑和目的必须简单而明确,简单就是美。简单也就易于阅读,程序必须可以被自己和他人阅读。大型程序往往制作时间较长,合作的人也很多。就算是自己,很可能也会对很早以前编写的代码感到生疏。由于开发人员的流动,当程序员本人不在时,也要求别人必须可以看懂甚至修改他的代码。简单也就易于修改和维护。
nCounter++;
// 不好的说明文字: 整数加1
// 好的说明文字: 学生人数加1
程序必须是正确的,这当然是无庸置疑的了。但是健壮同样是必须的。在这里,健壮的意思不是指:包容错误,掩盖错误;而是恰恰相反:在遇到错误的时候,要明确的报告出来,帮助别人尽快找到错误。
一段代码应该拥有自我检测的功能,在输入合理的情况下保证其正确性。这样无论外界发生了怎样的变化,别人使用它的时候都会放心。
这包括我们写代码的效率,和代码运行的效率。
虽然做到所有这一切并不是件简单的事情,但是一个好的程序能够在很大程度上提高软件的制作效率,使软件更加易于调试,修改和维护;同时减少潜在的程序错误,把错误尽可能多的消灭在编写代码的时段。