如何提高代码逼格?宏定义-从入门到放弃

道哥分享
关注

三、宏扩展

所谓的宏扩展就是代码替换,这部分内容也是我想表达的主要内容。宏扩展最大的好处有如下几点:

减少重复的代码;完成一些通过 C 语法无法实现的功能(字符串拼接);动态定义数据类型,实现类似 C++ 中模板的功能;程序更容易理解、修改(例如:数字、字符串常亮);

我们在写代码的时候,所有使用宏名称的地方,都可以理解为一个占位符。在编译程序的预处理环节,这些宏名将会被替换成宏定义中的那些代码段,注意:仅仅是单纯的文本替换。

1. 最常见的宏

为了方便后面的描述,先来看几个常见的宏定义:

(1) 数据类型的定义

#ifndef BOOL    typedef char BOOL;#endif
#ifndef TRUE    #define TRUE#endif
#ifndef FALSE    #define FALSE#endif

在数据类型定义中,需要注意的一点是:如果你的程序需要用不同平台下的编译器来编译,那么你要去查一下所使用的编译器对这些宏定义控制的数据类型是否已经定义了。例如:在 gcc 中没有 BOOL 类型,但是在 MSVC 中,把 BOOL 类型定义为 int 型。

(2) 获取最大、最小值

#define MAX(a, b)    (((a) > (b)) ? (a) : (b))#define MIN(a, b)    (((a) < (b)) ? (a) : (b))

(3) 计算数组中的元素个数

#define ARRAY_SIZE(x)    (sizeof(x) / sizeof((x)[0]))

(4) 位操作

#define BIT_MASK(x)         (1 << (x))#define BIT_GET(x, y)       (((x) >> (y)) & 0x01u)#define BIT_SET(x, y)       ((x) | (1 << (y)))#define BIT_CLR(x, y)       ((x) & (~(1 << (y))))#define BIT_INVERT(x, y)    ((x) ^ (1 << (y)))
2. 与函数的区别

从上面这几个宏来看,所有的这些操作都可以通过函数来实现,那么他们各有什么优缺点呢?

通过函数来实现:

形参的类型需要确定,调用时对参数进行检查;调用函数时需要额外的开销:操作函数栈中的形参、返回值等;

通过宏来实现:

不需要检查参数,更灵活的传参;直接对宏进行代码扩展,执行时不需要函数调用;如果同一个宏在多处调用,会增加代码体积;

还是举一个例子来说明比较好,就拿上面的比较大小来说吧:

(1) 使用宏来实现

#define MAX(a, b)    (((a) > (b)) ? (a) : (b))
int main(){    printf("max: %d ", MAX(1, 2));}

(2) 使用函数来实现

int max(int a, int b){    if (a > b)        return a;    return b;}
int main(){    printf("max: %d ", max(1, 2));}

除了函数调用的开销,其它看起来没有差别。这里比较的是 2 个整型数据,那么如果还需要比较 2 个浮点型数据呢?

使用宏来调用:MAX(1.1, 2.2);一切 OK;使用函数调用:max(1.1, 2.2); 编译报错:类型不匹配。

此时,使用宏来实现的优势就体现出来了:因为宏中没有类型的概念,调用者传入任何数据类型都可以,然后在后面的比较操作中,大于或小于操作都是利用了 C 语言本身的语法来执行。

如果使用函数来实现,那么就必须再定义一个用来操作浮点型的函数,以后还有可能比较:char 型、long 型数据等等。

在 C++ 中,这样的操作可以通过参数模板来实现,所谓的模板也是一种代码动态生成机制。当定义了一个函数模板后,根据调用者的实参,来动态产生多个函数。例如定义下面这个函数模板:

template

当编译器看到 max(1, 2) 时,就会动态生成一个函数 int max(int a, int b) { ... };

当编译器看到 max(1.1, 2.2) 时,又会动态生成另一个函数 float max(float a, float b) { ... }。

所以,从代码的动态生成角度看,宏定义和 C++ 中的模板参数有点神似,只不过宏定义仅仅是代码扩展而已。

下面这个例子也比较不错,利用宏的类型无关,来动态生成结构体:

#define VEC(T)              struct vector_##T {         T *data;               size_t size;        };
int main(){    VEC(int)   vec_1 = { .data = NULL, .size = 0 };    VEC(float) vec_2 = { .data = NULL, .size = 0 };}

这个例子中用到了 ##,下面会解释这个知识点。在前面的例子中,宏的参数传递的都是一些变量,而这里传递的宏参数是数据类型,通过宏的类型无关性,达到了“动态”创建结构体的目的:

struct vector_int {    int *data;    size_t size;}
struct vector_float {    float *data;    size_t size;}

这里有一个陷阱需要注意:传递的数据类型中不能有空格,如果这样使用:VEC(long long),那替换之后得到:

struct vector_long long {  // 语法错误    long long *data;    size_t size;}

四、符号:# 与 ##

这两个符号在编程中的作用也是非常巧妙,夸张的说一句:在任何框架性代码中,都能见到它们的身影!作用如下:

#:把参数转换成字符串;##:连接参数。1. #: 字符串化

直接看最简单的例子:

#define STR(x) #xprintf("string of 123: %s ", STR(123));

传入的是一个数字 123,输出的结果是字符串 “123”,这就是字符串化。

2. ##:参数连接

把宏中的参数按照字符进行拼接,从而得到一个新的标识符,例如:

#define MAKE_VAR(name, no) name##no
int main(void){    int MAKE_VAR(a, 1) = 1;     int MAKE_VAR(b, 2) = 2;
   printf("a1 = %d ", a1);    printf("b2 = %d ", b2);    return 0;}

当调用宏 MAKE_VAR(a, 1) 后,符号 ## 把两侧的 name 和 no 首先替换为 a 和 1,然后连接得到 a1。然后在调用语句中前面的 int 数据类型就说明了 a1 是一个整型数据,最后初始化为 1。

五、可变参数的处理 

1. 参数名的定义和使用

宏定义的参数个数可以是不确定的,就像调用 printf 打印函数一样,在定义的时候,可以使用三个点(...)来表示可变参数,也可以在三个点的前面加上可变参数的名称。

如果使用三个点(...)来接收可变参数,那么在使用的时候就需要使用 __VA_ARGS__来表示可变参数,如下:

#define debug1(...)      printf(__VA_ARGS__)
debug1("this is debug1: %d ", 1);

如果在三个点(...)的前面加上了一个参数名,那么在使用时就一定要使用这个参数名,而不能使用 __VA_ARGS__来表示可变参数,如下:

#define debug2(args...)  printf(args)
debug1("this is debug2: %d ", 2);
2. 可变参数个数为零的处理

看一下这个宏:

#define debug3(format, ...)      printf(format, __VA_ARGS__)
debug3("this is debug4: %d ", 4);

编译、执行都没有问题。但是如果这样来使用宏:

debug3("hello ");

编译的时候,会出现错误: error: expected expression before ‘)’ token。为什么呢?

看一下宏扩展之后的代码(__VA_ARGS__为空):

printf("hello ",);

看出问题了吧?在格式化字符串的后面多了一个逗号!为了解决问题,预处理器给我们提供了一个方法:通过 ## 符号把这个多余的逗号给自动删掉。于是宏定义改成下面这样就没有问题了。

#define debug3(format, ...)     printf(format, ##__VA_ARGS__)

类似的,如果自己定义了可变参数的名字,也在前面加上 ##,如下:

#define debug4(format, args...)  printf(format, ##args)

六、奇思妙想的宏

宏扩展的本质就是文本替换,但是一旦加上可变参数(__VA_ARGS__)和 ## 的连接功能,就能够变化出无穷的想象力。

我一直坚信,模仿是成为高手的第一步,只有见多识广、多看、多学习别人是怎么来使用宏的,然后拿来为己所用,按照“先僵化-再优化-最后固化”这个步骤来训练,总有一天你也能成为高手。

这里我们就来看几个利用宏定义的巧妙实现。

1. 日志功能

在代码中添加日志功能,几乎是每个产品的标配了,一般见到最普遍的是下面这样的用法:

#ifdef DEBUG    #define LOG(...) printf(__VA_ARGS__)#else    #define LOG(...) #endif
int main(){    LOG("name = %s, age = %d ", "zhangsan", 20);    return 0;}

在编译的时候,如果需要输出日志功能就传入宏定义 DEBUG,这样就能打印输出调试信息,当然实际的产品中需要写入到文件中。如果不需要打印语句,通过把打印日志信息那条语句定义为空语句来达到目的。

换个思路,我们还可以通过条件判断语句来控制打印信息,如下:

#ifdef DEBUG    #define debug if(1)#else     #define debug if(0)#endif
int main(){    debug {        printf("name = %s, age = %d ", "zhangsan", 20);    }    return 0;}

这样控制日志信息的看到的不多,但是也能达到目的,放在这里只是给大家开阔一下思路。

2. 利用宏来迭代每个参数#define first(x, ...) #x#define rest(x, ...)  #__VA_ARGS__
#define destructive(...)                                  do {                                                      printf("first is: %s", first(__VA_ARGS__));         printf("rest are: %s", rest(__VA_ARGS__));      } while (0)
int main(void){    destructive(1, 2, 3);    return 0;}

主要的思想就是:每次把可变参数 VA_ARGS 中的第一个参数给分离出来,然后把后面的参数再递归处理,这样就可以分离出每一个参数了。我记得侯杰老师在 C++ 的视屏中,利用可变参数模板这个语法,也实现了类似的功能。

刚才在有道笔记中居然找到了侯杰老师演示的代码,熟悉 C++ 的小伙伴可以研究下下面这段代码:

// 递归的最后一次调用void myprint(){}
template

在这个例子中,核心在于 TEST 宏定义,通过 ## 拼接功能,构造出 case 分支的比较目标,然后动态拼接得到对应的函数,最后调用这个函数。

4. 动态创建错误编码与对应的错误字符串

这也是一个非常巧妙的例子,利用了 #(字符串化) 和 ##(拼接) 这 2 个功能来动态生成错误编码码和相应的错误字符串:

#define MY_ERRORS         E(TOO_SMALL)          E(TOO_BIG)            E(INVALID_VARS)
#define E(e) Error_## e,typedef enum {    MY_ERRORS} MyEnums;#undef E
#define E(e) #e,const char *ErrorStrings[] = {    MY_ERRORS};#undef E
int main(){    printf("%d - %s ", Error_TOO_SMALL, ErrorStrings[0]);    printf("%d - %s ", Error_TOO_BIG, ErrorStrings[1]);    printf("%d - %s ", Error_INVALID_VARS, ErrorStrings[2]);
   return 0;}

我们把宏展开之后,得到一个枚举类型和一个字符串常量数组:

typedef enum {    Error_TOO_SMALL,    Error_TOO_BIG,    Error_INVALID_VARS,} MyEnums;
const char *ErrorStrings[] = {    "TOO_SMALL",    "TOO_BIG",    "INVALID_VARS",};

宏扩展之后的代码是不是很简单啊。编译、执行结果如下:

0 - TOO_SMALL 1 - TOO_BIG 2 - INVALID_VARS

七、总结

有些人对宏爱之要死,多到滥用的程度;而有些人对宏恨之入骨,甚至用上了邪恶(evil)这个词!其实宏对于 C 来说,就像菜刀对于厨师和歹徒一样:用的好,可以让代码结构简洁、后期维护特别方便;用的不好,就会引入晦涩的语法、难以调试的 Bug。

对于我们开发人员来说,只要在程序的执行效率、代码的可维护性上做好平衡就可以了。

不吹嘘,不炒作,不浮夸,认真写好每一篇文章!
欢迎转发、分享给身边的技术朋友,道哥在此表示衷心的感谢!转发的推荐语已经帮您想好了:

道哥总结的这篇总结文章,写得很用心,对我的技术提升很有帮助。好东西,要分享!

声明: 本文由入驻OFweek维科号的作者撰写,观点仅代表作者本人,不代表OFweek立场。如有侵权或其他问题,请联系举报。
侵权投诉

下载OFweek,一手掌握高科技全行业资讯

还不是OFweek会员,马上注册
打开app,查看更多精彩资讯 >
  • 长按识别二维码
  • 进入OFweek阅读全文
长按图片进行保存