文章目录
前言
我们接着详解预处理(上)内容给大家继续讲解预处理的有趣之处。
6. 宏和函数的对比
在详解预处理(上)我讲到定义宏时,如果比较两个数的大小,我们可以这样写一个宏:
#define MAX(a,b) ((a>b)?(a):(b))
当然我们也可以用函数来实现:
int MAX(int x,int y) { return x>y ? x : y; }
那这两种方法哪个更好呢?这就是我们接下来要讨论的问题了。
针对上述的例子,我更倾向使用宏。
原因有二:
- 用于调用函数和从函数返回得到代码可能比实际执行这个小型的计算工作所需要的时间更多(也就是创建函数栈帧需要时间)。所以宏比函数在程序的规模和速度方面更胜一筹。
- 更为重要的是函数的参数必须要其声明特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整型、长整型、浮点型等可以用
>
来比较类型,也就是宏参数不需要声明类型,这是宏参数的绝对优势。
和函数相比宏的劣势:
- 每次使用宏时,一份宏定义的代码插入到程序中。除非宏定义比较短,否则可能会大幅度提高程序的长度。
- 宏时无法调试的。因为它是处在预处理阶段的。
- 宏由于不需要规定参数类型,也就不够严谨。
- 宏可能会导致运算符优先级的问题,导致程序很容易出错。
宏有时候可以做到函数做不到的事情。比如:宏的参数可以出现各种类型,但是函数做不到。
#define MALLOC(num,type)\ (type*)malloc(num * sizeof(type)) ... //使用 MALLOC(10,int);//类型作为参数 //预处理器替换之后 (int*)malloc(10*sizeof(int));
7. #和##
7.1 #运算符
#运算符是将宏的一个参数转换为字符串字面量。它仅允许出现在带有参数的宏的替换列表中。
#运算符所执行的操作可以理解为“字符串化”。
比如当我们有一个变量int a = 10;
的时候,我们想打印出:the value of a is 10
。
我们就可以写成这样:
#define PRINT(n) printf("the value of "#n" is %d",n)
当我们按照下面的方法调用时,
PRINT(a);//当我们把a替换到宏的体内时,就会出现了#a,而#a就转换为了“a”的一个字符串。
7.2 ##运算符(运用较少,了解即可)
##
可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段中创建标识符。##被称为记号粘合。
这样的链接必须产生一个合法的标识符。否则其结果就是未定义的。
这里我们就想一想,写一个函数求两个数的较大值的时候,不同的数据类型就得写不同的函数。
比如:
int int_max(int x, int y) { return x>y?x:y; } float float_max(float x, float y) { return x>y?x:y; }
但是这样写过于繁琐了,现在我们这样写代码试试:
#define GENERIC_MAX(type) \ type type##_max(type x,type y)\ {\ return x>y?x:y;\ }
使用宏来定义不同的函数:
GENERIC_MAX(int); GENERIC_MAX(float); int main() { //调用函数 int m = int_max(2,3); printf("%d\n",m); float fm = float_max(3.5f,4.5f); printf("%f\n",fm); return 0; }
8. 命名的约定
一般来讲函数和宏的使用语法很相似。所以仅凭借复发本身没有办法帮我们区分二者。
那平时我们的一个习惯是:
把宏名全部大写
函数名不要大写
9. #undef (了解即可)
这条语句是用来移除一个宏定义。
#undef NAME //如果现存的一个名字需要被重新定义,那么它的久名字首先被移除
10. 条件编译(重点)
我们先来聊一聊为什么需要条件编译?
在编译一个程序的时候我们如果要将一条指令(一组指令)编译或者放弃是很方便的。因为我们有条件编译。
比如说:
调试性代码,删除了可惜,保留又碍事,所以我们可以选择性的编译
#include<stdio.h> #define __DEBUG__ int main() { int i = 0; int arr[10] = {0}; for(i = 0; i< 10;i++) { arr[i] = i; #ifdef __DEBUG__ printf("%d\n",arr[i]); #endif //__DEBUG__ } return 0; }
常见的条件编译的指令:
1. #if 常量表达式 //... #endif 如: #define __DEBUG__ 1 #if __DEBUG__ //... #endif 2.多个分支的条件编译 #if 常量表达式 //... #elif 常量表达式 //... #else //... #endif 3.判断是否被定义 #if defined(symbol) #ifdef symbol //上面的简化 #if !define(symbol) #ifndef symbol 4.嵌套条件编译指令 #if define(OS_UNIX) #ifdef OPTION1 unix_version_option1(); #endif #ifdef OPTION2 unix_version_option2(); #endif #elif defined(OS_MSDOS) #ifdef OPTION2 msdos_version_option2(); #endif #ednif
11. 头文件的包含
你是否还在问为什么得用“”
来括起来自己写的头文件名,而不是像stdio.h那样的头文件用<>
吗,本小节就来带大家解开谜语。
11.1 头文件被包含的方式:
11.1.1 本地文件包含
#include "filename.h"
查找策略:先在源文件所在的目录下查找,如果该头文件未找到,编译器就像查找函数库头文件一样在标准库位置查找头文件。
如果找不到则显示错误。
Linux环境的标准头文件路径:
/usr/include
Windows环境的标准头文件路径:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include //这是VS2013的默认路径
注意:按照自己安装的路径来查找。
11.1.2 库文件的包含
#include<filename.h>
查找头文件是直接去到标准文件的路径下去查找,如果找不到就提示错误。
这样就是不是可以说,对于库文件也可以使用“”
的形式包含?
答案是可以的,但是不推荐这么做。因为这样做查找的效率就会变低,当然这样也不容易区分包含的是本地文件还是库文件。
至此,预处理详解的内容就全部完成了。如果觉得讲的还不错的话,麻烦给偶点个赞吧!!!