首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >预处理(详解)

预处理(详解)

作者头像
25遇见
发布2025-11-05 12:48:01
发布2025-11-05 12:48:01
1240
举报

一、预定义符号

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

代码语言:javascript
复制
__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
代码语言:javascript
复制
#include<stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	printf("%d\n", __LINE__);
	//printf("%d\n", __STDC__);//error,说明当前VS2022环境不支持ANSIC
	return 0;
}

二、#define

1.#define定义常量

代码语言:javascript
复制
#define name stuff

举例:

代码语言:javascript
复制
#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定义标识符时,末尾要不要加上分号(;)?

代码语言:javascript
复制
#define MAX 1000;
#define MAX 1000

建议不要加上分号(;),这样容易出现问题。例如:

代码语言:javascript
复制
if (condition)
	max = MAX;
else
	max = 0;

如果加上分号,替换后,if和else之间就是2条语句,而没有大括号时,if后面只能有一条语句,这里会出现语法错误。

2.#define定义宏

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

宏的申明方式:

代码语言:javascript
复制
#define name( parament-list ) stuff

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

举例:

代码语言:javascript
复制
#define SQUARE(x) x*x
#include<stdio.h>
int main()
{
	int a = 5;
	int ret = SQUARE(a);
	printf("%d\n", ret);
	return 0;
}

这个宏接收一个参数x。如果上面声明后,置于程序中,预处理器就会替换成5*5。但是这个宏存在一个问题,看下面代码:

我们预期应该是36,但是打印结果却是11,为什么呢?

这里替换文本时,参数x替换成a+1,这条语句实际就变成了:

代码语言:javascript
复制
printf("%d\n", a + 1 * a + 1);

 这里就出现了优先级的问题,那如何解决呢?

在宏定义上加上两个括号就好了:

这样就达到预期效果了:

代码语言:javascript
复制
printf("%d\n", (a + 1)* (a + 1));

再看一段代码:

代码语言:javascript
复制
#define DOUBLE(x) (x)+(x)
#include<stdio.h>
int main()
{
	int a = 5;
	printf("%d\n", 10 * DOUBLE(a));
	return 0;
}

这段代码看上去应该打印100,但实际上打印的是55,为什么呢?

替换后:

代码语言:javascript
复制
printf("%d\n", 10 * (5) + (5));

这里也出现了优先级的问题,解决办法就是在宏定义表达式两边再加上一对括号:

代码语言:javascript
复制
#define DOUBLE(x) ((x)+(x))

提示:所有用于数值表达式进行求值的宏定义都应该加上括号,避免在使用宏时由于参数中的操作符或者邻近操作符之间不可预料的相互作用。

三、带有副作用的宏参数

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

代码语言:javascript
复制
x+1;//不带副作用
x++;//带有副作用

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

代码语言:javascript
复制
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
#include<stdio.h>
int main()
{
	int a = 3;
	int b = 5;
	int m = MAX(a++, b++);//int m = ((a++) > (b++) ? (a++) : (b++));
	printf("a=%d b=%d m=%d\n", a, b, m);//a=4 b=7 m=6
	return 0;
}

 四、宏替换规则

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

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

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

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

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

五、宏函数的对比

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

比如在两个数中找出较大数时,写成宏更有优势:

代码语言:javascript
复制
#define MAX(a,b) ((a)>(b)?(a):(b))
#include<stdio.h>
int Max(int x, int y)
{
	int r = x > y ? x : y;
	return r;
}
int main()
{
	int a = 3;
	int b = -6;
	int m1 = Max(a, b);
	int m2 = MAX(a, b);
	printf("%d\n", m1);
	printf("%d\n", m2);
	return 0;
}

原因: ①用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。 ②更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等可以用于比较的类型。宏的参数是类型无关的。

和函数相比,宏的劣势: ① 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。 ②宏是没法调试的。 ③宏是类型无关的,也就不够严谨。 ④宏可能会带来运算符优先级的问题,与导致程容易出现错。

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

代码语言:javascript
复制
#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
#include<stdio.h>
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	int* ptr = MALLOC(10, int);//int* ptr = (int*)malloc(10 * sizeof(int));
	return 0;
}

宏和函数的对比:

六、#和##

1.#运算符

#运算符将宏的一个参数转换为字符串字面量,它仅允许出现在带参数的宏的替换列表中。

#运算符执行的操作可以理解为“字符串化”。

例如:

代码语言:javascript
复制
#include<stdio.h>
int main()
{
	int a = 1;
	printf("the value of a is %d\n", a);
	int b = 20;
	printf("the value of b is %d\n", b);
	float f = 5.6f;
	printf("the value of f is %d\n", f);
	return 0;
}

我们可以看到a,b,f三个的结构是一样的,因此我们可以写成这样:

代码语言:javascript
复制
#define PRINT(n, format)  printf("the value of "#n " is "format"\n", n)
#include<stdio.h>
int main()
{
	int a = 1;
	PRINT(a, "%d");
	int b = 20;
	PRINT(b, "%d");
	float f = 5.6f;
	PRINT(f, "%f");
	return 0;
}

2.##运算符

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

代码语言:javascript
复制
int int_max(int x, int y)
{
	return x > y ? x : y;
}
float float_max(float x, float y)
{
	return x > y ? x : y;
}

我们可以看到这两个结构是一样的,但是这样写起来太麻烦了,因此我们可以写成:

代码语言:javascript
复制
#define GENERIC_MAX(type)		 \
type type##_max(type x, type y)	 \
{								 \
	return (x > y ? x : y);		 \
}

举例:求两个数的较大值。 

代码语言:javascript
复制
#include<stdio.h>
#define GENERIC_MAX(type)		 \
type type##_max(type x, type y)	 \
{								 \
	return (x > y ? x : y);		 \
}
GENERIC_MAX(int) //替换到宏体内后int##_max生成了新的符号int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max生成了新的符号float_max做函数名
int main()
{
	//调用函数
	int m = int_max(2, 3);
	printf("%d\n", m);
	float fm = float_max(3.5f, 4.5f);
	printf("%f\n", fm);
	return 0;
}

七、命名约定

宏名全部大写 函数名不要全部大写

并不是一定的,例如offsetof是宏,但全是小写。

八、#undef

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

代码语言:javascript
复制
#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字首先要被移除

九、命令行定义

 许多C的编译器提供了一种能力,挖件帝令行中是义符号。用于启动编译过程。

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

代码语言:javascript
复制
#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;
}

编译指令:

代码语言:javascript
复制
//linux 环境演⽰
gcc -D ARRAY_SIZE = 10 programe.c

十、条件编译

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

比如,调试性的代码删除可惜,保留有碍事,那我们就可以选择性的编译。

代码语言:javascript
复制
#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__
		printf("%d\n", arr[i]);
#endif 
	}
	return 0;
}

如果不需要,直接注释#define __DEBUG__即可。

常见的条件编译指令:

代码语言:javascript
复制
#if 常量表达式
//...
#endif
代码语言:javascript
复制
#include<stdio.h>
#define M 10
int main()
{
#if M>0
	printf("hehe\n");
#endif
	return 0;
}

②多个分支的条件编译

代码语言:javascript
复制
#define M 1
#include<stdio.h>
int main()
{
#if M==0
	printf("hehe\n");
#elif M==1
	printf("haha\n");
#elif M==2
	printf("xixi\n");
#else
	printf("heihei\n");
#endif
	return 0;
}

③判断是否被定义

代码语言:javascript
复制
//第一种
#include<stdio.h>
#define MAX 1
int main()
{
#if defined(MAX)
	printf("hehe\n");
#endif
	return 0;
}
//第二种
#include<stdio.h>
#define MAX 1
int main()
{
#ifdef MAX
	printf("hehe\n");
#endif
	return 0;
}
//第三种
#define MAX 1
#include<stdio.h>
int main()
{
#if !defined(MAX)
	printf("hehe\n");
#endif
	return 0;
}
//第四种
#define MAX 1
#include<stdio.h>
int main()
{
#ifndef MAX
	printf("hehe\n");
#endif
	return 0;
}

④嵌套指令

代码语言:javascript
复制
#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.头文件被包含的方式

①本地文件包含
代码语言:javascript
复制
#include "filename"

查找策略:现在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在表准位置查找头文件。如果找不到,就提示编译错误。 

②库文件包含
代码语言:javascript
复制
#include <filename>

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

对于库文件可以使用" "的形式包含吗? 答案是可以的,但是这样查找的效率会低些,也不容易区分是库文件还是本地文件。

2.嵌套文件包含

test.c:

代码语言:javascript
复制
#include<stdio.h>
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{

	return 0;
}

test.h:

代码语言:javascript
复制
void test();
struct Stu
{
	int id;
	char name[20];
};

test.c文件将test.h包含了5次,那么test.h文件的内容将会在test.c中拷贝5份。

如果test.h文件比较大,这样预处理后代码量会剧增。如果工程比较大,又公共使用头文件,并且不做任何处理,那么后果不堪设想。

那如何解决头文件被重复引用的问题呢?答案是条件编译。头文件这样写:

代码语言:javascript
复制
//第一种写法:
#ifndef __TEST_H__
#define __TEST_H__
void test();
struct Stu
{
	int id;
	char name[20];
};
#endif
//第二种写法:
#pragma once
void test();
struct Stu
{
	int id;
	char name[20];
};
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-07-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、预定义符号
  • 二、#define
    • 1.#define定义常量
    • 2.#define定义宏
  • 三、带有副作用的宏参数
  •  四、宏替换规则
  • 五、宏函数的对比
  • 六、#和##
    • 1.#运算符
    • 2.##运算符
  • 七、命名约定
  • 八、#undef
  • 九、命令行定义
  • 十、条件编译
  • 十一、头文件的包含
    • 1.头文件被包含的方式
      • ①本地文件包含
      • ②库文件包含
    • 2.嵌套文件包含
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档