捌(预处理、程序调试、编程风格)
最后更新于: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,防止同一个载入模块的两个不同外部对象的命名冲突。
';