预处理详解

43 阅读8分钟

3.预处理详解

3.1预定义符号

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

举个栗子:

#include<stdio.h>
int main()
{
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		printf("file:%s line=%d date:%s time:%s i=%d\n",__FILE__,__LINE__,__DATE__,__TIME__,i);
	}
	return 0;
}

预定义符号的结果.png

3.2 #define

3.2.1 #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 STR "hello bit"
int main()
{
	int m = MAX;
	printf("%d\n", MAX);
	printf("%s\n", STR);
	return 0;
}

在define定义标识符的时候,不要在最后加上;

3.2.2 #define定义宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏

宏的申明方式:

#define name(parament-list) stuff

其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中

注意:

参数列表的左括号必须与name紧邻。

如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

如:

#include<stdio.h>
#define SQUARE(X) X*X
//注意区分#define SQUARE (X) X*X
int main()
{
    int r = SQUARE(5);
	int r = SQUARE(5+1);
	//替换后:
	// int r = 5 + 1 * 5 + 1;
	printf("%d\n", r);
	return 0;
}

#define的结果.png

#define(5+1).png

修改后:

#include<stdio.h>
#define SQUARE(X) ((X)*(X))
int main()
{
	int r = SQUARE(5+1);
	//替换后:
	// int r =( (5 + 1) * (5 + 1));
	printf("%d\n", r);
	return 0;
}

#define(5+1)正确版本.png

所有写宏的时候不要吝啬括号,确保括号够够滴,因为存在优先级的问题

3.2.3 #define替换规则

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

1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换

2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。

3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程

注:1.宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。

2.当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

#define M 100
#define DOUBLE(X) ((X)+(X))
int main()
{
	DOUBLE(M+2);
    //替换:
    ((100+2)+(100+2));
	return 0;
}
3.2.4 #和##

如何把参数插入到字符串中?

首先我们可以看到打印hello world的两种方式:

int main()
{
	printf("hello world\n");
	printf("hello ""world\n");
	return 0;
}

hello world.png

第二种方式仍然可以打印出hello world,是因为在c语言中,自动将两个字符串中的内容合为一句:

hello wd的联机搜索.png

我们发现字符串具有自动连接的特点

1.#

#define PRINTF(N) printf("the value of"#N"is %d\n",N)

int main()
{/*
	printf("hello world\n");
	printf("hello ""world\n");*/

	int a = 10;
	PRINTF(a);
	//替换后:
	//printf("the value of""a""is %d\n", a);

	int b = 20;
	PRINTF(b);
	//替换后:
	//printf("the value of""b""is %d\n", b);


	return 0;
}

#替换的结果.png

使用#,可以将参数转换成字符串(把一个宏参数变成对应的字符串

2.##

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

它允许宏定义从分离的文本片段创建标识符。

注:这样的链接必须产生一个合法的标识符,否则其结果就是未定义的

#define CAT(Class,Num) Class##Num

int main()
{
	int Class106 = 100;
	printf("%d\n",CAT(Class,106));
	//printf("%d\n",Class106);
	return 0;
}

##的结果.png

3.2.5 带副作用的宏参数

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

例如:

X+1;//不带副作用
X++;//带有副作用

#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
	//int m = MAX(2, 3);
	int a = 5;//6
	int b = 4;//5
	int m = MAX(a++, b++);
	int m = ((a++) > (b++) ? (a++) : (b++));
	//  6	 //5(用完后变成6) > 4(用完后变成5)	?	6
	printf("m=%d\n", m);//6
	printf("a=%d b=%d\n", a, b);//7 5

	return 0;
}
3.2.6 宏和函数对比
//宏
#define MAX(x,y) ((x)>(y)?(x):(y))

//函数
int Max(int x, int y)
{
	return (x > y ? x : y);
}

一般选用宏,那为什么不用函数来完成任务?

1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。

函数:(1).函数调用(参数传参,栈帧创建)

(2).计算

(3).函数返回

而宏在预处理阶段就已经替换并计算好了

2.更为重要的是函数的参数必须声明为特定的类型。

所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整型,长整型,浮点型1等可以用于》来比较的类型。

宏的缺点

1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

2.宏是没法调试的。

3.宏由于类型无关,也就不够严谨。

4.宏可能会带来运算符优先级问题,导致程序容易出错。

宏和函数的对比.png

3.2.7 命令约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的一个1习惯是:

把宏名全部大写

函数名不要全部大写

3.3 undef

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

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

3.4 命令行定义

许多c的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些.)

3.5 条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译命令。

比如说:

调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

#define __DEBUG__

int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0;i < 10;i++)
	{
		arr[i] = i;
#ifdef __DEBUG__
		printf("%d\n",arr[i]);
#endif __DEBUG__
	}
	return 0;
}

如果不想执行这条调试性语句,那么就就将#define____DEBUG____注释掉

常见的条件编译指令:

1.
    #if 常量表达式  1为真,0为假...或者其他的常量表达式(只要判断是真是假)
    //。。。
    #endif
    //常量表达式由预处理器求值
    如:
    #define __DEBUG__  1
    #if __DEBUG__
    //..
    #endif
    
 2.多个分支的条件编译
    #if 常量表达式
    //。。。
    #elif
    //....
    #else
    //...
    #endif
    
    
如:    
#define M 3

int main()
{
#if M<5
	printf("hehe\n");
#elif M==5
	printf("haha\n");
#else
	printf("heihei\n");
#endif
	return 0;
}
  


 3.判断是否被定义
    #if defined(symbol)
    #ifdef symbol
    
    #if !defined(symbol)
    #ifndef symbol

如:     
#define MAX 100
int main()
{
#if defined(MAX)
	printf("max\n");
#endif
	return 0;
}
//#if defined(MAX)和#if !defined(MAX)是反面


或者:   
#define MAX 100
int main()
{
#ifdef MAX
	printf("max\n");
#endif
	return 0;
}
//#ifdef MAX和#ifndef MAX是反面


 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   

3.6文件包含

我们已经知道,#include指令可以使另外一个文件被编译。就像它实际出现于#include指令的地方一样。

这种替换的方式很简单:

预处理器先删除这条指令,并用包含文件的内容替换。

这样一个源文件被包含10次,那就实际被编译10次。

//方案1
#ifndef __TEST_H__
#define __TEST_H__

int Add(int x,int y);
#endif

//方案2
#pragma once
int Add(int x,int y);


//作用都是:防止头文件被多次重复的包含
    

<>和""的区别是查找的策略不同

<>直接去库目录下查找

"" 1.先去代码所在的路径下查找

2.如果上面找不到,再去库目录下查找

4.其他预处理指令

#error
#pragma
#line
...