【C语言】预处理

今天我们所讲的内容分为以下几个方面:

1. 预定义符号

C语言设置了⼀些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的
__FILE__ // 进⾏编译的源⽂件
__LINE__ // ⽂件当前的⾏号
__DATE__ // ⽂件被编译的⽇期
__TIME__ // ⽂件被编译的时间
__STDC__ // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义
使用方法,我举个例子:
#include<stdio.h>
int main()
{
	printf("file:%s line:%d\n", __FILE__, __LINE__);
	return 0;
}

运行结果:

2. #define定义常量

基本语法:
# define name stuff
举个例子:
# define MAX 1000
# define reg register // register 这个关键字,创建⼀个简短的名字
# define do_forever for(;;) // ⽤更形象的符号来替换⼀种实现
# define CASE break;case // 在写 case 语句的时候自动把 break 写上。
# define DEBUG_PRINT printf( "file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
// 如果定义的 stuff 过⻓,可以分成几行写,除了最后⼀⾏外,每行的后⾯都加⼀个反斜杠 ( 续行符 )
注意: 在define定义标识符的时候,一般不要在最后加上;
因为define一般是直接替换的

3. #define定义宏

#define 机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏 (define macro)。
声明方式:
# define name( parament-list ) stuff
举个例子:
#define ADD(a,b) a+b

这就声明好一个加法宏了,但是这样定义宏存在⼀个问题:

观察下面的代码:

#include<stdio.h>
#define SQUARE(x) x * x
int main()
{
	int a = 8;
	int b = 9;
	printf("%d",SQUARE(a + b));
	return 0;
}

那么大家觉得打印的多少?

有人可能会说:a+b=8+9=17

17*17=289

但真的是这样吗?

我们来看看结果:

结果是89,为什么呢?

我们吧鼠标移动到函数上,我们会发现:

替换文本时,参数x被替换成a + b,所以这条语句实际上变成了:a+b*a+b

这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。
但是,如果要解决这个问题,我们可以在宏定义加上括号,像这样
# define SQUARE(x) (x) * (x)
这样我们再一次运行
结果就是289了
但是这样 定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。
举个例子:
# define DOUBLE(x) (x) + (x)
int a = 5 ;
printf ( "%d\n" , 10 * DOUBLE(a));
这样的结果是多少呢?
看上去,好像打印100,但事实上打印的是55.
因为经过替换,实际的式子是:
printf ( "%d\n" , 10 * ( 5 ) + ( 5 ));
乘法运算先于宏定义的加法,所以出现了 55 .
这个问题,的解决办法是在宏定义表达式两边加上⼀对括号就可以了。
所以定义成这样:
# define DOUBLE( x) ( ( x ) + ( x ) )
注: 所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使⽤宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

4. 带有副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
x+ 1 ; // 不带副作用
x++; // 带有副作用
MAX宏可以证明具有副作用的参数所引起的问题。
举个例子:
# 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=6 y=10 z=9
所以在正常写代码的时候我们就要避免这种副作用,使用不带副作用的参数

5. 宏替换的规则

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

6. 宏和函数的对比

宏通常被应用于执行简单的运算。
比如在两个数中找出较大的⼀个时,写成下面的宏,更有优势⼀些
#define MAX(a, b) ((a)>(b)?(a):(b))

宏和函数相比的优势:

1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜⼀筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于 > 来比较的类型。宏是类型无关的。
和函数相比宏的劣势:
1. 每次使用宏的时候,⼀份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。⽐如:宏的参数可以出现类型,但是函数做不到。
举个例子:
#define MALLOC(num, type)\
 (type )malloc(num sizeof(type))
 ...
//使⽤
 MALLOC(10, int);//类型作为参数
//预处理器替换之后:
 (int )malloc(10 sizeof(int));

7. #和##

1 #运算符

#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。
#运算符所执行的操作可以理解为”字符串化“。
当我们有⼀个变量 int x = 1; 的时候,我们想打印出: the value of x is 1  .
就可以写:
#define PRINT(n) printf("the value of "#n " is %d", n);
当我们把a替换到宏的体内时,就出现了#x,而#x就是转换为"x",时⼀个字符串
代码就会被预处理为:
printf("the value of ""x" " is %d", x);

这样就能打出 the value of x is 1 .

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;
}

这里我们就可以使用宏定义来解决:

#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
 return (x>y?x:y); \
}

但是,其实在实际开发过程中##使用的很少,很难取出贴切的例子

8. 命名约定

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

9. #undef

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

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

10. 条件编译

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

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

12. 头文件的包含

本地文件包含
# include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件⼀样在标准位置查找头文件。
如果找不到就提示编译错误。
库文件包含:
# include <filename>
查找头文件直接去标准路径下去查找,如果找不到就提⽰编译错误。
这样是不是可以说,对于库文件也可以使用  “” 的形式包含?
答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

相关推荐

  1. C语言——预处理

    2024-01-26 16:50:04       43 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-01-26 16:50:04       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-01-26 16:50:04       101 阅读
  3. 在Django里面运行非项目文件

    2024-01-26 16:50:04       82 阅读
  4. Python语言-面向对象

    2024-01-26 16:50:04       91 阅读

热门阅读

  1. 贪吃蛇(C)

    2024-01-26 16:50:04       52 阅读
  2. 如何让haproxy不命中已经失效的服务器

    2024-01-26 16:50:04       52 阅读
  3. ES6对象新增了哪些扩展?

    2024-01-26 16:50:04       59 阅读
  4. Linux系统安全加固规范

    2024-01-26 16:50:04       52 阅读