赞
踩
宏,也叫宏定义,并不是C/C++语言本身的一部分,不能直接对宏进行编译。在编译程序前,必须先对宏进行预先的处理(专业术语叫作“预处理”)。预处理器会分析所有的源文件,当遇到宏名称严格匹配时,就会展开宏,也就是用定义好的文本来代替宏名称。宏的用途主要有三点:一是可以为程序员的开发提供一定的便利性;二是可以有效减少源文件中大量重复的、相似的代码;三是可以在一定程度上减少系统的开销,提高程序的运行效率。
在C/C++中,可以使用#define命令来定义宏,用于将宏名称或标识符指定为一个文本,比如:常量、语句等。具体的格式如下:
#define 宏名称 文本
宏名称一般采用全大写或者首字母大写的形式,以与普通的变量相区别。宏定义的末尾不能像普通语句一样加分号,如果加了分号,则分号也是上述文本的一部分。定义宏之后,如果不希望该宏定义继续生效,可以使用#undef命令来取消。不带参数的宏的定义和使用都非常简单,这里举几个例子:
- #define MATH_PI 3.1415926
- #define STR_HTTP "http"
- #define MAX_NUMBER 666 + 888
- #define EXPORT_CLASS
-
- double dbRadius = 2.0;
- double dbCircumference = 2 * MATH_PI * dbRadius;
- printf("string is %s, number is %d\n", STR_HTTP, MAX_NUMBER);
-
- class EXPORT_CLASS CTest
- {
- };
-
- #undef EXPORT_CLASS
可以看到,宏定义指定的文本可以是整型、浮点型、字符串、常量表达式等各种数据。当然,也可以不指定文本的值,比如上面的EXPORT_CLASS,只是定义了这个宏。预处理进行宏的替换后,class EXPORT_CLASS CTest实际上等价于class CTest。不指定文本值的宏定义,常用于条件编译,可参考下面的代码。
- #ifdef EXPORT_CLASS
- // 代码段1
- #else
- // 代码段2
- #endif
上面举例的宏定义都是一行,那能不能是多行呢?当然可以。如果宏定义指定的文本过长,一行写不下时,可以写成多行,并在每行的末尾(最后一行除外)加上"\"字符,用于表明后面的行仍是宏定义的内容。
- #define STR_POST \
- "POST /article/details HTTP/1.1\r\n\
- Host: hope-wisdom.blog.csdn.net\r\n\
- name1=value1&name2=value2"
-
- #define ADD_NUMBER \
- int nNumber = 66; \
- nNumber += 88;
-
- ADD_NUMBER
- ADD_NUMBER // 会报变量重复声明的错误
当宏定义中有多条或多行语句,且声明了变量时,连续多次使用宏,很容易导致变量重复声明的问题。此时,将多条或多行语句放到do {} while(0)代码块中,就可以起到作用域隔离的效果。因此,上例中的宏定义ADD_NUMBER最好修改成下面这样:
- #define ADD_NUMBER do { \
- int nNumber = 66; \
- nNumber += 88; \
- } while (0);
有两点需要注意的是:
1、在预处理进行宏的替换时,字符串中的内容不会被替换,即使其与某个宏名称相同。
printf("MATH_PI is %.7f\n", MATH_PI); // 输出为:MATH_PI is 3.1415926
可以看到,第一个MATH_PI由于出现在字符串中,不会进行宏替换,而是原样输出。
2、当要替换的文本是一个表达式时,宏替换后,可能会存在运算优先级错配导致计算结果不正确的问题。
- #define CIRCLE_RADIUS 2 + 3
-
- int nNumber = CIRCLE_RADIUS * CIRCLE_RADIUS;
宏替换后,CIRCLE_RADIUS * CIRCLE_RADIUS变成了:2 + 3 * 2 + 3。为了解决该问题,通常将整个表达式加上括号即可。
#define CIRCLE_RADIUS (2 + 3)
此时,宏替换后变成了:(2 + 3) * (2 + 3),符合预期。
另外,宏替换由于在预处理阶段进行,不会占用程序的运行时间,只会占用编译时间。
宏也可以像函数一样,带有参数列表。具体的格式如下:
#define 宏名称(参数列表) 文本
注意:宏名称和左括号之间不能有空格,否则,空格之后的部分都会成为文本,该宏也会成为一个不带参数的宏定义。
带参数的宏在进行文本替换之前,还需要进行参数替换。如果实参是一个表达式,宏替换时,并不会像函数一样先计算表达式的值,而是直接用表达式替换形参。
- #define MATH_PI 3.1415926
- #define MATH_CIRCUMFERENCE(r) 2 * MATH_PI * r
-
- MATH_CIRCUMFERENCE(2)
- MATH_CIRCUMFERENCE(2 + 3)
在上面的例子中,MATH_CIRCUMFERENCE(2)在进行参数替换后,变为:2 * MATH_PI * 2,结果计算正常。但MATH_CIRCUMFERENCE(2 + 3)在进行参数替换后,变为:2 * MATH_PI * 2 + 3,结果计算不正常。为了解决该问题,通常会将文本中的形参和整个表达式都加上括号,以确保替换后的结果符合预期。
- #define MATH_CIRCUMFERENCE(r) (2 * MATH_PI * (r))
- #define MATH_MULTIPLY(a, b) ((a) * (b))
注意:参数替换只是简单的实参代替形参,并不会像函数一样去检查参数的类型。因此,如果参数替换后发生类型不匹配等情况,后续的编译过程会报错。
MATH_MULTIPLY(2, "hello") // 编译出错
C99标准允许定义带有省略号的宏,类似于printf中的可变参数。省略号必须放在参数列表的最后面,用于表示可变参数。具体的格式如下:
#define 宏名称(参数列表, ...) 文本
参数替换时,预处理器会将所有可变参数和分隔它们的逗号打包成一个参数,传递给系统预定义宏__VA_ARGS__。
- #define LOG_DEBUG(format, ...) printf(format, __VA_ARGS__)
-
- LOG_DEBUG("no data\n");
- LOG_DEBUG("data is %d\n", 66);
- LOG_DEBUG("data is %d, %s\n", 66, "hope_wisdom");
在较低版本的编译器中,当可变参数为空时,LOG_DEBU会发生编译错误。这是因为宏替换后,后面会有一个多余的逗号,类似于下面这样。
printf("no data\n", ); // 宏替换后,后面会多出一个逗号
为了解决这个问题,兼容性更好的写法是:在__VA_ARGS__前面加上##字符。##字符的作用为:当可变参数为空时,预处理器将自动去除掉它前面的那个逗号。
#define LOG_DEBUG(format, ...) printf(format, ##__VA_ARGS__)
在GNU C标准中,还支持带可变参数的宏的另一种写法:不再使用__VA_ARGS__,而是直接使用args...来表示可变参数,文本中则可直接使用args。示例代码如下:
#define LOG_DEBUG(format, args...) printf(format, ##args)
特殊符号#相当于一个一元运算符,用于把#符号后边的参数变成一个字符串。变为字符串时,出现在#符号后边的空白符和#符号本身会一起被删除。示例代码如下:
- #define TO_STR(str) # str
- #define CALC_EXPRESSION(exp) printf(#exp" = %f\n", exp)
-
- printf("str is %s\n", TO_STR(hope_wisdom)); // 输出:str is hope_wisdom
- CALC_EXPRESSION(2 * sin(30)); // 输出:2 * sin(30) = -1.976063
特殊符号##相当于一个二元运算符,用于把##符号两边的参数结合在一起。结合后,出现在##符号两边的空白符和##符号本身会一起被删除。假如结合后的文本中还包含某个宏名称,则预处理器会继续进行宏替换。可参考下面的示例代码:
- #define STR_CSDN "Hello, CSDN"
- #define OUTPUT_STR(str) puts(STR_ ## str)
-
- OUTPUT_STR(CSDN);
在上面的例子中,OUTPUT_STR在进行参数替换后,变为:puts(STR_ ## CSDN)。接着##符号起作用,删除空白符和##符号本身,进一步变为:puts(STR_CSDN)。最后发现STR_CSDN是一个宏名称,替换后变为:puts("Hello, CSDN")。
宏是可以嵌套的,也就是,一个宏定义的参数可以是另一个宏。示例如下:
- #define CONST_NUMBER 2
- #define CONST_SQUARE(a) (a * a)
- #define CONST_POWER(a) int(a##e##a)
-
- int nSquare = CONST_SQUARE(CONST_NUMBER);
- int nPower1 = CONST_POWER(2);
- int nPower2 = CONST_POWER(CONST_NUMBER); // 发生编译错误
在上面的例子中,宏替换后,CONST_SQUARE(CONST_NUMBER)变为:(2 * 2),CONST_POWER(2)变为:int(2e2),但CONST_POWER(CONST_NUMBER)变为:CONST_NUMBEReCONST_NUMBER,会导致编译出错。CONST_NUMBER的值就是2,为什么后两条语句的结果不一样呢?更奇怪的是,为什么使用CONST_SQUARE却是正常的呢?这是因为,当宏定义中出现特殊符号#和##时,参数如果是另一个宏,则该宏不再展开,而是直接使用实参的值。
解决这个问题的方法是:多加一个用于转换的宏定义,该宏定义中不出现特殊符号#和##。可参考下面的代码:
- #define _CONST_POWER(a) int(a##e##a)
- #define CONST_POWER(a) _CONST_POWER(a)
C/C++语言预定义了一些宏定义,常用的宏定义如下表。
宏名称 | 数据类型 | 备注 |
__FILE__ | 字符串 | 表示当前文件的完整路径 |
__LINE__ | 数字 | 表示当前代码所在的行号 |
__FUNCTION__ | 字符串 | 表示当前代码所在的函数名 |
__DATE__ | 字符串 | 表示预处理当前代码的日期 |
__TIME__ | 字符串 | 表示预处理当前代码的时间 |
__STDC__ | 常数 | 用于判断编译器是否遵守ISO标准C |
__STDC_VERSION__ | 常数 | 表示当前使用的C语言标准的版本 |
__cplusplus | 常数 | 表示当前使用的C++语言标准的版本 |
除此之外,各操作系统、指令架构和IDE也预定义了一些宏定义,常用的宏定义如下表。
宏名称 | 备注 |
_WIN32 | Windows 32位和64位程序环境下,均会定义该宏 |
_WIN64 | 仅Windows 64位程序环境下,会定义该宏 |
__linux__ | Linux操作系统下,会定义该宏 |
__ANDROID__ | Android NDK环境下,会定义该宏 |
__APPLE__ | 苹果操作系统下,会定义该宏 |
TARGET_IPHONE_SIMULATOR | iOS模拟器环境下,会定义该宏 |
TARGET_OS_IPHONE | iOS环境(包括iOS模拟器环境)下,会定义该宏 |
TARGET_OS_MAC | Mac环境(包括iOS环境)下,会定义该宏 |
__i386__ | X86架构32位时,会定义该宏 |
__x86_64__ | X86架构64位时,会定义该宏 |
__arm__ | Arm架构32位时,会定义该宏 |
__aarch64__ | Arm架构64位时,会定义该宏 |
__mips__ | MIPS架构时,会定义该宏 |
__powerpc__ | RISC架构时,会定义该宏 |
_MSC_VER | VC的版本号 |
__GNUC__ | GNU C的版本号 |
__BORLANDC__ | Borland C的版本号 |
QT_VERSION | Qt的版本号 |
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。