大型软件编程规范

“安全第一”的C语言编程规范

编者按:C语言是开发嵌入式应用的主要工具,然而C语言并非是专门为嵌入式系统设计,相当多的嵌入式系统较一般计算机系统对软件安全性有更苛刻的要求。1998年,MISRA指出,一些在C看来可以接受,却存在安全隐患的地方有127处之多。2004年,MISRA对C的限制增加到141条。

  嵌入式系统应用工程师借用计算机专家创建的C语言,使嵌入式系统应用得以飞速发展,而MISRAC是嵌入式系统应用工程师对C语言嵌入式应用做出的贡献。如今MISRA C已经被越来越多的企业接受,成为用于嵌入式系统的C语言标准,特别是对安全性要求极高的嵌入式系统,软件应符合MISRA标准。

  从本期开始,本刊将分6期,与读者共同学习MISRAC。
  第一讲:“‘安全第一’的C语言编程规范”,简述MISRAC的概况。
  第二讲:“跨越数据类型的重重陷阱”,介绍规范的数据定义和操作方式,重点在隐式数据类型转换中的问题。
  第三讲:“指针、结构体、联合体的安全规范”,解析如何安全而高效地应用指针、结构体和联合体。
  第四讲:“防范表达式的失控”,剖析MISRAC中关于表达式、函数声明和定义等的不良使用习惯,最大限度地减小各类潜在错误。
  第五讲:“准确的程序流控制”,表述C语言中控制表达式和程序流控制的规范做法。
  第六讲:“构建安全的编译环境”,讲解与编译器相关的规范编写方式,避免来自编译器的隐患。

   C/C++语言无疑是当今嵌入式开发中最为常见的语言。早期的嵌入式程序大都是用汇编语言开发的,但人们很快就意识到汇编语言所带来的问题——难移植、难复用、难维护和可读性极差。很多程序会因为当初开发人员的离开而必须重新编写,许多程序员甚至连他们自己几个月前写成的代码都看不懂。C/C++语言恰恰可以解决这些问题。作为一种相对“低级”的高级语言,C/C++语言能够让嵌入式程序员更自由地控制底层硬件,同时享受高级语言带来的便利。对于C语言和C++语言,很多的程序员会选择C语言,而避开庞大复杂的C++语言。这是很容易理解的——C语言写成的代码量比C++语言的更小些,执行效率也更高。

  对于程序员来说,能工作的代码并不等于“好”的代码。“好”代码的指标很多,包括易读、易维护、易移植和可靠等。其中,可靠性对嵌入式系统非常重要,尤其是在那些对安全性要求很高的系统中,如飞行器、汽车和工业控制中。这些系统的特点是:只要工作稍有偏差,就有可能造成重大损失或者人员伤亡。一个不容易出错的系统,除了要有很好的硬件设计(如电磁兼容性),还要有很健壮或者说“安全”的程序。

  然而,很少有程序员知道什么样的程序是安全的程序。很多程序只是表面上可以干活,还存在着大量的隐患。当然,这其中也有C语言自身的原因。因为C语言是一门难以掌握的语言,其灵活的编程方式和语法规则对于一个新手来说很可能会成为机关重重的陷阱。同时,C语言的定义还并不完全,即使是国际通用的C语言标准,也还存在着很多未完全定义的地方。要求所有的嵌入式程序员都成为C语言专家,避开所有可能带来危险的编程方式,是不现实的。最好的方法是有一个针对安全性的C语言编程规范,告诉程序员该如何做。

1 MISRAC规范

  1994年,在英国成立了一个叫做汽车工业软件可靠性联合会(The Motor Industry Software Reliability Association,以下简称MISRA)的组织。它是致力于协助汽车厂商开发安全可靠的软件的跨国协会,其成员包括:AB汽车电子、罗孚汽车、宾利汽车、福特汽车、捷豹汽车、路虎公司、Lotus公司、MIRA公司、Ricardo公司、TRW汽车电子、利兹大学和福特VISTEON汽车系统公司。

  经过了四年的研究和准备,MISRA于1998年发布了一个针对汽车工业软件安全性的C语言编程规范——《汽车专用软件的C语言编程指南》(Guidelines for the Use of the C Language in Vehicle Based Software),共有127条规则,称为MISRAC:1998。[Page]

  C语言并不乏国际标准。国际标准化组织(International Organization of Standardization,简称ISO)的“标准C语言”经历了从C90、C96到C99的变动。但是,嵌入式程序员很难将ISO标准当作编写安全代码的规范。一是因为标准C语言并不是针对代码安全的,也并不是专门为嵌入式应用设计的;二是因为“标准C语言”太庞大了,很难操作。MISRAC:1998规范的产生恰恰弥补了这方面的空白。

  随着很多汽车厂商开始接受MISRAC编程规范,MISRAC:1998也成为汽车工业中最为著名的有关安全性的C语言规范。2004年,MISRA出版了该规范的新版本——MISRAC:2004。在新版本中,还将面向的对象由汽车工业扩大到所有的高安全性要求(Critical)系统。在MISRAC:2004中,共有强制规则121条,推荐规则20条,并删除了15条旧规则。任何符合MISRAC:2004编程规范的代码都应该严格的遵循121条强制规则的要求,并应该在条件允许的情况下尽可能符合20条推荐规则。

  MISRAC:2004将其141条规则分为21个类别,每一条规则对应一条编程准则。详细情况如表1所列。
表1MISRAC:2004规则分类
      

  最初,MISRAC:1998编程规范的建立是为了增强汽车工业软件的安全性。可能造成汽车事故的原因有很多,如图1所示,设计和制造时埋下的隐患约占总数的15%,其中也包括软件的设计和制造。MISRAC:1998就是为了减小这部分隐患而制定的。

  MISRAC编程规范的推出迎合了很多汽车厂商的需要,因为一旦厂商在程序设计上出现了问题,用来补救的费用将相当可观。1999年7月22日,通用汽车公司(General Motors)就曾经因为其软件设计上的一个问题,被迫召回350万辆已经出厂的汽车,损失之大可想而知。

  MISRAC规范不仅在汽车工业开始普及,也同时影响到了嵌入式开发的其他方向。嵌入式实时操作系统μC/OSII的2.52版本虽然已经于2000年通过了美国航空管理局(FAA)的安全认证,但2003年作者就根据MISRAC:1998规范又对源码做了相应的修改,如将

  if ((pevent->OSEventTbl[y] &= ~bitx) == 0) {
    /*… */
  }

的写法,改写成

  pevent->OSEventTbl[y] &= ~bitx;
  if (pevent->OSEventTbl[y] == 0) {
  /*… */
  }

发布了2.62的新版本,并宣称其源代码99%符合MISRAC:1998规范。

  一个程序能够符合MISRAC编程规范,不仅需要程序员按照规范编程,编译器也需要对所编译的代码进行规则检查。现在,很多编译器开发商都对MISRAC规范有了支持,比如IAR的编译器就提供了对MISRAC:1998规范127条规则的检查功能。

2 MISRAC对安全性的理解


  MISRAC:2004的专家们大都来自于软件工业或者汽车工业的知名公司,规范的制定不仅仅像过去一样局限于汽车工业的C语言编程,同时还涵盖了其他高安全性系统。
图1汽车事故原因分布图

  MISRAC:2004认为C程序设计中存在的风险可能由5个方面造成:程序员的失误、程序员对语言的误解、程序员对编译器的误解、编译器的错误和运行出错(runtime errors)。

  程序员的失误是司空见惯的。程序员是人,难免会犯错误。很多由程序员犯下的错误可以被编译器及时地纠正(如键入错误的变量名等),但也有很多会逃过编译器的检查。相信任何一个程序员都曾经犯过将“= =”误写成“=”的错误,编译器可能不会认为

   if(x=y)

是一个程序员的失误。

  再举个例子,大家都知道++运算符。假如有下面的指令:

  i=3;
  printf(“%d”,++i);

输出应该是多少?如果是:

  printf(“%d”,i++);

呢?如果改成-i++呢?i+++i呢?i+++++i呢?绝大多数程序员恐怕已经糊涂了。在MISRAC:2004中,会明确指出++或--运算符不得和其他运算符混合使用。

  C语言非常灵活,它给了程序员非常大的自由。但事情有好有坏,自由越大,犯错误的机会也就越多。[Page]

  如果说有些错误是程序员无心之失的话,那么因为程序员对C语言本身或是编译器特性的误解而造成的错误就是“明”知故犯了。C语言有一些概念很难掌握,非常容易造成误解,如表达式的计算。请看下面这条语句:

   if ( ishigh && (x == i++))

很多程序员认为执行了这条指令后,i变量的值就会自动加1。但真正的情况如何呢?MISRA中有一条规则:逻辑运算符&&或||的右操作数不得带有副作用(side effect)*,就是为了避免这种情况下可能出现的问题。

*所谓带有副作用,就是指执行某条语句时会改变运行环境,如执行x=i++之后,i的值会发生变化。

  另外,不同编译器对同一语句的处理可能是不一样的。例如整型变量的长度,不同编译器的规定就不同。这就要求程序员不仅要清楚C语言本身的特性,还要了解所用的编译器,难度很大。

  还有些错误是由编译器(或者说是编写编译器的程序员)本身造成的。这些错误往往较难发现,有可能会一直存留在最后的程序中。

  运行错误指的是那些在运行时出现的错误,如除数等于零、指针地址无效等问题。运行错误在语法检查时一般无法发现,但一旦发生很可能导致系统崩溃。例如:

#define NULL 0
   ……
  char* p;
  p=NULL;
  printf(“Location of 0 is %d\n”, *p);

语法上没有任何问题,但在某些系统上却可能运行出错。

  C语言可以产生非常紧凑、高效的代码,一个原因就是C语言提供的运行错误检查功能很少,虽然运行效率得以提高,但也降低了系统的安全性。

  有句话说得好,“正确的观念重于一切”。MISRAC规范对于嵌入式程序员来讲,一个很重要的意义就是提供给他们一些建议,让他们逐渐树立一些好的编程习惯和编程思路,慢慢摒弃那些可能存在风险的编程行为,编写出更为安全、健壮的代码。比如,很多嵌入式程序员都会忽略注释的重要性,但这样的做法会降低程序的可读性,也会给将来的维护和移植带来风险。嵌入式程序员经常要接触到各种的编译器,而很多C程序在不同编译器下的处理是不一样的。MISRAC:2004有一条强制规则,要求程序员把所有和编译器特性相关的C语言行为记录下来。这样在程序员做移植工作时,风险就降低了。

3 MISRAC的负面效应

  程序员可能会担心采用MISRAC:2004规范会对他们的程序有负面影响,比如可能会影响代码量、执行效率和程序可读性等。应该说,这种担心不无道理。纵观141条MISRAC:2004编程规范,大多数的规则并不会对程序的代码量、执行效率和可读性造成什么大的影响;一部分规则可能会以增加存储器的占用空间为代价来增加执行效率,或者增加代码的可读性;但是,也确实存在着一些规则可能会降低程序的执行效率。

  一个典型的例子就是关于联合体的使用。MISRAC:2004有一条规则明确指出:不得使用联合体。这是因为,在联合体的存储方式(如位填充、对齐方式、位顺序等)上,各种编译器的处理可能不同。比如,经常会有程序员这样做:一边将采集得到的数据按照某种类型存入一个联合体,而同时又采用另外一种数据类型将该数据读出。如下面这段程序:

  typedef union{
    uint32_t word;
    uint8_t bytes[4];
  }word_msg_t;
  unit32_t read_word_big_endian (void) {
    word_msg_t tmp;
    tmp.bytes[0] = read_byte();
    tmp.bytes[1] = read_byte();
    tmp.bytes[2] = read_byte();
    tmp.bytes[3] = read_byte();
    return (tmp.word);
  }

  原理上,这种联合体很像是一个硬件上的双口RAM存储器。但程序员必须清楚,这种做法是有风险的。MISRAC:2004推荐用下面这种方法来做:

   uint32_t read_word_big_endian (void) {
    uint32_t word;
    word=((unit32_t)read_byte())<<24;[Page]
    word=word|(((unit32_t)read_byte())<<16);
    word=word|(((unit32_t)read_byte())<<8);
    word=word| ((unit32_t)read_byte());
    return(word);
  }

  先不论为什么这样做会更安全,只谈执行效率,这种采用二进制数移位的方法远远不如使用联合体。到底是使用更安全的做法,还是采用效率更高的做法,需要程序员权衡。对于一些要求执行效率很高的系统,使用联合体仍然是可以接受的方法。当然,这是建立在程序员充分了解所用编译器的基础上的,而且程序员必须对这种做法配有相应的注释。

4 发展中的MISRAC

  MISRAC并非完美,它自身的发展也印证了这一点。MISRAC:2004就去掉了MISRAC:1998中的15条规则。今后的发展,MISRAC仍然要解决很多问题。比如,MISRAC:2004是基于C90标准的,但最新的国际C标准是C99,而C99中没有确切定义的C语言特性几乎比C90多了一倍,MISRAC如何适应新的标准还需要进一步探讨。

  另外,C++在嵌入式应用中也越来越受到重视,MISRA正在着手制定MISRAC++编程规范。读者可以通过访问网站http://www.misra.org.uk了解MISRAC的发展动向。

5 对MISRAC的思考

  嵌入式系统并不算是一个独立的学科,但作为一个发展中的行业,它确实需要有一些自己的创新之处。嵌入式工程师们不应仅仅局限于从计算机专家那里学习相关理论知识,并运用于自己的项目,还应该共同努力去完善自己行业的标准和规范,为嵌入式系统的发展做出贡献。MISRAC编程规范就是一个很好的典范。它始于汽车工程师和软件工程师经验的总结,然后逐渐发展成为一种对整个嵌入式行业都有指导意义的规范。对于推动整个嵌入式行业的正规化发展,MISRAC无疑有着重要意义。

  从另一个角度讲,MISRAC规范也可以看成是嵌入式工程师对软件业的一种完善。嵌入式工程师虽然不是计算机专家,但却对嵌入式应用有着最深刻的了解,将自己在嵌入式应用中的经验和体会贡献给其他行业,也是他们应该肩负的责任。

参考文献
1 MISRAC:2004, Guidelines for the use of the C language in critical systems. The Motor Industry Software Reliability Association, 2004
2 Harbison III. Samuel P, Steele Jr. Guy L. C语言参考手册. 邱仲潘,等译. 第5版. 北京:机械工业出版社,2003
3 Kernighan. Brian W, Ritchie. Dennis M. C程序设计语言. 徐宝文,等译. 第2版. 北京:机械工业出版社,2001
4 Koenig Andrew. C陷阱与缺陷. 高巍译. 北京:人民邮电出版社,2002
5 McCall Gavin. Introduction to MISRAC:2004, Visteon UK, http://www.MISRAC2.com/
6 Hennell Mike. MISRA CIts role in the bigger picture of critical software development, LDRA. http://www.MISRAC2.com/
7 Hatton Les. The MISRA C Compliance Suite—The next step, Oakwood Computing. http://www.MISRAC2.com/
8 Montgomery Steve. The role of MISRA C in developing automotive software, Ricardo Tarragon. http://www.MISRAC2.com/

非计算机专业C语言初学者编程规范(学生用)—概述

注:以下信息来源于成都信息工程学院

对于程序员来说,能工作的代码并不等于“好”的代码。“好”代码的指标很多,包括易读、易维护、易移植和可靠等。其中,可靠性对嵌入式系统非常重要,尤其是在那些对安全性要求很高的系统中,如飞行器、汽车和工业控制中。这些系统的特点是:只要工作稍有偏差,就有可能造成重大损失或者人员伤亡。一个不容易出错的系统,除了要有很好的硬件设计(如电磁兼容性),还要有很健壮或者说“安全”的程序。

然而,很少有程序员知道什么样的程序是安全的程序。很多程序只是表面上可以干活,还存在着大量的隐患。当然,这其中也有C语言自身的原因。因为C语言是一门难以掌握的语言,其灵活的编程方式和语法规则对于一个新手来说很可能会成为机关重重的陷阱。同时,C语言的定义还并不完全,即使是国际通用的C语言标准,也还存在着很多未完全定义的地方。要求所有的嵌入式程序员都成为C语言专家,避开所有可能带来危险的编程方式,是不现实的。最好的方法是有一个针对安全性的C语言编程规范,告诉程序员该如何做。

本规范在制定过程中,主要参考了业界比较推崇的《华为软件编程规范和范例》和《MISRA 2004规则》,适合于非计算机专业的C语言初学者使用,目的在于在教学中培养学生良好的编程规范和意识、素质,促进所设计程序安全、健壮、可靠、可读与可维护(程序简单、清晰)。考虑到面向的是初学者,为便于教学和课程考核操作,本规范中的要求比较基本。事实上,很多公司都有自己规定的代码风格,包括命名规则、缩进规则等,学生参加工作后,应再进一步学习和应用公司的规范。

建议学生在学习本规范的同时,花点时间阅读本规范的参考文献原文,特别是熟读本规范的参考文献之一的《“安全第一”的C语言编程规范》,深刻理解编程规范与程序安全、健壮、可靠、可读、可维护间的关系和作用,在学习和工作中养成良好的编程风格。

非计算机专业C语言初学者编程规范(学生用)—排版

1.1 严格采用阶梯层次组织程序代码

函数或过程的开始、结构的定义及循环、判断等语句中的代码都要采用缩进风格,case 语句下的情况处理语句也要遵从语句缩进要求。

程序块的分界符(如C/C++ 语言的大括号‘{’ 和‘}’)应各独占一行并且位于同一列,同时与引用它们的语句左对齐。在函数体的开始、类的定义、结构的定义、枚举的定义以及if 、for 、do 、while 、switch 、case 语句中的程序都要采用如上的缩进方式。

各层次缩进的风格采用TAB缩进(TAB宽度原则上使用系统默认值,TC使用8空格宽度,VC使用4空格宽度)。示例:
if (x is true)
{
we do y
}
else
{
if (a > b)
{
...
}
else
{
...
}
}
和:
if (x == y)
{
...
}
else if (x > y)
{
...
}
else
{
....
}

注意,右括号所在的行不应当有其它东西,除非跟随着一个条件判断。也就是do-while语句中的“while”,象这样:
do
{
body of do-loop
} while (condition);

说明:代码离不开缩进,缩进背后的思想是:清楚地定义一个控制块从哪里开始,到哪里结束。尤其是在你连续不断的盯了20个小时的屏幕后,如果你有大尺寸的缩进。你将更容易发现缩进的好处。

关于缩进主要有两个争论,一个是该用 空格(Space)还是用 制表符(Tab),另外一个是该用4格缩进还是8格缩进甚至都不是。建议总是使用Tab缩进,因为几乎所有的代码(不仅仅是C代码)都在使用Tab缩进。

现在,有些人说8个字符大小的缩进导致代码太偏右了,并且在一个80字符宽的终端屏幕上看着很不舒服。对这个问题的回答是:如果你有超过3个级别的缩进,你就有点犯糊涂了,应当修改你的程序。简而言之,8个字符的缩进使程序更易读,而且当你把功能隐藏的太深时,多层次的缩进还会对此很直观的给出警告。要留心这种警告信息。

例外:对于由开发工具自动生成的代码可以有不一致。

1.2 及时折行

较长的语句(>80 字符)要分成多行书写,长表达式要在低优先级操作符处划分新行,操作符放在新行之首,划分出的新行要进行适当的缩进(至少1个TAB位置),使排版整齐,语句可读。示例:
report_or_not_flag = ((taskno < MAX_ACT_TASK_NUMBER)
&& (n7stat_stat_item_valid (stat_item))
&& (act_task_table[taskno].result_data != 0));

循环、判断等语句中若有较长的表达式或语句,则要进行适应的划分,长表达式要在低优先级操作符处划分新行,操作符放在新行之首。示例:
if ((taskno < max_act_task_number)
&& (n7stat_stat_item_valid (stat_item)))
{
... // program code
}

for (i = 0, j = 0; (i < BufferKeyword[word_index].word_length)
&& (j < NewKeyword.word_length); i++, j++)
{
... // program code
}

for (i = 0, j = 0;
(i < first_word_length) && (j < second_word_length);
i++, j++)
{
... // program code
}

若函数或过程中的参数较长,则要进行适当的划分。示例:
n7stat_str_compare((BYTE *) & stat_object,
(BYTE *) & (act_task_table[taskno].stat_object),
sizeof (_STAT_OBJECT));
n7stat_flash_act_duration( stat_item, frame_id *STAT_TASK_CHECK_NUMBER
+ index, stat_object );

1.3 一行只写一条语句

不允许把多个短语句写在一行中,即一行只写一条语句。示例,如下例子不符合规范:
rect.length = 0; rect.width = 0;
应如下书写
rect.length = 0;
rect.width = 0;
1.4 if、for、do、while等语句格式规定
if 、for 、do 、while 、case 、switch 、default 等语句自占一行,且if 、for 、do 、while 等语句的执行语句部分无论多少都要加花括号{}。

1.5 空行

(1)变量说明之后必须加空行。
(2)相对独立的程序块之间应加空行。

1.6 空格

在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如-> ),后不应加空格。采用这种松散方式编写代码的目的是使代码更加清晰。
由于留空格所产生的清晰性是相对的,所以,在已经非常清晰的语句中没有必要再留空格,如果语句已足够清晰则括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格,因为在C/C++语言中括号已经是最清晰的标志了。
在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留两个以上空格。
(1)逗号、分号只在后面加空格。
int a, b, c;

(2)比较操作符, 赋值操作符"="、 "+=",算术操作符"+"、"%",逻辑操作符"&&"、"&",位域操作符"<<"、"^"等双目操作符的前后加空格。
if (current_time >= MAX_TIME_VALUE)
{
a = b + c;
}
a *= 2;
a = b ^ 2;

(3)"!"、"~"、"++"、"--"、"&"(地址运算符)等单目操作符前后不加空格。

*p = 'a'; // 内容操作"*"与内容之间
flag = !isEmpty; // 非操作"!"与内容之间
p = &mem; // 地址操作"&" 与内容之间
i++; // "++","--"与内容之间

(4)"->"、"."前后不加空格。
p->id = pid; // "->"指针前后不加空格

(5) if、for、while、switch等与后面的括号间应加空格,使if等关键字更为突出、明显。
if (a >= b && c > d)

1.7 对变量的定义,尽量位于函数的开始位置

(1)应避免分散定义变量。
(2)同一行内不要定义过多变量。
(3)同一类的变量在同一行内定义,或者在相邻行定义。
(4)数组、指针等复杂类型的定义放在定义区的最后。
(5)变量定义区不做较复杂的变量赋值。

1.8 程序各部分的放置顺序

在较小的项目中,按如下顺序组织安排程序各部分:
(1)#include <C的标准头文件>。
(2)#include 〞用户自定义的文件〞。
(3)#define 宏定义。
(4)全局变量定义。
(5)函数原型声明。
(6)main函数定义。
(7)用户自定义函数。

以上各部分之间、用户自定义的函数之间应加空行。注意,函数原型声明统一集中放在main函数之前,不放在某个函数内部。

非计算机专业C语言初学者编程规范(学生用)—注释

2.1 注释的原则和目的

注释的原则是有助于对程序的阅读理解,在该加的地方都加了,注释不宜太多也不能太少,注释语言必须准确、易懂、简洁。通过对函数或过程、变量、结构等正确的命名以及合理地组织代码的结构,使代码成为自注释的——清晰准确的函数、变量等的命名,可增加代码可读性,并减少不必要的注释——过量的注释则是有害的。
注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。 示例:如下注释意义不大。
/* if receive_flag is TRUE */
if (receive_flag)
而如下的注释则给出了额外有用的信息。
/* if mtp receive a message from links */
if (receive_flag)

2.2 函数头部应进行注释

函数头部应进行注释,列出:函数的目的/ 功能、输入参数、输出参数、返回值、调用关系(函数、表)等。
示例1:下面这段函数的注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。

/*************************************************
Function: // 函数名称
Description: // 函数功能、性能等的描述
Calls: // 被本函数调用的函数清单
Called By: // 调用本函数的函数清单
Input: // 输入参数说明,包括每个参数的作
// 用、取值说明及参数间关系。
Output: // 对输出参数的说明。
Return: // 函数返回值的说明
Others: // 其它说明
*************************************************/

对于某些函数,其部分参数为传入值,而部分参数为传出值,所以对参数要详细说明该参数是入口参数,还是出口参数,对于某些意义不明确的参数还要做详细说明(例如:以角度作为参数时,要说明该角度参数是以弧度(PI),还是以度为单位),对既是入口又是出口的变量应该在入口和出口处同时标明。等等。

在注释中详细注明函数的适当调用方法,对于返回值的处理方法等。在注释中要强调调用时的危险方面,可能出错的地方。

2.3 进行注释时的注意事项

(1)建议边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。

(2)注释的内容要清楚、明了,含义准确,防止注释二义性。说明:错误的注释不但无益反而有害。

(3)避免在注释中使用缩写,特别是非常用缩写。在使用缩写时或之前,应对缩写进行必要的说明。

(4)注释应与其描述的代码相近,对代码的注释应放在其上方或右方(对单条语句的注释)相邻位置,不可放在下面。除非必要,不应在代码或表达中间插入注释,否则容易使代码可理解性变差。

示例:如下例子不符合规范。

例1:
/* get replicate sub system index and net indicator */

repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;

例2:
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
/* get replicate sub system index and net indicator */

应如下书写
/* get replicate sub system index and net indicator */
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;

(5)对于所有有物理含义的变量、常量,如果其命名不是充分自注释的,在声明时都必须加以注释,说明其物理含义。变量、常量、宏的注释应放在其上方相邻位置或右方。

示例:
/* active statistic task number */
#define MAX_ACT_TASK_NUMBER 1000
#define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number */

(6)数据结构声明( 包括数组、结构、类、枚举等) ,如果其命名不是充分自注释的,必须加以注释。对数据结构的注释应放在其上方相邻位置,不可放在下面;对结构中的每个域的注释放在此域的右方。

示例:可按如下形式说明枚举/数据/联合结构。
/* sccp interface with sccp user primitive message name */
enum SCCP_USER_PRIMITIVE
{
N_UNITDATA_IND, /* sccp notify sccp user unit data come */
N_NOTICE_IND, /* sccp notify user the No.7 network can not */
/* transmission this message */
N_UNITDATA_REQ, /* sccp user's unit data transmission request*/
};

(7)全局变量要有较详细的注释,包括对其功能、取值范围、哪些函数或过程存取它以及存取时注意事项等的说明。

示例:
/* The ErrorCode when SCCP translate */
/* Global Title failure, as follows */ // 变量作用、含义
/* 0 - SUCCESS 1 - GT Table error */
/* 2 - GT error Others - no use */ // 变量取值范围
/* only function SCCPTranslate() in */
/* this modual can modify it, and other */
/* module can visit it through call */
/* the function GetGTTransErrorCode() */ // 使用方法
BYTE g_GTTranErrorCode;

(8)注释与所描述内容进行同样的缩排,让程序排版整齐,并方便注释的阅读与理解。

示例:如下例子,排版不整齐,阅读稍感不方便。
void example_fun( void )
{
/* code one comments */
CodeBlock One

/* code two comments */
CodeBlock Two
}
应改为如下布局。
void example_fun( void )
{
/* code one comments */
CodeBlock One

/* code two comments */
CodeBlock Two
}

(9)将注释与其上面的代码用空行隔开。

示例:如下例子,显得代码过于紧凑。
/* code one comments */
program code one
/* code two comments */
program code two
应如下书写
/* code one comments */
program code one

/* code two comments */
program code two

(10)对变量的定义和分支语句(条件分支、循环语句等)必须编写注释。这些语句往往是程序实现某一特定功能的关键,对于维护人员来说,良好的注释帮助更好的理解程序,有时甚至优于看设计文档。

(11)对于switch 语句下的case 语句,如果因为特殊情况需要处理完一个case 后进入下一个case 处理(即上一个case后无break),必须在该case 语句处理完、下一个case 语句前加上明确的注释,以清楚表达程序编写者的意图,有效防止无故遗漏break语句(可避免后期维护人员对此感到迷惑:原程序员是遗漏了break语句还是本来就不应该有)。示例:
case CMD_DOWN:
ProcessDown();
break;
case CMD_FWD:
ProcessFwd();
if (...)
{
...
break;
} else
{
ProcessCFW_B(); // now jump into case CMD_A
}
case CMD_A:
ProcessA();
break;
...

(12)在程序块的结束行右方加注释标记,以表明某程序块的结束。当代码段较长,特别是多重嵌套时,这样做可以使代码更清晰,更便于阅读。示例:参见如下例子。
if (...)
{
program code
while (index < MAX_INDEX)
{
program code
} /* end of while (index < MAX_INDEX) */ // 指明该条while语句结束
} /* end of if (...)*/ // 指明是哪条if语句结束

(13)在顺序执行的程序中,每隔3—5行语句,应当加一个注释,注明这一段语句所组成的小模块的作用。对于自己的一些比较独特的思想要求在注释中标明。

(14)注释格式尽量统一,建议使用“/* …… */”。

(15)注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达——注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。

非计算机专业C语言初学者编程规范(学生用)—命名规则

C是一门朴素的语言,你使用的命名也应该这样。与Modula-2和Pascal程序员不同,C程序员不使用诸如“ThisVariableIsATemporaryCounter”这样“聪明”的名字。C程序员应该叫它“tmp”,这写起来更简单,也不会更难懂。
然而,当面对复杂情况时就有些棘手,给全局变量取一个描述性的名字是必要的。把一个全局函数叫做“foo”是一种目光短浅的行为。全局函数也一样,如果你有一个统计当前用户个数的函数,应当把它命名为“count_active_user()”或者简单点些的类似名称,不应该命名为“cntusr()”。

3.1 三种流行的命名法则

目前,业界共有四种命名法则:驼峰命名法、匈牙利命名法、帕斯卡命名法和下划线命名法,其中前三种是较为流行的命名法。

(1)驼峰命令法。正如它的名称所表示的那样,是指混合使用大小写字母来构成变量和函数的名字。例如,下面是分别用骆驼式命名法和下划线法命名的同一个函数:
printEmployeePaychecks();
print_employee_paychecks();

第一个函数名使用了驼峰命名法,函数名中的每一个逻辑断点都有一个大写字母来标记。第二个函数名使用了下划线法,函数名中的每一个逻辑断点都有一个下划线来标记。

驼峰命名法近年来越来越流行了,在许多新的函数库和Microsoft Windows这样的环境中,它使用得当相多。另一方面,下划线法是C出现后开始流行起来的,在许多旧的程序和UNIX这样的环境中,它的使用非常普遍。

(2)匈牙利命名法。广泛应用于象Microsoft Windows这样的环境中。Windows 编程中用到的变量(还包括宏)的命名规则为匈牙利命名法,这种命名技术是由一位能干的 Microsoft 程序员查尔斯-西蒙尼(Charles Simonyi) 提出的。

匈牙利命名法通过在变量名前面加上相应的小写字母的符号标识作为前缀,标识出变量的作用域、类型等。这些符号可以多个同时使用,顺序是先m_(成员变量)、再指针、再简单数据类型、再其它。这样做的好处在于能增加程序的可读性,便于对程序的理解和维护。

例如:m_lpszStr, 表示指向一个以0字符结尾的字符串的长指针成员变量。
匈牙利命名法关键是:标识符的名字以一个或者多个小写字母开头作为前缀;前缀之后的是首字母大写的一个单词或多个单词组合,该单词要指明变量的用途。

(3)帕斯卡(pascal)命名法。与驼峰命名法类似,二者的区别在于:驼峰命名法是首字母小写,而帕斯卡命名法是首字母大写,如:
DisplayInfo();
string UserName;
二者都是采用了帕斯卡命名法。

(4)三种命名规则的小结:MyData就是一个帕斯卡命名的示例;myData是一个驼峰命名法,它第一个单词的第一个字母小写,后面的单词首字母大写,看起来像一个骆驼;iMyData是一个匈牙利命名法,它的小写的i说明了它的型态,后面的和帕斯卡命名相同,指示了该变量的用途。

3.2 命名的基本原则

(1)标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解——尽量采用采用英文单词或全部中文全拼表示,若出现英文单词和中文混合定义时,使用连字符“_”将英文与中文割开。较短的单词可通过去掉“元音”形成缩写;较长的单词可取单词的头几个字母形成缩写;一些单词有大家公认的缩写。例如:temp->tmp、flag->flg、statistic->stat、increment->inc、message->msg等缩写能够被大家基本认可。

(2)命名中若使用特殊约定或缩写,则要有注释说明。应该在源文件的开始之处,对文件中所使用的缩写或约定,特别是特殊的缩写,进行必要的注释说明。

(3)自己特有的命名风格,要自始至终保持一致,不可来回变化。个人的命名风格,在符合所在项目组或产品组的命名规则的前提下,才可使用。(即命名规则中没有规定到的地方才可有个人命名风格)。

(4)对于变量命名,禁止取单个字符(如i 、j 、k... ),建议除了要有具体含义外,还能表明其变量类型、数据类型等,但i 、j 、k 作局部循环变量是允许的。变量,尤其是局部变量,如果用单个字符表示,很容易敲错(如i写成j),而编译时又检查不出来,有可能为了这个小小的错误而花费大量的查错时间。

(5)除非必要,不要用数字或较奇怪的字符来定义标识符。

(6)命名规范必须与所使用的系统风格保持一致,并在同一项目中统一。

(7)在同一软件产品内,应规划好接口部分标识符(变量、结构、函数及常量)的命名,防止编译、链接时产生冲突。对接口部分的标识符应该有更严格限制,防止冲突。如可规定接口部分的变量与常量之前加上“模块”标识等。

(8)用正确的反义词组命名具有互斥意义的变量或相作的函数等。
下面是一些在软件中常用的反义词组。
add / remove begin / end create / destroy
insert / delete first / last g et / release
increment / decrement put / get
add / delete lock / unlock open / close
min / max old / new start / stop
next / previous source / target show / hide
send / receive source / destination
cut / paste up / down

示例:
int min_sum;
int max_sum;
int add_user( BYTE *user_name );
int delete_user( BYTE *user_name );

(9)除了编译开关/ 头文件等特殊应用,应避免使用_EXAMPLE_TEST_ 之类以下划线开始和结尾的定义。

3.3 变量名的命名规则

(1)变量的命名规则要求用“匈牙利法则”。
即开头字母用变量的类型,其余部分用变量的英文意思、英文的缩写、中文全拼或中文全拼的缩写,要求单词的第一个字母应大写。
即: 变量名=变量类型+变量的英文意思(或英文缩写、中文全拼、中文全拼缩写)
对非通用的变量,在定义时加入注释说明,变量定义尽量可能放在函数的开始处。
见下表:
bool 用b开头 bFlg
int 用i开头 iCount
short int 用n开头 nStepCount
long int 用l开头 lSum
char 用c开头 cCount
unsigned char 用by开头
float 用f开头 fAvg
double 用d开头 dDeta
unsigned int(WORD) 用w开头 wCount
unsigned long int(DWORD) 用dw开头 dwBroad
字符串 用s开头 sFileName
用0结尾的字符串 用sz开头 szFileName

(2)指针变量命名的基本原则为:
对一重指针变量的基本原则为:“p”+变量类型前缀+命名,如一个float*型应该表示为pfStat。对二重指针变量的基本规则为:“pp”+变量类型前缀+命名。对三重指针变量的基本规则为:“ppp”+变量类型前缀+命名。

(3)全局变量用g_开头,如一个全局的长型变量定义为g_lFailCount。即:变量名=g_+变量类型+变量的英文意思(或缩写)。此规则还可避免局部变量和全局变量同名而引起的问题。

(4)静态变量用s_开头,如一个静态的指针变量定义为s_plPerv_Inst。即: 变量名=s_+变量类型+变量的英文意思(或缩写)

(5)对枚举类型(enum)中的变量,要求用枚举变量或其缩写做前缀。并且要求用大写。如:
enum cmEMDAYS
{
EMDAYS_MONDAY;
EMDAYS_TUESDAY;
……
};

(6)对struct、union变量的命名要求定义的类型用大写。并要加上前缀,其内部变量的命名规则与变量命名规则一致。

结构一般用S开头,如:
struct ScmNPoint
{
int nX;//点的X位置
int nY; //点的Y位置
};

联合体一般用U开头,如:
union UcmLPoint
{
LONG lX;
LONG lY;
}

(7)对常量(包括错误的编码)命名,要求常量名用大写,常量名用英文表达其意思。当需要由多个单词表示时,单词与单词之间必须采用连字符“_”连接。
如:#define CM_FILE_NOT_FOUND CMMAKEHR(0X20B) 其中CM表示类别。

(8)对const 的变量要求在变量的命名规则前加入c_。即:c_+变量命名规则;示例:const char* c_szFileName;

3.4 函数的命名规范

(1)函数的命名应该尽量用英文(或英文缩写、中文全拼、中文全拼缩写)表达出函数完成的功能——函数名应准确描述函数的功能。遵循动宾结构的命名法则,函数名中动词在前,并在命名前加入函数的前缀,函数名的长度不得少于8个字母。函数名首字大写,若包含有两个单词的每个单词首字母大写。如果是OOP 方法,可以只有动词(名词是对象本身)。示例:
LONG GetDeviceCount(……);
void print_record( unsigned int rec_ind ) ;
int input_record( void ) ;
unsigned char get_current_color( void ) ;

(2)避免使用无意义或含义不清的动词为函数命名。如使用process、handle等为函数命名,因为这些动词并没有说明要具体做什么。

(3)必须使用函数原型声明。函数原型声明包括:引用外来函数及内部函数,外部引用必须在右侧注明函数来源: 模块名及文件名;内部函数,只要注释其定义文件名——和调用者在同一文件中(简单程序)时不需要注释。
应确保每个函数声明中的参数的名称、类型和定义中的名称、类型一致。

3.5 函数参数命名规范

(1)参数名称的命名参照变量命名规范。
(2)为了提高程序的运行效率,减少参数占用的堆栈,传递大结构的参数,一律采用指针或引用方式传递。
(3)为了便于其他程序员识别某个指针参数是入口参数还是出口参数,同时便于编译器检查错误,应该在入口参数前加入const标志。
如:……cmCopyString(const CHAR * c_szSource, CHAR * szDest)

3.6 文件名(包括动态库、组件、控件、工程文件等)的命名规范

文件名的命名要求表达出文件的内容,要求文件名的长度不得少于5个字母,严禁使用象file1,myfile之类的文件名。

非计算机专业C语言初学者编程规范(学生用)—可读性

4.1 避免使用默认的运算优先级

注意运算符的优先级,并用括号明确表达式的操作顺序,避免使用默认优先级,可防止阅读程序时产生误解,防止因默认的优先级与设计思想不符而导致程序出错。

示例:下列语句中的表达式
word = (high << 8) | low (1)
if ((a | b) && (a & c)) (2)
if ((a | b) < (c & d)) (3)

如果书写为:
high << 8 | low
a | b && a & c
a | b < c & d

由于
high << 8 | low = ( high << 8) | low,
a | b && a & c = (a | b) && (a & c),
(1)(2)不会出错,但语句不易理解;a | b < c & d = a | (b < c) & d,(3)造成了判断条件出错。

4.2 使用有意义的标识,避免直接使用数字

避免使用不易理解的数字,用有意义的标识来替代。涉及物理状态或者含有物理意义的常量,不应直接使用数字,必须用有意义的枚举或宏来代替。

示例:如下的程序可读性差。
if (Trunk[index].trunk_state == 0)
{
Trunk[index].trunk_state = 1;
... // program code
}

应改为如下形式。
#define TRUNK_IDLE 0
#define TRUNK_BUSY 1

if (Trunk[index].trunk_state == TRUNK_IDLE)
{
Trunk[index].trunk_state = TRUNK_BUSY;
... // program code
}

4.3 源程序中关系较为紧密的代码应尽可能相邻

这样做的好处是便于程序阅读和查找。示例:以下代码布局不太合理。
rect.length = 10;
char_poi = str;
rect.width = 5;

若按如下形式书写,可能更清晰一些。
rect.length = 10;
rect.width = 5; // 矩形的长与宽关系较密切,放在一起。
char_poi = str;

4.4 不要使用难懂的技巧性很高的语句、复杂的表达式

除非很有必要时,原则上不要使用难懂的技巧性很高的语句和复杂的表达式——高技巧语句不等于高效率的程序,源程序占用空间的节约并不等于目标程序占用空间的节约,实际上程序的效率关键在于算法。

(1)如下表达式,考虑不周就可能出问题,也较难理解。
* stat_poi ++ += 1;
* ++ stat_poi += 1;
应分别改为如下:
*stat_poi += 1;
stat_poi++; // 此二语句功能相当于“ * stat_poi ++ += 1; ”
++ stat_poi;
*stat_poi += 1; // 此二语句功能相当于“ * ++ stat_poi += 1; ”

(2)如下表达式,不同的编译器给出的结果不一样,b[i]是否先执行?
x=b[i] + i++;
应改为:
x = b[i] + i;
i++;

非计算机专业C语言初学者编程规范(学生用)—变量与结构

5.1 谨慎使用全局(公共)变量

(1)去掉没必要的公共变量。公共变量是增大模块间耦合的原因之一,故应减少没必要的公共变量以降低模块间的耦合度。

(2)仔细定义并明确公共变量的含义、作用、取值范围及公共变量间的关系。在对变量声明的同时,应对其含义、作用及取值范围进行注释说明,同时若有必要还应说明与其它变量的关系。

(3)防止局部变量与公共变量同名——通过使用较好的命名规则来消除此问题。

5.2 数据类型间的转换

(1)编程时,要注意数据类型的强制转换。当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患。

(2)对编译系统默认的数据类型转换,也要有充分的认识。
示例:如下赋值,多数编译器不产生告警,但值的含义还是稍有变化。
char chr;
unsigned short int exam;
chr = -1;
exam = chr; // 编译器不产生告警,此时exam为0xFFFF。

(3)尽量减少没有必要的数据类型默认转换与强制转换。例如,所有的 unsigned类型都应该有后缀“U”以明确其类型。

(4)合理地设计数据并使用自定义数据类型,避免数据间进行不必要的类型转换。

(5)对自定义数据类型进行恰当命名,使它成为自描述性的,以提高代码可读性。注意其命名方式在同一产品中的统一,并且保证没有多重定义。使用自定义类型,可以弥补编程语言提供类型少、信息量不足的缺点,并能使程序清晰、简洁。
示例:可参考如下方式声明自定义数据类型。下面的声明可使数据类型的使用简洁、明了。
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned int DWORD;

下面的声明可使数据类型具有更丰富的含义。
typedef float DISTANCE;
typedef float SCORE;

(6)不要用八进制数——整型常数以”0“开始会被认为是8进制。示例:
code[1]=109
code[2]=100
code[3]=052
code[4]=071
如果是对总线消息初始化,会有危险。

非计算机专业C语言初学者编程规范(学生用)—函数与过程

6.1 函数的功能与规模设计

(1)函数应当短而精美,而且只做一件事。

不要设计多用途面面俱到的函数,多功能集于一身的函数,很可能使函数的理解、测试、维护等变得困难。 一个函数应最多占满1或2个屏幕(就象我们知道的那样,ISO/ANSI的屏幕大小是80X24),只做一件事并且把它做好。

一个函数的最大长度与它的复杂度和缩进级别成反比。所以,如果如果你有一个概念上简单(案,“简单”是simple而不是easy)的函数,它恰恰包含着一个很长的case语句,这样你不得不为不同的情况准备不懂的处理,那么这样的长函数是没问题的。

然而,如果你有一个复杂的函数,你猜想一个并非天才的高一学生可能看不懂得这个函数,你就应当努力把它减缩得更接近前面提到的最大函数长度限制。可以使用一些辅助函数,给它们取描述性的名字(如果你认为这些辅助函数的调用是性能关键的,可以让编译器把它们内联进来,这比在单个函数内完成所有的事情通常要好些)。
对函数还存在另一个测量标准:局部变量的数目。这不该超过5到10个,否则你可能会弄错。应当重新考虑这个函数,把它分解成小片。人类的大脑一般能同时记住7个不同的东西,超过这个数目就会犯糊涂。或许你认为自己很聪明,那么请你理解一下从现在开始的2周时间你都做什么了。

(2)为简单功能编写函数。

虽然为仅用一两行就可完成的功能去编函数好象没有必要,但用函数可使功能明确化,增加程序可读性,亦可方便维护、测试。 示例:如下语句的功能不很明显。

value = ( a > b ) ? a : b ;改为如下就很清晰了。
int max (int a, int b)
{
return ((a > b) ? a : b);
}

value = max (a, b);或改为如下。
#define MAX (a, b) (((a) > (b)) ? (a) : (b))
value = MAX (a, b);

当一个过程(函数)中对较长变量(一般是结构的成员)有较多引用时,可以用一个意义相当的宏代替——这样可以增加编程效率和程序的可读性。 示例:在某过程中较多引用TheReceiveBuffer[FirstSocket].byDataPtr,则可以通过以下宏定义来代替:# define pSOCKDATA TheReceiveBuffer[FirstScoket].byDataPtr

(3)防止把没有关联的语句放到一个函数中,防止函数或过程内出现随机内聚。

随机内聚是指将没有关联或关联很弱的语句放到同一个函数或过程中。随机内聚给函数或过程的维护、测试及以后的升级等造成了不便,同时也使函数或过程的功能不明确。使用随机内聚函数,常常容易出现在一种应用场合需要改进此函数,而另一种应用场合又不允许这种改进,从而陷入困境。

在编程时,经常遇到在不同函数中使用相同的代码,许多开发人员都愿把这些代码提出来,并构成一个新函数。若这些代码关联较大并且是完成一个功能的,那么这种构造是合理的,否则这种构造将产生随机内聚的函数。
示例:如下函数就是一种随机内聚。
void Init_Var( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的长与宽 */
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标 */
}
矩形的长、宽与点的坐标基本没有任何关系,故以上函数是随机内聚。应如下分为两个函数:
void Init_Rect( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的长与宽 */
}

void Init_Point( void )
{
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标 */
}

(4)如果多段代码重复做同一件事情,那么在函数的划分上可能存在问题。若此段代码各语句之间有实质性关联并且是完成同一件功能的,那么可考虑把此段代码构造成一个新的函数。

(5)减少函数本身或函数间的递归调用。递归调用特别是函数间的递归调用(如A->B->C->A),影响程序的可理解性;递归调用一般都占用较多的系统资源(如栈空间);递归调用对程序的测试有一定影响。故除非为某些算法或功能的实现方便,应减少没必要的递归调用,对于safe-related 系统不能用递归,因为超出堆栈空间很危险。

6.2 函数的返回值

(1)对于函数的返回位置,尽量保持单一性,即一个函数尽量做到只有一个返回位置。(单入口单出口)。

要求大家统一函数的返回值,所有的函数的返回值都将以编码的方式返回。
例如编码定义如下:
#define CM_POINT_IS_NULL CMMAKEHR(0X200)
:
:
建议函数实现如下:
LONG 函数名(参数,……)
{
LONG lResult; //保持错误号
lResult=CM_OK;
//如果参数有错误则返回错误号
if(参数==NULL)
{
lResult=CM_POINT_IS_NULL;
goto END;
}
……
END:
return lResult;
}

(2)除非必要,最好不要把与函数返回值类型不同的变量,以编译系统默认的转换方式或强制的转换方式作为返回值返回。

(3)函数的返回值要清楚、明了,让使用者不容易忽视错误情况。函数的每种出错返回值的意义要清晰、明了、准确,防止使用者误用、理解错误或忽视错误返回码。

(4)函数的功能应该是可以预测的,也就是只要输入数据相同就应产生同样的输出。带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在C/C++语言中,函数的static局部变量是函数的内部存储器,有可能使函数的功能不可预测,然而,当某函数的返回值为指针类型时,则必须是STATIC的局部变量的地址作为返回值,若为AUTO类,则返回为错针。

示例:如下函数,其返回值(即功能)是不可预测的。
unsigned int integer_sum( unsigned int base )
{
unsigned int index;
static unsigned int sum = 0; // 注意,是static类型的。
// 若改为auto类型,则函数即变为可预测。
for (index = 1; index <= base; index++)
{
sum += index;
}
return sum;
}

6.3 函数参数

(1)只当你确实需要时才用全局变量,函数间应尽可能使用参数、返回值传递消息。

(2)防止将函数的参数作为工作变量。将函数的参数作为工作变量,有可能错误地改变参数内容,所以很危险。对必须改变的参数,最好先用局部变量代之,最后再将该局部变量的内容赋给该参数。
示例:下函数的实现不太好。
void sum_data( unsigned int num, int *data, int *sum )
{
unsigned int count;
*sum = 0;

for (count = 0; count < num; count++)
{
*sum += data[count]; // sum成了工作变量,不太好。
}
}

若改为如下,则更好些。
void sum_data( unsigned int num, int *data, int *sum )
{
unsigned int count ;
int sum_temp;
sum_temp = 0;

for (count = 0; count < num; count ++)
{
sum_temp += data[count];
}

*sum = sum_temp;
}

非计算机专业C语言初学者编程规范(学生用)—效率

(1)编程时要经常注意代码的效率。

代码效率分为全局效率、局部效率、时间效率及空间效率。全局效率是站在整个系统的角度上的系统效率;局部效率是站在模块或函数角度上的效率;时间效率是程序处理输入任务所需的时间长短;空间效率是程序所需内存空间,如机器代码空间大小、数据空间大小、栈空间大小等。

(2)在保证软件系统的正确性、稳定性、可读性及可测性的前提下,提高代码效率。不能一味地追求代码效率,而对软件的正确性、稳定性、可读性及可测性造成影响。

(3)局部效率应为全局效率服务,不能因为提高局部效率而对全局效率造成影响。

(4)循环体内工作量最小化。

应仔细考虑循环体内的语句是否可以放在循环体之外,使循环体内工作量最小,从而提高程序的时间效率。

示例:如下代码效率不高。
for (ind = 0; ind < MAX_ADD_NUMBER; ind++)
{
sum += ind;
back_sum = sum; /* backup sum */
}
语句“back_sum = sum;”完全可以放在for语句之后,如下。
for (ind = 0; ind < MAX_ADD_NUMBER; ind++)
{
sum += ind;
}
back_sum = sum; /* backup sum */

(5)不应花过多的时间拼命地提高调用不很频繁的函数代码效率。

对代码优化可提高效率,但若考虑不周很有可能引起严重后果。

(6)在多重循环中,应将最忙的循环放在最内层,以减少CPU切入循环层的次数。

示例:如下代码效率不高。
for (row = 0; row < 100; row++)
{
for (col = 0; col < 5; col++)
{
sum += a[row][col];
}
}
可以改为如下方式,以提高效率。
for (col = 0; col < 5; col++)
{
for (row = 0; row < 100; row++)
{
sum += a[row][col];
}
}

(7)尽量减少循环嵌套层次。

(8)避免循环体内含判断语句,应将循环语句置于判断语句的代码块之中。

目的是减少判断次数。循环体中的判断语句是否可以移到循环体外,要视程序的具体情况而言,一般情况,与循环变量无关的判断语句可以移到循环体外,而有关的则不可以。

示例:如下代码效率稍低。
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
if (data_type == RECT_AREA)
{
area_sum += rect_area[ind];
} else
{
rect_length_sum += rect[ind].length;
rect_width_sum += rect[ind].width;
}
}

因为判断语句与循环变量无关,故可如下改进,以减少判断次数。
if (data_type == RECT_AREA)
{
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
area_sum += rect_area[ind];
}
} else
{
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
rect_length_sum += rect[ind].length;
rect_width_sum += rect[ind].width;
}
}

(9)尽量用乘法或其它方法代替除法,特别是浮点运算中的除法——浮点运算除法要占用较多CPU资源。

示例:如下表达式运算可能要占较多CPU资源。
#define PAI 3.1416
radius = circle_length / (2 * PAI);
应如下把浮点除法改为浮点乘法。
#define PAI_RECIPROCAL (1 / 3.1416 ) // 编译器编译时,将生成具体浮点数
radius = circle_length * PAI_RECIPROCAL / 2;

(10)不要一味追求紧凑的代码,因为紧凑的代码并不代表高效的机器码。

非计算机专业C语言初学者编程规范(学生用)—质量保证

(1)代码质量保证优先原则:

①正确性,指程序要实现设计要求的功能。
②稳定性、安全性,指程序稳定、可靠、安全。
③可测试性,指程序要具有良好的可测试性。
④规范/可读性,指程序书写风格、命名规则等要符合规范。
⑤全局效率,指软件系统的整体效率。
⑥局部效率,指某个模块/子模块/函数的本身效率。
⑦个人表达方式/个人方便性,指个人编程习惯。

(2)过程/ 函数中分配的内存,在过程/ 函数退出之前要释放,过程/ 函数中申请的(为打开文件而使用的)文件句柄,在过程/ 函数退出之前要关闭。

分配的内存不释放以及文件句柄不关闭,是较常见的错误,而且稍不注意就有可能发生。这类错误往往会引起很严重后果,且难以定位。

示例:下函数在退出之前,没有把分配的内存释放。
typedef unsigned char BYTE;

int example_fun( BYTE gt_len, BYTE *gt_code )
{
BYTE *gt_buf;

gt_buf = (BYTE *) malloc (MAX_GT_LENGTH);
... //program code, include check gt_buf if or not NULL.

/* global title length error */
if (gt_len > MAX_GT_LENGTH)
{
return GT_LENGTH_ERROR; // 忘了释放gt_buf
}
... // other program code
free( gt_buf );
}

(3)防止内存操作越界。

内存操作主要是指对数组、指针、内存地址等的操作。内存操作越界是软件系统主要错误之一,后果往往非常严重,所以当我们进行这些操作时一定要仔细小心。

(4)编程时,要防止差1 错误。

此类错误一般是由于把“<=”误写成“<”或“>=”误写成“>”等造成的,由此引起的后果,很多情况下是很严重的,所以编程时,一定要在这些地方小心。当编完程序后,应对这些操作符进行彻底检查。

(5)要时刻注意易混淆的操作符。

当编完程序后,应从头至尾检查一遍这些操作符,以防止拼写错误。形式相近的操作符最容易引起误用,如C/C++中的“=”与“==”、“|”与“||”、“&”与“&&”等,若拼写错了,编译器不一定能够检查出来。

(6)有可能的话,if 语句尽量加上else 分支,对没有else 分支的语句要小心对待;switch 语句必须有default 分支。

(7)不要滥用goto 语句。

goto语句会破坏程序的结构性,所以除非确实需要,最好不使用goto语句。

(8)sizeof操作符不能用在包含边界作用(side effect)的表达式上。

例:
Int32_t = i;
Int32_t = j;
j = sizeof(i = 1234);
表达式i = 1234并没有执行,只是得到表达式类型int的size。

(9)逻辑操作符&&或者||右边不能包含边界作用(side effect)。

例:
if (ishight) && (x == i++))
如果ishight=0那么i++不会被执行。

(10)++和--不能和其他表达式用在一个表达式中。

(11)赋值语句不能用在一个产生布尔值的表达式中。

例:
if ((x = y) != 0)…
更差的用法:
if (x = y)…

(12)浮点表达式不应该测试其是否相等或者不相等,for控制表达式中不要包含任何浮点类型。

(13)数字变量作为for循环的循环计数不要在循环体内部被修改。

例:
flag=1;
for (i=0;(i<5)&&(flag==1);i++)
{
flag=0;
i=i+3;
}

(14)non-void类型函数的所有出口路径都应该有一个明确的return语句表达式。

(15)不要用2级以上指针。

(16)不要轻易使用用Union。

确需使用Union时,一定要注意和清楚在联合体的存储方式(如位填充、对齐方式、位顺序等)上,所使用编译器的处理方法。

(17)标准库中的保留标识符,宏和函数不能定义、重定义和undefined。

(18)时刻注意表达式是否会上溢、下溢。

示例:如下程序将造成变量下溢。
unsigned char size ;

while (size-- >= 0) // 将出现下溢
{
... // program code
}
当size等于0时,再减1不会小于0,而是0xFF,故程序是一个死循环。应如下修改。
char size; // 从unsigned char 改为char
while (size-- >= 0)
{
... // program code
}

(19)使用变量时要注意其边界值的情况。

示例:如C语言中字符型变量,有效值范围为-128到127。故以下表达式的计算存在一定风险。
char chr = 127;
int sum = 200;
chr += 1; // 127为chr的边界值,再加1将使chr上溢到-128,而不是128。
sum += chr; // 故sum的结果不是328,而是72。
若chr与sum为同一种类型,或表达式按如下方式书写,可能会好些。
sum = sum + chr + 1;

(20)系统应具有一定的容错能力,对一些错误事件(如用户误操作等)能进行自动补救。

非计算机专业C语言初学者编程规范(学生用)—宏

(1)用宏定义表达式时,要使用完备的括号。

示例:如下定义的宏都存在一定的风险。
#define RECTANGLE_AREA( a, b ) a * b
#define RECTANGLE_AREA( a, b ) (a * b)
#define RECTANGLE_AREA( a, b ) (a) * (b)

正确的定义应为:
#define RECTANGLE_AREA( a, b ) ((a) * (b))

(2)将宏所定义的多条表达式放在大括号中。

示例:下面的语句只有宏的第一条表达式被执行。为了说明问题,for语句的书写稍不符规范。
#define INTI_RECT_VALUE( a, b )\
a = 0;\
b = 0;
for (index = 0; index < RECT_TOTAL_NUM; index++)
INTI_RECT_VALUE( rect.a, rect.b );

正确的用法应为:
#define INTI_RECT_VALUE( a, b )\
{\
a = 0;\
b = 0;\
}
for (index = 0; index < RECT_TOTAL_NUM; index++)
{
INTI_RECT_VALUE( rect[index].a, rect[index].b );
}

(3)使用宏时,不允许参数发生变化。

示例:如下用法可能导致错误。
#define SQUARE( a ) ((a) * (a))
int a = 5;
int b;
b = SQUARE( a++ ); // 结果:a = 7,即执行了两次增1。

正确的用法是:
b = SQUARE( a );
a++; // 结果:a = 6,即只执行了一次增1。

华为C语言编程规范(1)—总则

编程规范总则

1 排版
2 注释
3 标识符命名
4 可读性
5 变量、结构
6 函数、过程
7 程序效率
8 质量保证1-1:程序块要采用缩进风格编写,缩进的空格数为4 个。

说明:对于由开发工具自动生成的代码可以有不一致。

1-2:相对独立的程序块之间、变量说明之后必须加空行。

示例:如下例子不符合规范。
if (!valid_ni(ni))
{
... // program code
}
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
应如下书写
if (!valid_ni(ni))
{
... // program code
}
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
Generated by Foxit PDF Creator © Foxit Software
http://www.foxitsoftware.com For evaluation only.

1-3:较长的语句(>80 字符)要分成多行书写,长表达式要在低优先级操作符处划分新行,操作符放在新行之首,划分出的新行要进行适当的缩进,使排版整齐,语句可读。

示例:
perm_count_msg.head.len = NO7_TO_STAT_PERM_COUNT_LEN+ STAT_SIZE_PER_FRAM * sizeof( _UL );
act_task_table[frame_id * STAT_TASK_CHECK_NUMBER + index].occupied= stat_poi[index].occupied;
act_task_table[taskno].duration_true_or_false= SYS_get_sccp_statistic_state( stat_item );
report_or_not_flag = ((taskno < MAX_ACT_TASK_NUMBER)&& (n7stat_stat_item_valid (stat_item))&& (act_task_table[taskno].result_data != 0));

1-4:不允许把多个短语句写在一行中,即一行只写一条语句。

示例:如下例子不符合规范。
rect.length = 0; rect.width = 0;
应如下书写
rect.length = 0;
rect.width = 0;

1-5:if、for、do、while、case、switch、default 等语句自占一行,且if、for、do、while

等语句的执行语句部分无论多少都要加括号{}。
示例:如下例子不符合规范。
if (pUserCR == NULL) return;
应如下书写:
if (pUserCR == NULL)
{
return;
}

1-6:对齐只使用空格键,不使用TAB 键。

说明:以免用不同的编辑器阅读程序时,因TAB 键所设置的空格数目不同而造成程序布局不整齐,不要使用BC 作为编辑器合版本,因为BC 会自动将8 个空格变为一个TAB 键,因此使用BC 合入的版本大多会将缩进变乱。

1-7:函数或过程的开始、结构的定义及循环、判断等语句中的代码都要采用缩进风格,case

语句下的情况处理语句也要遵从语句缩进要求。

1-8:程序块的分界符(如C/C++语言的大括号‘{’和‘}’)应各独占一行并且位于同一列,同时与引用它们的语句左对齐。在函数体的开始、类的定义、结构的定义、枚举的定义以及if、for、do、while、switch、case 语句中的程序都要采用如上的缩进方式。

示例:如下例子不符合规范。
for (...) {
... // program code
}
if (...)
{
... // program code
}
void example_fun( void )
{
... // program code
}
应如下书写。
for (...)
{
... // program code
}
if (...)
{
... // program code
}
void example_fun( void )
{
... // program code
}

1-9:一行程序以小于80 字符为宜,不要写得过长。

华为C语言编程规范(3)—注释

2-1:一般情况下,源程序有效注释量必须在20%以上。

说明:注释的原则是有助于对程序的阅读理解,在该加的地方都加了,注释不宜太多也不能太少,注释语言必须准确、易懂、简洁。

2-2:文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者、内容、功能、修改日志等。

示例:下面这段头文件的头注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。

/*****************************************************************************
Copyright: 1988-1999, Huawei Tech. Co., Ltd.
File name: 文件名
Description: 用于详细说明此程序文件完成的主要功能,与其他模块或函数的接口,输出值、取值范围、含义及参数间的控制、顺序、独立或依赖等关系
Author: 作者
Version: 版本
Date: 完成日期
History: 修改历史记录列表, 每条修改记录应包括修改日期、修改者及修改内容简述。
*****************************************************************************/
2-3:函数头部应进行注释,列出:函数的目的/功能、输入参数、输出参数、返回值、调用关系(函数、表)等。

示例:下面这段函数的注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。
/*************************************************
Function: // 函数名称
Description: // 函数功能、性能等的描述
Calls: // 被本函数调用的函数清单
Called By: // 调用本函数的函数清单
Table Accessed: // 被访问的表(此项仅对于牵扯到数据库操作的程序)
Table Updated: // 被修改的表(此项仅对于牵扯到数据库操作的程序)
Input: // 输入参数说明,包括每个参数的作// 用、取值说明及参数间关系。
Output: // 对输出参数的说明。
Return: // 函数返回值的说明
Others: // 其它说明
*************************************************/
2-4:边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。

2-5:注释的内容要清楚、明了,含义准确,防止注释二义性。说明:错误的注释不但无益反而有害。

2-6:注释应与其描述的代码相近,对代码的注释应放在其上方或右方(对单条语句的注释)相邻位置,不可放在下面,如放于上方则需与其上面的代码用空行隔开。

示例:如下例子不符合规范。
例1:
/* get replicate sub system index and net indicator */
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
例2:
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
/* get replicate sub system index and net indicator */

应如下书写
/* get replicate sub system index and net indicator */
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;

2-7:对于所有有物理含义的变量、常量,如果其命名不是充分自注释的,在声明时都必须加以注释,说明其物理含义。变量、常量、宏的注释应放在其上方相邻位置或右方。

示例:
/* active statistic task number */
#define MAX_ACT_TASK_NUMBER 1000
#define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number */

2-8:数据结构声明(包括数组、结构、类、枚举等),如果其命名不是充分自注释的,必须加以注释。对数据结构的注释应放在其上方相邻位置,不可放在下面;对结构中的每个域的注释放在此域的右方。

示例:可按如下形式说明枚举/数据/联合结构。
/* sccp interface with sccp user primitive message name */
enum SCCP_USER_PRIMITIVE
{
N_UNITDATA_IND, /* sccp notify sccp user unit data come */
N_NOTICE_IND, /* sccp notify user the No.7 network can not */
/* transmission this message */
N_UNITDATA_REQ, /* sccp user's unit data transmission request*/
};

2-9:全局变量要有较详细的注释,包括对其功能、取值范围、哪些函数或过程存取它以及存取时注意事项等的说明。

示例:
/* The ErrorCode when SCCP translate */
/* Global Title failure, as follows */ // 变量作用、含义
/* 0 - SUCCESS 1 - GT Table error */
/* 2 - GT error Others - no use */ // 变量取值范围
/* only function SCCPTranslate() in */
/* this modual can modify it, and other */
/* module can visit it through call */
/* the function GetGTTransErrorCode() */ // 使用方法
BYTE g_GTTranErrorCode;

2-10:注释与所描述内容进行同样的缩排。

说明:可使程序排版整齐,并方便注释的阅读与理解。示例:如下例子,排版不整齐,阅读稍感不方便。
void example_fun( void )
{
/* code one comments */
CodeBlock One
/* code two comments */
CodeBlock Two
}

应改为如下布局。
void example_fun( void )
{
/* code one comments */
CodeBlock One
/* code two comments */
CodeBlock Two
}

2-11:避免在一行代码或表达式的中间插入注释。

说明:除非必要,不应在代码或表达中间插入注释,否则容易使代码可理解性变差。

2-12:通过对函数或过程、变量、结构等正确的命名以及合理地组织代码的结构,使代码成为自注释的。

说明:清晰准确的函数、变量等的命名,可增加代码可读性,并减少不必要的注释。

2-13:在代码的功能、意图层次上进行注释,提供有用、额外的信息。

说明:注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。

示例:如下注释意义不大。
/* if receive_flag is TRUE */
if (receive_flag)

而如下的注释则给出了额外有用的信息。
/* if mtp receive a message from links */
if (receive_flag)

2-14:在程序块的结束行右方加注释标记,以表明某程序块的结束。

说明:当代码段较长,特别是多重嵌套时,这样做可以使代码更清晰,更便于阅读。示例:参见如下例子。
if (...)
{
// program code
while (index < MAX_INDEX)
{
// program code
} /* end of while (index < MAX_INDEX) */ // 指明该条while 语句结束
} /* end of if (...)*/ // 指明是哪条if 语句结束

2-15:注释格式尽量统一,建议使用“/* …… */”。

2-16:注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达。

说明:注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。


3-1:标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解。

说明:较短的单词可通过去掉“元音”形成缩写;较长的单词可取单词的头几个字母形成缩写;一些单词有大家公认的缩写。示例:如下单词的缩写能够被大家基本认可。
temp 可缩写为 tmp ;
flag 可缩写为 flg ;
statistic 可缩写为 stat ;
increment 可缩写为 inc ;
message 可缩写为 msg ;

3-2:命名中若使用特殊约定或缩写,则要有注释说明。

说明:应该在源文件的开始之处,对文件中所使用的缩写或约定,特别是特殊的缩写,进行必要的注释说明。

3-3:自己特有的命名风格,要自始至终保持一致,不可来回变化。

说明:个人的命名风格,在符合所在项目组或产品组的命名规则的前提下,才可使用。(即命名规则中没有规定到的地方才可有个人命名风格)。

3-4:对于变量命名,禁止取单个字符(如i、j、k...),建议除了要有具体含义外,还能表明其变量类型、数据类型等,但i、j、k 作局部循环变量是允许的。

说明:变量,尤其是局部变量,如果用单个字符表示,很容易敲错(如i 写成j),而编译时又检查不出来,有可能为了这个小小的错误而花费大量的查错时间。

示例:下面所示的局部变量名的定义方法可以借鉴。
int liv_Width
其变量名解释如下:
l 局部变量(Local) (其它:g 全局变量(Global)...)
i 数据类型(Interger)
v 变量(Variable) (其它:c 常量(Const)...)
Width 变量含义
这样可以防止局部变量与全局变量重名。

3-5:命名规范必须与所使用的系统风格保持一致,并在同一项目中统一,比如采用UNIX的全小写加下划线的风格或大小写混排的方式,不要使用大小写与下划线混排的方式,用作特殊标识如标识成员变量或全局变量的m_和g_,其后加上大小写混排的方式是允许的。

示例: Add_User 不允许,add_user、AddUser、m_AddUser 允许。

3-6:除非必要,不要用数字或较奇怪的字符来定义标识符。

示例:如下命名,使人产生疑惑。
#define _EXAMPLE_0_TEST_
#define _EXAMPLE_1_TEST_
void set_sls00( BYTE sls );

应改为有意义的单词命名
#define _EXAMPLE_UNIT_TEST_
#define _EXAMPLE_ASSERT_TEST_
void set_udt_msg_sls( BYTE sls );

3-7:在同一软件产品内,应规划好接口部分标识符(变量、结构、函数及常量)的命名,防止编译、链接时产生冲突。

说明:对接口部分的标识符应该有更严格限制,防止冲突。如可规定接口部分的变量与常量之前加上“模块”标识等。

3-8:用正确的反义词组命名具有互斥意义的变量或相作的函数等。

说明:下面是一些在软件中常用的反义词组。
add / remove begin / end create / destroy
insert / delete first / last get / release
increment / decrement put / get
add / delete lock / unlock open / close
min / max old / new start / stop
next / previous source / target show / hide
send / receive source / destination
cut / paste up / down

示例:
int min_sum;
int max_sum;
int add_user( BYTE *user_name );
int delete_user( BYTE *user_name );

3-9:除了编译开关/头文件等特殊应用,应避免使用_EXAMPLE_TEST_之类以下划线开始和结尾的定义。 4-1:注意运算符的优先级,并用括号明确表达式的操作顺序,避免使用默认优先级。

说明:防止阅读程序时产生误解,防止因默认的优先级与设计思想不符而导致程序出错。

示例:下列语句中的表达式
word = (high << 8) | low (1)
if ((a | b) && (a & c)) (2)
if ((a | b) < (c & d)) (3)

如果书写为
high << 8 | low
a | b && a & c
a | b < c & d

由于
high << 8 | low = ( high << 8) | low,
a | b && a & c = (a | b) && (a & c),
(1)(2)不会出错,但语句不易理解;a | b < c & d = a | (b < c) & d,(3)造成了判断条件出错。

4-2:避免使用不易理解的数字,用有意义的标识来替代。

涉及物理状态或者含有物理意义的常量,不应直接使用数字,必须用有意义的枚举或宏来代替。示例:如下的程序可读性差。
if (Trunk[index].trunk_state == 0)
{
Trunk[index].trunk_state = 1;
... // program code
}

应改为如下形式。
#define TRUNK_IDLE 0
#define TRUNK_BUSY 1
if (Trunk[index].trunk_state == TRUNK_IDLE)
{
Trunk[index].trunk_state = TRUNK_BUSY;
... // program code
}

4-3:源程序中关系较为紧密的代码应尽可能相邻。

说明:便于程序阅读和查找。

示例:以下代码布局不太合理。
rect.length = 10;
char_poi = str;
rect.width = 5;

若按如下形式书写,可能更清晰一些。
rect.length = 10;
rect.width = 5; // 矩形的长与宽关系较密切,放在一起。
char_poi = str;

4-4:不要使用难懂的技巧性很高的语句,除非很有必要时。

说明:高技巧语句不等于高效率的程序,实际上程序的效率关键在于算法。

示例:如下表达式,考虑不周就可能出问题,也较难理解。
* stat_poi ++ += 1;
* ++ stat_poi += 1;

应分别改为如下。
*stat_poi += 1;
stat_poi++; // 此二语句功能相当于“ * stat_poi ++ += 1; ”
++ stat_poi;
*stat_poi += 1; // 此二语句功能相当于“ * ++ stat_poi += 1; ” 5-1:去掉没必要的公共变量。

说明:公共变量是增大模块间耦合的原因之一,故应减少没必要的公共变量以降低模块间的耦合度。

5-2:仔细定义并明确公共变量的含义、作用、取值范围及公共变量间的关系。

说明:在对变量声明的同时,应对其含义、作用及取值范围进行注释说明,同时若有必要还应说明与其它变量的关系。

5-3:明确公共变量与操作此公共变量的函数或过程的关系,如访问、修改及创建等。

说明:明确过程操作变量的关系后,将有利于程序的进一步优化、单元测试、系统联调以及代码维护等。这种关系的说明可在注释或文档中描述。

示例:在源文件中,可按如下注释形式说明。
RELATION System_Init Input_Rec Print_Rec Stat_ScoreStudent Create Modify Access AccessScore Create Modify Access Access, Modify

注:RELATION 为操作关系;System_Init、Input_Rec、Print_Rec、Stat_Score 为四个不同的函数;Student、Score 为两个全局变量;Create 表示创建,Modify 表示修改,Access 表示访
问。其中,函数Input_Rec、Stat_Score 都可修改变量Score,故此变量将引起函数间较大的耦合,并可能增加代码测试、维护的难度。


5-4:当向公共变量传递数据时,要十分小心,防止赋与不合理的值或越界等现象发生。

说明:对公共变量赋值时,若有必要应进行合法性检查,以提高代码的可靠性、稳定性。

5-5:防止局部变量与公共变量同名。

说明:若使用了较好的命名规则,那么此问题可自动消除。

5-6:严禁使用未经初始化的变量作为右值。

说明:特别是在C/C++中引用未经赋值的指针,经常会引起系统崩溃。

5-7:结构的功能要单一,是针对一种事务的抽象。

说明:设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。

示例:如下结构不太清晰、合理。
typedef struct STUDENT_STRU
{
unsigned char name[8]; /* student's name */
unsigned char age; /* student's age */
unsigned char sex; /* student's sex, as follows */
/* 0 - FEMALE; 1 - MALE */
unsigned char
teacher_name[8]; /* the student teacher's name */
unisgned char
teacher_sex; /* his teacher sex */
} STUDENT;

若改为如下,可能更合理些。
typedef struct TEACHER_STRU
{
unsigned char name[8]; /* teacher name */
unisgned char sex; /* teacher sex, as follows */
/* 0 - FEMALE; 1 - MALE */
} TEACHER;
typedef struct STUDENT_STRU
{
unsigned char name[8]; /* student's name */
unsigned char age; /* student's age */
unsigned char sex; /* student's sex, as follows */
/* 0 - FEMALE; 1 - MALE */
unsigned int teacher_ind; /* his teacher index */
} STUDENT;

5-8:不要设计面面俱到、非常灵活的数据结构。

说明:面面俱到、灵活的数据结构反而容易引起误解和操作困难。

5-9:不同结构间的关系不要过于复杂。

说明:若两个结构间关系较复杂、密切,那么应合为一个结构。

示例:如下两个结构的构造不合理。
typedef struct PERSON_ONE_STRU
{
unsigned char name[8];
unsigned char addr[40];
unsigned char sex;
unsigned char city[15];
} PERSON_ONE;
typedef struct PERSON_TWO_STRU
{
unsigned char name[8];
unsigned char age;
unsigned char tel;
} PERSON_TWO;

由于两个结构都是描述同一事物的,那么不如合成一个结构。
typedef struct PERSON_STRU
{
unsigned char name[8];
unsigned char age;
unsigned char sex;
unsigned char addr[40];
unsigned char city[15];
unsigned char tel;
} PERSON;

5-10:结构中元素的个数应适中。若结构中元素个数过多可考虑依据某种原则把元素组成不同的子结构,以减少原结构中元素的个数。

说明:增加结构的可理解性、可操作性和可维护性。

示例:假如认为如上的_PERSON 结构元素过多,那么可如下对之划分。
typedef struct PERSON_BASE_INFO_STRU
{
unsigned char name[8];
unsigned char age;
unsigned char sex;
} PERSON_BASE_INFO;
typedef struct PERSON_ADDRESS_STRU
{
unsigned char addr[40];
unsigned char city[15];
unsigned char tel;
} PERSON_ADDRESS;
typedef struct PERSON_STRU
{
PERSON_BASE_INFO person_base;
PERSON_ADDRESS person_addr;
} PERSON;

5-11:仔细设计结构中元素的布局与排列顺序,使结构容易理解、节省占用空间,并减少引起误用现象。

说明:合理排列结构中元素顺序,可节省空间并增加可理解性。

示例:如下结构中的位域排列,将占较大空间,可读性也稍差。
typedef struct EXAMPLE_STRU
{
unsigned int valid: 1;
PERSON person;
unsigned int set_flg: 1;
} EXAMPLE;
若改成如下形式,不仅可节省1 字节空间,可读性也变好了。
typedef struct EXAMPLE_STRU
{
unsigned int valid: 1;
unsigned int set_flg: 1;
PERSON person ;
} EXAMPLE;

5-12:编程时,要注意数据类型的强制转换。

说明:当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患。

5-13:对编译系统默认的数据类型转换,也要有充分的认识。

示例:如下赋值,多数编译器不产生告警,但值的含义还是稍有变化。
char chr;
unsigned short int exam;
chr = -1;
exam = chr; // 编译器不产生告警,此时exam 为0xFFFF。

5-14:尽量减少没有必要的数据类型默认转换与强制转换。

5-15:合理地设计数据并使用自定义数据类型,避免数据间进行不必要的类型转换。

5-16:对自定义数据类型进行恰当命名,使它成为自描述性的,以提高代码可读性。注意其命名方式在同一产品中的统一。

说明:使用自定义类型,可以弥补编程语言提供类型少、信息量不足的缺点,并能使程序清晰、简洁。

示例:可参考如下方式声明自定义数据类型。下面的声明可使数据类型的使用简洁、明了。
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned int DWORD;

下面的声明可使数据类型具有更丰富的含义。
typedef float DISTANCE;
typedef float SCORE;6-1:对所调用函数的错误返回码要仔细、全面地处理。

6-2:明确函数功能,精确(而不是近似)地实现函数设计。

6-3:编写可重入函数时,应注意局部变量的使用(如编写C/C++语言的可重入函数时,应使用auto 即缺省态局部变量或寄存器变量)。

说明:编写C/C++语言的可重入函数时,不应使用static 局部变量,否则必须经过特殊处理,才能使函数具有可重入性。

6-4:编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V 操作)等手段对其加以保护。

说明:若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。

示例:假设Exam 是int 型全局变量,函数Squre_Exam 返回Exam 平方值。那么如下函数不具有可重入性。
unsigned int example( int para )
{
unsigned int temp;
Exam = para; // (**)
temp = Square_Exam( );
return temp;
}

此函数若被多个进程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的进程可能正好被激活,那么当新激活的进程执行到此函数时,将使Exam赋与另一个不同的para 值,所以当控制重新回到“temp = Square_Exam( )”后,计算出的temp很可能不是预想中的结果。此函数应如下改进。
unsigned int example( int para )
{
unsigned int temp;
[申请信号量操作] // 若申请不到“信号量”,说明另外的进程正处于Exam = para; // 给Exam 赋值并计算其平方过程中(即正在使用此temp = Square_Exam( ); // 信号),本进程必须等待其释放信号后,才可继[释放信号量操作]
// 续执行。若申请到信号,则可继续执行,但其
// 它进程必须等待本进程释放信号量后,才能再使
// 用本信号。
return temp;
}

6-5:在同一项目组应明确规定对接口函数参数的合法性检查应由函数的调用者负责还是由接口函数本身负责,缺省是由函数调用者负责。

说明:对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率。

6-6:函数的规模尽量限制在200 行以内。

说明:不包括注释和空格行。

6-7:一个函数仅完成一件功能,不要设计多用途面面俱到的函数。

说明:多功能集于一身的函数,很可能使函数的理解、测试、维护等变得困难。

6-8:函数的功能应该是可以预测的,也就是只要输入数据相同就应产生同样的输出。

说明:带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在C/C++语言中,函数的static 局部变量是函数的内部存储器,有可能使函数的功能不可预测,然而,当某函数的返回值为指针类型时,则必须是STATIC的局部变量的地址作为返回值,若为AUTO类,则返回为错针。

示例:如下函数,其返回值(即功能)是不可预测的。
unsigned int integer_sum( unsigned int base )
{
unsigned int index;
static unsigned int sum = 0; // 注意,是static 类型的。
// 若改为auto 类型,则函数即变为可预测。
for (index = 1; index <= base; index++)
{
sum += index;
}
return sum;
}

6-9:尽量不要编写依赖于其他函数内部实现的函数。

说明:此条为函数独立性的基本要求。由于目前大部分高级语言都是结构化的,所以通过具体语言的语法要求与编译器功能,基本就可以防止这种情况发生。但在汇编语言中,由于其灵活性,很可能使函数出现这种情况。

示例:如下是在DOS 下TASM 的汇编程序例子。过程Print_Msg 的实现依赖于Input_Msg的具体实现,这种程序是非结构化的,难以维护、修改。
... // 程序代码
proc Print_Msg // 过程(函数)Print_Msg
... // 程序代码
jmp LABEL
... // 程序代码
endp
proc Input_Msg // 过程(函数)Input_Msg
... // 程序代码
LABEL:
... // 程序代码
endp

6-10:检查函数所有参数输入的有效性。

6-11:检查函数所有非参数输入的有效性,如数据文件、公共变量等。

说明:函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入之前,应进行必要的检查。

6-12:函数名应准确描述函数的功能。

6-13:使用动宾词组为执行某操作的函数命名。如果是OOP 方法,可以只有动词(名词是对象本身)。

示例:参照如下方式命名函数。
void print_record( unsigned int rec_ind ) ;
int input_record( void ) ;
unsigned char get_current_color( void ) ;

6-14:避免使用无意义或含义不清的动词为函数命名。

说明:避免用含义不清的动词如process、handle 等为函数命名,因为这些动词并没有说明要具体做什么。

6-15:函数的返回值要清楚、明了,让使用者不容易忽视错误情况。

说明:函数的每种出错返回值的意义要清晰、明了、准确,防止使用者误用、理解错误或忽视错误返回码。

6-16:除非必要,最好不要把与函数返回值类型不同的变量,以编译系统默认的转换方式或强制的转换方式作为返回值返回。

6-17:让函数在调用点显得易懂、容易理解。

6-18:在调用函数填写参数时,应尽量减少没有必要的默认数据类型转换或强制数据类型转换。

说明:因为数据类型转换或多或少存在危险。

6-19:避免函数中不必要语句,防止程序中的垃圾代码。

说明:程序中的垃圾代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可能给程序的测试、维护等造成不必要的麻烦。

6-20:防止把没有关联的语句放到一个函数中。

说明:防止函数或过程内出现随机内聚。随机内聚是指将没有关联或关联很弱的语句放到同一个函数或过程中。随机内聚给函数或过程的维护、测试及以后的升级等造成了不便,同时也使函数或过程的功能不明确。使用随机内聚函数,常常容易出现在一种应用场合需要改进此函数,而另一种应用场合又不允许这种改进,从而陷入困境。在编程时,经常遇到在不同函数中使用相同的代码,许多开发人员都愿把这些代码提出来,并构成一个新函数。若这些代码关联较大并且是完成一个功能的,那么这种构造是合理的,否则这种构造将产生随机内聚的函数。

示例:如下函数就是一种随机内聚。
void Init_Var( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的长与宽 */
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标 */
}
矩形的长、宽与点的坐标基本没有任何关系,故以上函数是随机内聚。
应如下分为两个函数:
void Init_Rect( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的长与宽 */
}
void Init_Point( void )
{
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标 */
}

6-21:如果多段代码重复做同一件事情,那么在函数的划分上可能存在问题。

说明:若此段代码各语句之间有实质性关联并且是完成同一件功能的,那么可考虑把此段代码构造成一个新的函数。

6-22:功能不明确较小的函数,特别是仅有一个上级函数调用它时,应考虑把它合并到上级函数中,而不必单独存在。

说明:模块中函数划分的过多,一般会使函数间的接口变得复杂。所以过小的函数,特别是扇入很低的或功能不明确的函数,不值得单独存在。

6-23:设计高扇入、合理扇出(小于7)的函数。

说明:扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,如总是1,表明函数的调用层次可能过多,这样不利程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。函数较合理的扇出(调度函数除外)通常是3-5。扇出太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数进一步分解多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。
扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。

6-24:减少函数本身或函数间的递归调用。

说明:递归调用特别是函数间的递归调用(如A->B->C->A),影响程序的可理解性;递归调用一般都占用较多的系统资源(如栈空间);递归调用对程序的测试有一定影响。故除非为某些算法或功能的实现方便,应减少没必要的递归调用。

6-26:改进模块中函数的结构,降低函数间的耦合度,并提高函数的独立性以及代码可读性、效率和可维护性。优化函数结构时,要遵守以下原则:
(1)不能影响模块功能的实现。
(2)仔细考查模块或函数出错处理及模块的性能要求并进行完善。
(3)通过分解或合并函数来改进软件结构。
(4)考查函数的规模,过大的要进行分解。
(5)降低函数间接口的复杂度。
(6)不同层次的函数调用要有较合理的扇入、扇出。
(7)函数功能应可预测。
(8)提高函数内聚。(单一功能的函数内聚最高)

说明:对初步划分后的函数结构应进行改进、优化,使之更为合理。

6-27:在多任务操作系统的环境下编程,要注意函数可重入性的构造。

说明:可重入性是指函数可以被多个任务进程调用。在多任务操作系统中,函数是否具有可重入性是非常重要的,因为这是多个进程可以共用此函数的必要条件。另外,编译器是否提供可重入函数库,与它所服务的操作系统有关,只有操作系统是多任务时,编译器才有可能提供可重入函数库。如DOS 下BC 和MSC 等就不具备可重入函数库,因为DOS 是单用户单任务操作系统。

6-28:避免使用BOOL 参数。

说明:原因有二,其一是BOOL 参数值无意义,TURE/FALSE 的含义是非常模糊的,在调用时很难知道该参数到底传达的是什么意思;其二是BOOL 参数值不利于扩充。还有NULL也是一个无意义的单词。

6-29: 对于提供了返回值的函数,在引用时最好使用其返回值。

6-30:当一个过程(函数)中对较长变量(一般是结构的成员)有较多引用时,可以用一个意义相当的宏代替。

说明:这样可以增加编程效率和程序的可读性。
示例:在某过程中较多引用TheReceiveBuffer[FirstSocket].byDataPtr,则可以通过以下宏定义来代替:
# define pSOCKDATA TheReceiveBuffer[FirstScoket].byDataPtr7-1:编程时要经常注意代码的效率。

说明:代码效率分为全局效率、局部效率、时间效率及空间效率。全局效率是站在整个系统的角度上的系统效率;局部效率是站在模块或函数角度上的效率;时间效率是程序处理输入任务所需的时间长短;空间效率是程序所需内存空间,如机器代码空间大小、数据空间大小、栈空间大小等。

7-2:在保证软件系统的正确性、稳定性、可读性及可测性的前提下,提高代码效率。

说明:不能一味地追求代码效率,而对软件的正确性、稳定性、可读性及可测性造成影响。

7-3:局部效率应为全局效率服务,不能因为提高局部效率而对全局效率造成影响。

7-4:通过对系统数据结构的划分与组织的改进,以及对程序算法的优化来提高空间效率。

说明:这种方式是解决软件空间效率的根本办法。

示例:如下记录学生学习成绩的结构不合理。
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef struct STUDENT_SCORE_STRU
BYTE name[8];
BYTE age;
BYTE sex;
BYTE class;
BYTE subject;
float score;
} STUDENT_SCORE;
因为每位学生都有多科学习成绩,故如上结构将占用较大空间。应如下改进(分为两个结构),总的存贮空间将变小,操作也变得更方便。
typedef struct STUDENT_STRU
{
BYTE name[8];
BYTE age;
BYTE sex;
BYTE class;
} STUDENT;
typedef struct STUDENT_SCORE_STRU
{
WORD student_index;
BYTE subject;
float score;
} STUDENT_SCORE;

7-5:循环体内工作量最小化。
说明:应仔细考虑循环体内的语句是否可以放在循环体之外,使循环体内工作量最小,从而提高程序的时间效率。

示例:如下代码效率不高。
for (ind = 0; ind < MAX_ADD_NUMBER; ind++)
{
sum += ind;
back_sum = sum; /* backup sum */
}
语句“back_sum = sum;”完全可以放在for 语句之后,如下。
for (ind = 0; ind < MAX_ADD_NUMBER; ind++)
{
sum += ind;
}
back_sum = sum; /* backup sum */

7-6:仔细分析有关算法,并进行优化。仔细考查、分析系统及模块处理输入(如事务、消息等)的方式,并加以改进。

7-7:对模块中函数的划分及组织方式进行分析、优化,改进模块中函数的组织结构,提高程序效率。

说明:软件系统的效率主要与算法、处理任务方式、系统功能及函数结构有很大关系,仅在代码上下功夫一般不能解决根本问题。

7-8:编程时,要随时留心代码效率;优化代码时,要考虑周全。

7-9:不应花过多的时间拼命地提高调用不很频繁的函数代码效率。

说明:对代码优化可提高效率,但若考虑不周很有可能引起严重后果。

7-10:要仔细地构造或直接用汇编编写调用频繁或性能要求极高的函数。

说明:只有对编译系统产生机器码的方式以及硬件系统较为熟悉时,才可使用汇编嵌入方式。嵌入汇编可提高时间及空间效率,但也存在一定风险。

7-11:在保证程序质量的前提下,通过压缩代码量、去掉不必要代码以及减少不必要的局部和全局变量,来提高空间效率。

说明:这种方式对提高空间效率可起到一定作用,但往往不能解决根本问题。

7-12:在多重循环中,应将最忙的循环放在最内层。

说明:减少CPU 切入循环层的次数。
示例:如下代码效率不高。
for (row = 0; row < 100; row++)
{
for (col = 0; col < 5; col++)
{
sum += a[row][col];
}
}

可以改为如下方式,以提高效率。
for (col = 0; col < 5; col++)
{
for (row = 0; row < 100; row++)
{
sum += a[row][col];
}
}

7-13:尽量减少循环嵌套层次。

7-14:避免循环体内含判断语句,应将循环语句置于判断语句的代码块之中。

说明:目的是减少判断次数。循环体中的判断语句是否可以移到循环体外,要视程序的具体情况而言,一般情况,与循环变量无关的判断语句可以移到循环体外,而有关的则不可以。

示例:如下代码效率稍低。
Generated by Foxit PDF Creator © Foxit Software
http://www.foxitsoftware.com For evaluation only.
- 22 -
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
if (data_type == RECT_AREA)
{
area_sum += rect_area[ind];
}
else
{
rect_length_sum += rect[ind].length;
rect_width_sum += rect[ind].width;
}
}

因为判断语句与循环变量无关,故可如下改进,以减少判断次数。
if (data_type == RECT_AREA)
{
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
area_sum += rect_area[ind];
}
}
else
{
for (ind = 0; ind < MAX_RECT_NUMBER; ind++)
{
rect_length_sum += rect[ind].length;
rect_width_sum += rect[ind].width;
}
}

7-15:尽量用乘法或其它方法代替除法,特别是浮点运算中的除法。

说明:浮点运算除法要占用较多CPU 资源。
示例:如下表达式运算可能要占较多CPU 资源。
#define PAI 3.1416
radius = circle_length / (2 * PAI);

应如下把浮点除法改为浮点乘法。
#define PAI_RECIPROCAL (1 / 3.1416 ) // 编译器编译时,将生成具体浮点数
radius = circle_length * PAI_RECIPROCAL / 2;

7-16:不要一味追求紧凑的代码。
说明:因为紧凑的代码并不代表高效的机器码。8-1:在软件设计过程中构筑软件质量。

8-2:代码质量保证优先原则
(1)正确性,指程序要实现设计要求的功能。
(2)稳定性、安全性,指程序稳定、可靠、安全。
(3)可测试性,指程序要具有良好的可测试性。
(4)规范/可读性,指程序书写风格、命名规则等要符合规范。
(5)全局效率,指软件系统的整体效率。
(6)局部效率,指某个模块/子模块/函数的本身效率。
(7)个人表达方式/个人方便性,指个人编程习惯。

8-3:只引用属于自己的存贮空间。

说明:若模块封装的较好,那么一般不会发生非法引用他人的空间。

8-4:防止引用已经释放的内存空间。

说明:在实际编程过程中,稍不留心就会出现在一个模块中释放了某个内存块(如C 语言指针),而另一模块在随后的某个时刻又使用了它。要防止这种情况发生。

8-5:过程/函数中分配的内存,在过程/函数退出之前要释放。

8-6:过程/函数中申请的(为打开文件而使用的)文件句柄,在过程/函数退出之前要关闭。

说明:分配的内存不释放以及文件句柄不关闭,是较常见的错误,而且稍不注意就有可能发生。这类错误往往会引起很严重后果,且难以定位。

示例:下函数在退出之前,没有把分配的内存释放。
typedef unsigned char BYTE;
int example_fun( BYTE gt_len, BYTE *gt_code )
{
BYTE *gt_buf;
gt_buf = (BYTE *) malloc (MAX_GT_LENGTH);
... //program code, include check gt_buf if or not NULL.
/* global title length error */
if (gt_len > MAX_GT_LENGTH)
{
return GT_LENGTH_ERROR; // 忘了释放gt_buf
}
... // other program code
}

应改为如下。
int example_fun( BYTE gt_len, BYTE *gt_code )
{
BYTE *gt_buf;
gt_buf = (BYTE * ) malloc ( MAX_GT_LENGTH );
... // program code, include check gt_buf if or not NULL.
/* global title length error */
if (gt_len > MAX_GT_LENGTH)
{
free( gt_buf ); // 退出之前释放gt_buf
return GT_LENGTH_ERROR;
}
... // other program code
}

8-7:防止内存操作越界。

说明:内存操作主要是指对数组、指针、内存地址等的操作。内存操作越界是软件系统主要错误之一,后果往往非常严重,所以当我们进行这些操作时一定要仔细小心。

示例:假设某软件系统最多可由10 个用户同时使用,用户号为1-10,那么如下程序存在问题。
#define MAX_USR_NUM 10
unsigned char usr_login_flg[MAX_USR_NUM]= "";
void set_usr_login_flg( unsigned char usr_no )
{
if (!usr_login_flg[usr_no])
{
usr_login_flg[usr_no]= TRUE;
}
}

当usr_no 为10 时,将使用usr_login_flg 越界。可采用如下方式解决。
void set_usr_login_flg( unsigned char usr_no )
{
if (!usr_login_flg[usr_no - 1])
{
usr_login_flg[usr_no - 1]= TRUE;
}
}

8-8:认真处理程序所能遇到的各种出错情况。

8-9:系统运行之初,要初始化有关变量及运行环境,防止未经初始化的变量被引用。

8-10:系统运行之初,要对加载到系统中的数据进行一致性检查。

说明:使用不一致的数据,容易使系统进入混乱状态和不可知状态。

8-11:严禁随意更改其它模块或系统的有关设置和配置。

说明:编程时,不能随心所欲地更改不属于自己模块的有关设置如常量、数组的大小等。

8-12:不能随意改变与其它模块的接口。

8-13:充分了解系统的接口之后,再使用系统提供的功能。

示例:在B 型机的各模块与操作系统的接口函数中,有一个要由各模块负责编写的初始化过程,此过程在软件系统加载完成后,由操作系统发送的初始化消息来调度。因此就涉及到初始化消息的类型与消息发送的顺序问题,特别是消息顺序,若没搞清楚就开始编程,很容易引起严重后果。以下示例引自B 型曾出现过的实际代码,其中使用了FID_FETCH_DATA与FID_INITIAL 初始化消息类型,注意B 型机的系统是在FID_FETCH_DATA 之前发送FID_INITIAL 的。

MID alarm_module_list[MAX_ALARM_MID];
int FAR SYS_ALARM_proc( FID function_id, int handle )
{
_UI i, j;
switch ( function_id )
{
... // program code
case FID_INITAIL:
for (i = 0; i < MAX_ALARM_MID; i++)
{
if (alarm_module_list[i]== BAM_MODULE // **)
|| (alarm_module_list[i]== LOCAL_MODULE)
{
for (j = 0; j < ALARM_CLASS_SUM; j++)
{
FAR_MALLOC( ... );
}
}
}
... // program code
break;
case FID_FETCH_DATA:
... // program code
Get_Alarm_Module( ); // 初始化alarm_module_list
break;
... // program code
}
}
由于FID_INITIAL 是在FID_FETCH_DATA 之前执行的,而初始化alarm_module_list 是在FID_FETCH_DATA 中进行的,故在FID_INITIAL 中(**)处引用alarm_module_list 变量时,它还没有被初始化。这是个严重错误。应如下改正:要么把Get_Alarm_Module 函数放在FID_INITIAL 中(**)之前;要么就必须考虑(**)处的判断语句是否可以用(不使用alarm_module_list 变量的)其它方式替代,或者是否可以取消此判断语句。

8-14:编程时,要防止差1 错误。

说明:此类错误一般是由于把“<=”误写成“<”或“>=”误写成“>”等造成的,由此引起的后果,很多情况下是很严重的,所以编程时,一定要在这些地方小心。当编完程序后,应对这些操作符进行彻底检查。

8-15:要时刻注意易混淆的操作符。当编完程序后,应从头至尾检查一遍这些操作符,以防止拼写错误。

说明:形式相近的操作符最容易引起误用,如C/C++中的“=”与“==”、“|”与“||”、“&”与“&&”等,若拼写错了,编译器不一定能够检查出来。

示例:如把“&”写成“&&”,或反之。
ret_flg = (pmsg->ret_flg & RETURN_MASK);
被写为:
ret_flg = (pmsg->ret_flg && RETURN_MASK);
rpt_flg = (VALID_TASK_NO( taskno ) && DATA_NOT_ZERO( stat_data ));
被写为:
rpt_flg = (VALID_TASK_NO( taskno ) & DATA_NOT_ZERO( stat_data ));

8-16:有可能的话,if 语句尽量加上else 分支,对没有else 分支的语句要小心对待;switch语句必须有default 分支。

8-17:Unix 下,多线程的中的子线程退出必需采用主动退出方式,即子线程应return 出口。

8-18:不要滥用goto 语句。

说明:goto 语句会破坏程序的结构性,所以除非确实需要,最好不使用goto 语句。

8-19:精心地构造、划分子模块,并按“接口”部分及“内核”部分合理地组织子模块,以提高“内核”部分的可移植性和可重用性。

说明:对不同产品中的某个功能相同的模块,若能做到其内核部分完全或基本一致,那么无论对产品的测试、维护,还是对以后产品的升级都会有很大帮助。

8-20:精心构造算法,并对其性能、效率进行测试。

8-21:对较关键的算法最好使用其它算法来确认。

8-22:时刻注意表达式是否会上溢、下溢。

示例:如下程序将造成变量下溢。
unsigned char size ;
while (size-- >= 0) // 将出现下溢
{
... // program code
}
当size 等于0 时,再减1 不会小于0,而是0xFF,故程序是一个死循环。应如下修改。
char size; // 从unsigned char 改为char
while (size-- >= 0)
{
... // program code
}

8-23:使用变量时要注意其边界值的情况。

示例:如C 语言中字符型变量,有效值范围为-128 到127。故以下表达式的计算存在一定风险。
char chr = 127;
int sum = 200;
chr += 1; // 127 为chr 的边界值,再加1 将使chr 上溢到-128,而不是128。
sum += chr; // 故sum 的结果不是328,而是72。
若chr 与sum 为同一种类型,或表达式按如下方式书写,可能会好些。
sum = sum + chr + 1;

8-24:留心程序机器码大小(如指令空间大小、数据空间大小、堆栈空间大小等)是否超出系统有关限制。

8-25:为用户提供良好的接口界面,使用户能较充分地了解系统内部运行状态及有关系统出错情况。

8-26:系统应具有一定的容错能力,对一些错误事件(如用户误操作等)能进行自动补救。

8-27:对一些具有危险性的操作代码(如写硬盘、删数据等)要仔细考虑,防止对数据、硬件等的安全构成危害,以提高系统的安全性。

8-28:使用第三方提供的软件开发工具包或控件时,要注意以下几点:
(1)充分了解应用接口、使用环境及使用时注意事项。
(2)不能过分相信其正确性。
(3)除非必要,不要使用不熟悉的第三方工具包与控件。
说明:使用工具包与控件,可加快程序开发速度,节省时间,但使用之前一定对它有较充分的了解,同时第三方工具包与控件也有可能存在问题。

8-29:资源文件(多语言版本支持),如果资源是对语言敏感的,应让该资源与源代码文件脱离,具体方法有下面几种:使用单独的资源文件、DLL 文件或其它单独的描述文件(如数据库格式)9-1:打开编译器的所有告警开关对程序进行编译。

9-2:在产品软件(项目组)中,要统一编译开关选项。

9-3:通过代码走读及审查方式对代码进行检查。

说明:代码走读主要是对程序的编程风格如注释、命名等以及编程时易出错的内容进行检查,可由开发人员自己或开发人员交叉的方式进行;代码审查主要是对程序实现的功能及程序的稳定性、安全性、可靠性等进行检查及评审,可通过自审、交叉审核或指定部门抽查等方式进行。

9-4:测试部测试产品之前,应对代码进行抽查及评审。

9-5:编写代码时要注意随时保存,并定期备份,防止由于断电、硬盘损坏等原因造成代码丢失。

9-6:同产品软件(项目组)内,最好使用相同的编辑器,并使用相同的设置选项。

说明:同一项目组最好采用相同的智能语言编辑器,如Muiti Editor,Visual Editor 等,并设计、使用一套缩进宏及注释宏等,将缩进等问题交由编辑器处理。

9-7:合理地设计软件系统目录,方便开发人员使用。

说明:方便、合理的软件系统目录,可提高工作效率。目录构造的原则是方便有关源程序的存储、查询、编译、链接等工作,同时目录中还应具有工作目录----所有的编译、链接等工作应在此目录中进行,工具目录----有关文件编辑器、文件查找等工具可存放在此目录中。

9-8:某些语句经编译后产生告警,但如果你认为它是正确的,那么应通过某种手段去掉告警信息。

说明:在Borland C/C++中,可用“#pragma warn”来关掉或打开某些告警。

示例:
#pragma warn -rvl // 关闭告警
int examples_fun( void )
{
// 程序,但无return 语句。
}
#pragma warn +rvl // 打开告警
编译函数examples_fun 时本应产生“函数应有返回值”告警,但由于关掉了此告警信息显
示,所以编译时将不会产生此告警提示。

9-9:使用代码检查工具(如C 语言用PC-Lint)对源程序检查。

9-10:使用软件工具(如 LogiSCOPE)进行代码审查。

10-1:单元测试要求至少达到语句覆盖。

10-2:单元测试开始要跟踪每一条语句,并观察数据流及变量的变化。

10-3:清理、整理或优化后的代码要经过审查及测试。

10-4:代码版本升级要经过严格测试。

10-5:使用工具软件对代码版本进行维护。

10-6:正式版本上软件的任何修改都应有详细的文档记录。

10-7:发现错误立即修改,并且要记录下来。

10-8:关键的代码在汇编级跟踪。

10-9:仔细设计并分析测试用例,使测试用例覆盖尽可能多的情况,以提高测试用例的效率。

10-11:尽可能模拟出程序的各种出错情况,对出错处理代码进行充分的测试。

10-12:仔细测试代码处理数据、变量的边界情况。

10-13:保留测试信息,以便分析、总结经验及进行更充分的测试。

10-14:不应通过“试”来解决问题,应寻找问题的根本原因。

10-15:对自动消失的错误进行分析,搞清楚错误是如何消失的。

10-16:修改错误不仅要治表,更要治本。

10-17:测试时应设法使很少发生的事件经常发生。

10-18:明确模块或函数处理哪些事件,并使它们经常发生。

10-19: 坚持在编码阶段就对代码进行彻底的单元测试,不要等以后的测试工作来发现问题。

10-20:去除代码运行的随机性(如去掉无用的数据、代码及尽可能防止并注意函数中的“内部寄存器”等),让函数运行的结果可预测,并使出现的错误可再现。11-1:用宏定义表达式时,要使用完备的括号。

示例:如下定义的宏都存在一定的风险。
#define RECTANGLE_AREA( a, b ) a * b
#define RECTANGLE_AREA( a, b ) (a * b)
#define RECTANGLE_AREA( a, b ) (a) * (b)

正确的定义应为:
#define RECTANGLE_AREA( a, b ) ((a) * (b))

11-2:将宏所定义的多条表达式放在大括号中。

示例:下面的语句只有宏的第一条表达式被执行。为了说明问题,for 语句的书写稍不符规范。
#define INTI_RECT_VALUE( a, b )\
a = 0;\
b = 0;
for (index = 0; index < RECT_TOTAL_NUM; index++)
INTI_RECT_VALUE( rect.a, rect.b );

正确的用法应为:
#define INTI_RECT_VALUE( a, b )\
{\
a = 0;\
b = 0;\
}
for (index = 0; index < RECT_TOTAL_NUM; index++)
{
INTI_RECT_VALUE( rect[index].a, rect[index].b );
}

11-3:使用宏时,不允许参数发生变化。

示例:如下用法可能导致错误。
#define SQUARE( a ) ((a) * (a))
int a = 5;
int b;
b = SQUARE( a++ ); // 结果:a = 7,即执行了两次增1。
正确的用法是:
b = SQUARE( a );
a++; // 结果:a = 6,即只执行了一次增1。

应该在变量名中使用下划线吗?

在变量名中使用下划线是一种风格。使用或完全不使用下划线都没有错误,重要的是要保持一致性——在整个程序中使用相同的命名规则。这就是说,如果你在一个小组环境中编程,你和其它小组成员应该制定一种命名规则。并自始至终使用这种规则。如果有人使用了别的命名规则,那么集成的程序读起来将是很费劲的。此外,你还要与程序中用到的第三方库(如果有的话)所使用的风格保持一致。如果可能的话,你应该尽量使用与第三方库相同的命名规则,这将加强你的程序的可读性和一致性。

许多C程序员发现在命名变量时使用下划线是很方便的,这可能是因为加下划线会大大加强可读性。例如,下面的两个函数名是相似的,但使用下划线的函数名的可读性更强:
check disk space available(selected disk drive);
CheckDiskSpaceAvailable (Selected Disk Drive);
上例中的第二个函数名使用了骆驼式命名法——见19.5中关于骆驼式的解释。

可以用变量名来指示变量的数据类型吗?

可以。在变量名中指出数据类型已经成为今天的大型复杂系统中普遍使用的一条规则。通常,变量类型由一个或两个字符表示,并且这些字符将作为变量名的前缀。使用这一技术的一种广为人知的命名规则就是匈牙利命名法,它的名称来自于Microsoft公司的程序员CharlesSimonyi。表19.2列出了一些常用的前缀。
表1 9.2一些常用的匈牙利命名法前缀
---------------------------------------------------------------------------------
数据类型 前缀 例子
---------------------------------------------------------------------------------
char c clnChar
int i iReturnValue
longl lNumRecs
string sz szlnputString ( 以零字节结束 )
int array ai aiErrorNumbers
char * psz pszInputString
---------------------------------------------------------------------------------
象Microsoft Windows这样的环境,就大量使用了匈牙利命名法或其派生体。其它一些第四代环境,例如Visual Basic和Access,也采用了匈牙利命名法的一种变体。

在编写程序时,你不必拘泥于一种特定的命名法——你完全可以建立自己的派生命名法,特别是在为自己的typedef命名时。例如,有一个名为SOURCEFILE的typedef,用来保存源文件名、句柄、行号、最后编译日期和时间、错误号等信息。你可以引入一个类似“sf”(sourcefile)的前缀符号,这样,当你看到一个名为sfBuffer的变量时,你就会知道该变量保存了SOURCEFILE结构中的部分内容。

不管怎样,在命名变量或函数时,引入某种形式的命名规则是一个好主意,尤其是当你与其它程序员共同开发一个大的项目时,或者在Microsoft Windows这样的环境下工作时。采用一种认真设计好的命名规则将有助于增强你的程序的可读性,尤其是当你的程序非常复杂时。

使用注释、空白符会影响程序的速度、大小或效率吗?

使用注释会影响程序的速度、大小或效率吗?

不会。当你的程序被编译时,所有的注释都会被忽略掉,只有可执行的语句才会被分析,并且被放入最终编译所得的程序版本中。

因为注释并不会给程序的速度、大小或效率带来负担,所以你应该尽量多使用注释。你应该在每一个程序模块的头部都加上一段注释,以解释该模块的作用和有关的特殊事项。同样,你也要为每一个函数加上注释,其中应包括作者姓名、编写日期、修改日期和原因、参数使用指导、函数描述等信息。这些信息将帮助其它程序员更好地理解你的程序,也有助于你以后回忆起一些关键的实现思想。

在源代码中也应该使用注释(在语句之间)。例如,如果有一部分代码比较复杂,或者你觉得有些地方要进一步说明,你就应该毫不犹豫地在这些代码中加入注释。这样做可能会花费一点时间,但这将为你或其它人节省几个小时的宝贵时间,因为只需看一下注释,人们就能马上知道你的意图。

在19.4中有一个例子,它表明了使用注释、空白符和下划线命名规则是如伺使程序更清晰、更易懂的。

使用空白符会影响程序的速度、大小或效率吗?

不会。与注释一样,所有的空白符都会被编译程序忽略掉。当你的程序被编译时,所有的空白符都会忽略掉,只有可执行的语句才会被分析,并且被放入最终编译所得的程序版本中。

在C程序中用空白符隔开可执行语句、函数和注释等,将有助于提高程序的可读性和清晰度。许多时候,只要在语句之间加入空行,就可提高程序的可读性。请看下面的这段代码:
/ * clcpy by GBlansten * /
void clcpy(EMP * e,int rh,int ot)
{ e->grspy= (e->rt * rh)+ (e->rt * ot * 1.5) ;
e->txamt = e->grspy * e->txrt ;
e->ntpy = e->grspy-e->txamt ;
updacctdata (e);
if (e->dd= =false) cutpyck(e) ;
else prtstb (e) ; }
你可以看到,这个函数确实是一团糟。尽管这个函数显然是正确的,但恐怕这个世界上没有一个程序员愿意去维护这样的代码。如果采用本章中的一些命名规则(例如使用下划线,少用一些短而模糊的名字),使用一些大括号技巧,加入一些空白符和注释,那么这个函数将会是下面这个样子:
/********************************************************************************/
Function Name: calc_pay
Parameters: emp -EMPLOYEE pointer that points to employee data
reg-hours -The number of regular hours (<=40) employee
has worked
ot-hours -The number of overtime hours (>40) employee
has worked
Author : Gern Blansten
Date Written: 13 dec 1993
Modification: 04 sep 1994 by Lloyd E. Work
-Rewrote function to make it readable by human beings.
Description: This function calculates an employee's gross bay ,tax
amount, and net pay, and either prints a paycheck for the
employee or (in the case of those who have direct deposit)

prints a paycheck stub.
/*********************************************************************************/
void calc_pay (EMPLOYEE * emp, int reg hours, int or_hours)
{
/ * gross_pay = (employee rate * regular hours)+
(employee rate * overtime hours * 1.5) * /
emp->gross_pay= (emp->rate * reg_hours) +
(emp->rate * ot hours* 1.5);
/ * tax amount=gross_pay * employee's tax rate * /
emp->tax amount=emp->gross_pay * emp->tax-rate ;
/ * net pay=gross pay-tax amount * /
emp->net-pay=emp->gross pay-emp->tax_amount ;
/ * update the accounting data * /
update accounting data(emp);
/ * check for direct deposit * /
if (emp->direct_deposit= =false)
cut_ paycheck(emp); / * print a paycheck * /
else
print_paystub(emp); /* print a paycheck stub * /
}
你可以看到,Lloyd版本(该版本中使用了大量的注释、空行、描述性变量名等)的可读性比糟糕的Gern版本要强得多。你应该尽量在你认为合适的地方使用空白符(和注释符等),这将大大提高程序的可读性一一当然,这可能会延长你的工作时间。

什么是骆驼式命名法?

骆驼式命令法,正如它的名称所表示的那样,是指混合使用大小写字母来构成变量和函数的名字。例如,下面是分别用骆驼式命名法和下划线法命名的同一个函数:
PrintEmployeePaychecks();
print employee paychecks();
第一个函数名使用了骆驼式命名法——函数名中的每一个逻辑断点都有一个大写字母来标记;第二个函数名使用了下划线法----函数名中的每一个逻辑断点都有一个下划线来标记。

骆驼式命名法近年来越来越流行了,在许多新的函数库和Microsoft Windows这样的环境中,它使用得当相多。另一方面,下划线法是c出现后开始流行起来的,在许多旧的程序和UNIX这样的环境中,它的使用非常普遍。

较长的变量名会影响程序的速度、大小或效率吗?

不会,当你的程序被编译时,每一个变量名和函数名都会被转换为一个“符号”——对原变量或原函数的一种较短的符号性的表示。因此,无论你使用下面的哪一个函数名,结果都是一样的:
PrintOutAllTheClientsMonthEndReports();
prt_rpts();
一般说来,你应该使用描述性的函数名和变量名,这样可以加强程序的可读性。你可以查阅编译程序文档,看一下允许有多少个有效字符,大多数ANSI编译程序允许有至少31个有效字符。也就是说,只有变量名或函数名的前31个字符的唯一性会被检查,其余的字符将被忽略掉。

一种较好的经验是使函数名或变量名读起来符合英语习惯,就好象你在读一本书一样——人们应该能读懂你的函数名或变量名,并且能很容易地识别它们并知道它们的大概作用。

给函数命名的正确方法是什么?

函数名一般应该以一个动词开始,以一个名词结束,这种方法符合英语的一般规则。下面列出了几个命名比较合适的函数:
PrintReports();
SpawnUtilityProgram();
ExitSystem();
Initia|izeDisk():

请注意,在这些例子中,函数名都以一个动词开始,以一个名词结束。如果按英语习惯来读这些函数名,你会发现它们其实就是:
print the reports(打印报告)
spawn the utility program(生成实用程序)
exit the system(退出系统)
initialize the disk(初始化磁盘)
使用动词一名词规则(特别是在英语国家)能有效地加强程序的可读性,并且使程序看起来更熟悉。

使用大括号的正确方法是什么?

在C中,使用大括号的方法无所谓对还是错——只要每个开括号后都有一个闭括号,你的程序中就不再会出现与大括号有关的问题。然而,有三种著名的大括号格式经常被使用:
Kernighan和Ritchie,Allman,Whitesmiths。下文中将讨论这三种格式。

在《C程序设计语言(The C Programming Language)》一书中,Brian Kernighan和Dennis Ritchie介绍了他们所使用的大括号格式,这种格式如下所示:
if (argc<3) {
printf (" Error! Not enough arguments. Correct usage is ..\n" ) ;
printf("c:>eopyfile <source_file> <destination_file>\n") ;
exit (1) ;
}
else {
open_files () ;
while (! feof(infile)) {
read_data ( ) ;
write_data() ;
}
close files() ;
}

注意,在Kb&R格式中,开括号总是与使用它的语句在同一行上,而闭括号总是在它所关闭的语句的下一行上,并且与该语句对齐。例如,在上例中,if语句的开括号和它在同一行上,|f语句的闭括号在它的下一行上,并且与它对齐。在与if语句对应的else条件语句以及出现在程序段后部的while语句中,情况也是这样的。

下面是用Allman格式书写的同一个例子:
if (argc<3)
{
printf("Error! Not enough arguments. Correct usage is :\n" ) ;
printf("C:>copyfile <source_file> <destination_file>\n") ;
exit(1);
}
else
{
open_files ( );
while (! feof(infile))
{
read_data ( ) ;
write data();
}
close_files() ;
}
注意,在Allman格式中,每个大括号都单独成行,并且开括号和闭括号都与使用它们的语句对齐。

下面是用Whitesmiths格式书写的同一个例子:
if (argc<3)
{
printf("Error! Not enough arguments, Correct usage is :\n" );
printf ("C :> copyfile<source_file><destination_file>\n." ) ;
exit(1);
}
else
{
open files () ;
while (! feof(infile))
{
read_data() ;
write data();
}
close files () ;
}
与Allman格式相同,Whitesmiths格式也要求大括号单独成行,但是它们要和它们所包含的语句对齐。例如,在上例中,if语句的开括号是与第一个printf()函数调用对齐的。

不管你使用哪一种格式,一定要保持前后一致——这将有助于你自己或其它人更方便地读你的程序。

一个变量名应该使用多少个字母?ANSI标准允许有多少个有效字符?

一般说来,变量名或函数名应该足够长,以有效地描述所命名的变量或函数。应该避免使用短而模糊的名字,因为它们在别人理解你的程序时会带来麻烦。例如,不要使用象这样的短而模糊的函数名:
opndatfls();
而应该使用更长一些的函数名,象下面这样:
open data_files();
或者:
OpenDataFiles();
这对变量名也是同样适用的。例如,与其使用这样一个短而模糊的变量名:
fmem
不如将其扩展为完整的描述:
free_memory_available
使用扩展了的名字会使程序更易读,更易理解。大多数ANSI编译程序允许有至少31个有效字符——即只有变量或函数名的前31个字符的唯一性会被检查。一种较好的经验是使函数名或变量名读起来符合英语习惯,就好象你在读一本书一样一人们应该能读懂你的函数名或变量名,并且能很容易地识别它们并知道它们的大概作用。

什么是匈牙利式命名法?应该使用它吗?

匈牙利命名法是由Microsoft公司的程序员Charles Simonyi(无疑是个匈牙利人的后裔)提出的。在这种命名法中,变量名或函数名要加上一个或两个字符的前缀,用来表示变量或函数的数据类型。

这种命名法有许多好处,它被广泛应用于象Microsoft Windows这样的环境中。关于匈牙利命名法的详细解释以及其中的一些命名标准。

C语言重复处理是什么

重复处理是指反复执行相同的程序语句,但可能会在满足某个条件时退出循环。c语言提供了一些现成的结构来实现重复处理,例如while循环,do…while循环和for循环。在这些结构中,当某个条件为真时,预先定义的一批语句将被反复执行。下面是一个重复处理的例子:
while (x<lO0)
{
y=O;
do {
for(z =O;z<lOO;z++)
y++ ;
}while (y<1000) ;
x++;
}

在这个例子中,包含在while循环中的语句被执行100次,在while循环中还有一个do…while循环,在do…whlie循环中的for循环被执行10次;在for循环中,变量y作100次自增运算。因此,语句
y++;
总共被执行100,000次(100次while×10次do…while×100次for)。然而,在while循环结束时,y并不是100,000,因为每循环1000次后y都会被置为o。

在c程序中,重复处理的应用是非常广泛的,例如你经常要用重复处理来读写数组或文件。下面的程序用重复处理来读入并向屏幕打印你的AUTOEXEC.BAT文件:
# include <stdio. h>
# include <stdlib. h>
int main(viod) ;
int main (void)
{
FILE * autoexec_file ;
char buffer[250] ;
if ( (autoexec_file = fopen (" C : \\ AUTOEXEC. BAT", "rt ") ) = = NULL )
{
{printf (stderr,"Cannot open AUTOEXEC. BAT file. \n") ;
exit(1) ;
printf("Contents of AUTOEXEC. BAT file : \n\n" ) ;
while(! feof(autoexec file))
{
fgets (buffer, 200,autoexee_file) ;
printf(" %s" ,buffer) ;
}
felose (autoexee_file) ;
rerun(0) ;
}
注意,上例用一条while语句来反复地调用fgets()和printf()函数,以读入AUTOEXEC.BAT文件中的每一行,并把它们打印到屏幕上。这仅仅是如何使用重复处理的例子之一。

什么是C语言递归(recursion)?怎样使用递归?

在c语言中,一个调用自身(不管是直接地还是间接地)的函数被称为是递归的(recursive)。你可能不明白究竟为什么一个函数要调用自身,要解释这种情况,最好先举一个例子。一个经典的马上可以举出来的例子就是计算整数的阶乘。为了计算一个整数的阶乘值,你要用这个数(x)乘以它的前一个数(X一1),并且一直乘下去,直到到达1。例如,5的阶乘可以这样来计算:
5*4*3*2*1
如果X是5,你可以把这个算式转换为一个等式:
X!=X*(X-1)*(X-2)*(X-3)*(X-4)*1

为了用C语言完成这项计算任务,你可以编写一个名为calc_factorial()的函数,它反复调用自身,每次都把被计算的值减1,直到到达1。下面的例子说明怎样编写这个calc_factorial()函数:
#include<stdio.h>
void main(void);
unsigned long calc_factorial(unsigned long x);
void main(void)
{
int x=5;
printf("The factorial of %d is %ld. \n" ,x,calc_factorial(x));
}
unsigned long calc_factorial(unsigned long x)
{
if(! x)
return 1L ;
return(x * calc_factorial(x-1L)) ;
}
在上例中,calc_factorial()在调用自身前要先把x的值减去1。如果x等于O,if语句的条件将为真,calc factorial()将不再被递归调用。因此,当被计算的值到达O时,calc_factorial()作完最后一次递归调用并退出,其返回值为1。返回1是因为任何值都可以安全地和1相乘,并仍能保持其原来的值。如果程序中包含下述语句:
x=calc_factorial(5);
它将开展为:
x=5*(5-1)*(4-1)*(3-1)*(2-1)*1;
因此,x将等于5的阶乘,即120。

递归是一个简洁的概念,同时也是一种很有用的手段。但是,使用递归是要付出代价的。与直接的语句(如while循环)相比,递归函数会耗费更多的运行时间,并且要占用大量的栈空间。递归函数每次调用自身时,都需要把它的状态存到栈中,以便在它调用完自身后,程序可以返回到它原来的状态。未经精心设计的递归函数总是会带来麻烦。

如果可能的话,你应该避免使用递归函数。例如,前文中的阶乘函数可以写成下面这种形式:
# include <stdio. h>
void main(void) ;
unsigned long calc factorial(unsigned long x);
void main (void)
{
int x=5;
printf("The factorial of %d is %ld. \n" ,x ,calc_factorial (x)) ;
}
unsigned long calc-factorial(unsigned long x)
{
unsigned long factorial;
factorial=x;
while (x>1L)
{
factorial * =--x;
}
return (factorial);
}
这个版本的calc_factorial()函数用一个while循环来计算一个值的阶乘,它不仅比递归版本快得多,而且只占用很小的栈空间。

在C语言中,表示真和假的最好方法是什么?

在c语言中,任何等于零的东西都被认为是假,任何等于非零值的东西都被认为是真,因此,最常见的定义就是假为O,真为1。许多程序中都包含了具有如下定义的头文件:
#define FALSE O
#define TRUE 1
如果你在编写Windows程序,你应该注意头文件windows.h中的TRUE和FALSE的确切定义。上述真和假的定义方式非常普遍,并且是完全可以接受的,然而,还有其它几种定义方式,请看下例:
#define FALSE 0
#define TRUE !FALSE
上例把FALSE定义为0,把TRUE定义为非零值。注意,即使是负数,如-1,也是非零值,因此也被认为是真。

另一种定义方式是建立自己的枚举类型,如Boolean(或者BOOL),如下所示:
enurn BOOL{
FALSE,
TRUE
};
正象你所可能已经知道的那样,在缺省情况下,枚举类型的第一个元素被赋值为O,因此,在上述枚举定义中,FALSE被赋值为0,TRUE被赋值为1。与使用符号常量相比,使用枚举类型有它的一些好处,详见5.6和5.7中的有关内容。

哪种方法最好呢?这个问题没有一个唯一的答案。如果你在编写一个Windows程序,那么TRUE和FALSE都是已经为定义好的,你没有必要再建立自己的定义,在其它情况下,你可以从前文所介绍的几种方法中选择一种。

C语言空循环和无穷循环有的区别

空循环并不会无休止地进行下去——在重复预先指定的次数后,它就会退出循环。无穷循环会无休止地进行下去,并且永远不会退出循环。把空循环和无穷循环对比一下,就能很好地说明它们之间的区别。

下面是一个空循环的例子:
for(x=O;x<500000;x++);
注意,在上例中,在for循环的闭括号后直接加入了一个分号。正如你可能已经知道的那样,c语言并不要求在for循环后加分号,通常只有包含在for循环中的语句后面才会带分号。

在for循环后面直接加入分号(并且不使用大括号),即可建立一个空循环——实际上是一个不包含任何语句的循环。在上例中,当for循环执行时,变量x将自增500,000次,而在每一次自增运算期间,没有进行任何处理。

那么,空循环有什么用呢?在大多数情况下,它的作用就是在程序中设置一次暂停。前面的例子将使程序“暂停”一段时间,即计算机数到500,000所需的时间。然而,空循环还有更多的用处,请看下例:
while(!kbhit());
这个例子用一个空循环来等待一次击键操作。当程序需要显示类似"Press Any Key ToContinue"这样的信息时,这种方法是很有用的(假设你的用户很聪明,不会执着地在键盘上寻找"Any Key"!)。

无穷循环与空循环不同,它永远不会结束。下面是一个无穷循环的例子:
while(1);
在这个例子中,while语句中包含了一个非零常量,因此,while的条件永远为真,循环永远不会结束。注意,在闭括号后面直接加入一个分号,因此while语句中不包含任何其它语句,循环将无法终止(除非终止程序)。

C语言continue和break有的区别

continue语句用来返回循环的起始处,而break语句用来退出循环。例如,下例中就有一条典型的continue语句:
while(!feof(infile))
{
fread(inbuffer,80,1,infile);/*read in a line from input file*/
if(!strncmpi(inbuffer,"REM",3)) /*check if it is
a comment line*/
continue; /*it's a comment,so jump back to the while()*/
else
parse_line(); /*not a comment—parse this line*/
}
上例读入一个文件并对其进行分析。“REM(remark的缩写)”用来标识正在被处理的文件中的一个注释行。因为注释行对程序不起任何作用,所以可以跳过它。在读入输入文件的每一行时,上例就把该行的前三个字母与"REM"进行比较。如果匹配,则该行就是注释行,于是就用continue语句返回到while语句,继续读入输入文件的下一行;否则,该行就是一条有效语句,于是就调用parse_line()函数对其进行分析。

break语句用来退出循环。下面是一个使用break语句的例子:
while (! feof(infile))
fread(inbuffer,80,1,infile) ;/* read in a line from input file * /
if (! strncmpi (inbuffer,"REM",3)) / * check if it is
a comment line* /
continue; /* it's a comment, so jump back to the while() * /
else
{
if (parse_line()==FATAL_ERROR) / * attempt to parse
this line* /
break; /* fatal error occurred,so exit the loop * /
}
这个例子建立在使用continue语句的那个例子的基础上。注意,在这个例子中,要检查parse_line()函数的返回值。如果parse_line()的返回值为FATAL_ERROR,就通过break语句立即退出while循环,并将控制权交给循环后面的第一条语句。

华为C语言编程规范(4)—标识符命名


9 代码编辑、编译、审查
10 代码测试、维护
11 宏

华为C语言编程规范(2)—排版


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值