牛骨文教育服务平台(让学习变的简单)

捌(预处理、程序调试、编程风格)

预处理

C预处理器是一种简单的宏处理器。它在编译器读取源程序之前对C程序的源文本进行处理。预处理器一般从源文件中删除所有的预处理器命令行,并在源文件中执行这些预处理命令所指定的转换操作。

【宏只是进行简单的文本替换】

续行:

所有的源文件行(包括预处理器命令行)都可以在行末加个反斜杠( )进行续行。这个操作发生在对预处理器命令进行扫描之前。

【注意:续行符反斜杠之后不能有任何字符,尤其注意检查不能有空格等空白符。】

普通宏定义:

#define 命令有两种形式,取决于被定义的宏名后面是不是紧随一个左括号。若没有左括号,则为无参宏定义。

无参宏定义常用于:

1、在程序中引入名称常量。这样,可以在一个地方编写,然后通过名称在其它地方被引用,这样,以后修改这个数字就非常方便了。

2、改变外部定义的函数名或变量名。(有些外部函数的函数名过于简短或是与当前程序的命名风格不符,我们可以用宏定义一个新的函数名来代替它)

例:#define  error_handler  eh73

//我们用一个更具描述性的函数名error_handler来表示外部函数eh73

带参数的宏:

左括号必须紧随宏名之后,中间不能有空格。如果宏名和左括号之间被一个空格所分隔,则这个宏被定义为不接受任何参数,并且宏体从左括号开始。

注意

1、为了保证宏展开的正确性,应该给每个宏参数加上括号,且给整个表达式也加上括号。

(多余的括号保证了复杂的实际参数不会被编译器错误的解释)

2、使用类似函数的宏,可能存在一些陷阱。我们在调用宏函数时,会习惯地加一个分号,而额外的分号可能引发错误。

例:#define  SWAP(type, x, y)  { type _temp=x; x=y; y=_temp; }

若 if( x > y) SWAP(int , x, y) ;

      elsex = y ;

//这将产生错误,宏展开后有一个多余的分号,将导致else悬空。

★为了避免这个问题,可以把宏函数体定义为一条do-while语句,后者可以接受在末尾添加分号。


#define SWAP(type, x, y)  

do { type _temp=x; x=y; y=_temp; } while(0)

3、宏参数的副作用。(当宏参数含++、--操作符时一定要小心)

例:#define SQUARE(x)  ((x)*(x))

若 b =SQUARE(a++) ;       //则结果是未定义的,因为(a++)*(a++)的行为取决于编译器。

真正的函数调用不会出现这样的问题,真正的函数调用是先计算参数值,然后再调用函数。而宏函数,只是简单的文本替换。

【宏是与类型无关的,即其可以用类型做参数。故:宏有时可以完成无法用函数实现的任务】

例:#defineMY_MALLOC( n, type )  ((type)malloc((n)sizeof(type)))

取消宏定义:


# undef命令可以取消定义一个名称为宏:undef  name 


条件编译

条件编译指令允许预处理器根据一个经过计算所得出的条件,来选择不同的语句参加编译。

if  常量表达式

      文本行组1

else

      文本行组2

endif

常量表达式包括整数常量以及所有的整数算术、关系、位和逻辑操作符。

如果它的值不是0,则“文本行组1”则被编译器进行编译,而“文本行组2”则被丢弃。

defined操作符

defined 操作符只能在#if和#elif表达式中使用,而不能用于别处。

形式:definedname 或 defined(name)


#if defined( VAX )  可等同于 #ifdef VAX

但defined的使用更加灵活一些:

例:#ifdefined(VAX) && !defined(UNIX) && debugging

预定义的宏

标准C的预处理器定义了一些宏,这些宏的名称都是以两个下划线字符开始和结束的。程序员不能取消这些预定义宏的定义或对它们进行重新定义。

几个常用的预定义宏:

LINE             当前源程序行的行号,用十进制整数常量表示

FILE              当前源文件的名称,用字符串常量表示

DATA             编译时的日期,用“Mmm dd yyyy”形式的字符串常量表示

TIME             编译时的时间,用“hh:mm:ss”形式的字符串常量表示。

程序调试

一、使用断点和单步执行

详情请参阅具体的IDE使用说明

二、条件编译


#ifdef DEBUG

      printf(“File:%s line:%d, x=%d, y=%d”, __FILE__, __LINE__, x, y ) ;

endif

如果要编译它,只要使用#defineDEBUG 即可,如果要忽略它,注释掉即可。

C99引入了一个预定义标识符:func

这个标识符可以由调试工具使用,打印出外层函数的名称。

例:if(failed)  printf(“Function %s failed ”, func) ;

三、使用断言<assert.h>

断言就是声明某种东西应该为真。(预测某个值为多少,符合条件则继续,否则中止程序)

void assert( int express ) ;

当它被执行时,对表达式参数进行测试。

如果它的值为假(零),它就向标准错误打印一条诊断信息并中止程序。

否则它不打印任何东西,程序继续执行。

例:assert(value != NULL ) ;

//如果它接受了一个NULL参数,则打印类似:assertfailed :value != NULL.file.c line 273

【注意:断言只是在测试阶段,防御性地测试某个变量值的方法,不要再断言中写一些会对程序造成影响的表达式。因为在release版编译器会删除断言,若断言中的表达式对程序有影响,可能会产生错误!】

删除断言

当程序被完整地测试完毕之后,在源文件的头文件assert.h被包含之前,增加定义:


#define NDEBUG

当NDEBUG被定以后,预处理器将会丢弃所有断言。

编程风格:

以下内容摘自《代码大全》

变量命名

该名字要完全、准确地表述出该变量所代表的事物。

一个好名字通常表达的是“什么”(what),而不是“如何”(how)。

如果一个名字反映了计算机的某些方面而不是问题本身,那么它反映的就是“how”而非“what”了,

请避免选取这样的名字,而应该在名字中反映问题本身

(一条员工数据:称作:inputRec或employeeData。inputRec是一个反映输入、记录 这些计算机术语的,不能反映问题特征)

当变量名的长度在10到16个字符时,调试程序所花的力气是最小的。

记住把限定词加到名字最后,变量名最重要的部分,即为变量赋予主要含义的部分应当位于最前面

特例: Num的限定词的位置是约定俗成的。

Num放在变量的开始位置代表一个总数;例:numCustomers表示员工总数

Num放在变量名的结束位置代表一个序号;例:customerNum表示员工号

避免此问题的方法:

用Conut或Total来代表总数,用Index来代表序号

例:customerCount员工总数 customerIndex 员工序号

命名的一致性可提高可读性,简化维护工作。

如果你发现自己需要猜测某段代码的含义时,就该考虑为变量重新命名。

变量名中的对仗词

next/previous

source/destination 

。。。。。

1、为状态变量命名

标记的名字中不应该含有flag。标记应该用枚举类型、具名常量。

dataReady recalaNeeded 都是好名字

2、为布尔变量命名

以下是几个推荐的布尔变量名(可在其前加上具体的描述名称)

done:用done表示某件事已经完成。(在事情完成之前把done设为false,在完成之后设为true)

error:用error表示有错误发生。(在错误发生之前把变量值设置为false,在错误已经发生时把它设置为true)

found:用found来表示某个值已经找到了。(在还没有找到该值的时候把它设为false,找到之后设为true)

success或ok 用来表明一项操作时候成功。

(不要在布尔变量的前面加上Is)

命名规则可以根据局部数据、类数据、全局数据的不同而有所差别。

命名规则可强调相关变量之间的关系

★命名规则的指导原则

      区分变量名和子程序名

      变量名和对象名以小写字母开始,子程序名以大写字母开头。

      区分类和对象

      1、通过对对象采用更明确的名字区分类型和变量

      例:Widget employWidget ;

      2、通过给变量加"a"前缀区分类型和变量

      例:Widget aWidget      ;

      标识全局变量

      在全局变量名前加上"g_"前缀

      标识成员变量

      在成员变量名前加上"m_"前缀,可明确表示该变量既不是局部变量,也不是全局变量。

      标识自定义类型

      在自定义类型名前加上"t_"前缀,可明确表示一个名字是类型名,可避免类型名与变量名的冲突

      标识枚举类型

      在枚举类型名前加"e_"前缀,同时为该类型的成员名增加特定类型的前缀。

      例:Color或Planet

      标识只读参数

      在其前加上 const前缀,可防止给只读变量赋值的错误。例:constMax

变量名要包含以下三类信息

      1、变量的内容(它代表什么)

      2、数据的种类(具名变量、简单变量、用户自定义类型、类)

      3、变量的作用域(局部的、类的、全局的)

     

关于子程序

好的子程序名

      给子程序命名的重点是尽可能含义清晰,即:子程序的长短要视该名字是否清晰易懂而定。

      子程序的名字应当描述其所有输出结果以及副作用。

      (例:一个子程序的作用是计算报表总额并打开一个输出文件。若把它命名为computeReportTotals()还不算完整。computeReportTotalsAndOpenOutputFile()很完整但是名字太长。解决方法是 你应该换一种方式编写程序,直截了当地解决问题而不产生副作用。[即:UNIX的哲学,让一个模块只干一件事!])     

      给子程序起名时要用动词加宾语的形式。例:PrintDocument()

      在面向对象语言中,不必在过程名中加入对象的名字,因为对象本身就已经包含在调用语句中了。例:Document.Print() ;

【子程序的名字是它质量的指示器,如果名字糟糕且又不准确,那么它就反映不出程序是干什么的。糟糕的名字都意味着程序需要修改】

正确地使用输入参数

      1、对于在函数体中不变更的参数,用const关键字来限制。

      2、如果你假定了传递给子程序的参数具有某种特征,那就要对这种假定进行说明。比注释还好的方法是在代码中使用断言(assertions)

[对参数接口的假定进行说明:

1、参数是仅用于输入的、要被修改的、还是仅用于输出的

2、表示数量的参数的单位(英寸,米等)

3、所能接受的数值范围

4、不该出现的特定数值

5、说明状态代码和错误值的含义]

如果你向很多不同的子程序传递数据,就请把这些子程序组成一个类,并把那些经常使用的数据用作类的内部数据。

如果你觉得把输入、修改、输出参数区分开很重要,那么就建立一种命名规则来对它们进行区分。

可在这些参数名之前加上im o_ 前缀。也可以用InputModify Output_ 来当前缀

把对子程序的调用和对状态值的判断清楚地分开。把对子程序的调用和状态值的判断写在一行代码中,增加了该条语句的密度,也相应增加了其复杂度。

应该这样:


ouputStatus = report.FormatOutput(formattedReport ) ;

if( outputStatus = Success ) then ...

关于宏:

通常认为,用宏来代替函数调用的做法具有风险,而且不易理解,因此,除非必要,否则应该避免使用这种技术。

用给子程序命名的方法给宏函数命名,以便在需要时可以用子程序来替换宏。

宏对于支持条件编译非常有用,但对于细心的程序员来说,除非万不得已,否则是不会用宏来代替子程序的。(可用内联函数来实现宏函数的效果)

节制使用inline子程序!

其它

建议在真正需要用空语句时这样写:

NULL ;

而不是单用一个分号,这就好比汇编里面的空指令,这样做可以明显的区分真正必须的空语句和不小心多写的分号。

在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环的次数。

循环要尽可能的短,要使代码清晰,一目了然。

(如果你写的一个循环的代码超过一屏,那么会让读代码的人抓狂的。解决的办法有两个:

第一:重新设计这个循环。确认是否这些操作都必须放在这个循环里;

第二:将这些代码改写成一个子函数。循环中只调用这个子函数即可)

对于全局数据(全局变量、常量定义等)必须要加注释。

注释代码段时应注重“为何做(why)”,而不是“怎么做(how)”

对于函数的入口出口参数及函数的功能给出注释。

如果你的全局变量不用来多文件共享,那么就加上static,防止同一个载入模块的两个不同外部对象的命名冲突。