Object-like 宏
这种比较简单
Function-like 宏
宏的参数
#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));
用参数的话,后面定义的参数都要带上括号,不然会有二义性。
字符串化
字符串化指的是,可以在宏的参数前面加入#,使入参变成字符串。
eg
#include <stdio.h>
#define str(expr) printf("%s\r\n", #expr)
int main()
{
str(abc);
str(12345);
return 0;
}
这里运行代码会打印:
abc
12345
str宏的入参,都变成了字符串打印了出来。
连接符号
在宏中,可以使用两个#将两个符号连接成一个符号。
#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
可变参数
定义宏可以接受可变数量的参数,类似于定义函数一样。如下就是一个例子
#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)
预定义宏
标准预定义宏
标准的预定义宏都是用双下划线开头和结尾,例如__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
取消宏定义与重复宏定义
取消宏定义 使用#undef可以将已经定义的宏取消掉
#define BUFSIZE 1020
#undef BUFSIZE
如果宏重复的话,编译器会给出宏重复定义的警告。也只有最后一个宏才会生效。
do{}while(0)
用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;
}
使用do{}while(0)包含的话,可以作为一个独立的block,进行变量定义等一些复杂的操作
该用法主要是防止在使用宏的过程中出现错误。
#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));
}
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