c语言.docx
《c语言.docx》由会员分享,可在线阅读,更多相关《c语言.docx(13页珍藏版)》请在冰豆网上搜索。
![c语言.docx](https://file1.bdocx.com/fileroot1/2023-1/4/e40761cc-057d-4698-b8f7-87e5d3efbcba/e40761cc-057d-4698-b8f7-87e5d3efbcba1.gif)
c语言
错误一:
错记“<<”与“+”的优先级
因为“<<”和“>>”相当于乘除2^N,所以容易误认为它们的优先级高于加减运算,其实不然。
当把它们跟加减法一起用的时候一定要注意。
比如计算n*5:
result=n<<2+n;
这样就错了,应该用括号:
result=(n<<2)+n;
错误二:
“==”误写为“=”
这是一个比较低级却又难以发现的错误。
说它低级是因为它属于再基本不过的语法问题;而它之所以难以发现是因为它不会产生编译错误,唯有在调试过程中跟踪执行才会发现。
通常它导致的后果是某一个条件判断失效或者进一步导致死循环,比如:
if(rect.top=rect.bottom)
{
MessageBox(hwnd,"Invalidrectangle!
",NULL,MB_OK|MB_ICONERROR);
}
因为条件判断误写成了赋值,上面的判断将永远为真,除非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)
{
deletep;
p=p->next;
}
这样是很危险的。
正确的方法是:
while(p)
{
q=p->next;
deletep;
p=q;
}
错误六:
printf()/scanf()中类型不匹配
虽然WINDOWS下一般不用这两个函数了,但是与之类似的sprintf()/sscanf()和fprintf()/fscanf()还是经常使用的。
如果格式字串中说明的变量类型与后面的参数列表不一致,printf()将导致输出结果混乱,scanf()有可能导致程序执行结果不稳定,甚至导致非法操作。
初学者或许会以为类型不一致也无所谓,因为C语言可以自动进行类型转换。
这种想法是错误的。
类型转换是在编译时已知原类型和所需类型时由编译器产生代码来完成的,而格式字串对编译器来说只是一般的字串,编译器并不理解其中的含义,也就无法知道其中的类型信息;另一方面,从printf()/scanf()函数内部,虽然可以理解格式字串,但却无法知道后面变量表中的各变量的类型,对printf()/scanf()内部来说,变量表只呈现为一段连续的单元字节,唯一可知的是这段连续单元的起始地址
(待续……)
C语言基本功教程系列
(1)
看了那么多文章,感觉到大家学习游戏程序设计的热情.经常看到很多人提出关于openGLdirectX,和computergraphics的问题.但是我个人人为,游戏程序设计,最最最重要的还是CC++语言的基本功.如何编写高效率,整洁,和尽可能少的Bug的代码,是成为一个游戏程序设计员的关键.所以我开拉这个小系列,来帮C或C++语言基础不牢靠的人补补基础知识,希望能够对大家有所帮助.
至于内容嘛,我想起来什么就写什么,不一定有什么逻辑关系.毕竟我工作也很忙,只有在每个milestone完了以后才有时间干点别的.所以这里先道歉啦.
今天就讲讲最基本的循环.
inti;
for(i=0;i<100;i++)
{
//dosomething
}
也许很多人觉得这个代码是最简洁的了.其实不然,还有更快速的写法.
i=100;
do
{
//dosomething
}while(--j);
以下是visualstudio.net2003编译过的汇编代码.
================whileloop================
j=10;
00411A32mov dwordptr[j],0Ah
do
{
}while(--j);
00411A39mov eax,dwordptr[j]
00411A3Csub eax,1
00411A3Fmov dwordptr[j],eax
00411A42jne main+29h(411A39h)
================forloop================
for(i=0;i<10;i++)
00413656mov dwordptr[i],0
0041365Djmp main+58h(413668h)
0041365Fmov eax,dwordptr[i]
00413662add eax,1
00413665mov dwordptr[i],eax
00413668cmp dwordptr[i],0Ah
0041366Cjge main+60h(413670h)
{
}
0041366Ejmp main+4Fh(41365Fh)
仔细分析就会发现while循环比for循环在每次的循环中都少一条汇编语句.主要是因为while循环是从大到小的顺序循环,不需要和10进行比较就可以跳转.而且可以直接利用--j语句设置的符号标志进行条件判断.
同样是循环10次,但是少一条语句还很多关键的时候很有用哦.
以上是第一章,如果有不同意见,错误或者遗漏,请谅解哦.
这个,上边是debugversion的代码。
偷懒被人看出来,下面给出release版本经过编译器优化的代码,优化参数/02/0t:
============forloop=============
:
00401029 xoreax,eax
:
0040102b jmp00401030
......
:
00401030 .......
:
00401035 inceax
:
00401036 cmpeax,000000064
:
00401039 jl00401030
===========whileloop============
:
00401029 moveax,000000064
:
00401030 ..........
.....
:
00401035 deceax;
:
00401036 jne00401030
C语言基本功教程系列
(2)-if语句
趁周末再写一章。
今天就介绍下if语句
if语句很简单,相信大家都会,但是确有很多值得注意的。
首先来说一下codestyle的问题。
=========不好的风格===========
if((x+4-y*25)>10||y>1023||GetSomething())
{
....
}
=========好的风格============
if((x+4-y*25)>10
||y>1023
||GetSomething())
{
....
}
相信大家能看出来第2段代码的时候要比第1段代码容易读的多。
if语句虽然简单,但是涉及到CPU的branchprediction的问题。
简单的说,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
&&(float)rand()/RAND_MAX<0.3
&&(float)rand()/RAND_MAX<0.2)
{
......
}
根据C语言的规则(这点不同于Pascal),如果第一个条件(rand()/RAND_MAX<0.4)不成立,那么就不会运行第2和第3个条件,而直接跳转。
所以应该把最难成立的条件放在第一的位置上,正确的代码为:
if((float)rand()/RAND_MAX<0.2 //只有%20的可能
&&(float)rand()/RAND_MAX<0.3
&&(float)rand()/RAND_MAX<0.4)
{
......
}
由于编译器并无法计算和统计每种条件成立的可能性,只能靠大家手动的调整来提高代码的效率。
最后是if有一种技术叫做binarybranch,举个简单的例子,代码如下:
intx;
if(x==1)
{
}
elseif(x==2)
{
}
elseif(x==3)
{
}
elseif(x==4)
{
}
对付这段代码,可以用switch来解决,也可以用binarybranch,修改后的代码如下:
if(x<=2)
{
if(x==1)
{...}
else
{...}
}
else
{
if(x==3)
{...}
else
{...}
}
如果判断的情况复杂一点,编译器就没有优化的能力,需要考大家自己动手啦。
C语言基本功教程系列(3)-快速的函数调用
我又来了,今天坎坎函数调用的问题。
函数哪里都有,小的程序一两个函数,大的程序成百上千个函数。
即使在游戏的关键循环中,调用几十个函数也是很常见的。
所以函数调用代码的质量,在很大程度上影响着游戏的质量。
还是先说最基本的代码风格问题。
首先,对于函数的参数(特别是指针),如果函数内部不会修改其指针的内容,一定要用const来定义参数类型
=========不好的风格==========
voidfunction(char*ServerName)
{
//内部不允许对ServerName的内容进行修改
}
=========好的风格===========
voidfunction(constchar*ServerName)
{
//内部不允许对ServerName的内容进行修改
}
为什么这么做呢?
举个简单的例子:
在团队开发中程序员A写好了displayFunction,传了一个数据结构给displayFunction做图象显示,然后在接下来的程序中对数据进行计算。
A认为displayFunction不会对数据进行修改,所以在以后的数据运算中,没有进行一致性检测。
过了几天程序员B被派过来优化A的程序,因为不知道不能改数据,结果改了下,在displayFunction中改变了数据结构的内容,当时测试通过。
但是在产品发布的Alpha测试阶段,用realdata的时候出了问题。
我想通宵debug去差这么点个小问题,不是很值得吧。
只要稍微留点心,就可以避免了
==================分割线==================
下面谈谈函数的调用问题。
我们都知道,在调用的一个函数的时候,传给函数的参数是要压到栈里,然后才能被函数访问。
我们来看一下函数调用的汇编代码.(汇编代码是用VisualStudio.net2003编译,releaseversion。
优化参数/0t/02)
=======printf("%s%d%d%d%d",haha,m,n,p,i);======
00401000pushecx
00401001pushebx
00401002movebx,dwordptr[esp+04]
00401003pushebp
00401004movebp,dwordptr[esp+08]
00401005pushesi
00401006pushedi
00401007movedi,dwordptr[esp+10]
00401008xoresi,esi
00401009pushesi
0040100Apushedi
0040100Bpushebx
0040100Cpushebp
0040100Dpush00408040
0040100Epush004060FC
0040100Fcall00401054
我的天哪,这是多少代码,只不过为了把参数push到栈里就用了15条。
看我们看看另一段代码
===========printf("%s",haha);============
00401010push00408040
00401011push004060FC
00401012call00401054
现在我不用说大家都明白了吧。
传递给函数的参数越少越好,最好就是一个指针,指向一个structure。
这就是为什么大部分的directX的函数就是一个指针的大structure传过去。
里边的参数好几十个。
当然了voidfucntion(void)是最快的函数调用,也可以用inline来优化关键循环内的函数。
不过在每一个frame的执行代码中,有成百上千个函数,不可能所有的都inline吧。
所有能快点就快点喽。
当然了,传递structure的reference也是同样的效果,只要不把structure当参数就好。
============错误的方式===========
voidfunction(structOneStructureParameter);
============正确的方式===========
voidfunction(structOneStructure&Parameter);
or
voidfunction(structOneStructure*pParameter);
==================分割线==================
这个例子不是很好,因为降低了代码的可读性,不过做为参考。
。
。
。
很多人喜欢写代码的时候这么写:
charszName[]="Aear";
intlength;
length=strlen(szName);
if(length>0) //这行的效率不考虑
{
//dosomething
}
粗一看没什么问题,不过如果length在以后用不到的话,那么就浪费了。
因为length占用了内存,而且浪费了cpu资源。
让我们看带汇编代码(汇编代码是用VisualStudio.net2003编译,releaseversion。
优化参数/0t/02)
length=strlen(szName);
if(length>0) {...}
0040101F subeax,edx
00401021 movdwordptr[esp+4],eax//把返回值存到length中
00401025 je00401039 //判断跳转
========更快速的写法的代码========
if(strlen(szName)){...}
0040101F subeax,edx
00401021 movesi,eax //把返回值放在个临时寄存器中
00401023 je00401037
大家都知道寄存器之间进行数据操作是非常快的,而且是稳定的一个cpuclockcycle,至于00401021 movdwordptr[esp+4],eax到底要花多少个clockcycle,那只有天知道了。
因为这种从内存中读数据的指令,最少也是2个clockcycle,即使在L2cache中,也不会比movesi,eax快,而且浪费了栈空间。
==================再分割下吧,虽然不是很喜欢==================
最后说说一种类告诉的分枝判断参数传递。
在有些情况下,我们经常要传很多参数,比如pixelshader等等,这些函数根据参数的设置,进行不同的操作。
举个例子:
structParameter{
boolbDrawWater;
boolbDrawSkybox;
boolbDrawTerrain;
boolbDrawSepcialEffects;
}DrawParamter;
voidDrawEnvironment(structParameter*pPara)
{
if(pPara->bDrawWater){....};
if(pPara->bDrawSkybox){....};
if(pPara->bDrawTerrain){....};
if(pPara->bDrawSpecialEffects){....};
}
对于这样的代码,还有更快速,更节省内存的方法,那就是位操作。
conststaticUINT32DRAW_WATER_FLAG =1;
conststaticUINT32DRAW_SKYBOX_FLAG =1<<1;
conststaticUINT32DRAW_TERRAIN_FLAG =1<<2;
conststaticUINT32DRAW_SPECIALEFFECTS_FLAG=1<<3;
voidDrawEnvironment(UINT32DrawFlag)
{
//注意了,这里不需要pPara->,也就是节省了内存访问,速度至少提高了1到2个clockcycle
if(DrawFlag&DRAW_WATER_FLAG){.....};
if(DrawFlag&DRAW_SKYBOX_FLAG){.....};
//甚至还可以进行各种不同组合的判断,比如
if(DrawFlag&(DRAW_WATER_FLAG|DRAW_SKYBOX_FLAG)){....};
}
在调用的时候,代码更加简洁明了:
DrawEnvironment(DRAW_WATER_FLAG|DRAW_TERRAIN_FLAG);
C语言基本功教程系列(4)-高效无错的内存访问
大家周末好,希望一个星期的学习和工作没能把大家累垮,这样又可以在这里听Aear在这里讲废话了。
这个周末的主题就是内存访问,主要是谈谈写程序时候关于使用内存的技巧,以及一些应该注意的地方。
================分割线==================
首先说说动态内存分配。
在c语言里用的最多的是malloc和free,在c++则是newnew[]delete和delete[].这几个函数是动态内存分配的基础,最常用但也是最占用CPU资源的系统调用之一.而且在大量使用以后很容易造成内存的碎片。
如果系统内存中的碎片太多,就会在分配大块内存的时候失败或者只能在虚拟内存上分配内存,这就是为什么有些程序在运行了2,3个小时以后很容易速度不稳定和容易崩溃的原因。
另外一个重要的因素就是程序员在写程序的时候,经常会分配了内存而忘记释放。
特别是写超过10W行代码的时候往往忘记了在哪里分配了内存.所以内存的管理对于游戏的稳定性是非常重要的问题,毕竟大家都是动不动玩上10个小时不休息的主。
目前比较流行的解决方法就是在系统提供的内存分配函数上面,写自己的内存管理函数。
在C语言里重写malloc和free,对每个内存的分配和使用情况做跟踪记录。
在C++里则是重载操作符new和delete.通过提供自己的库,可以很容易检测到memoryleakage.通过在程序开始的时候从操作系统分配到一块足够大的内存,在此基础上进行内存管理,还可以有效的防止内存泄漏,并且还可以支持对象复用技术,提高游戏的速度和稳定性。
当然,你也可以使用一些memoryleakage的检测工具来检查内存使用情况(比如firefoxmemorylea