程序编写规则

程 序 编 写 规 则

概述

 

程序编写规则是附加在程序编译规则基础之上的一套计算机源程序编写方法。它建立在软件工程理论基础之上,经过程序员长时间经验的积累而产生出来的。不按照程序编写规则来写代码虽然不会发生程序编译错误,但是如果按照规则编写,对于程序的开发,调试和维护,程序员之间的交流有很大的好处。尤其在开发大型程序时,确定统一合理的程序编写规则更是保证程序编写质量的重要保证。

      程序编写规则使得程序代码风格更加统一,更加易于理解,更加易于使用,修改和维护,而且还能帮助程序员避开一些程序中的陷阱(编译正确但极易出错)。虽然有些要求相当严格,但这是提高编程水平,提高产品质量的必要条件。

本文主要涉及到两个部分的内容,代码编写格式对错误的防范。前者是为了让代码风格更加清晰简单,易于别人阅读;后者则是告诉我们如何尽量小心错误的发生,以及如何处理这些错误。当然,前者也是后者的基础,一段易于阅读,接口简单的程序也必然能减少错误的发生。

本规则要求目标软件的程序员必须认真学习和严格遵守,不按照本规则编写的程序将不被视为合格的程序。这里要特别说明,这里的要求都是最基本的,力求实用,所以,如果有哪些规则大家认为不合适,或根本做不到,应该进行沟通,我们一起来进行改进。更高的要求,相信会在各位进入项目开发的时候,会得到更加明确的指示。随着编程技术的不断发展,我们编写程序已经越来越容易了。但是这并不能说明大家已经可以随心所欲的写代码,相反,养成良好的编程习惯,对于我们未来的工作将会有莫大的益处。

 

一、命名规则和代码格式

命名规则和代码格式共同组建了程序员之间进行沟通的语言和语法。

 

通常一个名字由两个部分组成:模块名和功能名。其中,模块名可以省略,或者有多个。命名时应该本着能够最真实的反映代码模块层次结构的原则来决定。

ERFunc.cpp
DD_CreateSurface()
CAnimal::Move()
gpWorldPosition

模块名和功能名之间的连接可以使用下划线,或者采用大小写间隔的方法。模块名最好用该模块的缩写,一般是英文单词或汉语拼音的第一个大写字母。功能名则必须采用英文单词或者汉语拼音全称,不要用缩写。

  • 名字要在能够清晰表达出模块层次结构和功能的基础上尽可能的短
  • 不要使用匪夷所思的变量名,请尽量在变量名中使用英文单词表示变量的含义。如果不知道英文单词是什么,就应该先去询问一下别人是怎样命名这类变量的。如果没有人知道,再去查字典,然后将结果反馈给大家以便统一使用(这很重要,我们要争取最大程度上的步调一致,而不是比谁英文更好或者谁更会查字典)。

文件

文件名中要尽量体现程序的层次和模块和划分。如:main_startapp.cpp。层次之间用下划线连接或者大小写区分都可以。

  • 文件名要尽可能的短。因为,文件经常被要求进行拷贝或者罗列出来,所以文件名应该简单清晰。文件名的功能名如果太长可以考虑使用缩写,但是也必须能够让人看懂其功能。如:ERFunc.cpp

函数

函数的名字一般为1-n个词组成,每个词的第一个字母大写,其它小写,连续拼写,最终为动词。

  • 尽量使函数配对。如果编写了某个函数,它的功能名有明显的相反意义,请在编写一个相反功能的函数,使之配对。尽管当时可能用不上,也应该同时写上。例如:CreateSurface()和DestroySurface()。函数名使用只有动词不一样的名称,名词部分要一致。函数的参数要相对应。虽然可能不完全一样,但是参数和返回值的总和必须一样。
    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()函数。
  • 在类中对某成员赋值,请使用Set(),是该成员的属性功能名。取得该值,请使用Get(),或直接()。
  •  要测试类的实例中是否具有某属性,请使用Is() 的形式。一般对于BOOL型变量的查询应该采用这样的方法。
    SetBlood(), GetBlood()
    SetLength, Length()
    Cobj::IsStaticObj()

我们使用匈牙利命名法来给标识符命名。变量的名字一般为1-n个词组成,每个词的第一个字母大写,其它小写,连续拼写,最终为名词。

MAP_ptSavePos
SzUserName
m_bRead


  • 声明宏定义时全部使用大写字母和下划线。判定头文件是否包含的宏定义一般是由双下划线和大写的文件名组成,例如 __DDAPI_H__。定义常量的宏定义一般是由三部分组成:模块名1 + 模块名2 + 功能名,中间由下划线连接。
    MAP_DATA_NONE
    GAME_PLAYER_MAX

  •  类的名称第一个字母必须为C。对于Symbian等编译系统,可以根据该系统的规则来进行命名。
  •  成员变量的前两个字母必须为m_。
  •  全局变量的前两个字母必须为g_,或一个字母为g。
  •  一般变量的规则如下:

由两个部分组成,前缀和功能名。前缀要求用小写字母见下表:

数据类型

前缀

整型数(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


  •  对于布尔型变量,应该使用肯定词作为变量名。不要使用这样的变量:bNotActive,而要这样:bActive。否则使程序不易理解。这样也不是很好:bDestroyed,而要:bCreated,更容易理解。*目前这种用法已经比较少了,不推荐使用。

代码格式

  • 源文件每行尽量不超过80个字符,便于观看和窄行打印。每个源文件一般不超过3000行。如果文件中函数功能很紧密(如类的成员函数)而且比较长,则不应超过10000行。每个函数的长度则要求一般不超过500行,最长不应超过3000行。
  • 使用TAB键完成每行的缩进。设置TAB键的跨度是4个空格。
  • 遇到很长的复合判断语句,应按逻辑结构划分成为多行并把中间的逻辑符号放在每行的前面。

if( nFootballGameID != 1
&& nFootballGameID != 2
&& nFootballGameID != 3
&& nFootballGameID != 4 )
{…}


  • 不要在“->”,“.”,“::”和“[]”周围加空格。否则给人以不完整的感觉。
  • 不要在单元操作符“!”,“++”,“—”,“&”和它们的操作者之间加空格。
  • 在不是行末尾的“,”,“;”后面加一个空格。
  • 前后大括号必须有相同的缩进关系。中间的内容必须缩一个TAB。

 

while ( i<100 )
{
    i++;
};


  • 就算只有一行内容,大括号也不要省略。
  •   不要把case语句写得比switch还突出。
  •  所有函数名称必须从最左面开始,返回值必须和函数名在同一行。
  • 所有类的定义必须从最左面开始,不留空格,public,protected,private也应如此。
  • 使用一个空行来间隔函数中某些功能独立的代码。使用两个空行来间隔不同的函数。功能无关或属于不同类的函数要用更多的空行分割开来。
  • 对于空循环,对应循环体的分号必须另起一行,最好再加上一对大括号。

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.
//////////////////////////////

  • 类的成员函数定义(Definition)的顺序必须与在头文件中的声明顺序保持一致。
  •  对每一段功能独立的代码都可以写注释。对于采用了特殊算法的代码段(你特别得意的,你优化过的或者你认为不容易读懂的),须对算法进行详细的注释说明。
  •  对于Bug的修改,应该在相应地方写下注释。内容包括Bug出现的原因,修改方法和结果,修改日期,修改人等。这是对前人的一种尊重。

// BUG FIX : Apr.14.2003, Liu Gang, 对中毒以后的人,必须记住是谁下的毒。
//中毒时间结束以后,要删除这个变量

  • 在每个Include的头文件后注释说明所用到的函数名。这是为了让别人知道你为什么要包含这个头文件。以减少头文件的无意包含。

#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 );			// 返回值不可以修改


  • 从一个函数内部得到一个缓冲区,应该传入给定缓冲区的大小。这是为了保证在函数内部进行内存拷贝的时候不会超界。虽然最新的C++编译器(VS.Net2005)已经提供了方便的检查方案,但是这条内容仍需遵守。

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 );

函数的返回值

  • 对于底层函数,所有可能执行失败的函数必须有返回值,不能忽略可能发生的错误。错误复杂时,要有返回的错误代码或错误信息的显示。不得中途执行退出程序的命令(如PostQuitMessage()或exit()),应该由函数调用者提供退出整个程序的手段。
  • 返回值的取值,对于双值函数:成功为1,失败为0。对于有错误返回码的函数:错误代码为0或负数,成功返回值应为正数。

Inline函数

如果inline函数过多,把它们放到专门的*.inl文件中,在头文件最后包含进去。比如模版文件的定义部分就非常适合这样使用。这样可以让真正的头文件看起来更加简洁干净。

例如:
#include “test.inl”

类的成员

  • 禁止声明公有的成员变量。让成员函数控制对所有成员的操作。这样,同全局量一样,可以避免对其错误的读写。我们可以很容易使某个变量是只读的,也更容易调试。
  • 我们减少使用单个的全局量,可以使用结构和类。虽然类的静态成员不是全局量,但是在有时候会影响全局量。比如,在DLL中它的定义域是模糊不清的。
  • 注意,类本身所占内存空间的大小只与自己的成员和虚函数有关,而与实际的函数的个数无关。
  • 不要试图存取整个类。而应该把每个成员一个个的保存和读取。
  • 使用new和delete来申请类的实例。使用malloc()或free()都不调用类的构造和析构函数。但是注意:如果你用new申请一个数组,不要忘了使用[]来删除整个数组。当然,一般大型的工程会要求我们用自己定义的内存分配函数来使用内存。

char* str = new char[10];
delete [] str;


四、错误处理

程序中的错误千差万别,多种多样,程序员必须正确面对这些BUG,才能做到胸有成竹,写出的程序才能完美无缺。

正常错误与非正常错误

  •  正常错误是指在程序运行过程中可能会发生的错误。比如:图片装入失败,内存分配失败,数组分配已满,磁盘写入失败,文件没有找到等。这样的错误需要我们给出明显的错误信息,并在适当时候终止程序运行,不能出现让程序出现“非法指令”错,也不能不报任何错误就退出程序的运行
  • 非正常错误是指程序不应该发生的错误,也就是程序的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/assert/ASSERT

  • 在函数中使用Assert()函数来比较一切可能出错的地方,即使这些地方不应该出错。因为这些语句在正式版中是不执行的,所以在编写和调试时写得越多越好。
  • 在模块的接口处(比如函数的输入和输出的地方),必须用断言来确定外来的参数的取值范围是否合法,传递给外部的返回值的取值范围是否合法。
  • 常用的Assert的检查包括,指针是否为空,数据是否超界,参数传进来的数值是否与预期的相符,函数的返回值是否与预期的相符,某个全局量在此处是否已经被改变,某些大循环的调试中断等等。
  • 这个语句一般只用来处理“非正常错误”,不要用它来处理“正常错误”。因为Assert()Release版时不会被运行,所以它会导致在Release版里掩盖一些错误。
  • 注意Verify()与Assert()不同。在release里,Assert()的语句是不会被运行的。所以,在Assert()里不能放程序正式运行的代码。为了避免混淆,我们不建议使用Verify()

Assert( nIndex >= 0 && nIndex < MAX_NUM );
Assert( pChild != NULL );			


调试信息OutputDebugString()/TRACE()

  • 这个函数是在调试中向调试控制台输出信息用的。大量反复调用会影响程序运行速度,但是对于检查错误很有帮助。对Release版此函数无效,不会影响速度。
  • 函数发生错误时,在返回错误代码的同时,可以用此函数输出额外的有用信息。
  • 在编写高层函数时,可以在自行处理错误时使用此函数输出错误信息。
  • 对于警告性质的代码可以用此方法输出信息。
  • 可以在特定时刻输出一些程序数据的内容,比如文件名等,方便调试。

if( error != 0 )
{
	char szError[256];
	sprintf( szError, “DDAPI CDDSurface::LoadBitmap() Error (0) : File not found <%s>\n”, filename );
	OutputDebugString( szError ); 
}

日志文件

  • 在某些难以调试跟踪的情况下(如正式版中,或显示、网络模块,主循环中等),可以以文本文件的形式向磁盘输出一些程序数据,以检测某些死机和难以查到的错误。这些文件的后缀一般为Log
  •  对于在程序的运行期查找一些难以重现的BUG,log几乎是唯一的手段。
  •  这些代码最好用宏定义封装起来,把这些调试的代码屏蔽掉时比较容易。
  • 注意,如果Log写的太多,会严重影响代码执行效率。
  • 在正式发行前,别忘了要屏蔽所有这些代码(或大部分)。

#ifdef	LOG
WriteLogFile( "easyrpg.log", "Here cannot be a room!\n" );
#endif

错误对话框

  • “处理正常”错误时,要出现错误对话框,并正常退出程序。
  • 在程序底层,或本模块与窗口操作系统无关时,不能使用错误对话框。而直接返回错误信息和错误值。
  • 错误信息必须非常详尽和有效,便于迅速查找到出错的模块,文件,函数,行号和原因等。

_CrtSetBreakAlloc()

  • 这是Crt标准库提供的一个用来检查内存泄露的函数。当一个程序出现内存泄露的时候,这个函数可以帮助我们寻找泄露的这块内存在哪里。

Try和Catch

  • Try和catch最大作用是我们在不知道错误原因的时候迅速缩小嫌疑代码的范围。虽然发生错误的地方和产生错误的地方可能并不一定是一个地方,但是至少可以帮助我们尽快的确定到底是哪里出了错。
  • 一般是配合log文件一起用,一旦发生了错误就写入log文件。
  • 这对于需要长时间运行的服务器程序和找到那些出现频率很低的BUG会有很大的用处。
  • 当然,使用Try和catch也有一定的问题,当真的发生意外,程序却继续运行的话,可能导致其它更多的莫名其妙的BUG
  • 另外要注意的是,如果使用不当,反而可能会掩盖BUG。比如,程序员会忘记随时检查Log文件,要知道这可能是经常发生的。所以,如果我们有了更加高级的检查BUG的方法,建议只作为辅助调试方法来使用。

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提供的方法,可以更简单和方便的定位到错误。因为这些技术目前来看还比较前卫,暂时不再这里讨论。有兴趣的人可以自行研究。

 

六、常见错误及解决办法

  • 指针越界。对非自己控制的内存进行非法写入。常见的原因:字符串的使用,结构和联合的使用,数组、链表、堆栈和队列的使用等。对这些操作的返回值不加判断也是出错的原因之一。
  • 编写时分号和括号的使用,把“==”写成了“=”等。这种错误往往造成编译时出现许多不相干的错误。这属于笔误了,只有初学者才会出现此类问题。
  • 内存和资源的丢失。申请了内存或Windows资源而没有释放,造成多次调用后内存和资源的急剧减少。在使用Windows资源时,使用SelectObject()函数时,要取得并保留返回的句柄,等操作完成,要把旧的句柄置回到系统中,否则资源会丢失。Windows资源包括DGI资源,文件句柄,线程等。
  • 数组下标超界。在使用数组时没有对给定下标进行范围判定,造成数组取值越界。
  • 除零错。除数是零的操作。
  • 文件操作。写入或者读取了错误的数据,导致程序执行非法。
  • 强制类型转换错误,指针被强制转换为错误的类型并使用。
  • 循环体内修改了循环控制变量而造成死机错误,下标访问越界等错误。
  • 内存不够,在分配内存时没有判定是否成功,造成调用空指针。因为有时程序没有释放多余内存,系统内存不断被占用,交换文件不够,而产生内存不够现象。这种错误一般要运行数小时才会发生,所以比较隐蔽。而对错误的处理一般也不能仅限于对分配内存代码的错误处理,而且需要找到没有释放内存的地方,消除交换文件大小不停增长的现象。

指针

  • 请小心使用指针,多数程序错误都是由于指针的不良使用所造成的。
  • 对于游戏的主要控制模块,要避免使用链表。在使用队列和堆栈时可以用数组来模拟实现。没有任何一个程序员能够保证在使用链表中正确无误,尤其当程序已经编写了一年,程序长度超过10万行,有几百上千个节点的时候。在调试中,对于链表的跟踪也是很困难的事。宁肯浪费一些内存,也必须使用数组来代替它。对于封闭性极强的子模块,静态库,动态库等可以使用链表,但要非常注意其中的内存使用,在测试时,必须经过长时间的分配和释放的测试。

  • 对指针赋给内存和删除内存必须在固定的对称函数中或在同一个函数中。确保指针内容不被无意修改。必须自己主动释放指针。
  •  指针的用法:

必须要对指针进行初始化,也就是赋予NULL值。

给指针分配内存前必须要确保指针为NULL值。

对分配成功与否要进行判定。

使用指针之前要确保指针不为NULL值。

删除指针后,必须给指针赋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;				// 删除之后要确保指针为空。

  • 在使用数组时,要有数组下标范围的判定。在Debug版中,在数组取值时必须对数组下标进行Assert()限定,防止出现下标出界现象。

assert( nIndex >= MN_TYPE_FIRST && nIndex <= MN_TYPE_LAST );
int nData = m_MyData[nIndex];

模版的使用

  • 推荐自己编写的模版数据结构。因为模版的好处在于,我们在调试的时候可以直接看到节点的数据结构,而不必进行强制类型转换。
  •  但是,对于Microsoft提供的标准模版库“stl”系列,我们的建议是谨慎使用。因为这个数据结构过于复杂,对于初学者比较难于看懂,而且很难真正找到结点的信息。

字符串拷贝越界

  • 字符串最容易出错的地方就是字符串的拷贝,比如strcpy(),strcat(),sprintf()这样的函数。往往我们给定的字符串的大小要小于计算出来的或者是原始的字符串的大小,这样就会造成越界。尤其注意,字符串最后一位必须是’0’。
  • 最基本的解决办法:我们要求必须在任何一个strcpy(),strcat(),sprintf()这样的函数后面要写这样的限定代码:

char szDest[256], szSrc[256];
……
strcpy( szDest, szSrc );
assert( strlen(szDest) < sizeof(szDest) );
注意:如果使用sizeof()函数,szDest必须是字符串数组,而不能是指针,否则sizeof(pChar)只能是4。


  • 新版的VC编译器(VS.Net2005)对此提供了一系列很好的解决方案,程序员可以把strcpy()简单地替换为strcpy_s()即可。而且对于我们使用这些不安全的函数,编译器会报警(VS.Net2003以后)我们要全面替换和修改此类函数,而且在编译时不能关闭C4995号Warning
  • 在编译选项里启用字符串池。所有工程的所有版本在“Code Generation/Enable String Pool”选项里一律修改为:“是”。所有在程序中直接定义的常量字符串都应该被认为是只读的,覆盖这些字符串应该被认为是一种错误。打开此选项的意义不仅在于可以生成更小更快的代码,更重要的是可以将字符串池所在的内存页设为只读属性,程序运行期间对这块内存的任何写操作都将导致保护错,引起程序立即报错中止,防止问题扩大。(此项功能只对VS.Net2003以后版本有效)。

强制类型转换。

  • 在未判定类型是否正确就进行强制类型转换是导致指针潜在错误的原因之一。
  • ((Chuman *)lpSolider)->Die();当程序结构发生变化时,CSoldier不再继承Chuman时,((Chuman *)lpSolider)->Die()的用法就可能导致隐含的严重错误,所以在强制类型转换前,应检查指针类型,以确保正确转换

ASSERT(lpSolider->IsHuman());
((Chuman *)lpSolider)->Die();

除法

  • 在写除法符号时,必须在除号左右各空一个格,这样便于查找除号。否则容易与注释符号混杂。
  • 在做每次除法前,必须用Assert()确定除数不为零。这样做是为了向别人说明你知道这里有一个除法。

这行代码是《烈火文明》游戏中发现的致命死机错误之一,在该游戏发行两个月后找到。原因:除数为零。
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);				// 检测第二个参数也合法
…
}

文件操作

  • 在读写文件的时候,必须对fwrite()和fread()函数判断返回值和进行错误处理
  • 在写非常重要的文件的时候(比如用户的存盘文件),要先写到一个临时文件中,然后再拷贝成指定的后缀的文件。这样,一旦文件写入错误,不会对原来的文件造成丢失和损坏。

Debug版和Release之间的区别

  • Release版的程序因为增加了编译器对代码的优化,所以执行起来的结果与Debug会有所不同。所以我们必须同时保证两个版本都运行正常。
  • 另外还有一个最主要的区别就是Release版不会对一些变量进行初始化,所以在编写代码的时候我们必须自己做好初始化

兼容性问题

  • l对系统和硬件不要进行假设,要写专门的函数来判断该硬件是否存在,或者该硬件的某个标志位是否存在。
  • 尽量只使用ANSI C和ANSI C++运行库中的函数。使用等等技术都会导致向Windows以外的平台移植困难。
  • 使用Ole2/ActicveX、Winsocket2、ADO和Unicode等等技术时要格外留意它们在早期Windows版本上的兼容性问题。(例如Windows95第一版不支持 Ole2和Winsocket2,Windows 9x不都支持Unicode)

其它

  • 程序在编译过程中(包括Debug和Release版)不允许有任何Warning出现,也不允许随意修改编译器的Warning级别,更不允许关闭编译器的报警选项。
  • 制作一些调试专用的宏,这样可以加快调试的时间。
  • 使用右移指令(>>)时,注意变量一定是无符号数。否则对于负数,不同的编译器或不同的机器会有不同的结果。
  •  有时我们的程序中会加上一些从别的例程中COPY的段落,使用前需要确认它的兼容性和容错性。
  • VC等工具包提供的例程只是告诉你一种可能的方法,却不一定就是唯一的和最好的方法。
  • 不要把显示用字符串直接写在程序之中,底层模块的错误信息和调试用信息除外。所有文字尽量写在文本文件或资源文件中,这样便于进行多语言翻译
  • 游戏中的判定和显示用字符串要分开,以防止转换成其它语言版本时增加额外工作量,减少BUG
  • 在写判断语句时,小心使用缺省的判0操作。

if( nGeneral ),写成if( nGeneral > 0 )会保险一些。
注意,如果按照前者写法,该变量是负值的话也会返回真值的。


七、一个好的程序应该怎样

简单

程序的逻辑和目的必须简单而明确,简单就是美简单也就易于阅读,程序必须可以被自己和他人阅读。大型程序往往制作时间较长,合作的人也很多。就算是自己,很可能也会对很早以前编写的代码感到生疏。由于开发人员的流动,当程序员本人不在时,也要求别人必须可以看懂甚至修改他的代码。简单也就易于修改和维护。

  • 避免重复的变量、函数和功能模块。可以合并的变量、函数和模块应尽量合并,要做到只有一个途径来实现某一个功能。
  • 引入新的类,就像引入新的概念一样,须小心对待。应确认新的类确实与旧类之间有质的不同。常犯的错误是,为某种新的方法而随意引入新类。也不要盲目使用新技术,哪怕你是在做引擎。越新的技术,其消失的机会也会越大。
  • 不要有随便给变量起名字,为变量和函数命名时一定要有鲜明易懂的意义。
  • 要充分利用你的注释,它是你与你的前辈,同事和后继者沟通的最重要的方式。 不是说好的文档就是在程序中有很多的说明文字,这些说明文字只要恰到好处,正好能够说明程序的功能就可以了。说明和程序代码必须一致,如果修改了代码就一定要修改说明文字。说明文字不仅仅是程序语句的翻译,而要表达在功能中这代码究竟代表什么逻辑意义。

nCounter++; 		
// 不好的说明文字:	整数加1
// 好的说明文字:		学生人数加1

  •  模块要尽量独立, 模块之间的耦合度要低。不要随便包含其它的头文件,或者引用全局变量。底层模块不能知道上层模块的任何内容。减少或不使用全局量,减少本模块对其它模块的依赖,这样模块更容易调试和移植,其生命力也更强。也不会因为团队中某个不负责任的程序员将整个的代码结构搞乱。
  • 模块内部要简单。尽量将模块拆分,并定义好接口。接口要简单,只给别人看他应该看到的内容。不想给别人看到的东西,自己要隐藏好。尽管这可能与模块的效率是矛盾的,但是模块简单,就更容易被阅读,也更容易被修改。这样,才更容易让新手程序员加入,而不会影响到其它模块的品质。

正确与健壮

程序必须是正确的,这当然是无庸置疑的了。但是健壮同样是必须的。在这里,健壮的意思不是指:包容错误,掩盖错误;而是恰恰相反:在遇到错误的时候,要明确的报告出来,帮助别人尽快找到错误。

一段代码应该拥有自我检测的功能,在输入合理的情况下保证其正确性。这样无论外界发生了怎样的变化,别人使用它的时候都会放心。

高效率

这包括我们写代码的效率,和代码运行的效率。

  • 不要重写已经在系统函数库中的函数,别指望你可以写得比它更好。除非你是为了减少模块的依赖性,不想包含这个库。在网络上有许多程序高手和开放的代码,我们应该多学习多借鉴,站在“巨人”的肩膀上可以让我们快速编写代码。
  • 我们对代码运行高效率的追求往往和简单易读的追求是矛盾的,这就需要我们从中进行取舍。而对于那些新手程序员,我们希望以简单易读作为第一优先,在不影响简单易读的情况下,再追求效率。

 

八、后记

虽然做到所有这一切并不是件简单的事情,但是一个好的程序能够在很大程度上提高软件的制作效率,使软件更加易于调试,修改和维护;同时减少潜在的程序错误把错误尽可能多的消灭在编写代码的时段


你可能感兴趣的:(其他)