今天在看代码时,突然发现很多define的用法看不懂,故在此总结一下,顺便吐槽一下,C语言的宏复杂起来真的很难看懂。
不信的去看下这个的源码:【C语言开源库】lw_oopc:轻量级的C语言面向对象编程框架
一、宏的定义与撤销

需要注意的是:
(1)宏定义应注意添加括号,这样语义会比较清晰。
(2)使用#undef可以撤销宏定义。
(3)引号中的宏定义不会被替换。
(4)宏定义的宏名必须是合法的标识符。
(5)宏定义中单、双引号必须成对出现。
二、带有参数的宏定义

需要注意的是:
(1)宏调用时参数的个数要与定义时相同。
三、跨行的宏定义,使用反斜杠 分隔

这种跨行的代码就是函数宏的感觉了。
#include "stdio.h" #define test(x,y) do \ {
\ int a = x; \ int b = y; \ printf("%d",a); \ }while(0); \ int a = x + y; int main() {
test(1,2); printf("%d",a); }
四、三个特殊符号:#,,#@

五、常见的宏定义
1、防止头文件被重复包含

2、得到指定地址上的一个字节值或字值

3、得到一个field在结构体(struct)中的偏移量
#define OFFSETOF( type, field ) ( (size_t) &(( type *) 0)-> field )
4、*得到一个结构体中field所占用的字节数
#define FSIZ( type, field ) sizeof( ((type *) 0)->field )
5、得到一个变量的地址(word宽度)
#define B_PTR(var) ( (byte *) (void *) &(var) ) #define W_PTR(var) ( (word *) (void *) &(var) )
6、将一个字母转换为大写
#define UPCASE(c) ( ((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c) )
7、判断字符是不是10进制的数字
#define DECCHK(c) ((c) >= '0' && (c) <= '9')
8、判断字符是不是16进制的数字
#define HEXCHK(c) (((c) >= '0' && (c) <= '9') ||((c) >= 'A' && (c) <= 'F') ||((c) >= 'a' && (c) <= 'f'))
9、防止溢出的一个方法
#define INC_SAT(val) (val = ((val)+1 > (val)) ? (val)+1 : (val))
10、返回数组元素的个数
#define ARR_SIZE(a) ( sizeof((a)) / sizeof((a[0])) )
关于嵌套宏的使用
#define f(x) #x //结果将被扩展为由实际参数替换该参数的带引号的字符串 #define b(x) ax //连接实际参数 #define ac hello int main(void) {
f(b(c))//display "b(c)" }
如果最外层p(x)宏替换不以# 为前缀,则由内向外展开
#define f(x) #x #define p(x) f(x) #define b(x) ax #define ac hello int main(void) {
p(b(c))// display "hello" return 0; }
方法1:使用#运算符。出现在宏定义中的#运算符把跟在其后的参数转换成一个字符串。有时把这种用法的#称为字符串化运算符。
#include
#define PREX 1.3.6 #define FORMAT(n) #n".%d\n" int main() {
int ival = 246; printf(FORMAT(PREX), ival);// PREX.246 return 0; }
PS: 运算符用于把参数连接到一起。预处理程序把出现在两侧的参数合并成一个符号(非字符串)。
方法2:修改宏定义的格式,再添加一个中间宏TMP(x)实现对参数的替换,然后再替换为最终想要的字符串。
#define PREX 1.3.6 #define FORMAT(x) TMP(x) #define TMP(x) #x".%d\n" int main() {
int ival = 246; printf(FORMAT(PREX), ival);// 1.3.6.246 return 0; }
嵌套宏在某些情况下还是有一定的用处,但是我可能更愿意定义一个函数宏来完成上面这个工作:
#include
#define FORMAT_FUN(szPrex, szFormat) do {
\ char szTmp[128] = {
0}; \ _snprintf(szTmp, sizeof(szTmp)-1, "%s", szFormat); \ _snprintf(szFormat, sizeof(szFormat)-1, "%s%s", szPrex, szTmp); \ } while(0); \ const char *szPrex = "1.3.6"; int main() {
int ival = 246; char szFormat[128] = ".%d\n"; FORMAT_FUN(szPrex, szFormat); //printf("%s\n", szFormat); printf(szFormat, ival);// 1.3.6.246 return 0; } 617
举几个关于宏嵌套用法的例子:
Ex. 1 #include
#define TO_STRING2(x) #x #define TO_STRING(x) TO_STRING1(x) #define TO_STRING1(x) #x #define PARAM(x) #x #define ADDPARAM(x) INT_x int main() {
const char *str = TO_STRING(PARAM(ADDPARAM(1))); printf("%s\n",str); str = TO_STRING2(PARAM(ADDPARAM(1))); printf("%s\n",str); return 0; } /* output: "ADDPARAM(1)" PARAM(ADDPARAM(1)) */ Ex. 2 #include
#define TO_STRING2(x) a_x #define TO_STRING(x) TO_STRING1(x) #define TO_STRING1(x) #x #define PARAM(x) #x #define ADDPARAM(x) INT_x int main() {
const char *str = TO_STRING(TO_STRING2(PARAM(ADDPARAM(1)))); printf("%s\n",str); return 0; } /* VS2010 output: a_PARAM(ADDPARAM(1)) GCC 4.3.2 output: a_PARAM(INT_1) */ 44
注意:例子2的代码分别在不同的编译器上输出结果不相同。这是为什么呢?
改为:
#include
#define TO_STRING2(x) a_x #define TO_STRING(x) TO_STRING1(x) #define TO_STRING1(x) T(x) #define T(x) #x #define PARAM(x) #x #define ADDPARAM(x) INT_x int main() {
const char *str = TO_STRING(TO_STRING2(PARAM(ADDPARAM(1)))); printf("%s\n",str); return 0; } /* VS2010 output: a_PARAM(INT_1) GCC 4.3.2 output: a_PARAM(INT_1) (1) 对TO_STRING的参数TO_STRING2(...)进行检查替换,生成标记a_PARAM(ADDPARAM(1)) (2) 对TO_STRING1的参数a_PARAM(ADDPARAM(1))进行检查替换,生成标记a_PARAM(INT_1) (3) 对T的参数a_PARAM(INT_1)进行检查替换,生成字符串"a_PARAM(INT_1)" */ Ex. 3 #include
int ival = 0; #define A(x) printf("%d\n", ival+=1); #define B(x) printf("%d\n", ival+=2); #define C() printf("%d\n", ival+=3); int main() {
A(B(C())); printf("%d\n", ival);// ?, 1 return 0; }
关于#和在C语言的宏中,#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量 通过替换后在其左右各加上一个双引号。比如下面代码中的宏:
#define WARN_IF(EXP) / do{
if (EXP) / fprintf(stderr, "Warning: " #EXP "/n"); } / while(0)
那么实际使用中会出现下面所示的替换过程:
WARN_IF (divider == 0); 被替换为 do {
if (divider == 0) fprintf(stderr, "Warning" "divider == 0" "/n"); } while(0);
这样每次divider(除数)为0的时候便会在标准错误流上输出一个提示信息。
而被称为连接符(concatenator),用来将两个Token连接为一个Token。注意这里连接的对象是Token就行,而不一定 是宏的变量。比如你要做一个菜单项命令名和函数指针组成的结构体的数组,并且希望在函数名和菜单项命令名之间有直观的、名字上的关系。那么下面的代码就非常实用:
struct command {
char * name; void (*function) (void); }; #define COMMAND(NAME) {
NAME, NAME _command } // 然后你就用一些预先定义好的命令来方便的初始化一个command结构的数组了: struct command commands[] = {
COMMAND(quit), COMMAND(help), ... }
COMMAND宏在这里充当一个代码生成器的作用,这样可以在一定程度上减少代码密度,间接地也可以减少不留心所造成的错误。我们还可以n个符号连接 n+1个Token,这个特性也是#符号所不具备的。比如:
#define LINK_MULTIPLE(a,b,c,d) a_b_c_d typedef struct _record_type LINK_MULTIPLE(name,company,position,salary); // 这里这个语句将展开为: // typedef struct _record_type name_company_position_salary;
关于…的使用
…在C宏中称为Variadic Macro,也就是变参宏。比如:
#define myprintf(templt,...) fprintf(stderr,templt,__VA_ARGS__) // 或者 #define myprintf(templt,args...) fprintf(stderr,templt,args)
第一个宏中由于没有对变参起名,我们用默认的宏__VA_ARGS__来替代它。第二个宏中,我们显式地命名变参为args,那么我们在宏定义中就可以用 args来代指变参了。同C语言的stdcall一样,变参必须作为参数表的最有一项出现。当上面的宏中我们只能提供第一个参数templt时,C标准要 求我们必须写成:
myprintf(templt,);
的形式。这时的替换过程为:
myprintf("Error!/n",); 替换为: fprintf(stderr,"Error!/n",);
这是一个语法错误,不能正常编译。这个问题一般有两个解决方法。首先,GNU CPP提供的解决方法允许上面的宏调用写成:
myprintf(templt);
而它将会被通过替换变成:
fprintf(stderr,"Error!/n",);
很明显,这里仍然会产生编译错误(非本例的某些情况下不会产生编译错误)。除了这种方式外,c99和GNU CPP都支持下面的宏定义方式:
#define myprintf(templt, ...) fprintf(stderr,templt, __VAR_ARGS__)
这时,这个连接符号充当的作用就是当__VAR_ARGS__为空的时候,消除前面的那个逗号。那么此时的翻译过程如下:
myprintf(templt); 被转化为: fprintf(stderr,templt);
这样如果templt合法,将不会产生编译错误。
错误的嵌套-Misnesting
宏的定义不一定要有完整的、配对的括号,但是为了避免出错并且提高可读性,最好避免这样使用。
由操作符优先级引起的问题-Operator Precedence Problem
由于宏只是简单的替换,宏的参数如果是复合结构,那么通过替换之后可能由于各个参数之间的操作符优先级高于单个参数内部各部分之间相互作用的操作符优先级,如果我们不用括号保护各个宏参数,可能会产生预想不到的情形。比如:
#define ceil_div(x, y) (x + y - 1) / y
那么
a = ceil_div( b & c, sizeof(int) );
将被转化为:
a = ( b & c + sizeof(int) - 1) / sizeof(int); // 由于+/-的优先级高于&的优先级,那么上面式子等同于: a = ( b & (c + sizeof(int) - 1)) / sizeof(int);
这显然不是调用者的初衷。为了避免这种情况发生,应当多写几个括号:
#define ceil_div(x, y) (((x) + (y) - 1) / (y))
消除多余的分号-Semicolon Swallowing
通常情况下,为了使函数模样的宏在表面上看起来像一个通常的C语言调用一样,通常情况下我们在宏的后面加上一个分号,比如下面的带参宏:
MY_MACRO(x);
但是如果是下面的情况:
#define MY_MACRO(x) {
/ /* line 1 *// /* line 2 *// /* line 3 */ } //... if (condition()) MY_MACRO(a); else {
...}
这样会由于多出的那个分号产生编译错误。为了避免这种情况出现同时保持MY_MACRO(x);的这种写法,我们需要把宏定义为这种形式:
#define MY_MACRO(x) do {
/* line 1 *// /* line 2 *// /* line 3 */ } while(0)
这样只要保证总是使用分号,就不会有任何问题。
Duplication of Side Effects
这里的Side Effect是指宏在展开的时候对其参数可能进行多次Evaluation(也就是取值),但是如果这个宏参数是一个函数,那么就有可能被调用多次从而达到不一致的结果,甚至会发生更严重的错误。比如:
#define min(X,Y) ((X) > (Y) ? (Y) : (X)) //... c = min(a,foo(b));
这时foo()函数就被调用了两次。为了解决这个潜在的问题,我们应当这样写min(X,Y)这个宏:
#define min(X,Y) ({
/ typeof (X) x_ = (X);/ typeof (Y) y_ = (Y);/ (x_ < y_) ? x_ : y_; })
({…})的作用是将内部的几条语句中最后一条的值返回,它也允许在内部声明变量(因为它通过大括号组成了一个局部Scope)
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/223469.html原文链接:https://javaforall.net
