错误一:错记“<<”与“+”的优先级 因为“<<”和“>>”相当于乘除2^N,所以容易误认为它们的优先级高于加减运算,其实不然。当把它们跟加减法一起用的时候一定要注意。比如计算n*5: 这样就错了,应该用括号: 错误二:“==”误写为“=” 这是一个比较低级却又难以发现的错误。说它低级是因为它属于再基本不过的语法问题;而它之所以难以发现是因为它不会产生编译错误,唯有在调试过程中跟踪执行才会发现。通常它导致的后果是某一个条件判断失效或者进一步导致死循环,比如: if (rect.top = rect.bottom) { } 因为条件判断误写成了赋值,上面的判断将永远为真,除非rect.bottom为零。 错误三:头文件重复包含 当你的工程越来越庞大时,头文件的管理也麻烦起来。经常遇到这样的情况:在编译一个源文件时,发现因为没有包含某个头文件而导致“符号未定义”之类的错误,于是你加入了这个头文件,可是这个头文件中又包含另外一个头文件,而那个头文件原先已经加在源文件中了,结果产生了“符号重定义”错误,这样你又不得不把这个重复包含的文件去掉…… 为了避免出现“符号重定义”错误,可以采取条件编译技术。在创建头文件时,首先为这个头文件定义一个唯一的标识(假设是_SOME_SYMBOL_),然后在头文件的开头及结尾加上几行代码,像下面这样: #ifndef _SOME_SYMBOL_ #define _SOME_SYMBOL_ ....(头文件代码) #endif 第一行语句判断是否定义了符号 _SOME_SYMBOL_,如果没有,说明本次编译尚未扫描过这个头文件,于是编译正常进行,并且定义符号_SOME_SYMBOL_,以标明文件已被扫描过一次;反之,如果文件已被包含过一次,_SOME_SYMBOL_就有了定义,于是条件编译语句使编译器跳过整个文件。 如果所有的头文件都这样处理,就可以大量减少出现“符号重定义”错误的机率。 错误四:指针未初始化 对于一个熟练的程序员来说,这决对是一个不该犯的错误。不过,有些初学者确实经常被这个问题弄胡涂。比如,曾见过有人这样写: int *p; *p = 0; DOS下,这将有可能导致死机;WINDOWS下将导致一个非法操作。 切记,使用指针前一定要初始化,使它指向一个确实分配了的空间!!! 错误五:使用已释放了的指针 最常出现在释放链表时。初学者容易这样写: while (p) { } 这样是很危险的。正确的方法是: while (p) { } 错误六:printf()/scanf()中类型不匹配 虽然WINDOWS下一般不用这两个函数了,但是与之类似的sprintf()/sscanf()和fprintf()/fscanf()还是经常使用的。 如果格式字串中说明的变量类型与后面的参数列表不一致,printf()将导致输出结果混乱,scanf()有可能导致程序执行结果不稳定,甚至导致非法操作。 初学者或许会以为类型不一致也无所谓,因为C语言可以自动进行类型转换。这种想法是错误的。类型转换是在编译时已知原类型和所需类型时由编译器产生代码来完成的,而格式字串对编译器来说只是一般的字串,编译器并不理解其中的含义,也就无法知道其中的类型信息;另一方面,从printf()/scanf()函数内部,虽然可以理解格式字串,但却无法知道后面变量表中的各变量的类型,对printf()/scanf()内部来说,变量表只呈现为一段连续的单元字节,唯一可知的是这段连续单元的起始地址 (待续……) C语言基本功教程系列(1) 看了那么多文章,感觉到大家学习游戏程序设计的热情.经常看到很多人提出关于openGL directX,和computer graphics的问题. 但是我个人人为, 游戏程序设计,最最最重要的还是C C++语言的基本功. 如何编写高效率,整洁,和尽可能少的Bug的代码,是成为一个游戏程序设计员的关键. 所以我开拉这个小系列,来帮C或C++语言基础不牢靠的人补补基础知识,希望能够对大家有所帮助. 至于内容嘛,我想起来什么就写什么,不一定有什么逻辑关系.毕竟我工作也很忙,只有在每个milestone完了以后才有时间干点别的. 所以这里先道歉啦. 今天就讲讲最基本的循环. int i; for(i = 0; i < 100; i++) { } 也许很多人觉得这个代码是最简洁的了.其实不然, 还有更快速的写法. i = 100; do { // do something }while(--j); 以下是visual studio .net 2003编译过的汇编代码. ================while loop================ 00411A32 mov 00411A39 mov 00411A3C sub 00411A3F mov 00411A42 jne ================for loop================ 00413656 mov 0041365D jmp 0041365F mov 00413662 add 00413665 mov 00413668 cmp 0041366C jge 0041366E jmp 仔细分析就会发现while循环比for循环在每次的循环中都少一条汇编语句. 主要是因为while循环是从大到小的顺序循环,不需要和10进行比较就可以跳转.而且可以直接利用--j语句设置的符号标志进行条件判断. 同样是循环10次,但是少一条语句还很多关键的时候很有用哦. 以上是第一章,如果有不同意见,错误或者遗漏,请谅解哦. 这个,上边是debug version的代码。偷懒被人看出来,下面给出release版本经过编译器优化的代码,优化参数 /02 /0t: ============for loop============= :00401029 :0040102b ...... :00401030 :00401035 :00401036 :00401039 ===========while loop============ :00401029 :00401030 ..... :00401035 :00401036 C语言基本功教程系列(2) - if 语句 趁周末再写一章。今天就介绍下if语句 if语句很简单,相信大家都会,但是确有很多值得注意的。 首先来说一下code style的问题。 =========不好的风格=========== if( (x +4-y * 25) > 10 || y > 1023 || GetSomething()) { } =========好的风格============ if( (x +4-y * 25) > 10 { } 相信大家能看出来第2段代码的时候要比第1段代码容易读的多。 if 语句虽然简单,但是涉及到CPU的branch prediction的问题。简单的说, CPU有个指令缓存,会预先把一部分代码读到缓存中等待稍后执行。当CPU遇到 if语句的时候,会把条件判断为true的那段代码读到缓存中,然后对if(条件判断)中的条件判断语句进行运算。如果运算结果是 false,那么CPU就会重新从内存中载入false的代码,在这期间大部分CPU时间会被浪费点。 所以在写if语句的时候,一定要把最容易成立的条件放在最前面进行判断。 比如: ======错误的写法======= if( (float)rand() / RAND_MAX < 0.2 ) //只有20%的可能运行if部分 { } ======正确的写法======= if( (float)rand() / RAND_MAX > 0.2 ) //有80%的可能运行if部分。 { } if语句另外一个需要注意的地方是在进行多重条件判断的时候,要安排好顺序。比如: if ( (float)rand() / RAND_MAX < 0.4 { } 根据C语言的规则(这点不同于Pascal),如果第一个条件(rand() / RAND_MAX < 0.4)不成立,那么就不会运行第2和第3个条件,而直接跳转。 所以应该把最难成立的条件放在第一的位置上,正确的代码为: if ( (float)rand() / RAND_MAX < 0.2 { } 由于编译器并无法计算和统计每种条件成立的可能性,只能靠大家手动的调整来提高代码的效率。 最后是if有一种技术叫做binary branch,举个简单的例子,代码如下: int x; if( x == 1) { } else if( x == 2) { } else if( x == 3) { } else if( x == 4) { } 对付这段代码,可以用switch来解决,也可以用binary branch,修改后的代码如下: if( x <= 2) { } else { } 如果判断的情况复杂一点,编译器就没有优化的能力,需要考大家自己动手啦。 C语言基本功教程系列(3) - 快速的函数调用 我又来了,今天坎坎函数调用的问题。函数哪里都有,小的程序一两个函数,大的程序成百上千个函数。即使在游戏的关键循环中,调用几十个函数也是很常见的。所以函数调用代码的质量,在很大程度上影响着游戏的质量。 还是先说最基本的代码风格问题。首先,对于函数的参数(特别是指针),如果函数内部不会修改其指针的内容,一定要用const来定义参数类型 =========不好的风格========== void function(char * ServerName) { } =========好的风格=========== void function(const char * ServerName) { } 为什么这么做呢? 举个简单的例子: 在团队开发中程序员A写好了displayFunction,传了一个数据结构给displayFunction做图象显示,然后在接下来的程序中对数据进行计算。A认为displayFunction不会对数据进行修改,所以在以后的数据运算中,没有进行一致性检测。过了几天程序员B被派过来优化A的程序,因为不知道不能改数据,结果改了下,在displayFunction中改变了数据结构的内容,当时测试通过。但是在产品发布的Alpha测试阶段,用real data的时候出了问题。我想通宵debug去差这么点个小问题,不是很值得吧。只要稍微留点心,就可以避免了 ==================分割线================== 下面谈谈函数的调用问题。我们都知道,在调用的一个函数的时候,传给函数的参数是要压到栈里,然后才能被函数访问。我们来看一下函数调用的汇编代码.(汇编代码是用Visual Studio .net 2003 编译, release version。优化参数 /0t /02) =======printf("%s%d%d%d%d",haha,m,n,p,i);====== 00401000 push ecx 00401001 push ebx 00401002 mov ebx, dword ptr [esp+04] 00401003 push ebp 00401004 mov ebp, dword ptr [esp+08] 00401005 push esi 00401006 push edi 00401007 mov edi, dword ptr [esp+10] 00401008 xor esi, esi 00401009 push esi 0040100A push edi 0040100B push ebx 0040100C push ebp 0040100D push 00408040 0040100E push 004060FC 0040100F call 00401054 我的天哪,这是多少代码,只不过为了把参数push到栈里就用了15条。看我们看看另一段代码 ===========printf("%s",haha);============ 00401010 push 00408040 00401011 push 004060FC 00401012 call 00401054 现在我不用说大家都明白了吧。传递给函数的参数越少越好,最好就是一个指针,指向一个structure。这就是为什么大部分的directX的函数就是一个指针的大structure传过去。里边的参数好几十个。当然了 void fucntion(void)是最快的函数调用,也可以用inline来优化关键循环内的函数。不过在每一个frame的执行代码中,有成百上千个函数,不可能所有的都inline吧。所有能快点就快点喽。当然了,传递 structure的reference也是同样的效果,只要不把structure当参数就好。 ============错误的方式=========== void function(struct OneStructure Parameter); ============正确的方式=========== void function(struct OneStructure & Parameter); or void function(struct OneStructure * pParameter); ==================分割线================== 这个例子不是很好,因为降低了代码的可读性,不过做为参考。。。。 很多人喜欢写代码的时候这么写: char szName[] = "Aear"; int length; length = strlen(szName); if(length > 0) { } 粗一看没什么问题,不过如果length在以后用不到的话,那么就浪费了。因为length占用了内存,而且浪费了cpu资源。让我们看带汇编代码(汇编代码是用Visual Studio .net 2003 编译, release version。优化参数 /0t /02) length = strlen(szName); if(length > 0) 0040101F 00401021 00401025 ========更快速的写法的代码======== if(strlen(szName)) {...} 0040101F 00401021 00401023 大家都知道寄存器之间进行数据操作是非常快的,而且是稳定的一个cpu clock cycle,至于 00401021 ==================再分割下吧,虽然不是很喜欢================== 最后说说一种类告诉的分枝判断参数传递。在有些情况下,我们经常要传很多参数,比如pixel shader等等,这些函数根据参数的设置,进行不同的操作。举个例子: struct Parameter{ } DrawParamter; void DrawEnvironment( struct Parameter * pPara) { } 对于这样的代码,还有更快速, 更节省内存的方法,那就是位操作。 const static UINT32 DRAW_WATER_FLAG const static UINT32 DRAW_SKYBOX_FLAG const static UINT32 DRAW_TERRAIN_FLAG const static UINT32 DRAW_SPECIALEFFECTS_FLAG = 1 << 3; void DrawEnvironment(UINT32 DrawFlag) { } 在调用的时候,代码更加简洁明了: DrawEnvironment( DRAW_WATER_FLAG | DRAW_TERRAIN_FLAG ); C语言基本功教程系列(4) - 高效无错的内存访问 大家周末好,希望一个星期的学习和工作没能把大家累垮,这样又可以在这里听Aear在这里讲废话了。这个周末的主题就是内存访问,主要是谈谈写程序时候关于使用内存的技巧,以及一些应该注意的地方。 ================分割线================== 首先说说动态内存分配。在c语言里用的最多的是malloc和free,在c++则是new new[] delete 和delete[]. 这几个函数是动态内存分配的基础,最常用但也是最占用CPU资源的系统调用之一.而且在大量使用以后很容易造成内存的碎片。如果系统内存中的碎片太多,就会在分配大块内存的时候失败或者只能在虚拟内存上分配内存,这就是为什么有些程序在运行了2,3个小时以后很容易速度不稳定和容易崩溃的原因。另外一个重要的因素就是程序员在写程序的时候,经常会分配了内存而忘记释放。特别是写超过 10W行代码的时候往往忘记了在哪里分配了内存. 所以内存的管理对于游戏的稳定性是非常重要的问题,毕竟大家都是动不动玩上10个小时不休息的主。 目前比较流行的解决方法就是在系统提供的内存分配函数上面,写自己的内存管理函数。在C语言里重写malloc和free,对每个内存的分配和使用情况做跟踪记录。在C++里则是重载操作符 new和delete. 通过提供自己的库,可以很容易检测到memory leakage. 通过在程序开始的时候从操作系统分配到一块足够大的内存,在此基础上进行内存管理,还可以有效的防止内存泄漏,并且还可以支持对象复用技术,提高游戏的速度和稳定性。当然,你也可以使用一些memory leakage的检测工具来检查内存使用情况(比如 firefox memory leakage detection tool 或者 Visual leak detector)。 实际上,在游戏程序设计中,很少使用动态的内存分配,大部分的内存都是事先分配好的。即使是链表或者是树这一类的数据结构,也是用数组进行有效的模拟。 ================分割线================== 下面说点代码里边应该注意的问题。在相关内存相关的注意事项中,排在第一位的是内存对齐问题。也就是说,一块内存的首地址,必须要能被2,4,8,16,32 或者64整除。 不同的CPU对于这个数字有不同的需要。 针对Intel最新发布的 Pentium Dual Core系列 Xenon系列,以及早些日子的 Pentium 4系列。推荐使用64 Bytes 或者 128 Bytes的内存对齐。 因为在Pentium4 系列用,每当程序要进行内存访问的时候,CPU的一个预处理模块(Prefetch)会事先把内存中的数据读到Level1 cache中,并且每次读入的数据量是 64个 bytes(Pentium Xenon系列是 128 bytes)。如果没有进行内存对齐, 比如一个int占用4字节,第一个字节在前64bytes中,后3个字节在后64bytes中,那么 CPU在读取这个int的时候就需要多从内存中拿一次数据, 会大大增加代码的运行时间。让我们看下例子: __declspec(align(64)) int test[128]; int * pInt = (int *)((char *)test + 1); int * pInt2 = test; int f1(void) { int i, k=0; for(i = 0; i < 16; i++) k+=pInt[i]; return k; } int f2(void) { int i, k=0; for(i = 0; i < 16; i++) k+=pInt2[i]; return k; } 对照附件中的 VTune的测试结果(见附件1),我们可以看出非64bytes对齐的运行时间(clockticks值),几乎是对齐内存的运行时间的3 倍。所以在使用动态或者静态内存的时候,最好注意内存的字对齐问题。在Visual Studio .Net中,可以用 __declspec (align(64))对静态变量,数组或者结构进行内存对齐。动态内存分配可以使用_aligned_malloc() 和 _aligned_free(). 这些内存对齐的问题,当前的编译器一般都会帮你优化,但是如果要写自己的内存管理函数,就需要分外注意了。 ================分割线================== 下面说一下结构数组问题。经常我们会用到结构数组,形式如下: struct MyStructure{ } StructureArray[100]; 这种类型的数据结构,还有另外一种组织的方式,那就是数组结构,形式如下: struct MyStructure{ } ArrayStructure; 至于这两种形式用哪种好,要根据具体情况来判断。一般来说,如果要对所有结构中的同一个成员进行连续的访问,比如要求100个结构中所有FirstNumber的和,使用第2种形式会快很多。如果要分别求出每个结构所有成员的和,第一种形式要快很多。 ===========求所有结构第一个成员的和========== // 错误的选择 for(i = 0; i < 100; i++) Sum += StructureArray[i].FirstNumber; // 正确的选择 for(i = 0; i < 100; i++) Sum += ArrayStructure.FirstNumber[i]; ============求每个结构所有成员的和=========== // 错误的选择 for(i = 0; i < 100; i++) // 正确的选择 for(i = 0; i < 100; i++) 我想道理不用多说大家也明白了吧, 具体到程序设计中要根据哪种操作用的多来决定数据的组织方式。 关于内存访问,还有很多很多需要注意的事项,比如aliasing问题,store forward问题等等,建议大家去参考intel关于pentium的文档. C语言基本功教程系列(5) - 文件, Socket 和 其它 这个系列的最后一章了,基本上这篇里边拿不准的,或者记得不太清楚的,以及很确定的结论,都在网上或者书里找到了依据。如果大家不同意文章里的论点,拿砖头拍我或者拍原作者都可以。 今天的主题是文件访问,Socket,和其他一些值得注意的内容。文件和Socket比较类似,都是在说IO访问,不过在操作系统级别上的实现有很大不同。 IO访问无论从什么角度讲,都是计算机系统里最慢的操作。特别是在游戏制作中,动不动就几百兆的动态或者静态数据,贴图纹理,和各种音响音乐等。要一次性把这些所有的数据读到内存中是不大可能的,所以在游戏进行过程中要不短的从硬盘或者光驱里读文件。如何能够最小化这个瓶颈, 是值得注意的问题。先从文件说起吧。 ===================文件================== C语言里,正常的文件操作一般是3步。 fp = fopen(XXXX,XXX); 读写操作.... fclose(fp); 首先要注意的是,fopen里边要用binary mode打开文件,不要用ASCII mode. 很多人在处理文本类型的文件时候,喜欢用 ASCII mode,然后用fgets一行一行的读。实际上ASCII mode无论如何操作,都是非常慢的,而且fgets函数更加的慢 [1]。所以即使是文本文件,也要用binary mode打开,一次读入一大块近来,慢慢处理。 其次需要注意的是,每一次读取文件的时候,硬盘都会对磁头进行重新定位和寻址。 这点会根据操作系统的不同而不同,总的来说windows XP要比 windows2000好点,但是也只是在系统文件方面 [2]。因此每次读取的内容越多,平均效率就越高。同时操作系统提供磁盘缓存,当你写如磁盘的时候,只要不用fflush和fclose,数据在短时间内还是在内存中的,如果这时候再读出来写如的内容,也非常快。读写文件的时候不要用C++的流。写文件一次最好写不小于4k的数据, 而且最好对文件结构有效的安排,进行连续的访问(不要频繁使用fseek)[3]. 这些都有助于提高文件的访问速度。 最后,如上文中提到的,每次使用fclose和 fflush的时候,都会强迫文件从缓存中写如到磁盘里。这个过程极其缓慢而且耗费时间,所以不在必要的时候,不要使用fclose和fflush. 如果一个文件读写完毕,而你又不确定是否短时间内会用到它,那就不要用fclose.你可以专门写一个类,管理这写文件的指针。对于经常会进行操作的文件,比如大地图的texture文件等,fopen一次就ok了,直到游戏结束再fclose ===================SOCKET================== SOCKET虽然也是IO访问,要比文件快多了。但是在recv的时候,还是一次读的越多越好。这样效率更高。下面说一些SOCKET编程的技巧。 1. 使用异步socket (asynchronous IO). 在网络程序设计中,又2种处理方式,第一种是对每一个连接请求,都使用一个线程或者进程,第2种是使用一个线程同时使用异步IO. 第一种方式虽然程序设计上简单,但是创立进程的时候一般会有一些时间用在建立context上,进程间的转换和 mutex等也需要浪费很多CPU资源,总体来说不如异步 IO 有效率 [4]。 2. 如果必须要使用多线程,可以考虑事先就创建好该线程,然后在需要的时候,把socket发过去就行了[4]。 3. 在处理 UDP协议的时候,需要注意的是,UDP和TCP不一样。UDP没有control flow,如果接收端的buffer满掉了,再来的UDP包都会被drop掉。所以在处理UDP协议的时候,一般需要专门一个线程读UDP包,防止过多的数据包丢失。 4. 最后,网络数据包多大合适? 这个很难说。对于UDP来说,小包是不划算的. 我们通常用的Ethernet(也就是LAN),在第2层 Data link layer,最大的frame size 是1500 bytes,刨除最20 Bytes(最少)的IP头,8 bytes的 UDP头,所有最大的UDP包可以包含 1472个bytes。要是考虑IP包有可能会有附加头信息,一般1400就比较合适。但是如果有些老版本 router不地道,对你的UDP包分片的话,就比较惨了。能保证不分片的UDP包大小是513个byte左右 [5],不过毕竟现在这种老的 Router很少了,1400字节大小的UDP包还是比较安全的。对于TCP来说,因为是stream protocol,不用考虑包的大小。但是TCP 有个缺点,就是如果你一次发送很多很多数据,那么TCP的速度会一会快,一会慢(见[4]中的关于video streaming的介绍)。所以,需要程序调节,匀速发送数据。 ====================其他=================== 其他一些程序设计上的东西,很多可以参见 [6] 1. 能用UINT的地方就用UINT,因为很多是UINT最快,而且UINT的除法要比int快。 2. 尽量避免类型转换,如果最后要转成float,开始的时候就用float比较好。 3. 不要用double 4. 能用乘法就不要用除法。 比如 5. struct的大小尽量是2的倍数,如果不是就调整下,加pad。因为可以在level1 cache里放整数个。 6. 全局变量少用,如果要用,加上static 7. 局部变量也是越少越少。这样register的效率更高 8. 能用switch的地方,就不要用if,因为switch是直接生成跳转表,速度快很多 9. 互相关联的代码之间不要空行,功能不同的代码之间最好空上1行区分开 10. 用const static 代替 #define 定义常量 11. 统一你的代码风格,始终使用同样的命名规则 |
C语言常见错误
最新推荐文章于 2021-06-18 23:05:43 发布