
00. 前言
不知道这个问题的,有可能你一直都是用IDE做开发,而没思考过IDE里面经历了什么。如果你用过GCC,你应该大概知道这是一个怎样的过程: ![]() 这个过程里面有个Pre-processing,很不起眼,但是非常关键,如果用得好,你的代码也会变得很奇妙。 以下是基于C99标准,总结C语言预处理的知识点以及其相关用法和问题剖析。 注:本文所有案例测试都是基于Windows上的GCC(gcc version 6.3.0 (MinGW.org GCC-6.3.0-1)) 抛开大脑里零零散散的几个概念,我们直接参考C99标准(ISO/IEC 9899:1999 (E) ),简单汇总有以下命令或用法:
以上内容,大部分都很常见,如果要说用得好,那是个艺术活。 预 处理有以下语法形式: 2. 描述
在预处理指令中的预处理标记之间(从引入#预处理标记之后到结束换行符之前),唯一出现的空白字符是空格和水平制表符(包括替换了注释或可能的空格) /*comment*/# /*comment*/ include <stdio.h>/*comment*/以上代码在GCC上是可以编译通过的。 可以有条件地处理和跳过源文件的各个部分,包括其他源文件,并替换宏。这些功能称为预处理,因为从概念上讲,它们发生在生成的翻译单元的翻译之前。除非另有说明,否则预处理指令中的预处理token不会进行宏扩展。 #define EMPTYEMPTY # include <file.h> 以上第2行是有问题的,它并非以#开头,不能以其为预处理指令 控制条件包含的表达式,一定是一个整型常量的。不能包含类型转换和标识符(如C语言中的关键字、枚举常量等),其只认宏与非宏。我们可以将以下表达式把defined当做一元操作符:defined identifier或defined (identifier)以上如果identifier是一个有效的宏名,也就是说上文有用了#define进行定义,并且没用#undef来取消这个定义,那么上述表达式的结果为1,否则为0 我们用实际的例子来说明以上说法: enum{ ENUM_NO, ENUM_YES }; #define DEF_YES_NULL #define DEF_YES_0 0 #define DEF_YES_1 1 #define DEF_YES_2 2 #define DEF_YES_STR "ABC" #define DEF_NO_ENUM ENUM_NO 根据以上定义 例1: #if defined(DEF_YES_NULL) == 1printf("DEF_YES_NULL should be printed.\n"); #endif DEF_YES_NULL是有效的宏定义,defined(DEF_YES_NULL)的值是1,能够打印出DEF_YES_NULL should be printed. 例2: #if defined(DEF_YES_2) == 1printf("DEF_YES_2 should be printed.\n"); #endif DEF_YES_2是有效的宏定义,defined(DEF_YES_2)的值是1,能够打印出DEF_YES_2 should be printed. 例3: #if defined(DEF_YES_STR) == 1printf("DEF_YES_STR should be printed.\n"); #endif DEF_YES_STR是有效的宏定义,defined(DEF_YES_STR)的值是1,能够打印出DEF_YES_STR should be printed. 也许你想问,为什么啊?简单粗暴地记住一句:defined (identifier)认为,只要identifier是个女的宏就行……例4: #if defined(ENUM_YES) == 1printf("ENUM_YES should NOT be printed.\n"); #endif ENUM_YES不是有效的宏定义,defined(ENUM_YES)的值是0,以下代码不能打印出内容 例5: #if defined(DEF_NO_ENUM) == 1printf("DEF_NO_ENUM should be printed.\n"); #endif DEF_NO_ENUM是有效的宏定义,defined(DEF_NO_ENUM)的值是1,能够打印出DEF_NO_ENUM should be printed.但是将enum常量套进#define里面,这个却是有效的。如果不能理解,那就再看一遍那句粗暴的话。 我们再来一个例子例5: #define DEF_XXX What the f**k define#if defined(DEF_XXX) == 1 printf("DEF_XXX should be printed.\n"); #endif 看到这里,你也许已经理解defined的用法了,但是我不建议你用#if defined(identifier) == 1的形式,而是用#if defined(identifier)或者#if !defined(identifier),如果要问为什么,我的回答是:习惯很重要。 01.2 关于#if/#elif/#else等我们用以下形式的预处理指令 # if constant-expression new-line groupopt# elif constant-expression new-line groupopt 检测控制常量表达式的结果是否为0。 实际上这个#if跟if是类似的,用法也很像,但有一点点需要注意的:这个是在预处理阶段执行的。 举例说明: 例1: enum{ ENUM_NO, ENUM_YES }; #if ENUM_YES printf("Cannot print this message!\n"); #endif 例2: int n = 100;#if n printf("Cannot print this message!\n"); #endif 例3: #define DEF_YES_NULL#if DEF_YES_NULL printf("Cannot print this message! Compile error!\n"); #endif 例4: #define DEF_YES_0 0#if DEF_YES_0 printf("Cannot print this message!\n"); #endif 例5: #define DEF_YES_1 1#define DEF_YES_2 2 #if DEF_YES_2 // DEF_YES_1 printf("Can print this message!\n"); #endif 例6: #define DEF_YES_STR "ABC"#if DEF_YES_STR printf("Cannot print this message! Compile error!\n"); #endif 例7: #define DEF_YES_ENUM ENUM_YES#if DEF_YES_STR printf("Cannot print this message! Compile error!\n"); #endif 以上例3/6/7会有编译错误,具体的错误原因可以看编译日志,实际上#if后面的宏会发生替换的。如果你还想问为什么,那就再复习下这句话:控制条件包含的表达式,一定是一个整型常量的。 01.3 关于#ifdef/#ifndef等#ifdef和#ifndef实际上跟#if defined和#if !defined是一样的。 #include指令应标识实现可以处理的头文件或源文件。一般使用形式: # include <h-char-sequence># include "q-char-sequence" 当你第一天学C语言写"Hello World"程序的时候,就应该知道这个#include了,例如#include <stdio.h>,好像也没什么好研究的。我先问几个问题:
答案:
总而言之,对于这个#include,记住一句话:#include是将已存在文件的内容嵌入到当前文件中。 重头戏来了,这个内容也许是大家用的最多的了,里面的坑也特别多。我们先看看规则和要求:
这个好理解,例如教科书式的例子: #define PI 3.14还有 #define DEBUG_ERR -1这不但表明了这个-1是error,还可以很方便地移植到别的平台,如果平台表达error有差异的话,可以统一地将DEBUG_ERR换成别的值。实际上,我们在上面的例子已经讲了好多关于这个object-like macro了。 但要记住一句话:宏的动作只是一个替换。宏是没有类型的。当别人问你:宏定义和const数据有什么区别?应该不需要我的答案了吧。 问个问题,宏可以用作注释的替换吗? 大写的不可以,参考《C语言深度剖析》:
另外,有必要提一下,宏是有作用域的,这个跟C语言的其他作用域的情况类似,但是还有个#undef要注意下
03.2 关于function-like macro的使用 一个经典的笔试题:请写出一个MIN宏。也许你看这个例子看到烂了,也许你毫不犹豫写出个: #define MIN(x, y) x < y? x : y有可能面试官会给你0分,不信你试试,以下是不是你想要的结果: #define MIN(x, y) x < y? x : yint n = 3 * MIN(4, 5); 那我加个括号行了吧: #define MIN(x, y) (x < y? x : y)int n = 3 * MIN(4, 5); 面试官还是给你个0分呢?不信你试试: #define MIN(x, y) (x < y? x : y)int n = 3 * MIN(3, 4 < 5 ? 4 : 5); 想想那句话:宏的动作是个替换,你一步步替换出来看看是啥结果?(此处答案:略) 写成以下这样应该可以了吧: #define MIN(x, y) ((x) < (y)? (x) : (y))int n = 3 * MIN(3, 4 < 5 ? 4 : 5); 面试官可能会给你满分,但是我们这里分享干货,继续探讨下,试试这个: #define MIN(x, y) ((x) < (y)? (x) : (y))double xx = 1.0; double yy = MIN(xx++, 1.5); printf("xx=%f, yy=%f\n",xx,yy); 结果是不是很意外? xx=3.000000, yy=2.000000GNU有个改进的方法: #define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })double xx = 1.0; double yy = MIN(xx++, 1.5); printf("xx=%f, yy=%f\n",xx,yy); 你测试下看看,这回应该是你想要的结果了。 xx=2.000000, yy=1.000000还有个do{ }while(0)要讲讲 为什么要用这个东西,有什么好处? 举个例子: #define set_on() set_on_func1(1); set_on_func2(1);set_on(); 这样做似乎没啥问题。万一有以下形式呢? #define set_on() set_on_func1(1); set_on_func2(1);if(on) set_on(); 实际上就变成了 #define set_on() set_on_func1(1); set_on_func2(1);if(on) set_on_func1(1); set_on_func2(1); 这应该不是你想要的效果吧。那就改进下吧,加个{}可以么? #define set_on() {set_on_func1(1); set_on_func2(1);}if(on) set_on(); 问题是这样编译会出错的,在于后面那个;。 我们直接用do{ }while(0)试试? #define set_on() do{set_on_func1(1); set_on_func2(1);}while(0)if(on) set_on(); 实际上,这个do{ }while(0)用在宏后面,可以保证其内容在替换后不会被拆散,保持其一致性。 另外,在此说句题外话:你居然要做一个接口给别人使用,就应当把你的接口做得万无一失。C语言赋予了我们这么多特性和权力,就肯定会有人耍出“花样”来。 首先说一下:
直接在tutorialspoint找两个例子说明下: 例1: #include <stdio.h>#define message_for(a, b) \ printf(#a " and " #b ": We love you!\n") int main(void) { message_for(Carole, Debra); return 0; } 输出结果是: Carole and Debra: We love you!例1: #include <stdio.h>#define tokenpaster(n) printf ("token" #n " = %d", token##n) int main(void) { int token34 = 40; tokenpaster(34); return 0; } 输出结果是: token34 = 40tokenpaster(34);这个通过预处理后就变成了printf ("token34 = %d", token34) 到这来,我想大家基本上理解这#和##是什么意思了吧。 我们再来看看网上的另一个例子: #define f(a,b) a##b#define g(a) #a #define h(a) g(a) printf("h(f(1,2))-> %s, g(f(1,2))-> %s\n", h(f(1,2)), g(f(1,2))); 输出的结果是: h(f(1,2))-> 12, g(f(1,2))-> f(1,2)我们一步一步来解析下:先看h(f(1,2))
再看g(f(1,2))
再来一个例子: #define _STR(x) #x#define STR(x) _STR(x) char * pc1 = _STR(__FILE__); char * pc2 = STR(__FILE__); printf("%s %s %s\n", pc1, pc2, __FILE__); 输出: __FILE__ "c_test_c_file.c" c_test_c_file.c想想为什么,提示:宏中遇到#或##时就不会再展开宏中嵌套的宏了。 我们不钻牛角尖了,来个实际应用的例子: typedef struct os_thread_def {os_pthread pthread; ///< start address of thread function osPriority tpriority; ///< initial thread priority uint32_t instances; ///< maximum number of instances of that thread function uint32_t stacksize; ///< stack size requirements in bytes; 0 is default stack size } osThreadDef_t; #define osThreadDef(name, priority, instances, stacksz) \ const osThreadDef_t os_thread_def_##name = \ { (name), (priority), (instances), (stacksz) } 这个osThreadDef会根据输入的参数创建一个结构体变量(名字还根据输入的参数name不一样而不一样),然后包含了部分参数当做结构体内容。这样做不但简洁,而且还防止名字重复。 osThreadDef (Thread_Mutex, osPriorityNormal, 1, 0);这个会预处理成一个这样的变量: const osThreadDef_t os_thread_def_Thread_Mutex ={ Thread_Mutex, osPriorityNormal, 1, 0 }; 是不是很爽? 在C语言的标准库中,printf、scanf等函数的参数是可变的。而这个__VA_ARGS__就是C99定义的。为可变参数函数在宏定义中提供可能。那么,我们一般用来干嘛呢?举个例子,我们在调试程序时,不想直接用printf来打印log,而想通过一个宏函数来做,当不需要输出log的时候,可以将其定义成空的东西。 #define DEBUG_PRINTF(format, ...) printf(format, ...)DEBUG_PRINTF("Hello World!\n"); 然后当你高高兴兴地编译的时候,GCC无情地丢给你一个error: error: expected expression before '...' token#define DEBUG_PRINTF(format, ...) printf(format, ...) ^ note: in expansion of macro 'DEBUG_PRINTF' DEBUG_PRINTF("Hello World!\n"); ^ WHY??你需要__VA_ARGS__了,怎么搞?来试试这个: #define DEBUG_PRINTF(format, ...) printf(format, __VA_ARGS__)DEBUG_PRINTF("Hello World!\n"); 然而,GCC还是给你个错误: error: expected expression before ')' token#define DEBUG_PRINTF(format, ...) printf(format, __VA_ARGS__) ^ note: in expansion of macro 'DEBUG_PRINTF' DEBUG_PRINTF("Hello World!\n"); ^ 什么鬼?是不是使用方法有问题? #define DEBUG_PRINTF(format, ...) printf(format, __VA_ARGS__)DEBUG_PRINTF("%s","Hello World!\n"); 诶,好像可以了哦,但是我想用上面那样调用,怎么办?这就要请出##了: #define DEBUG_PRINTF(format, ...) printf(format, ##__VA_ARGS__)DEBUG_PRINTF("Hello World!\n"); DEBUG_PRINTF("Hello %s", "World!\n"); 这个##,的作用是将token(如format等)连接起来,如果token为空,那就不连接。 那么用宏定义一个开关,愉快地实现一个log输出宏了: #ifdef DEBUG#define DEBUG_PRINTF(format, ...) printf(format, ##__VA_ARGS__) #else #define LOG(format, ...) #endif 在来个例子: #define ABC(...) #__VA_ARGS__printf(ABC(123, 456)); 你觉得会输出什么结果? 123, 45604. 行控制 #line这个东西,是不是没见过,到底干嘛用的呢? 可以简单地理解为,可以改变行号的,甚至文件名都可以改变。它的基本形式如下: # line digit-sequence "s-char-sequenceopt"其中
我们直接用代码示例来看看其作用: #line 12345 "abcdefg.xxxxx"printf("%s line: %d\n", __FILE__, __LINE__); printf("%s line: %d\n", __FILE__, __LINE__); 输出: abcdefg.xxxxx line: 12345abcdefg.xxxxx line: 12346 可以看出,其可以改变下一行内容所在的行号,以及当前文件的文件名。看起来,这货貌似没啥用。 实际上,我们通过这个指令可以固定文件名和行号,以分析某些特定问题。 #error,这个东西很好理解,就是在编译器遇到这个#error的时候就会停下来,然后输出其后面的信息。 其一般形式如下: # error pp-tokensopt这个pp-tokensopt比较随意,可以省略,也不用是字符串,其他内容也行。 这个在我看来,真没看到有实际作用。它就是什么都不干。其形式为: ##后面什么都么有。 我们可以通过预定义宏名来或者某些信息,特别在调试的时候,是挺有用的。 其实我们在前面的04.行控制章节的例子就有例子了。 在此,我们汇总下各个名称以及其所代表的的含义:
大家可以将这些内容打印出来看看具体是什么内容,在此不累述了。 另外,特别注意,这些宏名,不可以被#define和#undef等修饰。 这个#Pragma在众多预处理命令中最为复杂了。 我先参考下cppreference.com的说法: 实现定义行为控制 这个标准的pragma貌似平时很少用,也有点费解。C99标准也有一些描述: # pragma pp-tokensopt
对于这个非标准的 pragma,好像我们用的还挺多的,例如#pragma once、#pragma message和#pragma warning等。
还有一个值得一提的是_Pragma,这个是C99新增加的,实际上跟#param一样,但是其有什么特别作用吗? 我们可以把_Pragma放在宏定义后面,因为它不需要这个#,不存在不能展开宏替换问题,例如: #define LISTING(x) PRAGMA(listing on #x)#define PRAGMA(x) _Pragma(#x) LISTING ( ..\listing.dir ) |
谢谢分享 |