预处理器
1,编绎C程序第一个步骤就是预处理阶段,在源代码编绎之前进行一些文本的操作:
1)删除注释;
2)插入#include指令包含的文件的内容、定义;
3)替换#define指令定义的符号;
4)确定代码是否根据条件编绎指令进行编绎。
2,预处理器定义了一些字符串常量,确认调试输出的来源,在程序中加入版本信息。
__FILE__,编绎的源文件名
__LINE__,文件当前行号
__DATE__,文件编绎日期
__TIME__,文件编绎时间
__STDC__,用于ANSI和非ANSI环境根据条件编绎,如果编绎器遵循ANSI C,值为1,否则未定义。
#define
1,#define可以将任何文本替换到程序中,不仅限于数值字面值常量,如果定义中名字过长可以分成几行,除了最后一行外,每行末尾加一个反斜杠,相邻的字符串常量自动连接为一个字符串。
2,#define可以声明一条调试语句,插入到程序中。
3,#define可以声明一序列语句插入到程序中,如果相同代码出现在程序多个地方,最好的办法还是实现为一个函数。
4,#define宏定义允许把参数替换到文本中,#define name(parameter-list) stuff;
1)parameter-list参数列表是一个逗号分隔的符号列表,参数列表左括号紧跟name,如果存在空白,参数列表会被解释为stuff的一部分。
2)当宏被调用,参数列表中参数替换到stuff中,整个列表使用一对括号,数值表达式求值 时需要正确的加上括号,避免参数中操作符或邻近操作符不可预料的相互作用。
5,宏参数和定义可以包含其他#define定义的符号,但宏不可以出现递归,调用宏时:
1)检查参数,是否包含了#define定义的其他符号,需要进行文本替换。
2)宏定义的替换文本插入到程序中,参数值替换参数名。
3)最后再对结果文本检查,是否包含#define定义的符号,重复以上步骤。
6,当预处理器搜索#define定义的符号时,不检查字符串常量内容,如果想把宏参数插入到字符串常量中,可以使用两种技巧:
1)利用邻近字符串自动连接特性,将字符串分成几段,每段实际上都是一个宏参数,运用在字符串常量作宏参数时。
2)利用预处理器将宏参数转换为字符串特性,结构#argument被预处理器翻译为”argument”,##将位于两边的符号连接成一个符号,允许宏定义从分离的文本片段创建标识符,连接后必须产生一个合法的标识符,否则结果未定义。
宏与函数
1,宏执行的简单计算,可以用函数实现。
1)但函数调用和函数返回的代码可能比计算的代码量都大。
2)而且函数参数必须声明为一种特定的类型,只能在合适的表达式上使用。
3)宏与类型无关。
4)宏定义的代码需要插入到程序中,适用于比较短的宏,否则可能会大幅度增加程序长度。
5)无法使用函数实现的也可以使用宏,比如类型无法作为函数参数传递。
2,带副作用的宏参数,当宏参数在宏定义中出现超过一次,如果参数有副作用,使用宏可能出现危险,导致不可预料结果,比如x++,每次执行都是不同的结果。
3,奇偶校验是一种错误检测机制,在数据存储和通信前,为一个值添加一个校验位,使数据的二进制模式下1的个数为偶数,数据可以通过计算位1的个数验证有效性,结果为奇数,数据错误,称为偶校验,奇校验原理相同。
4,使用宏的语法和函数语法一样,为了区分宏定义和函数,约定宏名称全大写,当宏使用可能副作用的参数时,先将参数存储到临时变量中。
5,#undef预处理指令用于移除一个宏定义,如果一个现存名字需要重新定义,旧定义需要用#undef移除。
6,命令行定义,编绎器允许在命令行中定义符号,用于启动编绎过程。
1)当同一源文件需要编绎不同版本时,-Dname=stuff,可在命令行中指定符号的值,例如程序声明了一个数组,某个机器内存有限,数组必须较小,但在另一个内存充裕的机器上,数组可以大一些,编绎程序时数组长度在命令行中指定。
2)提供符号命令行的编绎器同样提供在命令行中去除符号,-Uname忽略符号name的定义,与条件编绎结合使用。
3)Unix编绎器提供-D、-U的编绎选项。
条件编绎
1,编绎程序时,可以选择忽略某条或某组语句,通常应用于调试程序,不从源代码中物理删除,但不出现在程序的产品版本中,调试时需要这些语句,条件编绎实现这个目的,可以选择代码的一部分是正常编绎还是完全忽略。
1)#if指令和#endif指令,if constant-expression中constant-expression常量表达式由预处理器进行求值,如果非零statements部分正常编绎,否则忽略。
2)常量表达式是字面值常量或#define定义的符号,如果变量在执行之前无法获得值,值在编绎时不可预测,出现在常量表达式中是非法的。
2,还可以#if指令、#elif指令、#else指令、#endif指令选择编绎不同的代码部分,#elif子句出现次数不限,当前面所有的常量表达式都为假时编绎#else部分代码。
1)#if defined(symbol) = #ifdef symbol测试符号是否被定义
2)#if !defined(symbol) = #ifndef symbol
3)表达式等价,但if形式功能更强,可以&&或||其他表达式。
4)以上指令可以嵌套使用,提高代码可读性为每个#endif加上注释标签,标签内容即#if或#ifdef后的表达式。
文件包含
1,#include指令使另一个文件内容被编绎,预处理器删除这条指令,用包含文件的内容替代。
1)一个头文件被包含到10个源文件中,则实际被编绎了10次。
2)#include文件涉及一些开销,但开销不大且只在程序编绎时存在,对运行效率不影响,不需要担心这些开销,程序维护更简单,不需要一一拷贝到需要的源文件。
3)使用多个头文件,每个头文件包含用于特定函数或模块的声明,比将所有声明放入一个大头文件更好,文件中的语句也不会意外访问私有的函数或变量。
2,编绎器支持函数库文件和本地头文件包含。
1)使用尖括号包含库文件,使用双引号包含本地头文件。
2)本地头文件包含先在源文件所有目录查找,然后再到库文件路径找本地头文件。
3,标准要求编绎器必须支持至少8层的头文件嵌套,但没有限定嵌套的最大值,嵌套过深会导致难判断源文件之间真正的依赖关系。
1)unix的make需要知道依赖关系决定文件修改后哪些文件需要重新编绎。
2)且会导致同一头文件多次包含,使用条件编绎,在头文件中
#ifndef _HEADERNAME_H
#define _HEADERNAME_H
/********************/
#endif
可解决多重包含问题,第一次包含时正常处理,再次包含时忽略内容。
3)预处理器仍然读入整个头文件,虽然忽略了内容,仍然拖慢了编绎速度,应尽量避免多重包含。
其他指令
1,#error text of error message指令允许生成错误信息
2,#line number "string"通知预处理器number是下一行输入的行号,可选部分string作为当前文件的名字。
1)指令修改了__LINE__符号的值,可选部分修改了__FILE__符号的值。
2)将其他语言的代码转换为C代码的程序中,使用这条指令可将C编绎器产生的错误引用到源文件,而不是翻译程序产生的C中间源文件的文件名和行号。
3,#progma指令允许一些编绎选项,有些编绎器使用该指令在编绎过程打开或关闭清单显示,或者把汇编代码插入到C程序中,但有些编绎器不支持该指令,预处理器忽略#progma指令。
4,#无效指令,以#开头后面不跟任何内容的一行,预处理器删除该行内容,等同空行。
总结
1,编绎一个C程序首先进行预处理,预处理支持五种符号。
2,#define指令把一个符号名与一个任意字符序列联系在一起。
1)字符序列可能是字面值常量、表达式或程序语句,如果序列较长,可以分开数行,在除最后一行的每一行后加一个反斜杠。
2)#define指令可以用于重写C语言,使它看上去像其他语言。
3)宏就是一个被定义的序列,参数值被替换,当宏被调用,每个参数都由一个具体的值替换,为了防止出现与宏有关的错误表达式,可在宏定义中每个参数两边加上括号。
4)宏与类型无关,且执行速度快于函数,但宏会增加调用者的长度,具有副作用的参数可能在宏的使用过程中产生不可预料的结果,而函数参数的行为更容易预测。
3,在许多编绎器中,符号可以在命令行定义,#undef指令忽略符号的定义。
1)条件编绎可以从一组单一的源文件创建程序的不同版本,#if指令根据编绎时测试的结果,包含或忽略一个序列的代码,同时使用#elif和#else指令时,可以从几个序列的代码中选择其一进行编绎。
2)除了测试常量表达式外,还可以测试某个符号是否被定义,#ifdef和#ifndef指令也可以执行这个任务。
3,#argument结构由预处理器转换为字符串常量"argument",##连接两边的文本成一个标识符。
4,#include指令用于实现文件包含。
1)文件名位于尖括号中,编绎器在由编绎器定义的标准位置查找这个文件,通常用于包含库头文件。
2)文件名位于双引号内,不同编绎器使用不同的方式处理,如果无法找到本地文件,则在标准位置查找这个头文件,通常用于自己编写的头文件。
3)文件包含可以嵌套,嵌套的文件包含增加多次包含同一文件的危险,且难以确定文件依赖关系。
5,#error指令在编绎时产生一条错误信息,信息中包含选择的文本。
6,#line指令指定行号和源文件名字。
7,#progma因编绎器而异,指令允许编绎器提供不同的处理过程,例向一个函数插入内联的汇编代码。