一节课教你学会【预处理详解】

谢谢观看!希望以下内容帮助到了你,对你起到作用的话,可以一键三连加关注!你们的支持是我更新地动力。
因作者水平有限,有错误还请指出,多多包涵,谢谢!


一、预定义符号

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

__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	printf("%s\n", __TIME__);
	printf("%s\n", __DATE__);
	printf("%d\n", __LINE__);
	return 0;
}

在这里插入图片描述


二、#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定义标识符的时候,要不要在最后加上 ; ?

#define MAX 1000;
#define MAX 1000

建议不要加上 ; ,这样容易导致问题。

比如下面的场景:

if(condition)
 max = MAX;
else
 max = 0;

如果是加了分号的情况,等替换后,ifelse之间就是2条语句,
⽽没有⼤括号的时候,if后边只能有⼀条语句。这⾥会出现语法错误。

三、#define定义宏

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

下面是宏的申明方式:

#define name( parament-list ) stuff
parament-list表示参数列表
stuff表示内容
name表示宏的名字 
其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。

注意:

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

举例:

#define SQUARE(n) n*n
int main()
{
int x = 0;
scanf("%d", &x);
int ret = SQUARE(x);  //实际替换为 int ret = x*x;
printf("%d", ret);
return 0;
}

警告:
上面这个宏存在一个问题:

#define SQUARE(n) n*n
int main()
{
int ret = SQUARE(5+1);  
printf("%d", ret);//结果是多少?咋一看是36,其实结果是11
return 0;
}

那为什么是11而不是36呢?

int ret = SQUARE(5+1);//其中SQUARE(5+1)实际替换为 5+1*5+1=5+5+1=11
想解决这个问题,只需要将#define SQUARE(n) n*n 改为 #define SQUARE(n) ((n)*(n))

总结:宏的列表是整个替换文本,所以有时需要加括号来保证代码的健壮性。

 #define DOUBLE( x) ( ( x ) + ( x ) )
所以⽤于对数值表达式进⾏求值的宏定义都应该⽤这种⽅式加上括号,避免在使⽤宏时由于参数中的
操作符或邻近操作符之间不可预料的相互作⽤。

四、带有副作用的宏参数

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

x+1;//不带副作⽤
x++;//带有副作⽤
//为了让b得到11
int a = 10;
int b = a+1;//b=11,a=10   无副作用
int b = ++a;//b=11,a=11   有副作用,a的值永久的改变了

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

#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
	int a = 10, b = 20;
	int m = MAX(a++, b++);
	//实际替换为int m = ((a++)>(b++)?(a++):(b++));
	//a=11 , b=22 , m=21
	printf("%d", m);//结果为21
	return 0;
}

五、宏替换的规则

在这里插入图片描述


六、宏和函数的对比

宏通常被应用于执行简单的运算。
比如在两个数中找出较大的一个时,写成下面的宏,更有优势一些。

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

那为什么不用函数来完成这个任务?
因为执行函数需要多步骤

  1. 调用函数的准备工作
  2. 执行函数的核心运算
  3. 从函数调用中返回值
原因有二:
1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。
所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。
反之,这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏的参数是类型⽆关的。

和函数相比宏的劣势:

1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序
的⻓度。
2. 宏是没法调试的。因为调试时代码已经运行起来了,而宏在编译阶段中的预处理(预编译)段已经进行了文本替换,宏就没有了,所以根本无法调试
3. 宏由于参数类型⽆关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

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

#define Malloc(n,type) (type*)malloc(n*sizeof(type))
int main()
{
	int* p1 = (int*)malloc(10 * sizeof(int));
	int* p2 = Malloc(10, int);//简化代码量,宏可以传类型,但函数不可以传类型
	free(p1, p2);
	return 0;
}

宏和函数的对比:如图所示点击这里


七、###

7.1#运算符

#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。
#运算符所执⾏的操作可以理解为”字符串化“。
当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 .

在这里插入图片描述
补充知识:

printf("I am lisi\n");
printf("I " "am " "lisi" "\n");
//打印出来的结果一样,""可以连接起来
上面用定义宏来简化操作,确实达到了想要的效果,但是执行代码出的结果中,发现变量n无法与传入的参数变量一一对应起来
可以通过操作符#来达到想要的目的

只需要将
#define PRINT(format,n)  printf("The value of     n is "format"\n",n)
改为
#define PRINT(format,n)  printf("The value of " #n "is "format"\n",n)
就行

在这里插入图片描述

总结:当将a、b、f变量代入替换时,#n会变为"a""b""f".

7.2 ##运算符

## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称
为记号粘合
这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。
规则:标识符由字母、数字、下划线组成,并且首字母不能是数字
//宏定义,生成函数的模版
#define GENERIC_MAX(type) \
type type##_max(type x, type y) \
{ \
 return (x>y?x:y); \
}

//使用上面的模版来定义函数

GENERIC_MAX(int)  //实际替换为:int int_max(int x, int y) { return (x > y ? x : y); }
//替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名

GENERIC_MAX(float) //实际替换为:flaot float_max(float x, float y) { return (x > y ? x : y); }
//替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名

int main()
{
	//调⽤函数
	printf("%d\n", int_max(3, 5));
	
	printf("%f\n", float_max(3.0, 5.0));
	return 0;
}

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


八、命名约定

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

九、#undef

#undef NAME
这条指令⽤于移除⼀个宏定义。
#define M 100
int main()
{
	printf("%d\n", M);
#undef M
	printf("%d\n", M);//会报错
	return 0;
}

在这里插入图片描述


十、命令行定义

许多C 的编译器提供了⼀种能⼒,允许在命令⾏中定义符号。⽤于启动编译过程。
例如:当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处。(假定某
个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个
机器内存⼤些,我们需要⼀个数组能够⼤些。)

在这里插入图片描述

编译指令:
//linux 环境演⽰
gcc -D ARRAY_SIZE=10 programe.c

十一、条件编译

1.
#if 常量表达式
 //...
#endif
--------------------------------------
int main()
{
#if 1   //1为真,进行编译
	printf("hehe\n");
#endif
	return 0;
}


int main()
{
#if 0   //0为假,不进行编译
	printf("hehe\n");//在预处理阶段就被删除了,也就是不进行编译
#endif
	return 0;
}

int main()
{
int a = 2;
#if a==2    //不进行编译,需要为常量表达式,而且局部变量是在执行环境的才创建的,而预处理阶段早在编译阶段就进行了,那个时候还没有a
	printf("hehe\n");
#endif
	return 0;
}
2.多个分⽀的条件编译//与第一个类似
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif
3.判断是否被定义
#if defined(symbol)//写法1
        //...      //只要symbol被定义了,就会执行
#endif

#ifdef symbol//写法2
     //...       //只要symbol被定义了,就会执行
#endif


#if !defined(symbol)//写法1
    //...       //symbol没有被定义,就会执行
#endif

#ifndef symbol//写法2
    //...       //symbol没有被定义,就会执行
#endif
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.1 头文件被包含的方式:

  头文件的包含有2种形式:

第一种:
#include<stdio.h> //库文件包含,一般指标准库中头文件的包含
第二种:
#include"xxx.h"   //本地文件包含,一般指自己创建的头文件的包含

12.1.1本地文件包含

#include"xxx.h"   //本地文件包含,一般指自己创建的头文件的包含
查找策略:先在源⽂件所在⽬录下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在
标准位置查找头⽂件。
如果找不到就提⽰编译错误。

然而查找标准位置在不同环境下是不一样的
在这里插入图片描述

12.1.2库文件包含

#include<stdio.h> //库文件包含,一般指标准库中头文件的包含
// 尖括号符号<filename>表示去标准库中查找头文件 
查找策略:
查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误。
这样是不是可以说,对于库⽂件也可以使⽤ “” 的形式包含?
答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件
了。

12.2 嵌套文件包含

我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的
地⽅⼀样。
这种替换的⽅式很简单:预处理器先删除这条指令,并⽤包含⽂件的内容替换。
⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤。
//test.c
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
 
 return 0;
}
//test.h
void test();
struct Stu
{
 int id;
 char name[20];
};

通过上面test.ctest.h二个文件,在编译阶段中预处理阶段会对#include进行处理,处理很简单:预处理器先删除这条指令,并用包含文件的内容替换。变换后如下代码:

void test();
struct Stu
{
 int id;
 char name[20];
};
void test();
struct Stu
{
 int id;
 char name[20];
};
void test();
struct Stu
{
 int id;
 char name[20];
};
void test();
struct Stu
{
 int id;
 char name[20];
};
void test();
struct Stu
{
 int id;
 char name[20];
};

int main()
{
 
 return 0;
}
如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。
如果test.h ⽂件⽐较⼤,这样预处理后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家
都能使⽤,⼜不做任何的处理,那么后果真的不堪设想。
如何解决头⽂件被重复引⼊的问题?答案:条件编译。

解决办法如下:

每个头⽂件的开头写:
#ifndef __TEST_H__   //__TEST_H__是根据头文件文件名来取的
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__
或者
#pragma once

十三、其他预处理指令

#error
#pragma
#line
#define
#include
#ifdef
#elif
#undef
...
不做介绍,⾃⼰去了解。
#pragma pack()在结构体部分介绍
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值