预处理详解

⽬录
1. 预定义符号
2. #define定义常量
3. #define定义宏
4. 带有副作⽤的宏参数
5. 宏替换的规则
6. 宏函数的对⽐
7. #和##
8. 命名约定
9. #undef
10. 命令⾏定义
11. 条件编译
12. 头⽂件的包含
13. 其他预处理指令

1.预处理定义符号

语言中定义了一些预处理符号,在程序中可以直接使用,比如下面的

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

下面是使用他们的例子

int main()
{
	printf("%s\n", __FILE__);
	printf("%s\n", __DATE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __TIME__);
	//printf("%s", __STDC__);这里有的编译器可能会报错,说明该编译器不支持ASCIN c

	return 0;
}

运行结果

2. #define定义常量

语法

#define name(名字) stuff(内容),使用名字就相当于使用内容了

在这里可以定义常量,也可以定义代码,定义代码的话可以让程序更简单,更形象

#define MAX 1000
#define reg register //为 register这个关键字,创建⼀个简短的名字
#define do_forever for(;;) //⽤更形象的符号来替换⼀种实现
#define CASE break;case //在写case语句的时候⾃动把 break写上。
// 如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )

上面的

#define  do_forever for(;;;)//当for循环的条件部分什么都不写的时候,表示条件恒成立

在使用某些死循环的后,直接就可以使用do_forever来代替,更加简便和形象

int main()
{
    foever;//死循环
    return 0;
}
#define CASE break;case
int main()
{
    int a=0;
    switch(a)
    {
        case 1:
                //实现
        CASE 2:
                不用写break了

    }

    return 0;
}

值得注意的是,在用#define定义的时候,在后面不要加上分号,加上分号的时候,该变量就变成你要定义的后面还加上了一个分号,你在写代码的时候,某句话写完的时候,你再加上一个分号的话,就相当于有两个分号了,比如下面的

#define MAX 10;

int main()

{        

        int a=MAX;

        //上面的语句再程序预处理的时候,就会变成

       // int a=MAX;;//有了两个分号

        return 0;

}

3. #define定义宏

#define?机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义
(definemacro)
下⾯是宏的申明⽅式:

#define name( parament-list ) stuff //括号里面的是参数

比如下面的

#define add(x,y) x+y//记得括号应该挨着add
                    //否则旧成为后面的内容了

注意:在写函数参数的时候,给参数尽量的带上参数

           宏的参数不参与运算,直接代人到内容里面去

比如下面的

#define mul(x,y) x*y
int main()
{
	int ret = mul(2, 4);//结果是8
	printf("%d\n", ret);
	ret = mul(3 + 3, 3 + 3);//结果是15
	printf("%d\n", ret);
	return 0;
}

上面的运行结果分别是8和15,但是第2个的本意是计算6*6的,结果却为15,这是为什么嘞?原因是宏里面的参数是直接进行替换的,不管是加,减,乘除,它都不会经过计算,直接代入,上面的第二个相当于3+3*3+3,所以在定义参数是后,我们习惯于把每个参数都加上括号

即:
#define mul(x,y) (x)*(y),就能很好的避免这个问题了

4. 带有副作⽤的宏参数

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

比如下面的

x+1; //不带副作⽤
x++; //带有副作⽤

下面是一个例子:写一个宏,比较两个数的最大值,并且输出两个数的最大值

#define MAX(a,b) a>b?a:b
int main()
{
	int ret = MAX(1, 2);
	printf("%d", ret);
	return 0;
}//输出的结果为2

但是一旦带有副作用的参数,结果就会导致不可避免的后果,比如下面的代码

#define MAX(a,b) (a)>(b)?(a):(b)
int main()
{
	int a = 1, b = 2;
	int ret = MAX(a++,b++);
	printf("%d", ret);
	return 0;
}//输出的结果是3,并不是2

上面的结果出乎了我们的意料,分析:
因为宏的参数在进行替换的时候,不会经过任何计算,直接进行替换

上面的就相当于

int ret=a++>b++?a++:b++,结果结果就会出错了

5. 宏替换的规则

在程序中扩展#define定义符号和宏时,需要涉及⼏个步骤。
1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
2. 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和#define?定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

6. 宏和函数的对比

通过上面的内容,我们不难发现,有的时候宏和函数还是很相像的,但是,他们各有各的好处

1.宏和函数相比的不足

...每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。
... 宏是没法调试的。
 ...宏由于类型⽆关,也就不够严谨。
 ...宏可能会带来运算符优先级的问题,导致程容易出现错。

但是,宏相比于函数有时候也有好处

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

下面是一个例子:还是比较两个数的最大值,但是这两个数是任意的相同的类型,就用上面的比较两个数的最大值就行,因为对于宏,没有参数的检查,这样就显得很方便

还有一个例子,比如下面的例子

7. #和##

7.1 #运算符

这个运算符的作用是避免一个字符在定义宏的时候把它替换成为这个符号对应的值

比如下面的例子

#define PRINT print("the value of #n is %d",n);

如果上面的第一个n前面没有加上#的话,就会被变成n的值,但是有的话,在预处理的时候,就不会被替换成为n的值,直接就是一个n

7.2 ##运算符

 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称
为记号粘贴这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。
这⾥我们想想,写⼀个函数求2个数的较⼤值的时候,不同的数据类型就得写不同的函数。

比如:

int int_max(int x, int y)
{
return x>y?x:y;
}
float float_max(float x, float y)
{
return x>yx:y;
}

但是上面写出来的太繁琐了,下面是一个更好的例子

//宏定义
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
return (x>y?x:y); \
}     /这个是换行符,在定义宏的时候必须要要写成一排,如果空间不够,就需要用到换行符

使⽤宏,定义不同函数

GENERIC_MAX(int) //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名
int main()
{
    //调⽤函数
    int m = int_max(2, 3);
    printf("%d\n", m);
    float fm = float_max(3.5f, 4.5f);
    printf("%f\n", fm);
    return 0;
}

8. 命名约定

⼀般来讲函数的宏的使⽤语法很相似。所以语⾔本⾝没法帮我们区分⼆者。

那我们平时的⼀个习惯:

                                        把宏名全部⼤写
                                        函数名不要全部⼤写

9. #undef

这条指令⽤于移除⼀个宏定义。

#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。

比如下面的

#define MAX 10;
int main()
{
	int a = MAX;
#undef MAX//取消对MAX的宏定义
	int b = MAX;//编译器会报错
	return 0;
}

10. 命令⾏定义

许多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;

上面的代码不知道数组的长度ARR_SIZE的大小,但是可以在命令行定义它的大小

编译命令

//linux 环境演⽰
gcc -D ARRAY_SIZE=10 programe.c

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]); //条件符合就执行,不符合的话就不执行,和if语句一样
    #endif //__DEBUG__
}
return 0;

常见的调价编译语句

#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
#endif
#ifdef OPTION2
    unix_version_option2();
#endif
    #elif defined(OS_MSDOS)
    #ifdef OPTION2
    msdos_version_option2();
   #endif
#endif

12.头文件的包含

12.1.本地文件的包含

#include "add.h" 

本地文件就是自己写的,比如你写的函数,在头文件里面,你需要使用的时候,就需要使用上面的

关于头文件,编译器在处理的时候,也有自己的查找方式

查找策略:先在源⽂件所在⽬录下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在
标准位置查找头⽂件。
如果找不到就提⽰编译错误。

Linux环境的标准头⽂件的路径

/usr/include 

VS环境的标准头⽂件的路径:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径

13. 库⽂件包含

查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误。
这样是不是可以说,对于库⽂件也可以使⽤ “” 的形式包含?
答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件

14. 嵌套⽂件包含

我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的
地⽅⼀样。这种替换的⽅式很简单:预处理器先删除这条指令,并⽤包含⽂件的内容替换。
⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤

比如下面的

include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"//这样重复引入头文件,就会让编译器的压力很大(当头文件的内容比较的时候)
#include "test.h"
int main()
{
        return 0;
}

为了避免头文件的重复使用,需要用到条件编译

具体如下

#ifndef __ADD_H__
#define __ADD_H__
int add(int x,int y);
#endif //__ADD_H__

上面的执行逻辑就是,如果没有定义头文件,就定义,那如果定义的话,就直接结束

也可以用下面的方式

#pragma once 
int add(int x,int y);

  • 31
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值