C语言宏的用法详解

1、简介

宏在C语言中是一段有名称的代码片段。无论何时使用到这个宏的时候,宏的内容都会被这段代码替换掉。主要有两种宏,他们的区别主要是在使用上面,一种是在使用时类似于数据对象称为Object-like,另一种在使用时类似于函数调用称为Function-like。在C语言使用#define来定义宏
你可以将任意的有效的标识符定义为宏,设置C语言的关键字也可以。但是在C语言中defined不可以作为宏的名称。在C++中以下的关键字也不可以作为宏的名称and,and_eq,bitand,bitor,compl,not,not_eq,or,or_eq,xor,xor_eq

2、两种宏的类型

2.1 Object-like宏

Object-like宏,可以比较简单的进行代码段的替换。这种方式最常用做表示常量数字。例如:

#define BUFFER_SIZE 1024

使用该宏的时候就可以用来替换数字。

foo = (char *) malloc (BUFFER_SIZE);

预处理器将会把该宏替换为对应的数字,如下所示。

foo = (char *) malloc (1024);

按照惯例,宏一般都写作大写字母。

多行的宏

宏结束于#define的行尾,如果有必要的话,可以在末尾添加反斜杠来将宏定义成多行。

#define NUMBERS 1, \
                2, \
                3
int x[] = { NUMBERS };
//→ int x[] = { 1, 2, 3 };

多次宏替换

如果宏定义的代码段依然是宏的话,预处理器会继续进行宏替换的操作。

#define TABLESIZE BUFSIZE
#define BUFSIZE 1024
TABLESIZE
//→ BUFSIZE
//→ 1024

最终TABLESIZE会被替换成1024

2.2 Function-like宏

宏还可以被定义成下面的形式,使用该宏的时候,类似于调用函数,这类宏的定义中,宏的名称后面紧跟一堆括号(与括号之间不能有空格)。

#define lang_init()  c_init()
lang_init()
//→ c_init()

调用该类宏的时候,也必须跟一个括号,如果不跟括号的话,会显示语法错误。

3 宏的参数

Function-like宏可以接受参数,类似于真正的函数一样。参数必须是有效的C语言标识符,使用逗号隔开

#define min(X, Y)  ((X) < (Y) ? (X) : (Y))
  x = min(a, b);          //→  x = ((a) < (b) ? (a) : (b));
  y = min(1, 2);          //→  y = ((1) < (2) ? (1) : (2));
  z = min(a + 28, *p);    //→  z = ((a + 28) < (*p) ? (a + 28) : (*p));

在上面的例子中,x = min(a, b)调用宏的时候,将入参a,b替换到形参X, Y在宏内的位置,就变成了x = ((a) < (b) ? (a) : (b))

4 字符串化

字符串化指的是,可以在宏的参数前面加入#,使入参变成字符串。
例如:

#include <stdio.h>
#define str(expr) printf("%s\r\n", #expr)

int main()
{
    str(abc);
    str(12345);
    return 0;
}

这里运行代码会打印:

abc
12345

str宏的入参,都变成了字符串打印了出来。

5 连接符号

在宏中,可以使用两个#将两个符号连接成一个符号。

#include <stdio.h>
#define A1 printf("print A1\r\n")
#define A2 printf("print A2\r\n")
#define A(NAME) A##NAME
int main()
{
    A(1);
    return 0;
}

这里会打印

print A1

在该例子中,调用宏A(1)时,NAME为1。A##NAME这个符号连接,即将A和1连接成了一个符号A1,然后执行宏A1的内容。最终打印出来了print A1

6、 可变参数

定义宏可以接受可变数量的参数,类似于定义函数一样。如下就是一个例子

#include <stdio.h>
#define myprintf(...) fprintf (stderr, __VA_ARGS__)
int main()
{
    myprintf("1234\r\n");
    return 0;
}

这里会输出

1234

这种形式的宏,会把...的代表的参数扩展到后面的VA_ARGS中。在该例子中,就会扩展为fprintf(stderr, "1234\r\n")
如果你的参数比较复杂,上面的myprintf还可以定义为如下的形式,用自定义的名称args来表示参数的含义:

#define myprintf(args...) fprintf (stderr, args)

7 预定义宏

标准预定义宏

标准的预定义宏都是用双下划线开头和结尾,例如__FILE____LINE__,表示文件的名称和该行代码的行号。

#include <stdio.h>

int main()
{
    printf("FILE:%s,LINE:%d\r\n",__FILE__, __LINE__);
    printf("DATA:%s\r\n",__DATE__);
    printf("TIME:%s\r\n",__TIME__);
    printf("STDC:%d\r\n",__STDC__);
    printf("STDC_VERSION:%d\r\n",__STDC_VERSION__);
    printf("STDC_HOSTED:%d\r\n",__STDC_HOSTED__);
#ifdef __cplusplus
    printf("cplusplus:%d\r\n", __cplusplus);    
#else
    printf("complied by c\r\n");    
#endif
    
    return 0;
}

输出如下

FILE:macro.c,LINE:5
DATA:Jan 13 2019
TIME:21:41:14
STDC:1
STDC_VERSION:201112
STDC_HOSTED:1
complied by c

本文件名为macro.c,并且该行代码为第5行。
__DATA__表示当前的日期
__TIME__表示当前的时间
__STDC__在正常的操作中,此宏为1,表示编译器符合ISO C标准
__STDC_VERSION__表示ISO C的版本
__STDC_HOSTED__如果值为1的话,表示目标环境有完成的标准C库
__cplusplus如果该宏被定义了,表示是被C++编译器编译的

常见的预定义宏

该节中的宏是GNU C编译器的扩展实现。

#include <stdio.h>

int main()
{
    printf("__COUNTER_%d\r\n", __COUNTER__);
    printf("__COUNTER_%d\r\n", __COUNTER__);    
    printf("__GNUC__:%d\r\n",__GNUC__);
    printf("__GNUC_MINOR__:%d\r\n",__GNUC_MINOR__);
    printf("__GNUC_PATCHLEVEL__:%d\r\n",__GNUC_PATCHLEVEL__);
    #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
        printf("little endian\r\n");
    #elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
        printf("big endian\r\n");
    #elif __BYTE_ORDER__ == __ORDER_PDP_ENDIAN__
        printf('pdp endian\r\n')
    #endif
    #if __LP64__ == 1
        printf("64bit env\r\n");
    #else
        printf("other bit env\r\n");
    #endif
    return 0;
}

输出

__COUNTER_0
__COUNTER_1
__GNUC__:7
__GNUC_MINOR__:3
__GNUC_PATCHLEVEL__:0
little endian
64bit env


__COUNTER_:是生成一个唯一的数字。
__GNUC____GNUC_MINOR____GNUC_PATCHLEVEL__确定了你的GCC版本号。例如我的环境就是7.3.0
__BYTE_ORDER__表示当前环境的字节序
__LP64__ 表示当前环境是不是64位,如果该值为1,则环境为64位环境
更多GNU C编译器的预定义宏可以 点此连接查看

系统特定的预定义宏

系统特定的预定义宏,在不同的操作系统和CPU上面,呈现的结果可能会有所不同。例如我的环境是Linux X86_64平台。执行下面的代码

#include <stdio.h>

int main()
{
    printf("__unix_:%d\r\n", __unix__);
    printf("__x86_64__:%d\r\n", __x86_64__);
    return 0;
}

输出结果是:

__unix_:1
__x86_64__:1

如果是其他操作系统的CPU平台的话,执行的结果会有所不同。

C++的命名操作符

在第一节就说过C++ 中有and,and_eq,bitand,bitor,compl,not,not_eq,or,or_eq,xor,xor_eq这些命名不可以用作宏的名称。是因为在C++ 中系统将这些关键字预定义成了操作符。

命名操作符 符号
and &&
and_eq &=
bitand &
bitor |
compl ~
not !
not_eq !=
or
or_eq =
xor ^
xor_eq ^=

所以在C++ 中,你可以使用命名操作符来代替这些符号。例如:

#include <iostream>
using namespace std;
int main()
{
    int a = 10;
    int b = 20;
    int c = a bitor b; // a | b
    int d = a bitand b; //a & b
    cout << "c = " << c << endl;
    cout << "d = " << d << endl;

    if ( true and (a > b))
        cout << "true" << endl;
    else
        cout << "false" << endl;
        
    return 0;
}

输出:

c = 30
d = 0
false

8、取消宏定义与重复宏定义

取消宏定义

使用#undef可以将已经定义的宏取消掉

#define BUFSIZE 1020
#undef BUFSIZE

如果在#undef之后再使用BUFSIZE就会报错,没有定义BUFSIZE

重复宏定义

如果两个宏定义之间,仅有空格和注释不同的话,两个宏定义还是同一个宏定义。
例如:

#define FOUR (2 + 2)
#define FOUR         (2    +    2)
#define FOUR (2 /* two */ + 2)

这三个宏定义实际上是相同的,不算是重复定义。
而下面的宏定义则是不同的,编译器会给出宏重复定义的警告。也只有最后一个宏才会生效

#define FOUR (2 + 2)
#define FOUR ( 2+2 )
#define FOUR (2 * 2)
#define FOUR(score,and,seven,years,ago) (2 + 2)

9、几个常见的使用场景

替代魔法数字

这个可能是在C语言中非常常见的一种用法了,就是使用宏来替代一个魔法的数字,增加代码可读性。

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

#define BUFSIZE 1024
int main()
{
    char *buf = (char *)malloc(BUFSIZE);
    free(buf);
    return 0;
}

LOG日志与do{}while(0)

#include <stdio.h>
#include <stdlib.h>
#define BUFSIZE 1024
#define LOG(str) \
do \
{\
    fprintf(stderr, "[%s:%d %s %s]:%s\r\n",  __FILE__, __LINE__, __DATE__, __TIME__, str); \
}while(0)
int main()
{
    char *buf = (char *)malloc(BUFSIZE);
    LOG("malloc for buf");
    free(buf);
    return 0;
}

输出内容:

[macro.c:12 Jan 13 2019 22:38:33]:malloc for buf

这里定义了LOG宏,可以打印日志,输出当前的代码文件和行数,以及时间和用户定义的内容。自行扩展可以增加更丰富的内容。
这里使用了一个do{} while(0)来包含宏的内容。看似这个do() while(0)没有什么意义。但是这是一个编写宏内多行代码段的好习惯。

  • 使用do{}while(0)包含的话,可以作为一个独立的block,进行变量定义等一些复杂的操作
  • 该用法主要是防止在使用宏的过程中出现错误。
    例如
#define foo() \
    fun1(); \
    fun2()
if (a > 10)
    foo()

在这种情况下,if后面没有跟大括号,我们foo宏里面定义的是两个语句,其中fun2是在if条件判断之外的。这样就不符合我们的预期了。

如果使用大括号来避免上面的错误,还会出现下面的错误:

#include <stdio.h>
#include <stdlib.h>
#define add(x, y) {x += 1; y += 2;}

int main()
{
    int x = 10;
    int y = 20;
    if (x > y)
        add(x, y);
    else
        ;

    return 0;
}

这里在add(x, y)之后有个分号。会造成else匹配不到if编译错误。所以为了防止发生这些错误,可以使用do{}while(0)将函数体包含。

Linux内核中offsetof

在Linux的内核代码中,大量的使用到了offsetof这个宏,该宏的作用就是计算出一个结构体中的变量的偏移值是多少。

#include <stdio.h>
#include <stdlib.h>
#define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
typedef struct myStructTag
{
    int a;
    double b;
    float c;
    char szStr[20];
    long int l;
}myStruct;
int main()
{
    printf("%d\r\n", offsetof(myStruct, a));
    printf("%d\r\n", offsetof(myStruct, b));
    printf("%d\r\n", offsetof(myStruct, c));
    printf("%d\r\n", offsetof(myStruct, szStr));
    printf("%d\r\n", offsetof(myStruct, l));
}

输出结果:

0
8
16
20
40

该宏的入参第一项TYPE为结构体的类型,第二项MEMBER为结构体中的变量名称。该宏将0强转为TYPE *类型的指针,然后获取该结构体指针指向具体成员的地址。因为结构体指针的地址为0,所以取地址得到的成员地址就是以0为基址的偏移值。
有了该宏,我们就可以通过任意一个结构体成员的地址来得到结构体指针的地址了。

Linux内核中container_of宏

该宏的作用就是通过结构体任意成员的地址来获取结构体指针。该宏需要借助上一节的offsetof。
下面是使用该宏的代码:

#include <stdio.h>
#include <stdlib.h>
#define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
    const typeof(((type *)0)->member) * __mptr = (ptr); \
    (type *)((char *)__mptr - offsetof(type, member)); })

typedef struct myStructTag
{
    int a;
    double b;
    float c;
    char szStr[20];
    long int l;
}myStruct;
int main()
{
    myStruct *p = (myStruct *)malloc(sizeof(myStruct));
    printf("base ptr=%p\r\n", p);
    printf("base ptr by l=%p\r\n", container_of(&p->l, myStruct, l));
}

输出内容:

base ptr=0x55cc10d66260
base ptr by l=0x55cc10d66260

可以看出,通过container_of算出来的基址和直接打印的p的地址是相同的。Linux内核中很多基础的抽象数据结构,例如双线链表等,都大量使用到了container_of这个宏。有了这个宏,我们就可以写出来数据无关的抽象数据结构,例如我们可以写一个没有数据域的双向链表。

struct list_head {
    struct list_head *next, *prev;
};

实现的时候,我们只需要关系链表的操作即可,完全没有任何数据域的干扰。而在使用时,我们只需要把链表节点定义为具体数据结构中的一个节点即可。

struct person 
{ 
    int age; 
    char name[20];
    struct list_head list; 
};

插入和删除操作仅需要操作链表的节点,而通过container_of这个宏,我们完全可以通过链表的指针去获取到整个数据结构的首地址。这样就把数据结构抽象了,和具体的数据完全剥离。

VPP中节点注册的例子

VLIB_REGISTER_NODE宏的定义

首先看一段VPP中节点注册的宏的定义:

#define VLIB_REGISTER_NODE(x,...)                                       \
    __VA_ARGS__ vlib_node_registration_t x;                             \
static void __vlib_add_node_registration_##x (void)                     \
    __attribute__((__constructor__)) ;                                  \
static void __vlib_add_node_registration_##x (void)                     \
{                                                                       \
    vlib_main_t * vm = vlib_get_main();                                 \
    x.next_registration = vm->node_main.node_registrations;             \
    vm->node_main.node_registrations = &x;                              \
}                                                                       \
__VA_ARGS__ vlib_node_registration_t x
  1. 在该代码段中,VLIB_REGISTER_NODE宏有一个参数x,和可变参数。
  2. __VA_ARGS__ vlib_node_registration_t x声明了一个vlib_node_registration_t结构体变量 x,这里作用是仅声明。
  3. static void __vlib_add_node_registration_##x (void) \ __attribute__((__constructor__))这段代码是声明了一个函数,使用##连接符根据参数来生成函数名称。__constructor__是GNU编译器的一个扩展,把该函数作为构造函数,指明该函数会在模块初始化时调用。
  4. 接下来就是__vlib_add_node_registration_##x 函数的定义了。具体的内容我们可以先无视掉。
  5. 而最后一行,又定义了一遍x。这个需要结合宏调用的地方来看了。总之,这个宏声明了一个变量x,然后定义了一个

VLIB_REGISTER_NODE宏的使用

看完了宏的定义,我们看一下该宏是怎样调用的。

VLIB_REGISTER_NODE (ip4_icmp_echo_request_node,static) = {
  .function = ip4_icmp_echo_request,
  .name = "ip4-icmp-echo-request",

  .vector_size = sizeof (u32),

  .format_trace = format_icmp_input_trace,

  .n_next_nodes = 1,
  .next_nodes = {
    [0] = "ip4-load-balance",
  },
};

首先宏的参数x传入了ip4_icmp_echo_request_node,在宏的扩展时,x都会被替换成传入的参数。
而第二个参数是static,所以定义变量x时,都会static修饰。
最后在定义之后,有等号和大括号。这里是对宏的代码中最后一行__VA_ARGS__ vlib_node_registration_t x进行结构体赋值操作。这里就可以理解为什么__VA_ARGS__ vlib_node_registration_t x定义在宏里面进行了两次了。第一次是仅声明,后面定义的函数仅需要该值的地址去进行注册。而在宏的代码段的最后,是真正的结构体定义。
最后这段代码展开变成了下面的样子:

static vlib_node_registration_t ip4_icmp_echo_request_node;                             
static void __vlib_add_node_registration_ip4_icmp_echo_request_node (void)                     
    __attribute__((__constructor__)) ;                                  
static void __vlib_add_node_registration_ip4_icmp_echo_request_node (void)                     
{                                                                       
    vlib_main_t * vm = vlib_get_main();                                 
    ip4_icmp_echo_request_node.next_registration = vm->node_main.node_registrations;             
    vm->node_main.node_registrations = &ip4_icmp_echo_request_node;                              
}                                                                       
static vlib_node_registration_t ip4_icmp_echo_request_node = {
  .function = ip4_icmp_echo_request,
  .name = "ip4-icmp-echo-request",

  .vector_size = sizeof (u32),

  .format_trace = format_icmp_input_trace,

  .n_next_nodes = 1,
  .next_nodes = {
    [0] = "ip4-load-balance",
  },
};

VPP中错误码的定义

在实际C语言编程中,会有很多错误码的和对应的错误提示的定义。在VPP的代码中使用下面的方式来进行错误码和错误字符串的定义。

#include <stdio.h>
#define foreach_ethernet_arp_error                  \
  _ (replies_sent, "ARP replies sent")                  \
  _ (l2_type_not_ethernet, "L2 type not ethernet")          \
  _ (l3_type_not_ip4, "L3 type not IP4")                \
  _ (l3_src_address_not_local, "IP4 source address not local to subnet") \
  _ (l3_dst_address_not_local, "IP4 destination address not local to subnet") \
  _ (l3_src_address_is_local, "IP4 source address matches local interface") \
  _ (l3_src_address_learned, "ARP request IP4 source address learned")  \
  _ (replies_received, "ARP replies received")              \
  _ (opcode_not_request, "ARP opcode not request")                      \
  _ (proxy_arp_replies_sent, "Proxy ARP replies sent")          \
  _ (l2_address_mismatch, "ARP hw addr does not match L2 frame src addr") \
  _ (gratuitous_arp, "ARP probe or announcement dropped") \
  _ (interface_no_table, "Interface is not mapped to an IP table") \
  _ (interface_not_ip_enabled, "Interface is not IP enabled") \

static char *ethernet_arp_error_strings[] = {
#define _(sym,string) string,
  foreach_ethernet_arp_error
#undef _
};
typedef enum
{
#define _(sym,string) ETHERNET_ARP_ERROR_##sym,
  foreach_ethernet_arp_error
#undef _
    ETHERNET_ARP_N_ERROR,
} ethernet_arp_input_error_t;

int main()
{
    printf("%s\r\n", ethernet_arp_error_strings[ETHERNET_ARP_ERROR_interface_no_table]);
}

输出:

Interface is not mapped to an IP table

foreach_ethernet_arp_error中定义了该模块所有错误类型和错误码的对应关系。 ethernet_arp_error_strings定义了错误字符串的集合。
ethernet_arp_input_error_t定义了错误码的集合。
我们可以通过错误码作为索引去ethernet_arp_error_strings中查找对应的错误字符串。
这样我们就可以很方便的扩展和修改错误类型和错误码了。只需要修改foreach_ethernet_arp_error中的定义即可。
这里错误码和错误字符串都是用过宏来自动生成的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,733评论 0 38
  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,365评论 0 5
  • 原文地址:https://github.com/JuanitoFatas/slime-user-manual#24...
    四月不见阅读 3,100评论 0 2
  • 每个彩虹的到来之前都会有过风雨 而每个成功的背后都会经历过疼痛 预兆来临之前 你所做不是逃避 而是坚持下去...
    如妳所願阅读 232评论 0 1
  • 安: 此时我应该坐在图书馆好好学习的,但我忍不住想要给你写信。 我常常想,你要是能在我身边就好了,爱一个...
    由人阅读 173评论 0 0