1. 预定义符号
C语言设置了一些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的
__FILE__ 进行编译的源文件
__LINE__ 文件当前的行号
__DATE__ 文件被编译的日期
__TIME__ 文件被编译的时间
__STDC__ 如果编译器遵循ANSI C,其值为1,否则未定义
举个例子
这行代码的出现位置是在第16行,所以__LINE__的值就是16,注意这个词是两个下划线+LINE+两个下划线,文件名,行号,日期,时间都是编译当前文件当前位置当前时间的信息,注意是编译时的不是运行时的信息。
2. #define定义常量
基本语法:
#define name stuff
#define定义的stuff内容可以是数值,参数,甚至是语句,举个例子:
最后一段定义中我们定义了一个打印的语句,很明显我是将一行代码写成了3行。这是因为写成一行代码的时候代码量太长,因此我们可以借助续行符进行换行接着写。前两行结尾处的反斜杠就是续行符,我们可以理解成将反斜杠后面的回车转义掉了,所以回车换行相当于没换,视觉上像是换了行,但实际上还是在这一行上书写
现在我们思考一个问题:在使用#define定义标识符的时候要不要在最后加上 ;
我建议是不要加上的,因为#define是暴力替换掉常量名,如果不注意的话很容易出问题
观察num被替换之后的样子我们可以发现加 ; 的危害了
3. #define定义宏
于定义常量类似,定义宏也是将名暴力替换掉,只不过宏新增了一个参数的机制
下面是宏的声明方式
#define name(parament-list) stuff
parament-list参数列表是一个由符号隔开的符号表,其中的符号可能出现在stuff中
注意:参数列表的左括号必须和name紧邻,如果两者间由任何空白的存在,参数列表就会被认为是stuff的一部分
举例如何使用定义宏
我用宏实现了一个将参数变成其二倍的功能
但是这么写宏是有问题的
我在宏的参数部分写2+1,本意是计算DOUBLE(3),但是因为暴力替换的问题,表达式并没有按照我预想的循序计算,而计算的是2*2+1
因此我们在定义宏的时候要将表达式中的元素用小括号括起来,保证它们的运算顺序不会在暴力替换之后改变
但是问题还没有解决
很明显,这段代码也因为暴力替换的问题本想计算5*4,但是实际上计算的是5*2+2
因此我们要将整体也用小括号固定计算顺序
这样才算正确的定义宏
4. 带有副作用的宏参数
当宏参数在宏定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
x+1 不带有副作用
x++ 带有副作用
举一个副作用的例子
很明显结果和我们预期的不一样,判断3和5的大小最后答案竟然是6,而且a和b的值也被改变,这正是因为参数的副作用带来的问题,简单分析一下注释掉的那条语句就可以知道到底是怎么回事了
5. 宏的替换
我们之前已经提到过多次了,宏在使用时其实就是将名强行替换掉了,那么具体的替换方案涉及以下几个步骤
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值替换。
3.最后,再对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,重复以上处理步骤
注意:
1.宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归
2.当预处理器搜索#define定义的符号的时候,字符串常量的内容不被搜索
6. 宏和函数的对比
宏通常被应用于执行简单的运算,函数用于解决复杂的功能
比如在执行上面那个比较两个数大小的功能的时候,写成宏更有优势
宏的优势:
1. 进行简单计算的时候需要的计算量比函数更小,宏没有像函数的调用和返回那样复杂的汇编语句需要,因此速度也会比函数快
2. 宏是与类型无关的,所以参数想传什么就传什么,具体产生什么效果等替换完了再看。甚至宏可以传递类型名,这是函数绝对做不到的
宏的劣势:
1. 每次使用宏的时候,一份宏定义的代码将插到程序当中去。除非宏比较短,否则可能大幅增加宏的长度
2. 宏是没法调试的
3. 宏由于类型无关,因此不够严谨
4. 宏可能会带来运算符优先级的问题,导致容易出错
7. #和##
7.1 #运算符
#运算符将宏的一个参数转换成字符串字面量。它仅允许出现在带参数的宏的替换宏的替换列表中
#执行的操作可以理解为字符串化
举个例子:
这段代码中#n在替换时发生字符串化,将 a 变成了 "a"
当然在观察注释的时候可以注意到printf中包含了3段字符串,这种写法是允许的,它和第二行注释是等价的
7.2 ##运算符
##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。##被称为记号粘合
这样的连接必须是产生一个合法的标识符,否则结果是未定义的
下面我们举个例子:
当我们想比较两个数的大小的时候不同的类型int、float、double都要写一个不同名函数来运算,但其实它们的运算内容方法是一样的,所以能不能有一种方法把它们整合一下
##在这段代码中就起到了粘贴 type 和 _max 的作用,使得type会被替换之后还能与_max形成新的函数名,用这种方法通过一段代码生成了3个功能相似,函数名不同的函数
8. 命名约定
一般来讲,函数和宏的使用语法很相似,所以为了区分二者,我们有这样的书写习惯
把宏全部大写
函数名不要全部大写
9. #undef
这条指令用于移除一个宏定义
举个例子
在宏定义移除之后MAX就不能用了,但是我们可以用#define重新启用他,起到一个修改宏的作用
10. 命令行定义
许多C的编译器提供了一种能力,允许在命令行中定义符号,用于启动编译的过程
举个例子,如果我在编写代码的时候不能确定一个数组要分配多大的空间,只能在运行前的时候确定,可以这样先把程序写出来
这样在程序中并不指定SIZE的具体大小,在启动程序的时候用命令行
gcc -D SIZE=10 programme.c
这样程序就能正常跑起来,并且开辟一个10个元素的数组
这个功能vs中不好演示,感兴趣可以用Linux尝试一下
11. 条件编译
在编译一个程序的时候我们可以使用条件编译指令来控制要不要编译这条语句
比如说那些调试性的代码,删除可惜,保留又碍事,所以我们可以选择性编译
常见的条件编译指令
1.#if #endif
#if 常量表达式(为真编译,为假不编译)
······
#endif
注意#if后面跟的是常量表达式
这么写就是有错误的。第一,#if 后边要求跟常量表达式,a是变量。第二,#if 是在预处理阶段进行处理的,这时候a还没有定义呢,那a算什么?
2. 多分支的条件编译
#if 常量表达式
······
#elif 常量表达式
······
#else
······
#endif
3. 判断是否被定义
#if defined(symbol)
#ifdef symbol
这两条语句都是说如果这个symbol被定义了就编译下面的代码
#if !defined(symbol)
#ifndef symbol
这两条语句是说如果没定义symbol就编译下面的代码
当然不要忘记 #endif 结束条件编译,#endif和距它最近的 #if 或 #if··· 匹配,不管是 #if 还是 #if··· 都要记得用 #endif 结束
4.条件编译指令是可以嵌套的
12. 头文件的包含
12.1 头文件被包含的方式
12.1.1 本地文件包含
#include "filename.h"
查找策略:现在源文件目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果还是找不到就报错
12.1.2 库文件包含
#include <filename.h>
查找这种类型的头文件就直接去标准路径下查找(所以库函数所在位置),如果找不到就报错
这样是不是可以说,对于库函数包含的时候可不可以用 " " 的形式?
确实是可以的,但是这样做效率就会低一点,同时这样也不容易区分我包含的是库文件还是本地文件
12.2 嵌套文件包含
我们之前已经学到过#include就是把另一个文件复制到这条语句所在位置,并替换掉这条语句。
那么在一个大型的项目中,在所难免的会出现头文件被重复包含的现象,这会导致多次把头文件中的内容粘过来,如果头文件过大,这很影响编译的速度
所以我们可以用条件编译解决这个问题,在头文件开头写:
这样就可以通过判断__TEST_H__是否被定义来确定这个头文件有没有使用过,从而确定要不要把头文件的内容粘过去
还有一种方法,在头文件开头加上:
就可以避免头文件的重复引用,这个方法是比较常见的
13. 其他预处理指令
#error
#pragma
#line
#pragma pack() 修改默认对齐数,在结构体中有讲
······
更多内容可以参考《C语言深度解剖》中预处理一章