预处理详解

预定义符号

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

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

我们可以在VS上进行打印

#include<stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	//printf("%d\n", __STDC__);//VS不完全遵循ANSI C,这里未定义报错
		
	return 0;
}

运行结果如图:
在这里插入图片描述

也可在VScode上打印:(这里是以图片演示的过程,我还没有用过vscode,我们先暂时以了解为主)
在这里插入图片描述

#define定义常量

基本语法:

#define  常量名 常量值

那它具体是怎么用的呢?请看下图:
在这里插入图片描述
#define 将 M 所定义的值在预处理阶段,会将M所在的位置替换成值
比如上面在打印时,将M替换成了100,常量值可以是任意的值,不局限于整型
再举一个经典例子:
程序员张三之前使用的是 xxx语言,该语言在switch语言中的case中后面没有break,da但是现在翠花说用C语言实现switch语言,但张三很不习惯,他在VS上做出了以下改进:

//这是正常的写法
#include<stdio.h>
int main()
{
	int a = 0;
	scanf("%d", &a);
	switch (a)
	{
	case 0:
		break;
	case 1:
		
	case 2:
		break;
	case 3 :
		break;
	}
	return 0;
}

这是张三的做法:

#define CASE break;case


//通过使用#define 来解决忘记写break的问题,但是不推荐写这种,因为代码的可读性不好
#include<stdio.h>
int main()
{
	int a = 0;
	scanf("%d", &a);
	switch (a)
	{
	case 0:
	
	CASE 1:

	CASE 2:
		
	CASE 3:
	//......

	}
	return 0;
}

再举个例子:

//在#define定义的值中,如果值太长了,想要换行,可以换,但前提是要在下一行加上续行符 "\","\" 的后面啥不能加,空格也不行;
//其实也可将它认为成转义字符,\ enter(回车键),将回车进行转义了
#define DEBUG_PRINT printf("file:%s\tline:%d\t date:%s\ttime:%s\n",  __FILE__, __LINE__,  __DATE__, __TIME__)
#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

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

#define M 1500;
#include<stdio.h>
int main()
{
	int a = 0;
	if (a < 520)
		//M;//error
		// //实际上这里是 1500; ; --- 在1500; 加了个空语句,
	//如果在if语句后面想要加多个语句,请带上{}
	{ M; 
		}
	else
		1314;
	return 0;
}

#define定义宏

#define允许把参数替换到文本中,这个就叫宏或者定义宏
以下是宏的声明方式:

#define name(parament-list) stuff

parament-list :参数列表,它是由有一个逗号隔开的符号表,参数可能会出现在 文本中,再通过预处理将宏展开:删除#define的内容,将stuff替换到 有name的地方

注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
举个例子:通过#define定义宏,实现任意数的平方

#define Square(n) n * n

#include<stdio.h>
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Square(n);
	printf("%d\n",ret);
	return 0;
}

宏和函数非常相似,宏更适合用于简单的工作,

但是这个代码有bug,如果我将代码进行调整:

#define Square(n) (n) * (n)

#include<stdio.h>
int main()
{
	/*int n = 0;
	scanf("%d", &n);*/
	int ret = Square(6 + 1);//宏是不加计算直接完全替换的,这里的计算的内容是:6 + 1*6 + 1 = 13 
	//如果我就是想要计算7的平方呢,那就在文本的n处加上(),(n)
	printf("%d\n",ret);
	return 0;
}

因此在使用写宏时不要吝啬你的()
再举个例子:

//写一个代码,求一个数的2倍
//#define Double(n) n + n 10 * 6 + 1 + 6 + 1--需要加上括号
#define Double(n) ((n) + (n))
#include<stdio.h>
int main()
{
	int ret = 0;
	ret = 10 * Double(6+1);
	printf("%d\n", ret);
	return 0;
}

带有副作用的宏参数

什么是副作用呢?

通过表达式想要求一个值时,将将表达式中另外一个值的大小进行永久性的改变,这就是副作用
举个简单的案例:

#include<stdio.h>
int main()
{
	int n = 10;
	int m = 0;
	 m = n + 1;
	 //这里 m = 11, n = 10,m 的改变没有改变n的值,无副作用
	 m = n++;//或者++n都可以
	// m = 11 n = 11 --- 改变m的同时还影响n的改变,这里对n是有副作用的

	return 0;
}

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。
请看下面的代码加以理解:

//宏的副作用
//使用宏实现两个数比较大小,并且 打印最大值
#include<stdio.h>
//这里不不要吝啬你的括号  给整体的结果也带上括号
#define MAX(x, y) ((x)>(y)?(x):(y))//((x)>(y)?(x):(y)) --- 宏体,和函数体类似
//这里解释下宏的执行过程:先将宏的调用中的参数a,b替换到宏定义的参数列表x和y中,
// 参数列表中的x和y再将文本中的x和y替换成a和b,替换的同时将#define定义的内容删除
// 并将已替换好的文本放到有宏调用的地方
int main()
{
	int a = 10;
	int b = 20;
	int ret = MAX(a, b);
	//int ret = ((a)>(b)?(a):(b))
	printf("%d\n", ret);//20
	return 0;
}//这里的宏参数a, b在宏的定义中出现了两次,但是宏的参数无副作用

带有副作用的:

//看这段代码:
#define MAX(x, y) ((x)>(y)?(x):(y))

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int ret = MAX(a++, b++);
	//int ret = ((a)>(b)?(a):(b))
	printf("%d\n", ret);//20
	printf("a = %d b = %d\n", a, b);
	return 0;
}////这里的宏参数a, b在宏的定义中出现了两次,但是宏的参数有副作用

宏的替换规则:

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

1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
2. 替换⽂本随后被插⼊到程序中原来⽂本的位置(也就是上面宏的副作用案例中的解释)。对于宏(指的是程序中的宏),参数名被他们的值所替换。
3. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
4. `宏参数和`#define 定义中可以出现其他#define。但是对于宏,不能出现递归。(重点)
5. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

案例如下:

#define M 100		
#define MAX(x, y) ((x)>(y)?(x):(y))//---》 参数值
#include<stdio.h>
int main()
{
	int ret = MAX(M, 20);
	//int ret = MAX(100, 20)//验证步骤1. 
	//int ret = ((100)>(20)?(100):(20))//验证步骤2.
	printf("MAX(M, 20)");//验证注意点2
	printf("%d\n", ret);
	return 0;
}

宏函数的对比

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

//使用宏和函数对比:
//可以使用F10 + 反汇编进行观察汇编代码指令的条数
#define MAX(x, y) ((x)>(y)?(x):(y))
Max(int x, int y)
{
	return  ((x) > (y) ? (x) : (y));
}
#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int ret = MAX(a, b);//宏实现
	int	rt = Max(a, b);//函数实现
	//效果是一样的
	printf("%d\n", ret);
	printf("%d\n", rt);
	return 0;
}

上面的代码使用宏更有优势
原因:
1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
如图:
在这里插入图片描述

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

和函数相比宏的劣势:

  1. 每次使用宏的时候,⼀份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
//假设我要使用10次比较大小,看下面的操作:
//宏实现:
#define MAX(x, y) ((x)>(y)?(x):(y))
#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int ret = MAX(a, b);
	printf("%d\n", ret);
	int ret = MAX(a, b);
	printf("%d\n", ret);
	//.....
	//使用宏时我得比较一次就需要将宏定义中的值插入程序中,这样会增加程序的长度
	return 0;
}

//函数实现:
Max(int x, int y)
{
	return  ((x) > (y) ? (x) : (y));
}
#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int	rt = Max(a, b);
	printf("%d\n", rt);
	//函数实现时只需要调用即可,不需要将定义的那部分放入到程序中
	return 0;
}

  1. 宏是没法调试的。因为宏在调用之前的预编译等操作已经将宏替换了

//#define MAX(x, y) ((x)>(y)?(x):(y))
//#include<stdio.h>
//int main()
//{
//	int a = 10;
//	int b = 20;
//	int ret = MAX(a, b);
// //实际上在运行的是这段代码:int ret = ((100)>(20)?(100):(20));
//	printf("%d\n", ret);
//	return 0;
//}

  1. 宏由于类型无关,也就不够严谨。
  2. 宏可能会带来运算符优先级的问题,导致程容易出现错,所以在使用时不要吝啬你的括号

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

练习如下:

//使用malloc申请10 int型的空间

#define MALLOC(num, type) (type*)malloc(10 * sizeof(type))
#include<stdio.h>
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));//使用函数

	int* p = MALLOC(10, int);//宏实现
	//int* p = (int*)malloc(10 * sizeof(int));//实际得到的效果
	//注意这里的参数是你想要输入那种具体的类型,而不是 type,也可以使用float等等

	return 0;
}

宏和函数的对比:

在这里插入图片描述

#和##

#运算符

#运算符将宏的⼀个参数转换为字符串*。它仅允许出现在带参数的宏的替换列表中。
#运算符所执行的操作可以理解为字符串化。(看下面的练习,以及解释2)
一个练习:
使用变量 int a = 10; 的时候,我们想打印出: the value of a is 10 .
使用变量 int b = 100; 的时候,我们想打印出: the value of b is 100 .
使用变量 float c = 5.125f; 的时候,我们想打印出: the value of c is 5.125f.
就可以写:

#include<stdio.h>
int main()
{
	int a = 10;
	printf("the value of  a is %d\n", a);
	int b = 100;
	printf("the value of  b is %d\n", b);
	float c = 5.125f;
	printf("the value of  c is %f\n", c);

	return 0;
}

为了达到上面的效果可以使用宏来实现:

//为了达到上面的效果可以使用宏来实现:
#define PRINT(format, m) printf("the value of " #m " is "format "\n", m)//这里使用了解释1和解释2

#include<stdio.h>
int main()
{
	printf("hello world\n");
	printf("hello" " world\n");
	//解释1:因为在C语言中两个字符串可以天然的合成一个字符串
	//解释2:#m的作用就是将 m的内容转换为 "m" --- 这是通俗的解释 
	int a = 10;
	PRINT("%d", a);
	int b = 100;
	PRINT("%d", b);
	float c = 5.125f;
	PRINT("%f", c);

	return 0;
}

## 运算符

可以把位于它两边的符号合成一个符号

被称为记号粘合

假设我们要写⼀个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数。
我们可以使用宏写一个函数模板,不同的类型直接使用
代码如下:

//##操作符 ---记号粘合,使用它产生的标识符必须是合法的
//这是生成函数的模板,可以根据函数的类型不同进行不用的计算
//这里的宏参数别忘了		//根据函数的定义进行模板的编写,比如函数的返回值类型我前面就忘写了
#define GENERIC_MAX(type) type type##_max(type x, type y)\
{\
return ((x) > (y) ? (x) : (y));\
}
//上面函数具体计算那部分不用加上宏参数
//这里是函数的定义
GENERIC_MAX(int)//注意这里不加 ;

GENERIC_MAX(float)
#include<stdio.h>
int main()
{
	//我们现在就可以进行一系列函数的使用了
	printf("%d\n", int_max(6, 10));//函数的调用
	printf("%f\n", float_max(100.0, 2.0));
	
	return 0;
}

命名约定

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

#undef

这条指令用于移除⼀个宏定义。
例子如下:

#define MAX(x, y) ((x) > (y) ? (x) : (y))
#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int ret = MAX(a, b);
	printf("%d\n", ret);
#undef MAX//这里使用#undef是需要加上它定义时的参数名
	ret = MAX(a, b);
	return 0;
}

命令行定义

学习他之前我们要了解以下内容:
我们之前学的关机程序有以下操作

shutdown -s -t 60
//这里的-s -t 被称为命令行参数,在程序运行时给他个参数让他产生不同的效果

在编译时给命令行参数后面定义一个值(另外一个定义:在编译时我们根据需要对数据进行可大可小的调整),就叫做命令行定义,它在VS上不适用,在linux下的gcc编译器适用
案例如下:

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

在这里插入图片描述

条件编译

在编译程序时,条件成立,才会编译;不成立则不会编译
以下是一些常见的条件编译指令

1.普通条件编译

#if 常量表达式 
//...
#endif
//常量表达式由预处理器求值。

练习如下:

#include<stdio.h>
int main()
{
//反例	
	int a = 3;
#if a == 3//注意这里一定是常量表达式,不能有变量,因为变量是在运行时才会产生的,而#if...#endif 是在编译时就有了
	printf("hehe\n");
#endif
#if 1
	printf("haha\n");
#endif

	return 0;
}

//如果上述代码不想要它执行,想让它执行改变表达式的值即可,在最前面和最后面可以使用#if...#endif
#if 0
#include<stdio.h>
int main()
{
	//反例	
	int a = 3;
#if a == 3//注意这里一定是常量表达式,不能有变量,因为变量是在运行时才会产生的,而#if...#endif 是在编译时就有了
	printf("hehe\n");
#endif
#if 1
	printf("haha\n");
#endif

	return 0;
}
#endif

2.多个分支的条件编译


#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

实例如下:

#define M 3
#include<stdio.h>
int main()
{
#if M == 1
	printf("hehe\n");
#elif M ==2
	printf("haha\n");
#else M == 3
	printf("wuwu\n");
#endif
	return 0;
}

3.判断是否被定义

#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol

案例如下:

//这里是关于被定义的
//#if defined(symbol)
//#ifdef symbol

#define  cuihua
#include<stdio.h>
int main()
{
//#ifdef cuihua//效果是一样的
#if defined cuihua
	printf("beautiful\n");
#endif
	return 0;
}
//这里是关于未被定义的
//#if !defined(symbol)
//#ifndef symbol

#define  wangwu

#include<stdio.h>
int main()
{
//#ifndef cuihua
#if !defined (cuihua)
	printf("more beautiful\n");
#endif
	return 0;
}

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

案例如下:

#define wangqiang
#define wangqiang22
//#define cuihua
#define cuihua19
#include<stdio.h>
int main()
{
#if defined(cuihua)
	#ifdef cuihua18
		printf("hehe\n");
	#endif
	#ifdef cuihua20
		printf("haha\n");
	#endif
#elif defined(wangqiang)
	#ifdef wangqiang22
		printf("luoluoluo\n");
	#endif
#endif
	return 0;
}

上面的都是适用于跨平台性代码的编译

头文件的包含

头文件被包含的方式:

库文件包含

  1. #include <stdio.h> 库文件包含,一般指标准库中头文件的包含

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

本地文件包含

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

每台机器的安装路径不同,我们可以使用everthing搜索相关的头文件进行查找

库⽂件可以使用 “” 的形式包含,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

嵌套文件包含

我们有时候被迫会将头文件多次包含,对编译的压力就比较大。为了解决这个问题我们可以使用条件编译进行解决

以下是两个多次被包含的例子:

//存放在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];
};

下面这个例子比较贴近实际:
在这里插入图片描述
解决办法:
在每个头文件的开头写:

#ifndef __TEST_H__//--->这个__TEST_H__ 左右两边的"__"写一个两个都可以,
//一般都写两个,
//中间的大写英文字母随你自己定义的头文件变化而变化的,
//中间的"_",代表test.h中的'.',不可变 
#define __TEST_H__

//头⽂件的内容

#endif
//__TEST_H__

上述代码逻辑解析:

因为没有看到头文件,所以第一次没有定义会被执行,初次定义它,执行下面的代码;到了我第二次调用它时,发现他已经被定义过了,不会执行下面的代
码,从而防止被重复包含

另外一种写法:

#pragma once
//头文件内容

《高质量C/C++编程指南》中附录的考试试卷笔试题:(每次复习时将这两道题过一遍)

  1. 头⽂件中的 ifndef/define/endif是⼲什么⽤的?
  2. #include <filename.h> 和 #include “filename.h” 有什么区别?

预处理指令还有很多,参考《C语言深度解析》多多了解下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值