预处理详解

1. 预定义符号

C语言设置了一些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的

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

 以下代码用来验证

#include<stdio.h>
int main()
{
	printf("%s\n", __FILE__);//进行编译的源文件
	printf("%s\n", __DATE__);//文件当前的行号
	printf("%s\n", __TIME__);//文件被编译的日期
	printf("%d\n", __LINE__);//文件被编译的时间

	return 0;
}

执行结果如下

 我们再添加一段代码

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

 结果如下:

 出错说明,VS2022的环境是不完全支持ANSIC的

2. #define定义常量

基本语法

#define  name  stuff

 name 等价于stuff

举个例子

#include<stdio.h>
#define MAX 1000
#define STR "hello bit"
#define forever for(;;)
#define PRINTF  printf("FILE: %s\t DATA: %s\t \
						TIME: %s\tLINE: %d\n",\
						__FILE__,__DATE__,\
						__TIME__,__LINE__)
int main()
{

	forever
	{
		printf("%d  %s\n", MAX, STR);
        //printf("%d  %s\n", 1000, "hello bit");//与上等价
		PRINTF;//与下等价
  //      printf("FILE: %s\t DATA: %s\t \
//						TIME: %s\tLINE: %d\n",\
//						__FILE__,__DATE__,\
//						__TIME__,__LINE__)
	}
}

打印结果为

 \是续航符 后面不能有空格

由于我们的forever等价于for(;;),循环判断条件是没有的,所以是死循环。

我们在用define定义标识符的时候,没有加上   如果加上会怎么样呢?

代码如下

#include<stdio.h>
#define MAX 1000;
	
int main()
{

	int a = MAX;//实际上为  int a = 1000;; //为两条语句

    	int m = 0;
	if (1)
		m = MAX;//等价于m=1000;; //此处就为两条语句,下面的else就不知道与谁对应了
	else
		m = 1;

	printf("%d\n", MAX);//实际上为printf("%d\n", 1000;);//报错
    return 0;
}

所以加上分号完全就是画蛇添足,还很可能会引发很多错误,还是不要添加的好

3. #define定义宏

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

下面是宏的申明方式:

#define  name( parament-list ) stuff

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

注意 :

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

 举个栗子,实现一个宏用来计算一个数的平方:
 

#include<stdio.h>
#define POW(x) x*x
	
int main()
{
	int  x = 5;
	int sum = POW(x);
    //int sum = x*x;//与上等价
	int ret = POW(x + 1);
	printf("%d\n%d", sum,ret);
	printf("%d", sum);
}

得到结果为

 我们发现将x+1传进去得到的是11,这是为什么呢?

宏的使用方式是替换,POW(x+1)直接替换文本得到 的是x+1*x+1

代码中x值为5得到的式子为 5+1*5+1==11

那如果就想让其等于正确的值该怎么修改呢?

利用运算符优先级有两种办法

//1.修改宏定义为
#define POW(x) (x)*(x)//相比下面一劳永逸

//2.修改传的参数为
int ret = POW((x + 1));

4. 带有副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果

 我们用一段程序来证明具有副作用的参数所引起的问题:

#include<stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
	int a = 3;
	int b = 5;

	int m = MAX(a++, b++);

	printf("m=%d\n", m);
	printf("a=%d\n", a);
	printf("b=%d\n", b);
	return 0;
}

MAX宏经过预处理器预处理后的结果为

int m = (a++) > (b++) ? (a++) : (b++);

所以输出的结果为

 5.宏替换的规则

在程序中扩展#define定义符号和宏时,需要设计几个步骤。

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

2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。

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

注意:

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

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

 如以下代码:
 

#include<stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
#define M 10
int main()
{
	int m = MAX(M, MAX(2, 3));
	printf("M=%d\n", m);
    return 0;
}

上面的MAX里的宏作为MAX的参数,并不是递归,递归是从宏内部调用宏本身(宏不支持递归)。我们上述代码宏定义了M,自动检索出现的M,但是字符串常量的内容并未被搜索 如上述代码中printf里的M

6.宏 函数对比

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

 比如以下代码:
 

#include<stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))

int MAX1(int x, int y)
{
	int r = x > y ? x : y;
    return r;
}

int main()
{
	int m1 = MAX(3, -6);
	int m2 = MAX1(3, -6);
	printf("%d\n%d", m1, m2);
    return 0;
}

我们发现,宏和函数都能完成我们所需的功能,那为什么不用函数来完成这个任务呢?

原因有二

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

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

 和函数相比宏的劣势

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

2.宏是没法调试的。

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

4.宏可能会带来运算符优先问题,容易导致程序出错。

宏有时候可以做到函数做不到的事。比如:宏的参数可以出现类型,但是函数做不到。

宏和函数的对比表格如下

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

7.#和##

7.1 # 运算符

#运算符将一个宏的参数转换为字符串字面量,它仅允许出现在带参数的宏的替换列表中
#运算符所执行的操作可以理解为"字符串化"。

 例如以下代码:
 

#include<stdio.h>
#define Print(n,format) printf("the value of n is "format"\n",n);
int main()
{
	int a = 1;
	Print(a, "%d");

	int b = 20;
	Print(b, "%d");

	float f = 5.6f;
	Print(f, "%f");

	return 0;
}

输出结果为

我们发现并没有将变量名也进行替换

我们用下面的宏来替换上面的宏

#define Print(n,format) printf("the value of " #n " is "format"\n",n);

 当以这种方式调用的时候:
将a替换到宏的体内就出现了#a ,而#a就转换为"a"时一个字符串代码就会被预处理为:

 printf("the value of " "a" " is "%d"\n",a);

 现在的输出效果就是我们想要的

 7.2 ##运算符

##运算符,可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。##被称为记号粘合
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

 这里我们可以想一想,写一个函数求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;
}

但是这样写起来太过繁琐了,我们可以用以下写法:

#include<stdio.h>
#define GET_MAX(type) type type##_max(type x,type y)\
{  \
 return (x>y?x:y); \
}

//定义函数
GET_MAX(int);//int_max
GET_MAX(float);//float_max

int main()
{
	int m = int_max(3, 5);

	float n = float_max(3.1f, 4.5f);
	printf("%d  %f", m, n);
	return 0;
}

GET_MAX(int);//int_max
GET_MAX(float);//float_max

相当于定义了两个函数

 8. 命名约定

一般来讲函数的宏的使用语法很相似。所以语言没办法帮我们区分二者。

那我们平时的习惯是:

把宏名全部大写

函数名不要全部大写

 9. #undef

这条指令用于移除一个宏定义。

 以下代码演示:
 

#define MAX 100
int main()
{
	printf("%d\n", MAX);
#undef MAX
	printf("%d\n", MAX);
	return 0;
}

出现错误为:

 说明MAX已经被#undef取消了

如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除

10. 命令行定义

许多c的编译器(vs做不到)提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

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

11. 条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。

因为我们有条件编译指令。

比如说:

调试行的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

#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 !defined(symbol)
#ifndef symbol

4. 嵌套指令
#if defined(OS_UNIX)
    #ifdef OPTION1
        unix_version_option1();
    #endif
    #ifdef OPTION2
        unix_version_option2();
    #endif
#elif defined(OS_MSDDS)
    #ifdef OPTION2
        msdos_version_option2();
    #endif
#endif

以下为上述指令实际代码:

//1. 
#if 0
#define MAX 100
int main()
{
	printf("%d\n", MAX);
#undef MAX
	printf("%d\n", MAX);
	return 0;
}
#endif
//全部不执行

//2.
#define M 3
int main()
{
#if M==0
	printf("0");
#elif M==1
	printf("1");
#elif M==2
	printf("2");
#else 
	printf("ok");
#endif
	return 0;
}
//执行符合条件的
//3.
#define MAX 1
int main()
{
#if defined(MAX)
	printf("hehe\n");
#endif

#if !defined(MX)
	printf("haha");
#endif
	return 0;
}
//如果定义了就执行,不看它的值
//4.将上面嵌套即可

12. 头文件的包含

12.1 头文件被包含的方式
(1) 本地文件被包含
#include "xxxxx"

查找策略:

现在源文件所在目录下查找,如果头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果再找不到就提示编译错误。

Linux环境下的标准头文件路径:

/usr/include

 VS环境下的标准头文件的路径:

C:\Progran Files (x86)\Microsoft Visual Studop 12.0\VC\include

//这是vs2013的默认路径

 注意按照自己的安装路径去找

(2) 库文件包含
#include<stdio.h>

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

这样是不是说,对于库文件也可以使用""的形式包含呢?

答案是肯定的:可以,但是这样做查找的效率就会低一些,这样也不容易区分是库文件还是本地文件了

12.2 嵌套文件包含

#include指令可以使另外一个文件被编译。就像它实际出现于#include指令的地方一样。

这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。

一个头文件被包含10次,那就实际被编译10次,如果重复包含对编译的压力就比较大

 比如在test.c文件中这样写:

#include"test.h"
#include"test.h"
#include"test.h"
#include"test.h"
#include"test.h"
#include"test.h"

 test.c将会拷贝5份test.h的代码。

如果test.h文件比较大,这样预处理后代码量会剧增,如果工程比较大,有公共使用的头文件,大家都能使用,却不做任何处理的话,可能会造成很严重的后果。

那么如何解决呢?

可以用条件编译来解决:

每个头文件写:

#ifdef _TEST_H_
#define _TEST_H_
//头文件的内容
#endif  //_TEST_H_

 或者是

#pragma once
//防止头文件被重复包含

 就可以避免头文件的重复引入。

13.其它预处理指令

平时不是很常用,此处就不过多介绍了

#errof
#pragma
#line
//....
#pragma pack() //在结构体部分介绍

这篇就到这里了,喜欢的可以点一下赞支持一下哦

(づ ̄3 ̄)づ╭❤~

  • 45
    点赞
  • 65
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值