当前位置:   article > 正文

C语言预处理详解

C语言预处理详解

在这里插入图片描述

在这里插入图片描述

前言 \color{maroon}{前言} 前言

上一篇博客中我们讲了C语言的编译与链接,在编译过程中有三个小阶段:预处理、编译、汇编。本篇博客将详细讲述预处理部分的有关知识点

1.预定义符号

在C语言中,C语言本身设置了⼀些预定义符号,可以直接使⽤,预定义符号的处理也是在预处理期间进行的。

在这里介绍几个常用的预定义符号:

__FILE__:当前编译的源文件名。

__DATE__:源文件被编译的日期。

__TIME__:源文件被编译的时间。

__LINE__:当前源代码的行号。

__STDC__:如果编译器遵循ANSI C标准,其值为1,否则为未定义标识符。

__cplusplus:如果编译C程序并且在C++环境中,其值为1,否则也为未定义标识符。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

V S 2022 编译器就不遵循 A N S I   C 标准 : \color{red}{VS2022编译器就不遵循ANSI\ C标准:} VS2022编译器就不遵循ANSI C标准:
在这里插入图片描述


预定义符号使用举例:

#include <stdio.h>

int main()
{
	printf("文件名:%s\n", __FILE__);
	printf("日期:%s\n", __DATE__);
	printf("时间:%s\n", __TIME__);
	printf("代码行号:%d\n", __LINE__);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

打印结果:

在这里插入图片描述

  • 这里文件名是将文件的整个路径都写下来。

预定义符号的使用__DATE____TIME__ 可以记录日志时使用 \color{red}{可以记录日志时使用} 可以记录日志时使用,在向文件中输入数据时写入日期和时间:

#include <stdio.h>

int main()
{
	FILE* fp = fopen("Diary.txt", "w"); // 向名为Diary的文本文件中写入
	fprintf(fp, "日志日期:%s\n", __DATE__);
	fprintf(fp, "日志时间:%s\n", __TIME__);
	fclose(fp);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 输入后的Diary.txt文件:

在这里插入图片描述


2.#define 定义常量

基本语法

#define name stuff
  • 1

一些常见定义:

#define MAX 1000
#define reg register //为 register这个关键字,创建⼀个简短的名字 
#define do_forever for( ; ; ) //用更形象的符号来替换⼀种实现 
#define CASE break;case //在写case语句的时候自动把 break写上。 
// 如果定义的 stuff过长,可以分成几行写,除了最后⼀行外,每行的后面都加⼀个反斜杠(续行符)。
// 因为按下回车键等于输入一个 \tab 字符 ,输入一个 反斜杠\ 相当于把回车抵消
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
 							date:%s\ttime:%s\n",\
 							__FILE__,__LINE__, \
 							__DATE__,__TIME__) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注意: \color{red}{注意:} 注意:在define定义标识符的时候,不要在最后加上分号;,有可能会导致出错

例如:

#define MAX 100

if(condition)
	max = MAX;
else
	max = 0;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

3.#define 定义宏

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

宏的声明方式

#define name( parament-list ) stuff
  • 1

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

注意: \color{red}{注意:} 注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的
⼀部分。

强调: \color{red}{强调:} 强调:宏的参数及宏的表达式都要要加上括号,不然可能发生错误
例如:

#define SQUARE( x ) x * x

int main()
{
	printf("%d",SQARE(3 + 1));
	//宏替换之后就变成:printf("%d", 3 + 1 * 3 + 1);
	//最后得到的结果是7,不是我们预期的16
	//但如果宏改为 #define SQUARE( x ) ( x ) * ( x )
	//结果就是16
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

不仅参数要加上括号,宏表达式也要加上括号: \color{red}{不仅参数要加上括号,宏表达式也要加上括号:} 不仅参数要加上括号,宏表达式也要加上括号:
例如:

#define DOUBLE( x ) ( x ) + ( x )

int main()
{
	printf("%d",DOUBLE(3 + 1) * 2);
	//宏替换之后变成:printf("%d", (3 + 1) + (3 + 1) * 2);
	//最后得到的结果是12,不是我们预期的16
	//但是如果我们把宏表达式加上括号:#define DOUBLE( x ) (( x ) + ( x ))
	//宏替换之后就是 ((3 + 1) + (3 + 1)) * 2
	//结果就是预期的16
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

正确的宏定义形式

#define DOUBLE( x ) (( x ) + ( x )) //不该省的括号不要省
  • 1

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

4.带有副作用的宏参数

当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果(参数带自++或- -改变参数的原值)。

例如:

x+1;  //  不带副作用
x++;  //  带有副作用 
  • 1
  • 2

副作用实例:

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

输出的结果是什么?
因为宏替换之后变成:

z = MAX( (x++) > (y++) ? (x++) : (y++) );
  • 1

所以输出结果是:

x=6 y=10 z=9
  • 1

我们只是想求出x和y中的最大值z,但是因为参数是带++的,导致x和y在运算过程中值被改变,结果z也不是预期的8,这不是我们想达到的效果,所以出现了副作用, 这种带 + + , − − 的参数就叫做带副作用的参数。 \color{red}{这种带++,- -的参数就叫做带副作用的参数。} 这种带++的参数就叫做带副作用的参数。

5.宏替换的规则

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

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

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

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

这里是指宏里面有#define 定义的其他常量的情况:

#define MAX 100
#define ADD(x) ((x) + MAX)
// 因为#define 定义的本质是在编译的预处理阶段将文本中的代码替换
// 所以这种包含#define定义的常量的宏,不能一遍就替换完成
// 因为在替换之前,编译器不知道宏里有MAX
...

int z = ADD(3);
//第一遍替换:int z = ((3) + MAX);
//第二遍替换:int z = ((3) + 100); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注意: \color{red}{注意:} 注意:

  1. 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。(因为本质是替换)
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

6.宏和函数的对比

宏通常被应⽤于执⾏简单的运算。
⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些。

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

那为什么不⽤函数来完成这个任务?

原因有⼆:

  1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多(函数的调用需要建立堆栈)。所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹。
  2. 更为重要的是函数的参数必须声明为特定的类型(只能传与形参类型一致的参数 例如 i n t   M a x ( i n t   a , i n t   b ) \color{blue}{例如int\ Max(int\ a,int\ b) } 例如int Max(int a,int b),这个函数只能传两个整型,也就是只能比较两个整型的大小,比较局限)。反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏的参数是类型⽆关的。

宏也有一些比较明显的缺点: \color{crimson}{宏也有一些比较明显的缺点:} 宏也有一些比较明显的缺点:

  1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。(如果一个宏定义的代码有100行,而程序中调用了100次,替换后就多了近10000行代码大大增加了代码的长度
  2. 宏是没法调试的。(调试时不会进入宏内部,而且宏替换的代码我们也看不到,只会在编译阶段的 test.i 文件中看到)
  3. 宏由于类型⽆关,也就不够严谨。(用整型和浮点型比较等)
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。(上文提到的忘加括号的情况)

宏有时候可以做函数做不到的事情: \color{crimson}{宏有时候可以做函数做不到的事情:} 宏有时候可以做函数做不到的事情:⽐如,宏的参数可以出现类型,但是函数做不到。

#define MALLOC(num, type) (type*)malloc(num * sizeof(type))
 ...
 //使⽤ 
 int* tmp = MALLOC(10, int);//类型作为参数 
 
 //预处理器替换之后: 
 int* tmp = (int*)malloc(10 * sizeof(int));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

宏和函数的一一对比:
在这里插入图片描述


7.#和##运算符

7.1 #运算符

1.在了解#运算符之前,首先我们要知道一个知识点:两个字符串连在一起时会自动转化成一个字符串。

看下面的代码:

#include <stdio.h>

int main()
{
	printf("%s","Hello"" World");
	// 这里两个""靠在一起会转化成printf("%s","Hello World");
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

它的输出结果:
在这里插入图片描述


2.然后我们再了解一下#运算符:#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。#运算符所执⾏的操作可以理解为”字符串化“。(即 #a 变为 “a”)

看下面这个例子理解一下:
当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 就可以写:

#define PRINT(n) printf("the value of "#n" is %d", n);
...

 int a = 10;
 PRINT(a);
  • 1
  • 2
  • 3
  • 4
  • 5

当我们把a替换到宏的体内时,就出现了#a,⽽#a就是转换为:

printf("the value of ""a"" is %d", a);
// 就相当于printf("the value of a is %d", a);
  • 1
  • 2

运行代码就能在屏幕上打印:

在这里插入图片描述


7.2 ##运算符

##可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称为记号粘合。
这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。

例如:

#define link(a,b) (a##b)

int main()
{
	int data_max = 10;
	printf("%d", link(data,_max));
	// 宏替换以后就是printf("%d",(data##_max));
	// ##再转换就是:printf("%d",data_max);
	// 结果就是10
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

输出结果:

在这里插入图片描述

在实际开发过程中##使⽤的很少,所以很难取出⾮常贴切的例⼦。

8.命名约定

⼀般来讲函数的宏的使⽤语法很相似。所以语⾔本⾝没法帮我们区分⼆者。
那我们平时的⼀个习惯是:
把宏名全部⼤写
如:#define ADD(a,b) ((a) + (b))
函数名不要全部⼤写
如:int Add(int a, int b) { return a + b }

9.#undef

这条指令⽤于移除⼀个宏定义。

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

10.命令行定义

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

#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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

编译指令:

//linux 环境演⽰ 
gcc -D ARRAY_SIZE=10 programe.c
  • 1
  • 2

11.条件编译

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

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

#include <stdio.h>
#define __DEBUG__ 
//当我们不需要打印观察数组内容时可以将这个定义注释掉
//下面的条件编译就不会执行,很方便

int main()
{
	int i = 0;
	int arr[10] = {0};
	
	for(i=0; i<10; i++)
	{
		arr[i] = i;
		#ifdef __DEBUG__ //如果定义了__DEBUG__则执行下面的语句
		printf("%d\n", arr[i]);//为了观察数组是否赋值成功。  
		#endif //__DEBUG__
	}
	
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

常⻅的条件编译指令:

1.
#if 常量表达式
	//...
#endif
//常量表达式由预处理器求值。 
如:
#define __DEBUG__ 1
#if __DEBUG__
	//..
#endif

2.多个分支的条件编译
#if 常量表达式
	//...
#elif 常量表达式
	//...
#else
	//...
#endif

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

#if !defined(symbol)
#ifndef symbol

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

12.头文件的包含

12.1 头文件被包含的方式

1.本地文件包含

#include "filename.h"
  • 1

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

Linux环境的标准头⽂件的路径:

/usr/include
  • 1

VS环境的标准头⽂件的路径:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径 
  • 1
  • 2

注意按照⾃⼰的安装路径去找。


2.库文件包含

#include <filename.h>
  • 1

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

  • 这样是不是可以说,对于库⽂件也可以使⽤ “ ” 的形式包含?
    • 答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件了。

12.2 嵌套文件包含

我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的地⽅⼀样。 (将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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

test.h

void test();

struct Stu
{
	int id;
	char name[20];
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。

  • 如果 t e s t . h 文件⽐较大,这样多次重复包含后代码量会剧增。 \color{red}{如果test.h文件⽐较大,这样多次重复包含后代码量会剧增。} 如果test.h文件较大,这样多次重复包含后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家都能使⽤,⼜不做任何的处理,那么后果真的不堪设想。

如何解决头⽂件被重复引⼊的问题?答案:条件编译

每个头⽂件的开头写:

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容 
//例如 #include <stdio.h>
#endif //__TEST_H__
// 这段代码的意思是如果没定义 __TEST_H__ 则 定义__TEST_H__ 和下面的头文件内容
// 如果定义了__TEST_H__就不会再重复定义
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

或者写:

#pragma once
  • 1

就可以避免头⽂件的重复引⼊。

13.其他预处理指令

#error
#pragma
#line
...
不做介绍,自行了解
#pragma pack()在博客结构体部分已介绍。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

例题 \color{maroon}{例题} 例题
下面哪个不是宏和函数的区别?( )
A.函数可以递归,宏不能递归
B.函数参数有类型检查,宏参数无类型检查
C.函数的执行速度更快,宏的执行速度慢
D.由于宏是通过替换完成的,所以操作符的优先级会影响宏的求值,应该尽量使用括号明确优先级

答案:C

解析:宏不存在执行速度,它是查找替换,选C。A中宏是查找替换,无法设定递归跳出条件,自然无法递归。B中宏是查找替换,都没有执行,类型更是无从谈起。D中直接说了宏的本质。所以只要知道了宏是查找替换,其他问题也就不是问题了。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/469919
推荐阅读
相关标签
  

闽ICP备14008679号