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

最后更新于:2022-04-01 20:30:15

捌 ***预处理*** 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 \n”, __func__) ; **三、使用断言** 断言就是声明某种东西应该为真。(预测某个值为多少,符合条件则继续,否则中止程序) 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、说明状态代码和错误值的含义] 如果你向很多不同的子程序传递数据,就请把这些子程序组成一个类,并把那些经常使用的数据用作类的内部数据。 如果你觉得把输入、修改、输出参数区分开很重要,那么就建立一种命名规则来对它们进行区分。 可在这些参数名之前加上i_m_ o_ 前缀。也可以用Input_Modify_ Output_ 来当前缀 把对子程序的调用和对状态值的判断清楚地分开。把对子程序的调用和状态值的判断写在一行代码中,增加了该条语句的密度,也相应增加了其复杂度。 应该这样: ~~~ ouputStatus = report.FormatOutput(formattedReport ) ; if( outputStatus = Success ) then ... ~~~ **关于宏:** 通常认为,用宏来代替函数调用的做法具有风险,而且不易理解,因此,除非必要,否则应该避免使用这种技术。 用给子程序命名的方法给宏函数命名,以便在需要时可以用子程序来替换宏。 宏对于支持条件编译非常有用,但对于细心的程序员来说,除非万不得已,否则是不会用宏来代替子程序的。(可用内联函数来实现宏函数的效果) 节制使用inline子程序! **其它**: 建议在真正需要用空语句时这样写: NULL ; 而不是单用一个分号,这就好比汇编里面的空指令,这样做可以明显的区分真正必须的空语句和不小心多写的分号。 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环的次数。 循环要尽可能的短,要使代码清晰,一目了然。 (如果你写的一个循环的代码超过一屏,那么会让读代码的人抓狂的。解决的办法有两个: 第一:重新设计这个循环。确认是否这些操作都必须放在这个循环里; 第二:将这些代码改写成一个子函数。循环中只调用这个子函数即可) 对于全局数据(全局变量、常量定义等)必须要加注释。 注释代码段时应注重“为何做(why)”,而不是“怎么做(how)” 对于函数的入口出口参数及函数的功能给出注释。 如果你的全局变量不用来多文件共享,那么就加上static,防止同一个载入模块的两个不同外部对象的命名冲突。
';