预处理详解

1.预定义符号

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

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

int main() {
	printf("file:%s\n", __FILE__);
	printf("line:%d\n", __LINE__);
	printf("date:%s\n", __DATE__);
	printf("time:%s\n", __TIME__);
	printf("stdc:%d\n", __STDC__);//在vs2022中并定义,说明vs2022不完全支持c99
	return 0;
}

 2.#define定义常量

基本语法:

# define 常量名 常量值

例如,定义一个表示圆周率的常量,可以这样做:

#include<stdio.h>
#define PI 3.1415926

int main(){

    printf("圆的圆周率为:%lf",PI)

    return 0;
}

当我们在进过预处理之后PI就会被直接替换成3.1415926

使用define定义标识符的建议

当我们用#define定义标识符的时候,最好不要在后面加上分号( ;)

因为这可能会为我们造成些许麻烦,例如:

#include<stdio.h>

#define a = 100;
#define b = 1;

int main() {
    //例子a

    printf("%d ", a);

        //预处理后的代码:printf("%d ",a;);这显然是错误的

    //例子b

        int m = 0;
    if (1)
        m = b;  //预处理后的代码: m = 1;;我们都知道ife lse语句如果不带大括号
    else        //那么它后面只能跟一条语句,而这样写却有了两条语句,明显是错误的
        m = -b


        return 0;
}

所以建议不要加上分号( ;) ,这样容易导致问题。

3.#define定义宏

#define 机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏 (define macro)。
宏定义的语法如下:
# define 宏名(参数列表)  宏体

其中的 参数列表 是⼀个个由逗号隔开的符号表,它们可能出现在宏体中。 

注意:

参数列表的左括号必须与宏名紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为宏体的 ⼀部分。

下面是一个简单的宏定义,该宏用于计算两个数的最大值:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

在这个例子中MAX是宏名,a和b是参数。宏体是条件表达式,它使用三元运算符 ?:来觉定谁更大

在使用这个宏的时候,我们可以像函数一样调用它:

#include<stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main(){

    int x = 5;
    int y = 10;
    int max_val = MAX(x, y);  // 这会被预处理器替换为 ((x) > (y) ? (x) : (y))

    return 0 ;
}

要注意的是,由于宏只是文本替换,所以使用宏的时候需要格外小心

在宏的定义中参数应该被括号,以避免替换文本后发生的优先级问题

例如:

#include<stdio.h>
#define MAX(a, b) a > b ? a : b
int main(){

    int x = 5;
    int y = 10;
    int z = 10;
    int max_val = MAX(x , y + z);

    return 0 ;
}

展开后得到:

x > y + z ? x + z : y + z

这通常是我们所期望的行为,因为 y + z 的优先级高于比较级操作 > 。

然而,宏中的表达式包含其他优先级较低的操作符,问题就会出现

#include<stdio.h>
#define MAX(a, b) a > b ? a : b
int main(){

    int x = 5;
    int y = 10;
    int z = 10;
    int max_val = MAX(x , y + z) * 2;  

    return 0 ;
}

展开后得到:

x > y + z ? x + z : y + z * 2

这里,由于乘法 * 的优先级高于加法+,y + z * 2实际上会被解释为c + (d * 2),这可能不是我们的初衷。正确的计算顺序应该是(c + d) * 2。

为了避免这种问题,我们应该始终在宏定义中为参数和操作符使用括号,以确保正确的运算顺序。这就是为什么在原始的MAX宏定义中,每个参数和整个条件表达式都被括号包围起来的原因。

4.带有副作用的宏参数

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

MAX宏可以证明具有副作⽤的参数所引起的问题。  

#include<stdio.h>
#define MAX(a,b) ((a) > (b) ? (a) : (b))

int main() {
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n", x, y, z);
	return 0;
}

输出结果:x=6 y=10 z=9


这是为什么呢?主要就是参数带有副作用的结果

文本替换后:

z = ( (x++) > (y++) ? (x++) : (y++));

// x++ > y++ 吗?即 6 > 9 ?为假,x++不执行,执行y++
//y++为先使用后++,z = 9 之后y++ y = 10 

//即x = 6 z = 9 y = 10

5.宏替换的规则

  1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先
  2. 被替换。
  3. 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
  4. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#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));

参数列表写个类型?nono这是函数不敢的事情

宏和函数的对比

 7.奇怪的 # 和 ##

7.1#运算符

在c语言中,当你使用双引号( " " )来定义字符串常量的时,如果多个常量紧挨着写在一起,没有使用任何操作符和分隔符,编译器会自动的将它们连成一个单一的字符串常量。

例如:

#include<stdio.h>
int main() {

	char* str = "Hello """"World";

	printf("%s", str);

	return 0;
}

输出结果:Hello World

 #运算符的作用就是将带宏的某个参数转换为一个用双引号扩起来的字符串常量。它仅允许出现在带参数的宏的宏体中。

#运算符也被称为"字符串化"运算符。

它在你想要将一个宏参数作为字符串处理时非常有用

例子一:

#define EXAMPLE(x) #x
int main() {

	printf("%s", EXAMPLE(Hello world));

	return 0;
}

当我们把 Hello world,替换至宏体中,就变成了# Hello world,它转化成 "Hello world"

#define EXAMPLE(x) #x 

int main() {

	printf("%s", "Hello world");

	return 0;
}

例子二:

当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 
#define PRINT(n) printf("the value of "#n " is %d", n);
当我们把a替换到宏的体内时,就出现了#a,⽽#a转换为"a",字符串代码就会被预处理为:
printf("the value of ""a" " is %d", a);

输出:the value of a is 10

 7.2 ## 运算符

## 可以把位于它两边的符号合成⼀个符号 ,## 被称为记号粘合

这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。

例如:

#define EXAMPLE1(x) x_max //这样写,x_max属于一个整体,参数x并不可以替换宏体x_max中的x

#define EXAMPLE2(x) x##_max//这样写,相当于x 和_max两个标识符合为一体,而x可以被参数x所替换

利用 ## 我们就可以写出一些函数模板

写⼀个函数求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;
}

输出结果:

3
4.500000

8.宏和函数的命名约定

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

9.#undef

#undef 用于移除一个宏定义

#define NAME(x) x + x

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

10.条件编译

C语言的条件编译代码允许程序员根据特定的条件或者定义来决定是否编译某一部分代码。

 #ifdef 和 #endif

#ifdef 和 #endid : 检查是否定义了某个宏

#ifdef _DEBUG_      //也可以写成:#if defined _DEBUG_ 这两种写法没有区别

//_DEBUG_被定义的时候,编译这一段代码

#endif

#ifndef 和 # endif

#ifndef 和 # endif :检查是否没有定义某个宏

#ifndef _DEBUG_      //也可以写成:#if !defined _DEBUG_ 这两种写法没有区别

//_DEBUG_没有被定义的时候,编译这一段代码

#endif

#if、#elif、#else 和 #endif

#if、#elif、#else 和 #endif:基于表达式的条件编译。

#if defined(MACRO_NAME)
    // 当MACRO_NAME被定义时,编译这部分代码
#elif defined(ANOTHER_MACRO)
    // 当ANOTHER_MACRO被定义时,编译这部分代码
#else
    // 当以上条件都不满足时,编译这部分代码
#endif

11.头文件的包含

11.1头文件被包含的方式:

11.1.1 本地文件包含

#include "filename"

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

如果找不到就提示编译错误

11.1.2 库文件包含

#include <filename.h>
查找头⽂件直接去标准路径下去查找,如果找不到就提示编译错误。
注:
头文件的包含也可以用双引号 ( "" ) 的形式去包含, 但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件了。

11.2 防止头文件多次包含的方法

我们都知道#include可以使得另一个文件被编译

这种替换的⽅式很简单:预处理器先删除这条指令,并⽤包含文件的内容替换。
⼀个头文件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就⽐较⼤。

 

如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。
如果test.h ⽂件⽐较⼤,这样预处理后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被大家都能使用,⼜不做任何的处理,那么后果真的不堪设想。
如何解决头⽂件被重复引⼊的问题?答案:条件编译
每个头头件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__

或者这样写:

#pragma once
就可以避免头⽂件的重复引⼊。
  • 48
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值