C语言的宏,必须了解的知识点

C语言里宏没有过时,在一些著名库或者底层源码中,它们的身影无处不在。虽然宏有很多陷阱(以至于有些编码规范中要求“尽量少用宏”),针对“宏”的缺陷,C++发明出template(模板),inline function(内联函数),constexpr(常量表达式)等等瑞士军刀工具(针对某种场景的专用工具)。在有些场合“宏”是无可替代的必需品。标识符拼接生成,或者字符串拼接自动生成领域,宏不可替代。因为模板只能繁殖一些类型,无法繁殖标识符或者字符串。在真正用好宏这个工具前,需要理解一下知识点。


宏拒绝递归(Self-Referential)

函数是可以递归的,自己调用自己。宏却不可以递归,因为宏是没有办法定义终止递归的条件。例如:#define A (A+10)这个宏,假设递归下去,就是A  =>  (A+10)  =>  ((A+10)+10) => (((A+10)+10)+10)  ....  直到内存耗尽,根本停不下来。
那么#define A (A+10)就是非法的宏了?不是的,这个宏是合法的,A就会直接被替换为A+10,宏替换结束,代码被扔给编译器(之前是预处理器做宏替换)。编译器如何理解A+10?假设恰好有个变量叫做A,则编译通过;假设找不到A这个标识符,则编译报错。
自引用(Self-Referential)是终止宏向更深层替换的一个条件(另外还有#,##也会终止宏向更深处滑落)。分析下面这个例子,可以更深刻的理解自引用以及“间接自引用”的问题:

#define A(x,y) B(x,y) @@@ C(x,y)
#define B(x,y) D(x,y) @@  E(x,y)
#define C(x,y) F(x,y) @@  G(x,y)

#define D(x,y) x+y
#define E(x,y) x-y

#define F(x,y) x*y
#define G(x,y) A(x,y) $ B(x,y)

// A(1,2)  这个宏展开的结果是:1+2 @@ 1-2 @@@ 1*2 @@ A(1,2) $ 1+2 @@ 1-2 

下图为语法树的样子。当A(1,2)层层展开,遇到G这个宏时,G的宏定义里有个A,A是直系祖先,应停止展开A(1,2)。当G这个宏遇到B(1,2)时,B不是直系祖先,所以展开了B。
预处理器内部应该能记忆这个“家谱”

宏的PreScan

宏是允许有参数,宏的实参也允许是一个宏。问题来了,函数的实参是先求值,然后再传入函数的。宏的实参是先展开再传入宏体的?还是不展开实参,先带入宏体,再展开呢?先举一个例子:

#define min(x,y)  ( (x)<(y) ? (x) : (y) )
int main()
{
   std::cout <<   min(min(1,2) ,3)  ;
}  

首先,这条语句是合法合理的,cout输出1,符合预期。然后,假设它是先把min(1,2)带入宏体的,则第一次展开为(min(1,2)>3?min(1,2):3), 我们会发现递归了,因为min的直系祖先就是min。由此可知,是先把参数展开,然后才带入宏体的。
再看一个例子:

#define str(s) #s
#define foo 4
int main()
{
   std::cout <<   str (foo) ;  //打印 foo
}

 打印foo,说明str(foo)是先把foo直接带入到宏体,变成#foo,然后字符串化为"foo"。现在可以总结,是否把实参先展开,取决于包体的定义,是否存在#,##。在举一个例子:

#define str(s) #s lose(s)
#define foo 4
str (foo)  //展开为:"foo" lose(4)

宏体中,一部分用到不展开的参数,一部分用到展开的参数。预处理器处理foo这个参数,实际上拿着foo的两种形态(4 以及 foo) 去展开宏体的。
这个机制叫做prescan,宏展开前是先窥视了宏体的,然后才决定如何处理宏的实际参数。

 函数式的宏不是真的函数

宏虽然外观貌似函数,但是与函数有一个严重不同之处,宏的实参数没有先求表达式的值。因为宏无法识别C语言的表达式,它不能计算++i之类的值。宏只是一个文本处理机制。

#define min(x,y) ((x)<(y)?(x):(y))
int main()
{
    int i=8;
    int result = min(++i,10);  //((++i)<(10)?(++i):(10))
    std::cout << result << " " << i;
}

在上面那个例子中,i被执行2次加一运算。这个陷阱比较经典,因此需要用模板这个新技术替换它。模板函数最终被实例化为一个函数,而函数是先求值参数的。
宏参数的分隔符是逗号,下面一个例子是与逗号有关的BUG

#define LOG(p) printf("the result is: %d,%d", p.x, p.y)

typedef struct {
    int x;
    int y;
} Point;

int main()
{
    LOG( Point{1,2} ); //实际传递了两个参数,Point{1 以及 2}
}

在函数的语法中,{}必须左右匹配,因此{1,2}被当做一个整体。但是在宏中, 大括号和中括号只是普通文字,就像abc那样普通,宏会忽略。为了解决这个问题,需要用小括号帮助一下,宏对小括号是敏感的。

LOG( (Point{1,2}) )

另一个例子是:

#define fun(x,y) x + y

int main()
{
  int x = 2 * fun(3,4);         //外部误结合: 展开为2*3+4=10, 期望为14
  int y = fun( 24 & 0x07 , 2) ; //内部误结合: 展开为24 & 0x07 + 2 = 24 & (0x07 + 2) = 8
}

为了解决不恰当的运算子结合,我们要用小括号,宏对小括号是敏感的:

#define fun(x,y) ((x) + (y))

著名的do{...} while(0) 

宏的本质是一个代码段的标签,我贴上这个标签,相当于插入了一段确定的代码。但是在某些场合,需要用do{...}while(0)封装一下。例如:

#define log(frm, argc...) {\
    printf("[%s : %d] ", __func__, __LINE__);\
    printf(frm, ##argc);\
    printf("\n");\
} 

int main()
{
    if (true)
        log("%s,%s","hello","world")
    else
        log("%s","hello")
}

虽然插入了一段{}括起来的代码,但是log这个貌似函数的宏没有分号。因为加上分号后,代码就变成了:

if(true)
{ 
    bla bla bla ... 
};
else  //意外遇到else的错误
    ....

 为了解决这个问题,或者说为了满足分号强迫症患者的需要,我们需要做如下改造:

#define log(frm, argc...) do{\
    printf("[%s : %d] ", __func__, __LINE__);\
    printf(frm, ##argc);\
    printf("\n");\
}while(0)

int main()
{
    if (true)
        log("%s,%s","hello","world");
    else
        log("%s","hello");
}

Stringification字符串生成

宏参数可以被当做新字符串生成的原料。在宏的定义中,参数前面有#字符的,就会被替换为字符串。例如:

#define STR_NOT_WORK(x) "hello #x world\n"
#define STR(x) "hello " #x " world\n"
int main()
{
    std::cout << STR_NOT_WORK("Jimmy"); //错误的例子, hello #x world
    std::cout << STR("Jimmy");   //hello "Jimmy" world
    std::cout << STR(Jimmy);     //hello Jimmy world
}

第一个,"hello #x world\n" ,宏对双引号敏感,引号内部认为是不可改动的,因此不会展开宏参数x
第二个,宏的实参为"Jimmy", 宏展开后形如:"hello "   "\"Jimmy\""   " world\n"  
第三个,宏的实参为Jimmy, 宏展开后形如:"hello "   "Jimmy"   " world\n" 
最后是编译器发挥作用,自动合并临近的字符串,"hello "   "Jimmy"   " world\n"    ===>  "hello Jimmy world\n"

拼接产生新符号

宏的一大作用是生成新符号。这适用于防止冗余的代码。符号拼接的方法是##,预处理器遇见##,就把宏参数拼接,而不是去展开。例如:

struct command
{
  char *name;
  void (*function) ();
};

struct command commands[] =
{
  { "quit", quit_command},  //quit敲打两遍
  { "help", help_command},  //help敲打两遍
  ...
};

改进的代码为:

struct command
{
  char *name;
  void (*function) ();
};

#define COMMAND(NAME)  { #NAME, NAME ## _command }

struct command commands[] =
{
  COMMAND (quit),   //展开后是 { "quit", quit_command }
  COMMAND (help),   //help只需敲打一遍,因此依靠自动化的代码生成技术,减少笔误
  ...
};

宏的作用范围

一个源文件(编译单元),顺序看下去,只有先定义宏,才能使用。宏不能作用于定义前面的代码文本、例如:

const int FOO = 12;

int main()
{
  int x=FOO; 
  std::cout << x << std::endl;  //12。宏FOO还不存在,所以引用const int FOO
    
  #define FOO 4
  x = FOO;
  std::cout << x << std::endl;   //4。宏被使用
    
  #undef FOO
  x = FOO;
  std::cout << x << std::endl;   //12
}

宏的调试 

这里给出一个观察宏展开后的样子的技术。

#define TO_STRING1( x ) #x
#define TO_STRING( x )  TO_STRING1( x )

#define log(frm, argc...) do{\
    printf("[%s : %d] ", __func__, __LINE__);\
    printf(frm, ##argc);\
    printf("\n");\
}while(0)

int main()
{ 
   std::cout <<   TO_STRING(log("%s,%s","hello","world") ) ;
}

如果是在用户代码直接写 TO_STRING1(log("%s,%s","hello","world") ),则会产生“log("%s,%s","hello","world")”这个字符串,显然不是我们期望的。
如果是TO_STRING(log("%s,%s","hello","world") ),则先展开宏参数。过程为:
TO_STRING( log("%s,%s","hello","world")  )  ==>  TO_STRING( do{\ printf("[%s...  ) ==> TO_STRING1( do{\ printf("[%s...  ) ==> #do{\ printf("[%s... ==> "do{\ printf("[%s..."

 

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值