c@TOC
前言
在想要深度理解预处理之前,编译和链接的过程一节必修课。链接:http://t.csdn.cn/fycnZ
宏定义
#define定义数值宏常量(标识符常量)
数值宏常量是我们目前阶段最常用的一种形式,也是非常简单的一种形式。
为什么要有数值宏常量呢?它又是什么时候才使用呢?其实这个问题非常简单,
当我们的代码中出现一个数值或者变量频繁出现的时候,就需要用到宏。简单举个例子:
例如之前有写过的一个扫雷小游戏,当设计棋盘大小时,最好将行和列定义为一个数值宏常量,如果之后我想改变棋盘的大小,只需要将宏常量的数值改一下即可,如果每一个地方都是固定的数字的话则需要改很多处地方,所以数值宏常量是非常好用的,宏常量是可以极大地提高代码的可维护性。
#define定义字符串宏常量
字符串宏常量和数值宏常量是比较类似但是又有一些独特细节的东西,先来看最简单的例子:
#define PATH1 "This is a example sentence" int main() { printf("%s\n", PATH1); return 0; }
需要注意的就是字符串必须要带双引号,这个很简单。
但是如果字符串比较长,放在一行显得特别难看的时候我们怎么能主动换行呢?这个C中其实也有规定,只需要在每一行后面加上反斜杠即可续行。
#define PATH1 "This is a example sentence" #define PATH2 "This is a \ example \ sentence" int main() { printf("%s\n", PATH1); printf("%s\n", PATH2); return 0; }
可以看到这样也是可以的。
用宏定义充当注释符号
这个其实是个有趣的现象,要想彻底看清楚这个过程是什么情况我们在VS下是不好观察的,我们需要在
Linux
下来演示:
我们很明显可以看到,其实我们要的结果并不是这个,如果宏定义可以充当注释符号的话应该是看不到这句话的。
其实这个例子也侧面说明了,在预处理时:宏替换是要后于删除注释的,那么这个代码在预处理的时候,具体细节是什么样的的,我们也可以看一下:
可以看到,其实在编写出这个代码时,双斜杠就已经是注释符了,所以是先被删除掉,这个也可以看出,在预处理时,删除注释确实是要先于进行宏替换的。
#define定义表达式
很多人一直以为宏就是个简单的无脑文本替换罢了,其实宏的作用远远不至于这些,首先打破这个偏见,宏不是无脑文本替换
例如以下这个例子:
例1:
#include <stdio.h> #define SUM(x) (x)+(x) int main() { printf("%d\n", SUM(10)); return 0; }
可以看到,实际上结果是正确的,我们再来看一下预处理后的代码,
这下我相信你应该稍微有点感觉了,我们下面再来看一个更复杂的例子
带副作用参数的宏
看下面这样一段代码:
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) ... x = 5; y = 8; z = MAX(x++, y++); printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么
由于我们的
x++
和y++
都是带副作用的,所以这个结果可能并不是你想的那么简单,
看到这个结果,我相信很多人都是一脸懵逼的,但是我们可以看一下预处理后的结果,
在宏进行替换之后,发现代码长这个样子,这时候再仔细分析一下就很容易得出结果了。
#define定义表达式
还有一些更加令人费解的情况,这里再举个例子:
然后我们来看结果,
可以看到实际上也是可以实现的。但是这种做法当然不推荐。没有必要写出一些令人费解的代码。
宏和函数对比
宏通常被应用于执行简单的运算。
例如求两个数的较大值:
#define MAX(a, b) ((a)>(b)?(a):(b))
那相比较哪个更好呢?别急,要先理清楚各自的优缺点:
先来看宏相比函数的优点:
用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。
那么宏的缺点呢?
每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度
宏是没法调试的
- 宏由于类型无关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到
例如,将类型作为参数:
#define MALLOC(num, type) (type *)malloc(num * sizeof(type)) ... //使用 MALLOC(10, int);//类型作为参数 //预处理器替换之后: (int *)malloc(10 * sizeof(int));
下面就可以总结一下了:
属 性 | #define定义宏 | 函数 |
---|---|---|
代 码 长 度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个 地方的同一份代码 |
执 行 速 度 | 更快 | 存在函数的调用和返回的额外开 销,所以相对慢一些 |
操 作 符 优 先 级 | 宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函 数。表达式的求值结果更容易预 测。 |
带 有 副 作 用 的 参 数 | 参数可能被替换到宏体中的多个位置,所以带有副作 用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一 次,结果更容易控制。 |
参 数 类 型 | 宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 相同的。 |
调 试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递 归 | 宏是不能递归的 | 函数是可以递归的 |
命名约定
这个其实没有什么好说的,仅仅是因为习惯问题,所以我们统一遵循以下两条原则:
- 宏的名字全部大写
- 函数名不要全部大写
undef
undef
比较简单,就是用来移除一个宏定义,也就是说限定了宏的范围。举个例子很容易理解:
然后当我们去编译的时候则会发现报错了:
也就是说从
undef
开始M这个宏就已经移除了,下面再使用就会报错。
命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我
们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)
这是个好玩的东西,但是我个人并没有感觉它有多大用处,就是说在命令行的时候定义一个宏在代码中去使用
然后我们看这行指令:
gcc -D VAL=10 test4.c
可以看到确实是实现了我们的目的。这方法作为了解即可(我还没有见过到底在什么场景下能用得上)。
那么在VS中我们怎么能实现呢?其实也是可以的,
只需要在预处理器这里定义上需要的宏即可。
条件编译
条件编译的使用
条件编译绝对是我们要学习的重中之重,这也是我们重点要掌握的内容。
第一对:
#if 常量表达式 #endif
来看一段代码:
#include <stdio.h> #define PRINT 1 int main() { #if PRINT printf("you can see me!!\n"); #endif return 0; }
代码中的
#if
和#endif
就是我们要了解的第一对,条件编译的符号,这段代码意思就是如果PRINT为真则执行下面代码。注意
#if
后面跟的是常量表达式,不能是变量,因为是在预处理阶段进行的,不可能是变量。执行结果呢也就显而易见了,
因为
再来看一组多分支的情况:
#if #elif #else #endif
#define PRINT 1 int main() { #if PRINT==1 printf("hehe\n"); #elif PRINT==2 printf("haha\n"); #else printf("heihei\n"); #endif return 0; }
其实编译器是比较聪明的,直接在写出这个代码的时候,就会有颜色上的变化,结果
其他情况也就很容易理解,就不再验证了。
我们来看第二组
#if defined() #if !defined() #endif
这个单纯来解释不太好描述,看一个具体代码:
#define PRINT int main() { #if defined(PRINT) printf("you can see me!!\n"); #endif return 0; }
意思就是说如果定义了
如果是用!取反一下,结果也很好理解,正好和结果相反即可。
上面这种判断其实还有另外一种写法,
#ifdef #ifndef #endif
这组和上面其实是一样的意思,只是书写方式不同,例如
#define PRINT 0 int main() { #ifdef PRINT printf("you can see me!!\n"); #endif return 0; }
这个和上面的例子其实是一样的,
#ifndef
也是逻辑取反的意思。同样也是只关注宏是否被定义,和真假无关。再来看一个例子:
#define PRINT 0 int main() { #ifndef PRINT printf("you can see me!!\n"); #else printf("1111111"); #endif return 0; }
如果没有定义宏
youcanseeme
,否则打印11111
,结果:
以上就是条件编译的基本使用。另外还值得一提的是条件编译是可以嵌套使用的,举个例子:
在我们初学C语言使用VS写代码的时候,是否还记得第一次使用
scanf
报错的时候,当我们今天再看到这句话,是否就明白它的原理了,写到这里我也是有点感慨啊,距离第一次学习编程第一次敲代码已经过去5个月了。如今再回过头来,是否感觉到自己变强了呢。好了,言归正传,这里就是一个条件编译嵌套的形式,所以记住,条件编译是可以嵌套使用的。
为什么要有条件编译
那么为什么要有条件编译呢?有什么应用场景呢?这个是我们要思考清楚的问题。
我们很容易得出结论,首先来总结一下,本质认识:条件编译,其实就是编译器根据实际情况,对代码进行裁剪。而这里“实际情况”,取决于运行平台,代码本身的业务逻辑等 ,
可以认为有两个好处:
可以只保留当前最需要的代码逻辑,其他去掉。可以减少生成的代码大小
可以写出跨平台的代码,让一个具体的业务,在不同平台编译的时候,可以有同样的表现
那么应用场景呢?
举一个比较容易理解的例子,我们日常所使用的软件,有收费版或者付费版,或者充会员之类的,也就是使用的条件编译,当你充了钱,商家才会给你放开一部分代码进而使用某些功能。著名的Linux内核,功能上,其实也是使用条件编译进行功能裁剪的,来满足不同平台的软件。
那么我们就很容易理解为什么条件编译也是很重要了。其实在我们日常使用的头文件中有大量的条件编译,例如最常用的
stdio.h
,这个文件在你的电脑中一定是有的,可以去翻翻看一下。