| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 5594 人关注过本帖, 3 人收藏
标题:一次过修理C语言
只看楼主 加入收藏
do8do8do8
Rank: 10Rank: 10Rank: 10
来 自:沙滩
等 级:贵宾
威 望:17
帖 子:366
专家分:1845
注 册:2010-7-2
结帖率:57.14%
收藏(3)
已结贴  问题点数:100 回复次数:53 
一次过修理C语言
  音乐不完美,因为有“再牛逼的萧邦也谈不出老子的悲伤”
  语言也不完美,因为有“再优美的词语也不能让你对我笑一次”

  所以 从新手的角度 向大家求C语言的注意事项,越多越好。
  范围是c语言的基础: 数据类型 运算符  指针 文件 等等。
  心得体会,bug 最好。
  分只给一个人  只给最好的贴子。
  
  鬼精灵喜欢金子,我喜欢帖子,请留下你的言语,让世人记得你。

  (每个帖子 别那么长(不超过编辑框为宜) 分开来,便于阅读,简约而不简单)

[ 本帖最后由 do8do8do8 于 2010-7-30 09:14 编辑 ]
搜索更多相关主题的帖子: C语言 
2010-07-28 12:36
laazyt
Rank: 2
等 级:论坛游民
帖 子:25
专家分:19
注 册:2010-7-17
收藏
得分:5 
额,说个比较尴尬的,当年p的大小写各定义了个变量,结果愣是没查出错误来,悲剧了。
2010-07-28 12:46
do8do8do8
Rank: 10Rank: 10Rank: 10
来 自:沙滩
等 级:贵宾
威 望:17
帖 子:366
专家分:1845
注 册:2010-7-2
收藏
得分:0 

scanf()注意事项:
scanf读取的内容 跟%后面的格式有关,列如:scanf("%d",&str); d表示一个整型数字,如果你输入字母,scanf()认为这个是非法值。
从而不读取,放到缓冲区去,当再次调用scanf()时,此时程序没结束,那么scanf()就会读取刚才放到缓冲区里面的非法值。
如果是要循环调用scanf()的话,这个就要小心了,因为它会自动读取值,不会让用户介入输入。
所以一般在scanf()后面
清除缓冲区fflush(stdin);

但是这个对移植性不好,所以最好能自己编程 清空缓冲区
这样在scanf()后面添加如下代码 一个循环
while ( (c = getchar()) != '\n' && c != EOF ) ;
这样既可清空缓冲区

学C语言从底层开始,学编程从问题开始,一日学会C!!!
2010-07-28 12:58
playmyself
Rank: 5Rank: 5
来 自:第3系4级宇宙空间
等 级:职业侠客
帖 子:76
专家分:399
注 册:2009-7-8
收藏
得分:5 
多花点时间弄代码缩进和注释。

无聊创造奇迹。
2010-07-28 16:26
sunyh1999
Rank: 16Rank: 16Rank: 16Rank: 16
等 级:版主
威 望:14
帖 子:1178
专家分:3032
注 册:2009-5-17
收藏
得分:5 
本人并非语言天才,但我还是想说一下自己学习C语言的心得体会,发表我的愚见:

   所谓万事开头难:C语言的IT的入门语言,所以我们应当而且必须把它学好,这就是学好c语言的动力。

   我觉得可以把c语言分成2部分,第一部分是for循环以及前面部分,第二部分就是for循环后面的了。为什么这么分呢?因为for循环之前的总体来说比较容易。

   首先来说怎么学习第一部分:

   第一:做好预习。和我们以前念书一样,你可以先自己看着去学,把一些基本的概念先记一记,在大脑中形成初步的记忆。

   第二:利用大脑。书看好了,就要开始动动脑子了。看一下书后的习题,然后想想书本上的结构,去琢磨一下。

   第三:上机操作。当你的头脑中已经形成基本的结构之后,你要去实现一下,看有没有不对的。如果有错就继续看书,直到把那个题做好。

   第四:做好复习。因为,前面学的是后面的基础,所以在学习后一张之前要把前面的理解的通通透透。像造房子一样,基础越牢造的越高。

   另外:有任何不会的问题,要立即解决,不能让千里之堤,溃于蚁穴。一个问题是一块砖,你看到了可以随手把它丢到。如果不丢,等他堆成了墙,那就有点晚了(长城也不过就是堆起来)。还有一点就是要勤奋,所谓眼看千遍不如手动一遍,所以操作使很重要的。

   你也可以把学的东西和以前学的联系起来,例如:逻辑(&&,||,!)就像数学中命题一样。

   其次是学第二部分:

  因为学习第二部分大多是要运用到循环的,所以要把以前的搞好。

  其实,主要步骤与学习前一部分一样,不同的就是要加倍练机加倍的去思考。且,在学习后面部分要有点耐心,不要稍微有点不理想就觉得自己不适合,说白了大家的基础都一样,不同是怎么去看待它。

   此上就是本人的见地,虽然少,但是浓缩的都是精华,并且使用性比较高。

欢迎来到我的博客:http://blog..cn/noisunyuhong
2010-07-28 16:32
BlueGuy
Rank: 16Rank: 16Rank: 16Rank: 16
等 级:版主
威 望:29
帖 子:4476
专家分:4055
注 册:2009-4-18
收藏
得分:5 
变量初始化:
除了 循环计数器 i,j,k 不用声明的时候就初始化,因为已经习以为常了。
其他的变量声明的时候都应该初始化
int count = 0;
char *s = NULL;
char str[MAX] = {0};
memset(&structX,0,sizeof(structX)) ;
这样做可以 避免很多 逻辑错误。
比如说,引用了初化始为 NULL 的指针,程序会因为非法仿问直接崩了,程序直接崩了是一个好事,错误很容易查
出来。否则的话,会出现一些 莫名其妙的、随机的内存错误,是很难跟踪出来的。

一:数据类型:
1>float
由于浮点数的 精度问题,浮点数一般是不能精确表示的。所以,
所以,当判断两个 浮点数是否相等的时候, 应该使用类似下面的
代码
if (fabs(a - b) < 0.000001)
    // do something

二:运算符:
(1) "/"
最常见的两个错误
1>
main()
{
   float blue;
   blue = 1/2;
   printf("%f",blue);
}
最后输出的答案是 0, 而不是 0.5。 因为两个整数相除,结果为整数。
2>
main()
{
   float blue;
   blue = 1/0;
   printf("%f",blue);
}
被0除,会直接导致程序崩溃
所以函数里涉及到 "/"操作符的时候,应该先断言下除数是否 为0

(2) "<<"
这个运算符有个细微的好处,就是提高运算效率
例如,i*10、i*100 之类乘法运算, 可以写成 i*(2<<3)+i*(2<<1)


三:c库函数:
1> strlen()
不要在循环条件测试中 使用 strlen(), 这是一个很不明智的作法。
每一次条件测试都会遍历 整个字符串 直到 遇到 NULL; 下面的就是
一个 bad code
for (i = 0; i < strlen(str); i++)
{
     str[i] = tolower( (unsigned char)str[i] );
}
一个好的做法就是 把 strlen() 拖到循环外面来
int len = strlen(str);
for (i = 0; i < len; i++)
{
     str[i] = tolower( (unsigned char)str[i] );
}

2>rand()
这个函数使用前一定要记得初始化随机种子, 不然你得出的数就不是随机数。
如果你想要一个随机数,结果得到的却不是随机数,就属于一种逻辑错误。


[ 本帖最后由 BlueGuy 于 2010-7-29 12:41 编辑 ]

我就是真命天子,顺我者生,逆我者死!
2010-07-28 18:22
sunyh1999
Rank: 16Rank: 16Rank: 16Rank: 16
等 级:版主
威 望:14
帖 子:1178
专家分:3032
注 册:2009-5-17
收藏
得分:0 
一.先说C语言运算符方面的:
1.在"=="比较运算中,可能会把"=="写成"=",例如while(i==1)写成while(i=1)这将导致无限循环.

2.自增自减运算,注意前缀++与后缀++的区别,a++与++a的区别就在于前者的值是a原先的值,而后者是a自增1后的值.在具体的使用应更加注意.

3.注意逻辑运算符||与&&的短路效应.

4./与\的区别.



二.说下输入输出函数的:
1.scanf中,可能变量前面忘加&,例如int a; scanf("%d",a);的形式.

2.多个gechar函数出现在主函数中,导致有字符残留现象,会出现把回车符赋给字符变量的情况.

3.也是scanf函数里面的,出现int a,b; scanf("%d,%d",a,b);的情况,如果输入的是12空格45回车,则会导致a,b无法接收数据.正确应该输入
12,45回车.


三.说下程序设计初步里面重要函数的问题:
1.if,while,for等语句的主体语句必须是一个语句,即要有多个语句的话必须要用大括号括起来.

2.switch语句的子case语句后面忘加break的情形.

3.注意出现死循环,无限循环的情况.


四.还有就是编写要规范,常量标识符最好用大写,比如#define PI 3.14159   ,以和变量名区分开来.


还有很多,但问题总是你自己的,我们的问题不一定是你的问题,所以你自己要多多摸索.多多注意,养成良好的编程风格,多看书上的书写,希望能帮助到你.

欢迎来到我的博客:http://blog..cn/noisunyuhong
2010-07-29 09:54
sunyh1999
Rank: 16Rank: 16Rank: 16Rank: 16
等 级:版主
威 望:14
帖 子:1178
专家分:3032
注 册:2009-5-17
收藏
得分:0 
错误一:错记“<<”与“+”的优先级
因为“<<”和“>>”相当于乘除2^N,所以容易误认为它们的优先级高于加减运算,其实不然。当把它们跟加减法一起用的时候一定要注意。比如计算n*5:
    result = n << 2 + n;
这样就错了,应该用括号:
    result = (n << 2) + n;


错误二:头文件重复包含
当你的工程越来越庞大时,头文件的管理也麻烦起来。经常遇到这样的情况:在编译一个源文件时,发现因为没有包含某个头文件而导致“符号未定义”之类的错误,于是你加入了这个头文件,可是这个头文件中又包含另外一个头文件,而那个头文件原先已经加在源文件中了,结果产生了“符号重定义”错误,这样你又不得不把这个重复包含的文件去掉……
为了避免出现“符号重定义”错误,可以采取条件编译技术。在创建头文件时,首先为这个头文件定义一个唯一的标识(假设是_SOME_SYMBOL_),然后在头文件的开头及结尾加上几行代码,像下面这样:

#ifndef _SOME_SYMBOL_
#define _SOME_SYMBOL_
....(头文件代码)
#endif
第一行语句判断是否定义了符号 _SOME_SYMBOL_,如果没有,说明本次编译尚未扫描过这个头文件,于是编译正常进行,并且定义符号_SOME_SYMBOL_,以标明文件已被扫描过一次;反之,如果文件已被包含过一次,_SOME_SYMBOL_就有了定义,于是条件编译语句使编译器跳过整个文件。
如果所有的头文件都这样处理,就可以大量减少出现“符号重定义”错误的机率。

错误四:指针未初始化
对于一个熟练的程序员来说,这决对是一个不该犯的错误。不过,有些初学者确实经常被这个问题弄胡涂。比如,曾见过有人这样写:
int *p;
*p = 0;

DOS下,这将有可能导致死机;WINDOWS下将导致一个非法操作。
切记,使用指针前一定要初始化,使它指向一个确实分配了的空间!!!

错误五:使用已释放了的指针
最常出现在释放链表时。初学者容易这样写:
while (p)
{
    delete p;
    p = p->next;
}

这样是很危险的。正确的方法是:

while (p)
{
    q = p->next;
    delete p;
    p = q;
}

错误六: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++)
{
    // do something
}

也许很多人觉得这个代码是最简洁的了.其实不然, 还有更快速的写法.

i = 100;
do
{
// do something
}while(--j);

以下是visual studio .net 2003编译过的汇编代码.

================while loop================
    j = 10;
00411A32 mov         dword ptr [j],0Ah
    do
    {
      
    }while(--j);
00411A39 mov         eax,dword ptr [j]
00411A3C sub         eax,1
00411A3F mov         dword ptr [j],eax
00411A42 jne         main+29h (411A39h)

================for loop================

    for(i = 0; i < 10; i++)
00413656 mov         dword ptr [i],0
0041365D jmp         main+58h (413668h)
0041365F mov         eax,dword ptr [i]
00413662 add         eax,1
00413665 mov         dword ptr [i],eax
00413668 cmp         dword ptr [i],0Ah
0041366C jge         main+60h (413670h)
    {

    }
0041366E jmp         main+4Fh (41365Fh)

仔细分析就会发现while循环比for循环在每次的循环中都少一条汇编语句. 主要是因为while循环是从大到小的顺序循环,不需要和10进行比较就可以跳转.而且可以直接利用--j语句设置的符号标志进行条件判断.

同样是循环10次,但是少一条语句还很多关键的时候很有用哦.

以上是第一章,如果有不同意见,错误或者遗漏,请谅解哦.

这个,上边是debug version的代码。偷懒被人看出来,下面给出release版本经过编译器优化的代码,优化参数 /02 /0t:
============for loop=============
:00401029          xor eax, eax
:0040102b          jmp 00401030
......
:00401030          .......
:00401035          inc eax
:00401036          cmp eax, 000000064
:00401039          jl 00401030
===========while loop============
:00401029          mov eax, 000000064
:00401030          ..........
.....
:00401035          dec eax;
:00401036          jne 00401030


C语言基本功教程系列(2) - if 语句

趁周末再写一章。今天就介绍下if语句

if语句很简单,相信大家都会,但是确有很多值得注意的。 首先来说一下code style的问题。

=========不好的风格===========
if( (x +4-y * 25) > 10 || y > 1023 || GetSomething())
{
   ....
}

=========好的风格============
if( (x +4-y * 25) > 10
    || y > 1023
    || GetSomething() )
{
   ....
}
相信大家能看出来第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
      && (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有一种技术叫做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)
{
     if( x == 1)
     {...}
     else
     {...}
}
else
{
     if( x == 3)
     {...}
     else
     {...}
}
如果判断的情况复杂一点,编译器就没有优化的能力,需要考大家自己动手啦。


C语言基本功教程系列(3) - 快速的函数调用

我又来了,今天坎坎函数调用的问题。函数哪里都有,小的程序一两个函数,大的程序成百上千个函数。即使在游戏的关键循环中,调用几十个函数也是很常见的。所以函数调用代码的质量,在很大程度上影响着游戏的质量。

还是先说最基本的代码风格问题。首先,对于函数的参数(特别是指针),如果函数内部不会修改其指针的内容,一定要用const来定义参数类型
=========不好的风格==========
void function(char * ServerName)
{
   // 内部不允许对ServerName的内容进行修改
}

=========好的风格===========
void function(const char * ServerName)
{
   // 内部不允许对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)   // 这行的效率不考虑
{
   // do something
}
粗一看没什么问题,不过如果length在以后用不到的话,那么就浪费了。因为length占用了内存,而且浪费了cpu资源。让我们看带汇编代码(汇编代码是用Visual Studio .net 2003 编译, release version。优化参数 /0t /02)

length = strlen(szName);
if(length > 0)   {...}

0040101F       sub eax, edx
00401021       mov dword ptr [esp+4], eax // 把返回值存到length中
00401025       je 00401039                       // 判断跳转

========更快速的写法的代码========
if(strlen(szName)) {...}

0040101F       sub eax, edx
00401021       mov esi, eax   //把返回值放在个临时寄存器中
00401023       je 00401037

大家都知道寄存器之间进行数据操作是非常快的,而且是稳定的一个cpu clock cycle,至于 00401021       mov dword ptr [esp+4], eax 到底要花多少个clock cycle,那只有天知道了。因为这种从内存中读数据的指令,最少也是2个clock cycle,即使在L2 cache中,也不会比 mov esi, eax 快,而且浪费了栈空间。


==================再分割下吧,虽然不是很喜欢==================
最后说说一种类告诉的分枝判断参数传递。在有些情况下,我们经常要传很多参数,比如pixel shader等等,这些函数根据参数的设置,进行不同的操作。举个例子:

struct Parameter{
     bool bDrawWater;
     bool bDrawSkybox;
     bool bDrawTerrain;
     bool bDrawSepcialEffects;
} DrawParamter;

void DrawEnvironment( struct Parameter * pPara)
{
     if(pPara->bDrawWater) {....};
     if(pPara->bDrawSkybox) {....};
     if(pPara->bDrawTerrain) {....};
     if(pPara->bDrawSpecialEffects) {....};
}

对于这样的代码,还有更快速, 更节省内存的方法,那就是位操作。
const static UINT32 DRAW_WATER_FLAG               = 1;
const static UINT32 DRAW_SKYBOX_FLAG              = 1 << 1;
const static UINT32 DRAW_TERRAIN_FLAG             = 1 << 2;
const static UINT32 DRAW_SPECIALEFFECTS_FLAG = 1 << 3;

void DrawEnvironment(UINT32 DrawFlag)
{
    //注意了,这里不需要 pPara->,也就是节省了内存访问,速度至少提高了1到2个clock cycle
     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) - 高效无错的内存访问


================分割线==================

首先说说动态内存分配。在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];       // 64字节对齐

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{
    int FirstNumber;
    int SecondNumber;
    int ThiredNumber;
    int FourthNumber;
} StructureArray[100];

这种类型的数据结构,还有另外一种组织的方式,那就是数组结构,形式如下:

struct MyStructure{
    int FirstNumber[100];
    int SecondNumber[100];
    int ThridNumber[100];
    int FourthNumber[100];
} 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++)
    Sum =    ArrayStructure.FirstNumber[i]
              + ArrayStructure.SecondNumber[i]
              + ArrayStructure.ThirdNumber[i]
              + ArrayStructure.FourthNumber[i];

// 正确的选择
for(i = 0; i < 100; i++)
    Sum =    StructureArray[i].FirstNumber
              + StructureArray[i].SecondNumber
              + StructureArray[i].ThirdNumber
              + StructureArray[i].FourthNumber;

我想道理不用多说大家也明白了吧, 具体到程序设计中要根据哪种操作用的多来决定数据的组织方式。

关于内存访问,还有很多很多需要注意的事项,比如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. 能用乘法就不要用除法。 比如   3/2 可以换成 3 * 0.5
5. struct的大小尽量是2的倍数,如果不是就调整下,加pad。因为可以在level1 cache里放整数个。
6. 全局变量少用,如果要用,加上static
7. 局部变量也是越少越少。这样register的效率更高
8. 能用switch的地方,就不要用if,因为switch是直接生成跳转表,速度快很多
9. 互相关联的代码之间不要空行,功能不同的代码之间最好空上1行区分开
10. 用const static 代替 #define 定义常量
11. 统一你的代码风格,始终使用同样的命名规则


[ 本帖最后由 sunyh1999 于 2010-7-29 20:02 编辑 ]
收到的鲜花
  • Devil_W2010-07-29 12:25 送鲜花  -3朵   附言:傻x一个。

欢迎来到我的博客:http://blog..cn/noisunyuhong
2010-07-29 09:55
BlueGuy
Rank: 16Rank: 16Rank: 16Rank: 16
等 级:版主
威 望:29
帖 子:4476
专家分:4055
注 册:2009-4-18
收藏
得分:0 
一点极积性都没了,

我就是真命天子,顺我者生,逆我者死!
2010-07-29 12:35
漫天
Rank: 1
等 级:新手上路
帖 子:2
专家分:5
注 册:2010-7-28
收藏
得分:5 
青蜂侠 很厉害 这么能说 这样下去就能整出一部教材了
2010-07-29 15:47
快速回复:一次过修理C语言
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.018637 second(s), 8 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved