预处理详解

目录

1.预定义符号

​2.宏定义

3.#和##

4.条件编译

5.文件包含


1.预定义符号

__FILE__           //进行编译的源文件

__LINE__          //当前文件的行号

__DATE__        //文件被编译的日期(格式"mm dd yyyy")

__TIME__          //文件被编译的时间 (格式"hh : mm : ss")

__STDC__         //如果编译器符合C标准(C89或C99),那么值为1

示例:

预处理详解_第1张图片2.宏定义

简单的宏:

格式:#define 标识符 替换列表

示例:

#define MAX 100

#define reg register         //为register这个关键字,创建一个简短的名字

#define LOOP for( ; ; )      //创建一个 LOOP语句,来实现一个无限循环

如果定义的替换列表过长,可以分成几行写,除了最后一行,每行的后面都要加一个反斜杠'\'

比如 :

#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                                       date:%s\ttime:%s\n" ,\
                                       __FILE__,__LINE__ , \
                                       __DATE__,__TIME__ )

注意:

在define定义标识符的时候,不要在最后加上;

比如下面的场景:

#define MAX 1000;

if(condition)

        max = MAX;           //max就变成了1000;;,会报错

else

        max = 0;

带参数的宏:

格式:#define name( parament-list ) stuff

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

注意:参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

下面我们来分析一段代码:

#define SQUARE(X)  X*X

int a = 5;

printf("%d\n",SQUARE(a+1));

 答案是11,不是36,因为替换文本时x被替换为a+1,所以printf语句就变成了printf("%d\n",a+1*a+1);为了避免出错,应该在宏定义上加上两个括号,

#define SQUARE(X)  (X)*(X)

宏可能会不止一次地计算它的参数,如果参数有副作用,多次计算参数地值可能会产生不可预知地结果,看下面这个例子

预处理详解_第2张图片

 预处理器处理之后z = ( (x++) > (y++) ? (x++) : (y++)),因为x和y是后置++,所以先比较x和y,x

为了自我保护,最好避免使用带有副作用地参数。

宏和函数对比:

宏的优点:

1.宏可能比函数调用稍微快些,因为执行时调用函数通常会有写额外开销——存储上下文信息、复制参数的等,而调用宏就没有这些运行开销。

2.宏更通用,函数的参数必须声明为特定的类型,但是宏可以接受任何类型的参数,数的类型可以是int、long、float、double。

宏的缺点:

1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度

2.宏是没法调试的。

3.宏可能会带来运算符优先级的问题,导致程容易出现错。

4.宏由于类型无关,也就不够严谨

5.宏可能不止一次计算它的参数,如果参数有副作用,多次计算参数的值可能会产生不可预知的结果。

6.无法用一个指针来指向一个宏,因此宏不能用于处理某些情况。

我从网上找了一张图详细对比了宏和函数,如下图所示:

属 性 #define定义宏 函数
代 码 长 度 每次使用时,宏代码都会被插入到程序中。除了非常
小的宏之外,程序的长度会大幅度增长
函数代码只出现于一个地方;每
次使用这个函数时,都调用那个
地方的同一份代码
执 行 速 度 更快 存在函数的调用和返回的额外开
销,所以相对慢一些
操 作 符 优 先 级 宏参数的求值是在所有周围表达式的上下文环境里,
除非加上括号,否则邻近操作符的优先级可能会产生
不可预料的后果,所以建议宏在书写的时候多些括
号。
函数参数只在函数调用的时候求
值一次,它的结果值传递给函
数。表达式的求值结果更容易预
测。
带 有 副 作 用 的 参 数 参数可能被替换到宏体中的多个位置,所以带有副作
用的参数求值可能会产生不可预料的结果。
函数参数只在传参的时候求值一
次,结果更容易控制。
参 数 类 型 宏的参数与类型无关,只要对参数的操作是合法的,
它就可以使用于任何参数类型。
函数的参数是与类型有关的,如
果参数的类型不同,就需要不同
的函数,即使他们执行的任务是
不同的。
调 试 宏是不方便调试的 函数是可以逐语句调试的
递 归 宏是不能递归的 函数是可以递归的

#define替换规则:

1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首      先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复     上述处理过程
注意:

1.宏参数和#define定义中可以出现其它#define定义的符号,但是对于宏,不能出现递归

2.当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

  比如:

  #define M 30

  printf("M is %d",M),红颜色的M不会被替换

3.#和##

#:把一个宏参数变成对应的字符串

例如:

#define  PRINT_INT(n)  printf(#n " = %d\n",n)

n之前的#运算符会通知预处理器根据PRINT_INT的参数创建一个对应的字符串,因此调用PRINT_INT(i/j)会变为printf(“i/j” " = %d\n",n),因为C语言中相邻的字符串字面量会被合并,所以这个句子等价于printf("i / j= %d\n",n),printf会显示i/j和它的值。

##:可以把位于它两边的符号合成一个符号

预处理详解_第3张图片

CAT(class,110)等价于class110。

所以##运算符也被称为"记号粘合"。

4.条件编译

在讲条件编译之前,先介绍一下#undef指令,格式:#define 标识符,可以使用#undef指令"取消定义"

比如#define N ,它会删除宏N当当前的定义

条件编译是指根据预处理器所执行的测试结果来包含或排除程序的片段。

#if指令和#endif指令

格式如下:

#if 常量表达式

#endif

举个例子:

#define N 1

#if N

printf("%d\n",i);

printf("%d\n",j);

#endif

在预处理过程中,#if指令会测试N的值,由于N的值不是0,因此预处理器会将这两个printf函数调用保留在程序中(但#if和#endif行会消失)。如果将N的值改为0并重新编译程序,预处理器会将这4行代码都删除,编译器不会看到这些printf函数调用。

注意:#if指令会把没有定义过的标识符当作是值为0的宏对待。

defined运算符

当defined应用于标识符时,如果标识符是一个定义过的宏则返回1,否则返回0。

defined运算符通常和#if指令结合使用,可以这样写:

#if defined(N)

.....

#endif

仅当N被定义成宏时,#if和#endif之间的代码会被保留在程序中。

#ifdef指令和#ifndef指令

#ifdef指令测试一个标识符是否已经定义为宏:

#define 标识符

#ifdef和指令的使用和#if指令类似

#ifndef指令和#ifdef指令类似,但测试的是标识符是否没有被定义为宏

#ifndef 标识符 等价于 #if !defined(标识符),这个括号不是必需的,可以删除。

#elif指令和#else指令

#elif 常量表达式

#else

#elif指令和#else指令可以于#if指令、#ifdef指令和#ifndef指令结合使用

如:

#if 表达式1

当表达式1非0时需要包含的代码

#elif 表达式2

当表达式1为0但表达式2非0时需要包含的代码

#else

其它情况下需要包含的代码

#endif

注意:在#if指令和#endif指令之间可以有任意多个#elif指令,但最多只能有一个#else指令

还有一些其它指令

#error 消息

如果预处理器遇到#error指令,它会显示一条包含消息的出错消息。遇到#error指令预示着程序中出现了严重的错误,有些编译器会立即终止编译而不再检查其它错误,#error指令通常和条件编译指令一起用于检测正常编译过程中不应出现的情况。

例如:

#if defined WIN64

....

#elif defined MAC_OS

.....

#elif defined LINUX

....

#else

#error No operating system specified

#endif

#pragma 指令

#pragma once可以避免头文件的重复引入

5.文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
本地文件包含(自己写给自己的)

#include "filename"

 查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置(存放库函数头文件位置)查找头文件。如果找不到就提示编译错误。
库文件包含

#include

 查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

对于库文件也可以使用 “ ” 的形式包含,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

如果文件嵌套包含,造成了文件内容的重复,该怎么办?使用条件编译

每个头文件开头这样写:

#ifndef __TEST_H__              //如果没有定义__TEST__
#define __TEST_H__            //那么定义__TEST__
//头文件的内容
#endif //__TEST_H__          //结束

但是写成#pragma once更方便

‍感谢大家的阅读,如有错误请指出,我们下次再见。

你可能感兴趣的:(c语言,c语言)