c语⾔max函数⽤法_C语⾔预处理命令【长⽂详解】
⼀  前⾔
预处理(或称预编译)是指在进⾏编译的第⼀遍扫描(词法扫描和语法分析)之前所作的⼯作。预处理指令指⽰在程序正式编译前就由编译器进⾏的操作,可放在程序中任何位置。
预处理是C语⾔的⼀个重要功能,它由预处理程序负责完成。当对⼀个源⽂件进⾏编译时,系统将⾃动引⽤预处理程序对源程序中的预处理部分作处理,处理完毕⾃动进⼊对源程序的编译。
C语⾔提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、⽂件包含(#include)、条件编译(#ifdef)等。合理使⽤预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
⼆  宏定义
C语⾔源程序中允许⽤⼀个标识符来表⽰⼀个字符串,称为“宏”。被定义为宏的标识符称为“宏名”。在编译预处理时,对程序中所有出现的宏名,都⽤宏定义中的字符串去代换,这称为宏替换或宏展开。
宏定义是由源程序中的宏定义命令完成的。宏替换是由预处理程序⾃动完成的。
在C语⾔中,宏定义分为有参数和⽆参数两种。下⾯分别讨论这两种宏的定义和调⽤。
2.1 ⽆参宏定义
⽆参宏的宏名后不带参数。其定义的⼀般形式为:
#define  标识符  字符串
其中,“#”表⽰这是⼀条预处理命令(以#开头的均为预处理命令)。“define”为宏定义命令。“标识符”为符号常量,即宏名。“字符串”可以是常数、表达式、格式串等。
宏定义⽤宏名来表⽰⼀个字符串,在宏展开时⼜以该字符串取代宏名。这只是⼀种简单的⽂本替换,预处理程序对它不作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。
注意理解宏替换中“换”的概念,即在对相关命令或语句的含义和功能作具体分析之前就要进⾏⽂本替换。
【例1】定义常量:
#define MAX_TIME 1000
若在程序⾥⾯写if(time < MAX_TIME){.........},则编译器在处理该代码前会将MAX_TIME替换为1000。
注意,这种情况下使⽤const定义常量可能更好,如const int MAX_TIME = 1000;。因为const常量有数据类型,⽽宏常量没有数据类型。编译器可以对前者进⾏类型安全检查,⽽对后者只进⾏简单的字符⽂本替换,没有类型安全检查,并且在字符替换时可能会产⽣意料不到的错误。
【例2】反例:
#define pint (int*)
pint pa, pb;
本意是定义pa和pb均为int型指针,但实际上变成int* pa,pb;。pa是int型指针,⽽pb是int型变量。本例中可⽤typedef来代替define,这样pa和pb就都是int型指针了。
因为宏定义只是简单的字符串代换,在预处理阶段完成,⽽typedef是在编译时处理的,它不是作简单的代换,⽽是对类型说明符重新命名,被命名的标识符具有类型定义说明的功能。
typedef的具体说明见附录6.4。
⽆参宏注意事项:
宏名⼀般⽤⼤写字母表⽰,以便于与变量区别。宏定义末尾不必加分号,否则连分号⼀并替换。宏定义可以嵌套。
可⽤#undef命令终⽌宏定义的作⽤域。
使⽤宏可提⾼程序通⽤性和易读性,减少不⼀致性,减少输⼊错误和便于修改。如数组⼤⼩常⽤宏定义。预处理是在编译之前的处理,⽽编译⼯作的任务之⼀就是语法检查,预处理不做语法检查。宏定义写在函数的花括号外边,作⽤域为其后的程序,通常在⽂件的最开头。字符串" "中永远不包含宏,否则该宏名当字符串处理。
宏定义不分配内存,变量定义分配内存。
2.2 带参宏定义
C语⾔允许宏带有参数。在宏定义中的参数称为形式参数,在宏调⽤中的参数称为实际参数。
对带参数的宏,在调⽤中,不仅要宏展开,⽽且要⽤实参去代换形参。
带参宏定义的⼀般形式为:
#define  宏名(形参表)  字符串
在字符串中含有各个形参。
带参宏调⽤的⼀般形式为:
宏名(实参表);
在宏定义中的形参是标识符,⽽宏调⽤中的实参可以是表达式。
在带参宏定义中,形参不分配内存单元,因此不必作类型定义。⽽宏调⽤中的实参有具体的值,要⽤它们去代换形参,因此必须作类型说明,这点与函数不同。函数中形参和实参是两个不同的量,各有⾃⼰的作⽤域,调⽤时要把实参值赋予形参,进⾏“值传递”。⽽在带参宏中只是符号代换,不存在值传递问题。
【例3】
#define INC(x) x+1  //宏定义
y = INC(5);        //宏调⽤
在宏调⽤时,⽤实参5去代替形参x,经预处理宏展开后的语句为y=5+1。
【例4】反例:
#define SQ(r)    r*r
上述这种实参为表达式的宏定义,在⼀般使⽤时没有问题;但遇到如area=SQ(a+b);时就会出现问题,宏展开后变为area=a+b*a+b;,显然违背本意。
相⽐之下,函数调⽤时会先把实参表达式的值(a+b)求出来再赋予形参r;⽽宏替换对实参表达式不作计算直接地照原样代换。因此在宏定义中,字符串内的形参通常要⽤括号括起来以避免出错。
进⼀步地,考虑到运算符优先级和结合性,遇到area=10/SQ(a+b);时即使形参加括号仍会出错。因此,还应在宏定义中的整个字符串外加
括号,
综上,正确的宏定义是#define SQ(r) ((r)*(r)),即宏定义时建议所有的层次都要加括号。
【例5】带参函数和带参宏的区别:
#define SQUARE(x) ((x)*(x))int Square(int x){return (x * x); //未考虑溢出保护
}int main(void){int i = 1;while(i <= 5)printf("i = %d, Square = %d\n", i, Square(i++));int j = 1;while(j <= 5)printf("j = %d, SQUARE = %d\n", j, SQUARE(j++));return 0; }
执⾏后输出如下:
i = 2, Square = 1i = 3, Square = 4i = 4, Square = 9i = 5, Square = 16i = 6, Square = 25j = 3, SQUARE = 1j = 5, SQUARE = 9j = 7, SQUARE = 25
本例意在说明,把同⼀表达式⽤函数处理与⽤宏处理两者的结果有可能是不同的。
调⽤Square函数时,把实参i值传给形参x后⾃增1,再输出函数值。因此循环5次,输出1~5的平⽅值。调⽤SQUARE宏
时,SQUARE(j++)被代换为((j++)*(j++))。在第⼀次循环时,表达式中j初值为1,两者相乘的结果为1。相乘后j⾃增两次变为3,因此表达
式中第⼆次相乘时结果为3*3=9。同理,第三次相乘时结果为5*5=25,并在此次循环后j值变为7,不再满⾜循环条件,停⽌循环。
从以上分析可以看出函数调⽤和宏调⽤⼆者在形式上相似,在本质上是完全不同的。
带参宏注意事项:
宏名和形参表的括号间不能有空格。
宏替换只作替换,不做计算,不做表达式求解。
函数调⽤在编译后程序运⾏时进⾏,并且分配内存。宏替换在编译前进⾏,不分配内存。
函数只有⼀个返回值,利⽤宏则可以设法得到多个值。
宏展开使源程序变长,函数调⽤不会。
宏展开不占⽤运⾏时间,只占编译时间,函数调⽤占运⾏时间(分配内存、保留现场、值传递、返回值)。
为防⽌⽆限制递归展开,当宏调⽤⾃⾝时,不再继续展开。
如:#define TEST(x)  (x + TEST(x))被展开为1 + TEST(1)。
2.3 实践⽤例
包括基本⽤法(及技巧)和特殊⽤法(#和##等)。
#define可以定义多条语句,以替代多⾏的代码,但应注意替换后的形式,避免出错。宏定义在换⾏时要加上⼀个反斜杠”\”,⽽且反斜杠
后⾯直接回车,不能有空格。
2.3.1 基本⽤法
1. 定义常量:
#define PI  3.1415926
将程序中出现的PI全部换成3.1415926。
2. 定义表达式:
#define M  (y*y+3*y)
编码时所有的表达式(y*y+3*y)都可由M代替,⽽编译时先由预处理程序进⾏宏替换,即⽤(y*y+3*y)表达式去置换所有的宏名M,然后再进⾏编译。
注意,在宏定义中表达式(y*y+3*y)两边的括号不能少,否则可能会发⽣错误。如s=3*M+4*M在预处理时经宏展开变为s=3*(y*y+3*y)+4* (y*y+3*y),如果宏定义时不加括号就展开为s=3*y*y+3*y+4*y*y+3*y,显然不符合原意。因此在作宏定义时必须⼗分注意。应保证在宏替换之后不发⽣错误。
3. 得到指定地址上的⼀个字节或字:
#define MEM_B(x)    (*((char *)(x)))
#define MEM_W(x)    (*((short *)(x)))
4. 求最⼤值和最⼩值:
#define MAX(x, y)    (((x) > (y)) ? (x) : (y))
#define MIN(x, y)    (((x) < (y)) ? (x) : (y))
以后使⽤MAX (x,y)或MIN (x,y),就可分别得到x和y中较⼤或较⼩的数。
但这种⽅法存在弊病,例如执⾏MAX(x++, y)时,x++被执⾏多少次取决于x和y的⼤⼩;当宏参数为函数也会存在类似的风险。所以建议⽤内联函数⽽不是这种⽅法提⾼速度。不过,虽然存在这样的弊病,但宏定义⾮常灵活,因为x和y可以是各种数据类型。
以下给出MAX宏的两个安全版本(源⾃linux/kernel.h):
#define MAX_S(x, y) ({ \const typeof(x) _x = (x);  \const typeof(y) _y = (y);  \
(void)(&_x == &_y);      \
_x > _y ? _x : _y; })#define TMAX_S(type, x, y) ({ \
type _x = (x);  \
type _y = (y);  \
_x > _y ? _x: _y; })
Gcc编译器将包含在圆括号和⼤括号双层括号内的复合语句看作是⼀个表达式,它可出现在任何允许表达式的地⽅;复合语句中可声明局部变量,判断循环条件等复杂处理。⽽表达式的最后⼀条语句必须是⼀个表达式,它的计算结果作为返回值。MAX_S和TMAX_S宏内就定义局部变量以消除参数副作⽤。
MAX_S宏内(void)(&_x == &_y)语句⽤于检查参数类型⼀致性。当参数x和y类型不同时,会产⽣” comparison of distinct pointer types lacks a cast”的编译警告。
注意,MAX_S和TMAX_S宏虽可避免参数副作⽤,但会增加内存开销并降低执⾏效率。若使⽤者能保证宏参数不存在副作⽤,则可选⽤普通定义(即MAX宏)。
5. 得到⼀个成员在结构体中的偏移量(lint 545告警表⽰"&⽤法值得怀疑",此处抑制该警告):
#define FPOS(type, field) \
/*lint -e545 */ ((int)&((type *)0)-> field) /*lint +e545 */
6. 得到⼀个结构体中某成员所占⽤的字节数:
#define FSIZ(type, field)    sizeof(((type *)0)->field)
7. 按照LSB格式把两个字节转化为⼀个字(word):
#define FLIPW(arr)          ((((short)(arr)[0]) * 256) + (arr)[1])
8. 按照LSB格式把⼀个字(word)转化为两个字节:
#define FLOPW(arr, val) \
(arr)[0] = ((val) / 256); \
(arr)[1] = ((val) & 0xFF)
9. 得到⼀个变量的地址:
#define B_PTR(var)      ((char *)(void *)&(var))
#define W_PTR(var)      ((short *)(void *)&(var))
typeof的用法
10. 得到⼀个字(word)的⾼位和低位字节:
#define WORD_LO(x)      ((char)((short)(x)&0xFF))
#define WORD_HI(x)      ((char)((short)(x)>>0x8))
11. 返回⼀个⽐X⼤的最接近的8的倍数:
#define RND8(x)          ((((x) + 7) / 8) * 8)
12. 将⼀个字母转换为⼤写或⼩写:
#define UPCASE(c)        (((c) >= 'a' && (c) <= 'z') ? ((c) + 'A' - 'a') : (c))
#define LOCASE(c)        (((c) >= 'A' && (c) <= 'Z') ? ((c) + 'a' - 'A') : (c))
注意,UPCASE和LOCASE宏仅适⽤于ASCII编码(依赖于码字顺序和连续性),⽽不适⽤于EBCDIC编码。
13. 判断字符是不是10进值的数字:
#define ISDEC(c)          ((c) >= '0' && (c) <= '9')
14. 判断字符是不是16进值的数字:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。