预处理详解

文章目录


一、预定义符号的介绍

预定义符号是语言内置的,即是已经用#define定义好的,我们可以直接使用,是在预处理阶段被处理的,在代码运行后的预处理阶段会被替换为相应的内容

c 复制代码
__FILE__        //进行编译的源文件
__LINE__        //文件当前的行号
__DATE__        //文件被编译的日期
__TIME__        //文件被编译的时间
__FUNCTION__    //进行编译的函数
__STDC__        //如果编译器遵循ANSI C,其值为1,否则未定义

这些预定义符号的使用方法一致,只是需要注意打印的格式是%s还是%d就可以了

举例:

c 复制代码
#include <stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	printf("%s\n", __FUNCTION__);
	//printf("%d\n", __STDC__);
	//因为VS2013不遵循ANSI C,该符号未定义,所以进行了注释
	return 0;
}

运行结果如下:

二、预处理指令#define

2.1 #define定义标识符

语法:

c 复制代码
# define name stuff
//举例
#define MAX 100
#define reg register

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

2.2 #define定义宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)

宏的声明方式:

c 复制代码
#define name(parament-list) stuff

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

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

举例,用宏实现求一个数的平方:

c 复制代码
#include <stdio.h>
#define SQUARE(x) x*x//求x的平方
int main()
{
	int ret = SQUARE(5);
	//相当于int ret = 5*5;
	printf("%d\n", ret);//结果为25
	return 0;
}

警告:这个宏存在一个问题,当你传入的宏参数是2+3时,打印的结果不是25而是11,这是因为进行了直接替换,相当于

c 复制代码
	int ret = 2+3*2+3;
	//替换产生的表达式并没有按照预想的次序进行求值

在宏定义上加上两个括号,这个问题便轻松的解决了

所以用于对数值表达式进行求值的宏定义都应该加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用

2.3 #define的替换规则

在程序中替换#define定义的宏和标识符时,需要涉及几个步骤:

我们用以下代码进行举例:

c 复制代码
#include <stdio.h>
#define MAX 100
#define SQUARE(x) ((x)*(x)*MAX)
int main()
{
	int ret = SQUARE(5);
	printf("%d\n", ret);
	return 0;
}

① 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,它们首先被替换

例如,#define定义的宏中含有#define定义的符号MAX,则调用该宏时,首先将MAX替换

c 复制代码
#include <stdio.h>
#define SQUARE(x) ((x)*(x)*100)
int main()
{
	int ret = SQUARE(5);
	printf("%d\n", ret);
	return 0;
}

② 替换文本随后被插入到程序中原来文本的位置,对于宏,参数名被它们的值替换

例如,上例经过该步骤之后,等价于:

c 复制代码
#include <stdio.h>
int main()
{
	int ret = ((5)*(5)*100);
	printf("%d\n", ret);
	return 0;
}

③ 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是,则重复上述处理过程

上例不再包含任何由#define定义的符号
注意:
① 宏参数和#define定义中可以出现其他#define定义的变量,但是对于宏,不能出现递归

例如:

c 复制代码
#define FAC(x) (x)*FAC(x-1)//error

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

例如,以下代码字符串中的MAX不会被替换为100,而字符串外的MAX会被替换

c 复制代码
#include <stdio.h>
#define MAX 100
int main()
{
	printf("MAX = %d\n", MAX);//结果为MAX = 100
	return 0;
}

三、带副作用的宏参数

首先,来来解释以下什么是带有副作用,在之前,我们也接触过,例如:

c 复制代码
	int a = 10;
	int b = a + 1;//无副作用
	int c = a++;//有副作用

简单来说,代码执行后,除了达到我们想要的结果之外,还导致了其他问题的发生,我们就说该条语句带有副作用

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果

副作用就是表达式求值的时候出现的永久性效果

例如,MAX宏可以证明具有副作用的参数所引起的问题

c 复制代码
#include <stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
	int a = 10;
	int b = 20;
	int c = MAX(a++, b++);
	printf("%d\n", c);
	printf("a=%d b=%d\n", a, b);
	return 0;
}

运行结果:

这段代码看似没有问题,但是结果是不正确的,因为该宏经过替换后,等价于以下代码

c 复制代码
#include <stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int c = ((a++)>(b++)?(a++):(b++));
	printf("%d\n", c);
	printf("a=%d b=%d\n", a, b);
	return 0;
}

经过替换后,我们经过分析就可以得到,c的结果是21,并且代码执行后,a和b的值并不是同时加1,a的值变为了11,而b的值却变为了22

所以,当我们使用宏的时候,应该避免传入带有副作用的宏参数

四、宏和函数的对比

宏通常被应用于执行简单的运算

比如在两个数中找出较大的一个

c 复制代码
#define MAX(x,y) ((x)>(y)?(x):(y))

那为什么不用下面的函数来完成这个任务?

c 复制代码
int Max(int x, int y)
{
	return x > y ? x : y;
}

原因有二

①用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多(函数调用有一个过程,运算也是一个过程,函数的返回也是有一个过程的)。所以宏比函数在程序的规模和速度方面更胜一筹

②更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。但是宏可以适用于整形、长整型、浮点型等可以用于来比较的类型。宏是类型无关的

并且,宏有时候可以做到函数做不到的事情。例如,宏的参数可以出现类型,但是函数却不可以

举例,当我们使用malloc函数开辟内存空间时,可能会觉得代码太多

c 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p1 = (int*)malloc(10 * sizeof(int));
	if (p1 == NULL)
	{
		printf("p1开辟失败\n");
		return 1;
	}
	free(p1);
	p1 = NULL;
	return 0;
}

这时,我们可以实现一个宏,使我们用malloc开辟空间时,只用传入开辟的类型和该类型的元素个数即可

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
	int* p2 = MALLOC(10, int);
	if (p2 == NULL)
	{
		printf("p2开辟失败\n");
		return 1;
	}
	free(p2);
	p2 = NULL;
	return 0;
}

当然,和函数相比,宏也有劣势的地方,例如

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

②宏是没法调试的(宏的替换是在预处理的时候,调试是在运行的时候)

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

④宏可能会带来运算符优先级的问题,导致程容易出现错
宏和函数的一个对比

命名约定

一般来讲,函数和宏的使用语法很相似,所以语言本身没法帮我们区分二者,那我们平时的习惯是:把宏名全部大写,函数名不要全部大写

五、预处理操作符#和##的介绍

5.1 #的作用

这里所说的#并不是#define和#include中的#,这里所说的#的作用是:把一个宏参数变成对应的字符串

首先,我们要知道字符串是有自动连接的特点的

举例:

c 复制代码
	char arr[] = "hello ""world!";
	//等价于char arr[] = "hello world!";
	printf("helll ""world!\n");
	//等价于printf("helll world!\n");

然后,我们观察下面代码:

c 复制代码
#include <stdio.h>
int main()
{
	int age = 10;
	printf("The value of age is %d\n", age);
	double pi = 3.14;
	printf("The value of pi is %f\n", pi);
	int* p = &age;
	printf("The value of p is %p\n", p);
	return 0;
}

经过观察可以发现,printf函数要打印的内容大部分是一样的,那么,为了避免代码冗余,我们可不可以将其封装成一个函数或者是宏呢,经过尝试可以得到,函数和普通的宏是不能实现该功能的

此时,我们可以使用#

c 复制代码
#include <stdio.h>
#define print(data,format) printf("The value of "#data" is "format"\n",data)
//format:这个参数没有#,所以预处理器会直接把调用时传入的值拿过来替换。
//当调用 print(age, "%d") 时,format 会被直接替换为%d,所以宏展开后的另一部分会变成:" is "%d"\n"。同样,字符串连接后就是 " is %d\n"
//data:最后那个单独的 data(在 printf 的逗号后面)也是直接替换。它会替换成变量本身,也就是 age
//替换后:printf("The value of ""age"" is " "%d" "\n", age);
//字符串连接(编译器的步骤):printf("The value of age is %d\n", age);
int main()
{
	int age = 10;
	print(age, "%d");
	double pi = 3.14;
	print(pi, "%f");
	int* p = &age;
	print(p, "%p");
	return 0;
}

这时我们只需将打印的变量的变量名和打印格式传入即可,该代码经过预处理后等级于以下代码:

c 复制代码
#include <stdio.h>
int main()
{
	int age = 10;
	printf("The value of ""age"" is ""%d""\n", age);
	double pi = 3.14;
	printf("The value of ""pi"" is ""%f""\n", pi);
	int* p = &age;
	printf("The value of ""p"" is ""%p""\n", p);
	return 0;
}

又因为字符串有自动连接的特点,所以可以打印出期望的结果

5.2 ##的作用

##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符

举例:

c 复制代码
#include <stdio.h>
#define CAT(x,y) x##y
int main()
{
	int workhard = 100;
	printf("%d\n", CAT(work, hard));//打印100
	return 0;
}

注意:这样的连接必须产生一个合法的标识符,否则其结果是未定义的

六、预处理指令#undef

#undef可以移除一个#define定义的标识符或宏,如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除

例如,下列代码将#define定义的标识符MAX移除后,编译器便不能识别之后的MAX

c 复制代码
#include <stdio.h>
#define MAX 100
int main()
{
	printf("%d\n", MAX);//正常使用
#undef MAX
	printf("%d\n", MAX);//报错,MAX未定义
}

七、命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程

例如,当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性便起到了作用(假定某个程序中声明了一个某长度的数组,但是一个机器的内存有限,我们需要一个很小的数组,但是另外一个机器的内存很大,我们需要一个较大的数组)

c 复制代码
#include <stdio.h>
int main()
{
	int array[ARRAY_SIZE];
	int i = 0;
	for (i = 0; i< ARRAY_SIZE; i++)
	{
		array[i] = i;
	}
	for (i = 0; i< ARRAY_SIZE; i++)
	{
		printf("%d ", array[i]);
	}
	printf("\n");
	return 0;
}

可以看到,代码中没有明确定义数组的大小。在编译这种代码时,我们需要使用命令行对数组的大小进行定义

例如,在Linux环境下,编译指令如下:

c 复制代码
gcc -D programe.c ARRAY_SIZE = 10

经过该编译指令后,便可以打印出0到9的数字

八、条件编译

条件编译:满足条件就参与编译,不满足条件就不参与编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令,比如,调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译

常见的条件编译指令有以下几种:
① 单分支的条件编译

c 复制代码
#if 常量表达式
	//待定代码
#endif
//常量表达式由预处理器求值

如果#if后面的表达式为真,则"待定代码"的内容将参与编译,否则"待定代码"的内容不参与编译
② 多分支的条件编译

c 复制代码
#if 表达式
	//待定代码1
#elif 表达式
	//待定代码2
#elif 表达式
	//待定代码3
#else 表达式
	//待定代码4
#endif

多分支的条件编译类似于if-else语句,"待定代码1,2,3,4"之中只会有一段代码参与编译
③ 判断是否被定义

c 复制代码
//第一种的正面
#if defined(表达式)
	//待定代码
#endif

//第一种的反面
#if !defined(表达式)
	//待定代码
#endif

如果"表达式"被#define定义过,则"第一种的正面"的"待定代码"将参与编译,否则不参与编译。"第一种的反面"的执行机制与"第一种的正面"恰好相反

c 复制代码
//第二种的正面
#ifdef 表达式
	//待定代码
#endif

//第二种的反面
#ifndef 表达式
	//待定代码
#endif

如果"表达式"被#define定义过,则"第二种的正面"的"待定代码"将参与编译,否则不参与编译。"第二种的反面"的执行机制与"第二种的正面"恰好相反
④ 嵌套指令

c 复制代码
#include <stdio.h>
#define MIN 10
int main()
{
#if !defined(MAX)
  #ifdef MIN
	printf("hello\n");
  #else
	printf("world\n");
  #endif
#endif
	return 0;
}

这里条件编译指令的嵌套类似于if-else语句的嵌套

注意:未满足条件编译指令的代码,在预处理阶段将被编译器自动删除,不参与后面的代码编译过程

举例:

c 复制代码
#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d\n", i);
#if 0
		printf("hello world!\n");
#endif
	}
	return 0;
}

因为#if后面的表达式为假,语句 #if 0 和 #endif 之间的代码将不参与编译,所以在预处理阶段过后,编译器编译的代码是:

c 复制代码
//#include <stdio.h>
//预处理阶段头文件也被包含了
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d\n", i);
	}
	return 0;
}

所以,代码运行后只会打印0到9的数字

九、文件包含

我们知道,#include指令可以使被包含的文件参与编译,在预处理阶段,就会进行文件的包含

例如:

c 复制代码
#include <stdio.h>

在预处理阶段,编译器会先删除该指令,并用stdio.h文件中的内容进行替换

但是,文件的包含有两种:

c 复制代码
#include <stdio.h>//一种是用尖括号将要包含的文件括起来
#include "stdio.h"//一种是用双引号将要包含的文件引起来

< >:一般应用于C语言提供的库函数的头文件,如果使用尖括号的方式对头文件进行包含,那么当代码运行到预处理阶段,将对头文件进行包含时,编译器会自动去自己的安装路径下查找库目录,若库目录中含有该头文件,则将其进行包含,若库目录下不存在该头文件,则提示编译错误

" ":一般应用于自定义的头文件,如果使用双引号的方式对头文件进行包含,那么当代码运行到预处理阶段,将对头文件进行包含时,编译器会首先去正在编译的源文件目录下进行查找,若没有找到目标头文件,则再去库目录下进行查找,若两处都没有找到目标头文件,则提示编译错误

这样看来,当我们要包含的头文件是库函数的头文件的时候,我们使用尖括号或者双引号都可以,但是当我们要包含的头文件是自定义的头文件时,我们只能用双引号进行头文件的包含

但是如果我们明明知道自己要包含的头文件是库函数的头文件,那我们就没有必要使用双引号去包含,因为那样会降低代码的效率

关于头文件,还有一点值得注意的是,当我们使用#include来包含头文件时,如果我们重复包含同一个头文件,那么在预处理阶段就会重复包含该头文件的内容,会大大加长代码量,导致代码冗余

避免该问题(头文件被重复包含和重复编译)的发生,有以下两种方法:

方法一:

c 复制代码
#ifndef __ADD_H__  //如果未定义(if not define),检查一个宏是否未被定义
#define __ADD_H__  //定义一个宏

//头文件内容

#endif  //结束条件编译块

当第一次包含该头文件时,会用#define定义符号__ADD_H__,当第二次重复包含该头文件时,因为__ADD_H__已经被定义过,就无法再次包含该头文件的内容了

方法二:

c 复制代码
#pragma once

//头文件内容

只需在头文件开头加上这句代码,那么该头文件就只会被包含一次