预处理详解

一.预定义符号

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

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

ps:VS2019编译器不遵循ANSI C,故在此未定义会报错,需要屏蔽掉。 

二.#define

1.#define定义标识符

语法:#define name stuff

举一些例子:

#define MAX 100
#define red register //为register这个关键字,创建一个简短的名字
#define do_forever for(;;) //for的死循环
#define CASE break;case //在写case语句的时候自动把break加上
//如果定义的stuff太长,可以分几行写,除了最后一行外,每行的后面都加一个反斜杠(\)也就是续写符
#define PRINT printf("file:%s\tline:%d\tdate:%s\ttime:%s\n",__FILE__,__LINE__,__DATE__,__TIME__);
①for的死循环
int main()
{
	do_forever
	{
		printf("haha");
	}
	return 0;
}

 ②在case后自动加break
int main()
{
	int a = 0;
	switch (a)
	{
	case 1:
	CASE 2 :
	CASE 3 :
	CASE 4 ://如果CASE1 2 3都不执行,直接执行CASE 4不用跳出,因为它是末尾
			;
	}
	return 0;
}

这里有一个问题,在#define定义标识符时,后面要不要加上“ ;”呢?

例如:

#define MAX;
#define MAX

答案是否定的,因为如果加上“ ; ”可能出现意想不到的结果,例如:

#define MAX;
int main()
{
	int a = 1;
	int max = 0;
	if (a == 1)
		max = MAX;
	else
		max = 0;
	return 0;
}

这里会报错,因为if后默认只能跟一条语句,而MAX后本就有一个“ ;”,再手动输入一个“ ;”

会被编译器解读为两条语句。

2.#define定义宏 

①宏的定义与申明

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

宏的申明方式:

#define name( parament-list) stuff

ps:参数的左括号必须与name紧邻,否则会被定义为name的符号,但在调用宏时可以不紧邻。

②宏在定义时需要注意的问题 

由于宏在引用时会完全替换参数,不进行人和运算,所以存在以下问题: 

问题一:
#define SQUARE(x) x*x
int main()
{
	printf("%d\n", SQUARE(5+1));
	return 0;
}

#define定义了一个宏,用于求得一个整数的平方,再将结果打印出来,显然我们预计的是屏幕上会打印出36,但是否真的会是36呢?

显然,结果并不是25而是11,这是为什么呢?

这里就不得不提到宏的替换规则(尤为需要引起重视的是第二步):

在程序中扩展#define定义符号和宏时,需要以下步骤

1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,则它们首先被替换。(并不是说宏可以递归,而是只能替换符号参数

2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替换(不像函数一样,先对实际参数运算再传到形参里,宏的实参会原封不动地传给形参)

3.最后,再次对文件进行扫描,看看是否包含任何由#define定义的符号,如果有重复以上步骤。

那该如何解决这个问题呢?很简单,导致实际结果与预期不同的原因是富豪的优先级问题,我们只需要在宏定义时加上小括号来改变优先级即可。 

这样问题就得到了很好的解决,但只要给各个参数加上小括号就能一劳永逸了吗?

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#define SQUARE(x) (x)+(x)
int main()
{
	printf("%d\n", 10*SQUARE(5));
	return 0;
}

我们想得到100,但却得到了55

因此,这个新的问题警示我们,仅仅给宏的参数加上小括号并不能一劳永逸,我们还需要给整个宏加上小括号才能确保万无一失。

问题二:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#define SQUARE(x) ((x)+(x))
int main()
{
	printf("%d\n", 10*SQUARE(5));
	return 0;
}

结论 :

在定义宏的主体时,不要吝啬小括号,给每个参数和整体加上小括号,改变运算的优先级才能确保结果的正确。 

3.#和##

首先我们观察一段代码:

int main()
{
	char* p = "hello ""world\n";
	printf("hello ""world\n");
	printf("%s\n", p);
	return 0;
}

由此我们得出一个结论:字符串具有自动连接的特点。

所以我们可以这么写代码:

#define PRINT(FORMAT,VALUE) printf("the value is "FORMAT"\n",VALUE);
int main()
{
	PRINT("%d",10);
	return 0;
}

 

当然我们也有另外一种方法就是使用#,#可以把一个宏参数变成对应的字符串 。

①#
#define PRINT(FORMAT,VALUE) printf("the value of "#VALUE " is "FORMAT"\n",VALUE);
int main()
{
	int i = 4;
	PRINT("%d",i+7);
	return 0;
}

②## 

##可以把位于它两边的符号合成一个符号,允许宏定义从分离的文本片段创建标识符

#define CAT(x,y) x##y
int main()
{
	int Class110 = 2023;
	printf("%d\n", CAT(Class, 110));
	printf("%d\n", Class110);
	return 0;
}

4.带副作用的宏参数

当宏参数在宏的定义中出现超过一次时,如果是带有副作用的参数,那么会产生永久性的不可预测的后果。

x++ //带有副作用的参数
x+1 //不带有副作用的参数

MAX宏可以证明具有副作用参数带来的问题:

#define MAX(a,b) (a)>(b)?(a):(b)
int main()
{
	int a = 1;
	int b = 2;
	int c = MAX(a++, b++);
	printf("a=%d\n", a);
	printf("b=%d\n", b);
	printf("c=%d\n", c);

	return 0;
}

 这里宏替换下来c = (a++)>(b++)?(a++):(b++);

a(1)b(2)先进行比较(由于是后置++,所以先使用后++),因为a<b所以三目操作符执行第二个(b),此时a(2) b(3),此时c=b=3,然后由于b又被使用一次,再++一次变为b(4),所以打印结果为a=2 b=4 c=3。

 5.宏和函数的对比

 ①速度

宏比函数在程序的规模和速度方面要强,这里通过实现同一功能,例如两数相加,查看反汇编来验证。

宏的反汇编代码 
#define SQUARE(x,y) ((x)+(y))
int main()
{
	int c = SQUARE(5, 5);
	printf("%d\n", c);
	/*int d = Add(5, 5);
	printf("%d\n", d);*/
	return 0;
}

函数的反汇编代码
#define SQUARE(x,y) ((x)+(y))
int main()
{
	int c = SQUARE(5, 5);
	//printf("%d\n", c);
	int d = Add(5, 5);
	printf("%d\n", d);
	return 0;
}
 

以上的宏和函数实现了相同的功能,但在反汇编代码里,参数的多余宏的,因此宏的运行速度高于函数。 

6.命名约定

一般来讲,函数和宏的使用方法相似。所以语言本身无法区分二者。

因此我们平时就有一个习惯是:

把宏名全部大写

函数名不要全部大写

三.#undef

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

 

 四.条件编译

某些调试性的代码,保留碍事,删除又可惜,因此我们可以选择性地进行条件编译,其主要应用于跨平台的程序。

1.#if和#endif

#if和#endif可以与if else语句做类比,两者皆符合多分支的语法规则,但#if是选择性编译,在预处理阶段不符合条件的代码会被移除,而if语句是选择性执行,其余不符合条件的代码不会被删除。 

 基本用法:

#if 常量表达式

#elif 常量表达式

 .......

#else

#endif

#define NUM 1
int main()
{
#ifdef NUM == 1 //常量表达式在预处理阶段求值
	printf("hehe\n");
#elif NUM == 2
	printf("haha\n");
#else NUM != 0
	printf("hoho\n");
#endif

	return 0;
}

 

 2.判断是否被定义(#ifdef和#ifndef)

#if defined(symbol)

#ifdef symbol

#if !definrd(symbol)

#ifndef symbol

ps:以上符号需与#endif成对使用才符合语法规则。 

①定义NUM
#define NUM 1
int main()
{
#ifdef NUM
	printf("hehe\n");
#else
	printf("haha\n");
#endif // NUM

	return 0;
}

②未定义NUM 
//#define NUM 1
int main()
{
#ifdef NUM
	printf("hehe\n");
#else
	printf("haha\n");
#endif // NUM

	return 0;
}

五.文件包含

 1.头文件被包含的方式

  • 本地文件包含

#include"stdio"

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

找不到就提示编译错误。

  • 库文件包含

#include<stdio.h> 

查找策略:直接在标准路径下查找,找不到会提示编译错误。 

ps:事实上,头文件的包含都可以使用本地文件包含的方式,但那样效率不高,所以不建议那么使用。

2.头文件的反复包含问题 

每个头文件的开头写:

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif

或者

#pragma once

 以上两种方法可以避免头文件的重复引入。

 

  • 8
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值