C语言学习之宏定义用法——并不只是用来提高可读性

(一)用法

  1. 普通替换
  2. 宏函数
    用法和普通函数一样,只不过在预处理阶段,宏函数会被展开,没有普通函数保存寄存器和参数传递的开销。
    #define min(X, Y)  ((X) < (Y) ? (X) : (Y))
    y = min(1, 2);//会被扩展成y = ((1) < (2) ? (1) : (2));
    

(二)宏操作符

  1. # 它可以把宏的参数转换成字符串字面值(相当于给参数加上双引号)

    #include <stdio.h>
    #define STR(n) #n
    int main(){
    	int val = 123;
    	STR(val);
    	STR(2+3);
    	return 0;
    }
    

    预处理后的部分截图如下
    在这里插入图片描述可以清楚得看到,变量名加了双引号,2+3也是

    应用:可以用来重新定义专用的打印函数

    #define PRINT_INT(n) printf(#n " = %d\n",n)
    #include <stdio.h>
    int main (){
    	int a = 1;
    	int b = 2;
    	int hello = 3;
    	PRINT_INT(a);
    	PRINT_INT(b);
    	PRINT_INT(hello);
    	return 0;
    }
    

    运行结果如图
    在这里插入图片描述
    注:在printf中,“a”" = %d\n"和"a = %d\n"是等价的,#会把"变成\", 把\ 变成\\

  2. ## 它可以把一个代表标识符的参数和其他内容连接得到一个新的标识符,比如你有一个命名习惯,习惯把给int a声明的指针标识符叫做 int *p_a;可以这样做

    #include <stdio.h>
    #define P_INT_ID(n) p_##n
    
    int main(){
    	int a = 1;
    	int b = 2;
    	int hello = 3;
    	int *P_INT_ID(hello), *P_INT_ID(b), *P_INT_ID(a);
    	return 0;
    }
    

    执行预处理后截图如下
    在这里插入图片描述
    当命名规则很复杂,有一大串相同的字符作为固定格式时,这种写法就有很大的优势。

(三)进阶使用

宏定义可以在编译时,更改或增加宏定义如增加宏定义 gcc -DAAA test.c 定义了一个叫AAA的宏,gcc -DLEN=10 test.c 定义了个叫LEN的宏,值为10,可以通过这种方法,实现编码一次,编译时更改宏,达到生成不同可执行程序的目的

#include <stdio.h>
#include <time.h>
#include <stdlib.h>

#define RANGE(a,b) ((b)-(a)+1) //带参数的宏可以用来表示公式,可以用这个公式来计算MIN和MAX之间的数字个数

/*
 * 生成指定个数,指定范围的连续随机数
 * 根据编译时的参数,可以编译出不同的程序
 * NUM表示随机数个数,MIN和MAX用来表示随机数的范围
 * */
int main(){
    int arr[NUM] = {0};
    srand(time(0));
    for(int i=0; i<NUM; i++){
        arr[i] = rand() % RANGE(MIN,MAX) + MIN;
    }
    for(int j=0; j<NUM; j++){
        printf("%d ", arr[j]);
    }
    return 0;
}

编译指令 gcc test.c -DNUM=6 -DMIN=10 -DMAX=17
运行结果:
在这里插入图片描述
参考:

  1. 字符串化(Stringification)
    在宏体中,如果宏参数前加个#,那么在宏体扩展的时候,宏参数会被扩展成字符串的形式。如\用来标记换行:

    #define WARN_IF(EXP) \
     do { if (EXP) \
     	fprintf (stderr, "Warning: " #EXP "\n"); } \
     while (0)
    

    WARN_IF (x == 0);会被扩展成:

    do { if (x == 0)
    	fprintf (stderr, "Warning: " "x == 0" "\n"); }
    while (0);
    

    这种用法可以用在assert中,如果断言失败,可以将失败的语句输出到反馈信息中

  2. 连接(Concatenation)
    在宏体中,如果宏体所在标示符中有##,那么在宏体扩展的时候,宏参数会被直接替换到标示符中。如:

    #define COMMAND(NAME)  { #NAME, NAME ## _command }
    struct command
    {
    	char *name;
    	void (*function) (void);
    };
    

在宏扩展的时候

			struct command commands[] =
			{
				COMMAND (quit),
				 COMMAND (help),
				 ...
			};

会被扩展成:

	struct command commands[] =
	{
	    { "quit", quit_command },
	    { "help", help_command },
	    ...
	};

这样就节省了大量时间,提高效率。

(四)易错

  1. 算符优先级问题
    仅宏体是纯文本替换,宏参数也是纯文本替换。有以下一段简单的宏,实现乘法:
    #define MULTIPLY(x, y) x * y
    MULTIPLY(1, 2)没问题,会正常展开成1 * 2。有问题的是这种表达式MULTIPLY(1+2, 3),展开后成了1+2 * 3,显然优先级错了。在宏体中,给引用的参数加个括号就能避免这问题。
    #define MULTIPLY(x, y) (x) * (y)
    MULTIPLY(1+2, 3)就会被展开成(1+2) * (3),优先级正常了。其实这个问题和下面要说到的某些问题都属于由于纯文本替换而导致的语义破坏问题,要格外小心。

  2. 分号吞噬问题
    有如下宏定义:

#define SKIP_SPACES(p, limit)  \
     { char *lim = (limit);         \
       while (p < lim) {            \
         if (*p++ != ' ') {         \
           p--; break; }}}

假设有如下一段代码:

if (*p != 0)
   SKIP_SPACES (p, lim);
else ...

一编译,GCC报error: ‘else’ without a previous ‘if’。原来这个看似是一个函数的宏被展开后是一段大括号括起来的代码块,加上分号之后这个if逻辑块就结束了,所以编译器发现这个else没有对应的if。

这个问题一般用do … while(0)的形式来解决:

#define SKIP_SPACES(p, limit)     \
     do { char *lim = (limit);         \
          while (p < lim) {            \
            if (*p++ != ' ') {         \
              p--; break; }}}          \
     while (0)

展开后就成了

if (*p != 0)
    do ... while(0);
else ...

这样就消除了分号吞噬问题。

这个技巧在Linux内核源码里很常见,比如这个置位宏#define SET_REG_BIT(reg, bit) do { (reg |= (1 << (bit))); } while (0)(位于arch/mips/include/asm/mach-pnx833x/gpio.h)

  1. 宏参数重复调用
    有如下宏定义:
    #define min(X, Y) ((X) < (Y) ? (X) : (Y))
    当有如下调用时next = min (x + y, foo (z));,宏体被展开成next = ((x + y) < (foo (z)) ? (x + y) : (foo (z)));,可以看到,foo(z)被重复调用了两次,做了重复计算。更严重的是,如果foo是不可重入的(foo内修改了全局或静态变量),程序会产生逻辑错误。所以,尽量不要在宏参数中传入函数调用。

  2. 对自身的递归引用。有如下宏定义:
    #define foo (4 + foo)
    按前面的理解,(4 + foo)会展开成(4 + (4 + foo)),然后一直展开下去,直至内存耗尽。但是,预处理器采取的策略是只展开一次。也就是说,foo只会展开成(4 + foo),而展开之后foo的含义就要根据上下文来确定了。对于以下的交叉引用,宏体也只会展开一次。
    #define x (4 + y)
    #define y (2 * x)
    x展开成(4 + y) -> (4 + (2 * x)),y展开成(2 * x) -> (2 * (4 + y))。
    注意,这是极不推荐的写法,程序可读性极差。

  3. 宏参数预处理
    宏参数中若包含另外的宏,那么宏参数在被代入到宏体之前会做一次完全的展开,除非宏体中含有#或##。
    有如下宏定义:
    #define AFTERX(x) X_ ## x
    #define XAFTERX(x) AFTERX(x)
    #define TABLESIZE 1024
    #define BUFSIZE TABLESIZE
    AFTERX(BUFSIZE)会被展开成X_BUFSIZE。因为宏体中含有##,宏参数直接代入宏体。
    XAFTERX(BUFSIZE)会被展开成X_1024。因为XAFTERX(x)的宏体是AFTERX(x),并没有#或##,所以BUFSIZE在代入前会被完全展开成1024,然后才代入宏体,变成X_1024。

(五)其他

  1. 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令
  2. 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换
  3. 应注意用宏定义表示数据类型和用 typedef 定义数据说明符的区别。宏定义只是简单的字符串替换,由预处理器来处理;而 typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型
    请看下面的例子:
    #define PIN1 int *
    typedef int *PIN2; //也可以写作typedef int (*PIN2);
    从形式上看这两者相似, 但在实际使用中却不相同。
    下面用 PIN1,PIN2 说明变量时就可以看出它们的区别:
    PIN1 a, b;
    在宏代换后变成:
    int * a, b;
    表示 a 是指向整型的指针变量,而 b 是整型变量。然而:
    PIN2 a,b;
    表示 a、b 都是指向整型的指针变量。因为 PIN2 是一个新的、完整的数据类型。由这个例子可见,宏定义虽然也可表示数据类型, 但毕竟只是简单的字符串替换。在使用时要格外小心,以避出错。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值