概述

宏定义是一个给定名称的代码片段,当我们使用这个名称的时候,预处理器会自动将其替换为宏定义的内容。宏定义有两种,一种是object-like宏定义,在使用的时候相当于一个数据对象;另一种是function-like,在使用的时候就像调用函数那样。

宏定义的内容可以是任意的,甚至是C关键词(不可以的内容特例[预处理命令]defined,[c++ named operators]and_eq,bitand,bitor,compl,not,not_eq,or,or_eq,xor,xor_eq)

宏展开会使源程序变长,但是宏展开发生在编译过程中,不占运行时间,只占编译时间。

宏展开因为在预处理阶段发生,不会分配内存。
宏替换发生时机

编译c源程序的过程:

    预处理
    编译
    汇编
    连接

预处理产生编译器的输出,实现功能如下
文件包含

把#include中包含的内容拓展为文件的正文,即找到.h文件,同时展开到#include所在处
条件编译

根据#if和#ifdef等编译命令,将源程序文件中的部分包含进来,部分排除,排除在外的一般转换为空行
宏展开

将对宏的调用展开成相对应的宏定义
object-like宏定义
形式

    通常形式:#define <宏名> <字符串>

#define后面紧跟的是宏定义的代码段的名称,再然后是宏名所要表示的token序列

    通常来说我们用object-like宏定义对于数字常量给出一个符号代替

    例子: #define BUFFER_SIZE 1024

    这里面宏名就是BUFFER_SIZE,所要代替的token序列就是1024。然后当你使用BUFFER_SIZE的时候,预处理器就会将其拓展,这样编译器在处理的时候就会将其看作1024

    例如源代码foo = (char*)malloc(BUFFER_SIZE),经过预处理器处理后,编译器将其等同于foo = (char*)malloc(1024)。

用法

    通常来说宏名以全大写字母书写(约定俗成,方便阅读)

    #define命令会在该行结束的时候停止,如果想要同时作用于多行,那么需要在后面加上\,注意\后面不要在加空格等空白符,直接是换行符

    例如

    #define NUMBERS 1, \
                    2, \
                    3
        1
        2
        3

    等价于

    #define NUMBERS 1, 2, 3
        1

    宏定义的字符串内容几乎没有限制(需要保证括号的相互平衡)

    由于C预处理器在处理程序的时候是顺序的,因此宏定义从其定义的位置开始作用。

    宏定义可以嵌套定义,也就是说如果在替换的时候,遇到了另一个宏定义名称,那么将会继续进行替换

    也就是说

    #define A B
    #define B 1
        1
        2

    在预处理的时候 A ⟶ B A\longrightarrow B A⟶B,然后继续检查 B B B是不是宏名,将 B ⟶ 1 B\longrightarrow1 B⟶1,最终替换之后A替换成了1。要注意的是,在嵌套定义的时候,如果在递归检查的时候,遇到了曾检查过的某个宏名,那么将不会继续拓展,这样避免了递归调用。

疑问

查阅到的一篇文章认为

#define A B
#define B 1

    1
    2

#define B 1
#define A B

    1
    2

会有不同,不同在于后者定义方式中A会一直等于B的当前有效值。

我做的实验结果是两种都可以等于B的当前有效值,不明白文章什么意思.
自引用宏(self-referential macros)
概念

自引用宏就是宏名出现在自己的宏定义中。例如#define foo(4 + foo)

自引用宏的规则为了避免无穷无尽的递归调用,产生像(4+(4+(4+(4+foo))))这样的调用,禁止对一个出现过的宏名再次进行替换,也就是说上面的定义只会产生4+foo为止。

间接自引用宏 例如

#define x (4 + y)
#define y (2 * x)

    1
    2

也遵循上面的规则,不对一个已经出现过的宏名再次进行替换。
用法

通常是直接自引用宏

#define EPERM EPERM

    1

这里面EPERM是一个已经被定义过的量,因此#ifdef EPERM后的代码会成功运行。
function-like宏定义
概念
不带参数

形如#define <宏名>(<参数表>) <宏体>

即宏名后面必须要紧跟一对圆括号。

在使用的时候,也必须紧跟一对圆括号,要不然不进行替换

也就是说foo经过预处理之后,还是foo,而只有foo()才会被替换为<宏体>

如果在定义的时候,宏名和圆括号之间有空白符(空格,tab之类的),例如#define test () my(),那么这时候你定义的就是一个object-like宏定义,对于test就会替换成() my(),进而test()就会替换成() my()(),注意不是function-like宏定义,test()后面的圆括号不会消失。
带参数

带参数的仍要遵循上述规则,区别只是宏名后面紧跟的圆括号中放置了参数,就像真正的函数那样。

例如#define <宏名>(<参数列表>) <宏体>

注意参数列表中的参数必须是有效的c标识符,同时以,分隔
用法

当定义了一个function-like宏定义如#define min(X,Y) ((X) < (Y) ? (X) : (Y)),想要调用它,只需要使用宏名,后面加上参数列表。注意你调用的时候的参数数目要恰好和宏定义中的参数数目相同。

例如min(a,b)等价于((a) < (b) ? (a) : (b))这样一个三目运算符的使用。参数列表以,作为分隔符,同时()必须成对出现,同时**(,)圆括号中的,不作为分隔符**,也就是说min((a,b))实际上你只传递了一个参数(a,b),但是对于中括号[]和大括号就没有这样的要求和规则,不必成对出现,也不影响,作为分隔符的作用,例如min([a,b])传递的就是两个参数[a和b]。

同时function-like宏定义的参数一定是已经经过宏完全替换之后的,也就是说假如调用min(min(a,b),c),传递了两个参数min(a,b),c,首先对这两个参数进行宏替换,那么min(a,b)也就会变成((a) < (b) ? (a) : (b)),然后以这样的形式传递。

宏定义的参数传递过程中,空白符允许是传递的参数,不过不管是怎样的空白符,传递过程中都认为是一个单纯的空格,就算原来是5个空格,也只认为是一个参数。例如调用min( ,b)就是传递两个参数和b,宏替换的结果就是(( ) < (b) ? () : (b))
注意事项

    在<宏体>中,如果对参数加上" "那么该参数不会被替换

    例如

    #define foo(x) x,"x"
        1

    调用为foo(bar),替换成为bar,"x",并没有把"x"中的x作为参数看待。

特殊用法
字符串化
概念

使用#将参数x变成对应的字符串

    演示

    #define TOSTRING(X) #x,应用这个宏定义,最后产生的替换就为字符串。

    比如说TOSTRING(x==0),产生的替换就是"x==0"

注意事项

    要注意对于#字符串化的参数,不用进行宏拓展替换,直接进行代入

    #define A B
    TOSTRING(A)
        1
        2

    这里产生的替换是"A",而不是"B",因为字符串化是直接替换。

    #字符串化并不是简单的在原参数两侧加上"",而是对其中需要转义的字符变成转义字符。

    例如当参数为p = "foo\n";最终产生的字符串就是"p = \"foo\\n\";|。

    如果其中有空白符,在转换成字符串的时候,开头和结尾的空白符会被丢弃,中间的都会变成一个空格。

    这样输出的字符串可以和周围的字符串连接称为一个长字符串

    例如:#define TOSTRING(X) "test" #X "temp",假设参数为p = "foo\n",那么最终产生的字符串就为"testp = \"foo\\n\temp";|

token连接(token concatenation)
概念

使用##将两个token连接在一起,这里的token可以是宏体中的内容,也可以是参数。

使用方法如下A##B,如果A和B都是宏体中的内容,那么要求其连接之后的内容必须是有效的c内容,例如x##+这样的字符就不允许存在,当然x##+5是可以存在的。
注意事项

    ##的连接作用同样优先于宏拓展,也就是说先调用##运算符。
    ##和其操作数之间可以放置任意长度的空白符,但是##运算符不能放置在宏体的任何一端。显然##和空白符相连接不会有任何操作

可变宏定义
概念

function-like宏定义也可以不固定参数的数目,将所有的参数全部用一个特殊标识符进行代替。

例如#define eprintf(...) fprintf(stderr,__VA_ARGS__)

这里面的...就表明eprintf中的参数可变,其中所有的参数用来替代__VA_ARGS__,而__VA_ARGS__是一个保留关键字,不要在其他地方使用,它专门用来代替可变参数。

这样当使用eprintf("%s:%d",input_file,lineno)就被fprintf(stderr,"%s:%d",input_file,lineno)所代替。

这里面也可以给可变参数加上自定义的名称,只许在...的前面直接写自定义名称,例如#define eprintf(st...) fprintf(stderr,st),这里面st就是自定义的名称。
特殊用法

    可变参数和固定参数可以同时存在,例如#define eprintf(format, ...) fprintf(stderr,format,__Va_ARGS__),在旧的编译器下,必须提供至少两个参数,一个给format,一个给...。而且第二个参数不能为空

    更多的内容

预定义宏(predefined macros)

    有一些提前定义的宏定义,都是object-like宏定义,分为三类,standard,common,system-specific

    在c++中,还有命名操作符(named operators)

standard预定义宏

standard预定义宏由语言标准规定,在所有符合标准的编译器上均可用,名称均以双下划线__开头
__FILE__

当前源文件名称,类型:c风格字符串数组
__LINE_

当前输入的行数,类型:int常数

#line lineNum,其中lineNum是一个非负十进制整常数,指定下一行的__LINE_为lineNum

#line lineNum fileName,lineNum非负十进制整常数,指定下一行的__LINE__为lineNum,fileName字符串常量。改变下一行开始所有的__FILE__值为fileName,其中字符仍要转义
__DATE_

预处理器运行时的日期,类型:字符串常量

包含11个字符,例子Feb 12 1996

如果无法确定日期,发出warning,__DATE_变成??? ?? ????
__TIME_

预处理器运行时的时间,类型:字符串常量

包含8个字符,例子:23::50::01

同样无法确定发出warning,变成??:??:??
__STDC__

如果编译器遵循ISO 标准 C,置为1

如果GNU CPP用的不是GCC编译器,不必为真

-traditional-cpp选项和__STDC__互斥,只有一个被使用
__STDC_VERSION__

C标准的版本数字,类型:long

形式:yyyymmL,其中yyyy是标准版本的年份,mm是月份

同样和-traditional-cpp互斥,而且当编译c++也不会被定义
__STDC_HOSTED

编译器的目标是hosted environment,置为1

hosted environment由完全可用的c标准库工具
__cplusplus

当使用c++编译器,该宏被定义

拓展为版本数字,同样也为yyyymmL
__OBJC__

当使用objective-c编译器,被置为1
__ASSEMBLER__

当预处理汇编语言,置为1
common预定义宏

    是GNU C拓展。当使用GNU C或者GNU Fortran的时候,可用。

    同样以双下划线开头__

__COUNTER__

该宏扩展为从0开始的顺序整数值。

与##操作符一起,这为生成惟一标识符提供了一种方便的方法。必须小心确保在包含使用它的预编译头之前不展开COUNTER。否则,将不使用预编译的标头
__GFORTRAN__

GNU Fortan编译器定义
__GNUC__
__GNUC_MINOR__
GNUC_PATCHLEVEL__

GNU编译器定义。分别为主要版本、次要版本,以及补丁程序的级别,类型:整数
__GNUG__

GNU C++编译器定义

等价于__GNUC__ && __cplusplus
__STRICT_ANSI__

主要用于指示GNU libc的头文件仅使用在标准C中找到的定义。
__BASE_FILE__

主输入文件的名称,类型:c字符串
__INCLUDE_LEVEL__

包含文件的嵌套深度,类型:十进制整型常量
__ELF__

使用ELF对象格式,定义此宏
__VERSION__

使用的编译器的版本,类型:字符串常量

没有特定的格式
system-specific预定义宏

使用cpp -dM来查看全部内容。

根据正在使用的系统和机器类型有所不同。

    C标准要求所有特定于系统的宏都必须是保留名称空间的一部分。 所有以两个下划线或下划线和大写字母开头的名称都保留给编译器和库使用,以供其使用。

c++命名运算符

c++中有11个关键字,是运算符的操作符的替换。

    and -> &&
    and_eq -> &=
    bitand -> &
    bitor -> |
    compl -> ~
    not -> !
    not_eq -> !=
    or -> ||
    or_eq -> |=
    xor -> ^
    xor_eq -> ^=

取消宏定义和重新定义
undef取消宏定义

停止中断一个宏定义的使用,只需要在宏定义名称前面加上#undef,function-like宏定义也不要加圆括号。

例如

#define  FOO 4
#undef   Foo     //停止使用FOO
#define   FOO(X)  X*X
#undef    FOO    //function-like宏定义也是同样取消

    1
    2
    3
    4

重新定义

    可以对一个已经#undef的标识符进行定义,这时候就相当于定义一个全新的宏,该宏不需要和标识符原来的作用有任何关系,可以随便定义

    假设重定义的标识符此时还是有效的宏定义,那么新的定义必须和原来的效用相同。

    效用相同的要求如下:
        是相同类型的宏定义(object-like或者function-like)
        替换列表中的token相同
        参数相同
        空白符出现在相同的位置(数目不重要,位置很重要),注释会被空白符替换掉

    效用不同的例子

    #define FOUR (2 + 2)    //原来定义的
    #define FOUR ( 2+2 )    //空白符位置不同
    #define FOUR (2 * 2)    //token不同
    #define FOUR(score,and,seven,years,ago) (2 + 2) //参数列表不同
        1
        2
        3
        4

    如果这种方式重定义的宏前后不相似,那么会产生warning,然后将宏定义替换为后者

宏定义常见用法
防止头文件被重复包含

#ifndef BODY_H   //保证只有未包含该头文件才会将其内容展开

#define BODY_H

//头文件内容

#endif

    1
    2
    3
    4
    5
    6
    7

得到指定地址上的一个字节或者一个字

#define MEM_B(x) (*(byte*)(x))    //得到x表示的地址上的一个字节
#define MEM_W(x) (*(word*)(x))    //得到x表示的地址上的一个字

    1
    2

得到一个field在结构体中的偏移量

#define OFFSET(type,field) (size_t)&((type*)0->field)

    1

得到一个结构体中field所占用的字节数

#define FSIZ(type,field) sizeof(((type*)0)->field)

    1

得到一个变量的地址

#defien B_PTR(var) ((byte*)(void*)&(var))   //得到字节宽度的地址
#define W_PTR(var) ((word*)(void*)&(var))   //得到字宽度的地址

    1
    2

将一个字母转换成大写

#define UPCASE(c) (((c) >= "a" && (c) <= "z") ? ((c) - 0x20) : (c) )

    1

防止溢出

#define INC_SAT(val) (val = ((val) +1 > (val)) ? (val) + 1 : (val))

    1

返回数组元素的个数

#define ARR_SIZE(a) (sizeof( (a) ) / sizeof(a[0]) ) )
————————————————

©著作权归作者所有:来自51CTO博客作者wx607823dfcf6a9的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. Uboot启动参数说明
  2. jQuery:常用 jQuery方法,$()的四种类型参数,jQuery对象转js对象方
  3. 使用Linux命令cURL实现文件定时上传到ftp服务器的程序
  4. Linux基础:xargs命令
  5. 【DB笔试面试710】在Oracle中,用哪个参数可以判断一个数据库是否
  6. C语言中结构体的位域(bit-fields)
  7. 第8部分- Linux ARM汇编 定义操作
  8. Java如何获取方法参数具体名称?这是个好问题!
  9. RTX3080Ti和RTX2080Ti性能差距 RTX3080Ti和RTX2080Ti参数对比哪

随机推荐

  1. android ndk 开发之 在 应用程序中使用 j
  2. [置顶] android调用第三方库——第二篇—
  3. Relativelayout的一些属性
  4. Android摄像头照相机技术-android学习之
  5. Android XMl文件中tools前缀
  6. android Textview过长时显示省略号
  7. Android程序反编译的方法[已更新]
  8. Android 内存管理机制
  9. Android API课程1: Application Fundament
  10. Android优秀开源项目收集