目录
1. 预处理详解
1.1 预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这些预定义符号都是c语言内置的符号
演示如下:
printf("file:%s line:%d\n", __FILE__, __LINE__);
printf("date:%s time:%s\n", __DATE__, __TIME__);
1.2 #define
1.2.1 #define 定义标识符
语法:
#define name stuff
举个例子:
#define MAX 1000 // 定义常量MAX为100
#define reg register // 相当于给关键字用另一种符号替代换
#define do_forever for(;;) // 也是相当于做了一次替换
#define CASE break;case // 在写case语句的时候自动把 break 写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s line:%d
date:%s time:%s\n",
__FILE__, __LINE__ ,
__DATE__, __TIME__)
那么看一下下面这段代码有什么问题?
#define MAX 100;
再定义标识符的时候加上了一个分号,在正常的语句下,连续使用两个分号,编译器会认为后面那个是一个空语句,但是在下面的场景会出现问题:
if(condition)
max = MAX; // 这里被替换成了max = MAX;;
else
max = 0;
我们知道if语句不加中括号,默认只会跟着一条语句,这里相当于是有两条语句了,下面再出现else的时候就会出现语法错误。
1.2.2 #define 定义宏
#define 机制包含了一个规定,允许把参数替换到文本中,这种实现通常称为宏或者定义宏
下面是宏的定义方式:
#define name( parament-list ) stuff
parament-list 为参数
注意:
- 参数列表的左括号必须与name紧邻,否则可能会被当成stuff
以下是宏的定义案例:
#define SQUARE( x ) x * x
宏的调用方法与函数相似,将参数传进去
SQUARE(5);
这个时候宏接收到一个参数之后,会替换成下面的表达式
5 * 5
这样的宏本质上是存在一个问题的,观察下面的代码:
int a = 5;
printf("%d\n",SQUARE(a + 1));
我们的意图是希望打印出6*6的结果36这个值,可实际上他输出的是11。这是因为宏其实是做的替换文本,相当于替换成了下面的表达式:
a + 1 * a + 1
宏在做替换的时候是不会默认加上括号的,所以在做运算的过程中,可能会出现优先级的问题。
所以我们需要对宏定义做以下修改:
#define SQUARE( x ) (x) * (x)
这样就能能够顺利打印36这个值了,我们再来看另一个宏定义
#define DOUBLE(x) (x) + (x)
这里可能会因为优先级的问题,在替换后出现新的错误,如下:
int a = 5;
printf("%d\n", 10 * DOUBLE(a));
在我们输入这个表达式的时候,我们其实想得到的结果是100,但实际输出55,我们来看一下替换后的情况:
10 * (5) + (5)
因为优先级的问题,所以是先算了乘法,再进行加法运算,那么针对这个问题,我们的办法就是在宏定义的表达式两边再加上一次括号
#define DOUBLE(x) ((x) + (x))
根据上面这两种情况,我们再定义宏用来求值的时候尽量都以这种方式加上括号,避免在进行替换之后会因为优先级问题,导致结果不是我们预期想要的结果。
1.2.3 #define 替换规则
在程序中对#define定义符号和宏,需要涉及几个步骤:
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果有,它们首先被替换。
- 替换文本随后被插入到程序中原来的位置。对于宏,参数名被他们的值所替换。
- 最后,再对结果文件进行扫描,如果还有#define定义的符号,则重复上述过程。
注意:
- 宏参数和#define定义中可以出现#define定义的其他符号,但是对于宏,不能出现递归
- 党处理器搜索#define定义的符号时,不会从字符串常量的内容去查找
1.2.4 #和##
我相信很多人都没有见过以上这两个符号,那么这有什么用处呢,我们先看这样一段代码:
char* p = "hello ""world\n";
printf("%s", p);
这个结果是不是输出hello world ?
答案:是的
我们发现字符串是有自动连接的特点。
我们来看一下下面这段代码:
#define PRINT(FORMAT, VALUE)\
printf("the value is "FORMAT"\n", VALUE);
PRINT("%d",10);
怕大家看不懂反斜杠在这里的用处,所以先解释下,在C语言以反斜杠 ( ' \ ' ) 表示断行,跟在反斜杠后的字符自动接续到前一行。反斜杠后不能跟空格。
上面这段代码替换后就变成了:
printf("the value is "%d"\n", 10);
这样就能通过宏定义进行输出函数。这个时候,我们发现,只有把宏参数写出字符串的形式才可以起到连接的效果,那么这个时候,就可以使用 # 这个符号。
# 是把一个宏参数变成相应的字符串,我们来修改以下刚刚的宏定义
#define PRINT(FORMAT, VALUE)\
printf("the value of " #VALUE " is "FORMAT"\n", VALUE);
int i = 10;
PRINT("%d",i+3);
上面这段代码替换后就变成了
printf("the value of " "i+3" " is " " %d " "\n", i+3);
符号有一些多,我在替换基础上加了一些空格,方便大家理解。
那么最终输出的结果应该是:
the value of i+3 is 13
## 的作用
- ## 可以把位于它两边的符号和成一个符号
- 它允许宏定义从分离的文本片段创建标识符
#define ADD_TO_SUM(num, value) \
sum##num += value;
int sum5 = 0;
ADD_TO_SUM(5, 10);
printf("%d\n", sum5);
上面这段代码的宏会被替换成下面这样:
sum5 += 10;
1.2.5 带副作用的宏参数
首先,我们先了解一下什么是有副作用的参数
x+1; // 不带副作用
x++; // 带有副作用
为什么说x++是带了副作用的,副作用就是表达式求值的时候出现危险,导致不可预测的结果,看一下下面这个宏定义:
#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);//输出的结果是什么?
首先,我们根据前面的宏可能会出现的问题,做了一些处理,将宏的每个值都加上了括号,防止出现问题,但是上面这段代码还是得不到预期的结果,这个宏会替换为下面的结果:
z = ((x++) > (y++) ? (x++) : (y++));
我们来分析以下上面的执行过程,首先先进行x和y的比较
1 : x > y // 5 > 8
2 : x++ // x = 6
3 : y++ // y = 9
之后根据三目运算符的结果,我们得到
1 z = y++ // z = 9
2 y++ // y = 10
所以输出的结果是:
x = 6 y = 10 z = 9
1.2.6 宏和函数的对比
我们会发现宏无论是在定义上,还是实现上都与函数很相似,那么这两者有什么区别呢?为什么有的时候用宏,有的时候却用函数。
宏通常被用于执行简单运算。比如用于一个数翻倍。
#define DOUBLE(x) ((x) + (x))
那为什么不用函数来完成这个任务
int double(int x)
{
return x+x;
}
原因如下:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
- 宏与类型无关。
当然,宏与函数对比也有一定缺陷:
- 每次使用宏的时候,一份宏定义的代码插入到程序中。除非宏比较短,否则可能会大幅度增加程序的长度。
- 宏没办法进行调试。
- 宏虽然与类型无关,但是这样不够严谨。
- 宏可能会带来运算级优先的问题,容易导致出错。
宏和函数的对比
属 性 | #define定义宏 | 函数 |
---|---|---|
代 码 长 度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长。 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码。 |
执 行 速 度 | 更快。 | 存在函数的调用和返回的额外开销,所以相对慢一些。 |
操 作 符 优 先 级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带 有 副 作 用 的 参 数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参 数 类 型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是 不同的。 |
调 试 | 宏是不方便调试的。 | 函数是可以逐语句调试的。 |
递 归 | 宏是不能递归的。 | 函数是可以递归的。 |
1.2.7 命名规定
一般函数和宏的使用语法很相似。为了方便区分,我们通常会:
把宏名全部大写
函数名部分大写
1.3 #undef
这条指令用来移除一个宏定义。
语法如下:
#undef NAME
举个例子:
#define MAX 100
#undef MAX
MAX; // 这里会报错
1.4 条件编译
常见的条件编译指令
1.常量表达式由预处理器求值
#if 常量表达式
//...
#endif
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
1.5 文件包含
我们在初学阶段,通常会用#include 指令调用头文件和自己定义的文件
// 标准头文件
#include <stdio.h>
// 自己的源文件
#include "filename"
我们使用<> 和 "" 来区分是库文件还是本地文件
建议不要使用
#define "stdio.h"
来去查找库文件,大大降低查找查找效率。
注意:我们在进行一段工程的时候,会经常出现嵌套文件的存在,这个时候一个文件底下可能调用了多次同样的头文件或者本地文件,这样会大大降低效率,避免出现这种问题,可以加上:
#pragma once
这段代码是防止头文件被重复引入