C预处理详解

 

零.什么是预处理

预处理是程序在翻译环境中编译过程的第一步。下面来说一说预处理阶段都发生了什么。

1.处理预定义的符号

__FILE__:正在编译的源文件。

__LINE__:文件当前行号。

__DATE__:文件被编译日期。

__TIME__:文件被编译时间。

__STDC__:判断编译器是否遵循ANSI C若遵循则为1,否则未定义。

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	printf("%s\n", __STDC__);
	return 0;
}

在VS2019下__STDC__没有定义,所以将它删去再运行。

可以清楚看到打印的文件路径,行号,日期,时间。

预处理阶段会将这些符号翻译成这些内容。

 2.处理#define定义的标识符

我们可以把数字,类型,甚至字符串定义成标识符。

#include<stdio.h>
#define MAX 1000
#define reg register
#define int_t int
#define STR "hehe"
int main()
{
	int_t a = 10;
	int b = MAX;
	reg int c = 30;
	printf("%s", STR);
	return 0;
}

在预处理阶段,int_t变成了int,MAX变成了1000,reg变成了register,STR变成了"hehe"。

 3.处理#define定义的宏

1.什么是宏

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

2.宏的声明方式

#define  name(list)  stuff

其中name为宏名,list为参数列表,stuff为执行的操作。

3.举例

#define SQU(X) ((X)*(X))
int main()
{
	int a = 5;
	int b = SQU(a);
	printf("%d", b);
	return 0;
}

即定义了一个乘法的宏SQU,这段代码打印的结果是:

4.替换规则

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

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

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

5.注意:

1.为了和函数区分,宏名一般都是全部大写的,而函数一般只有几个字母大写。

 2.在书写宏的表达式时,每一个参数和操作都要用()否则会出现问题,比如:

#define SQU(X) X*X
int main()
{
	int a = 5;
	int b = SQU(a+5);
	printf("%d", b);
	return 0;
}

 要打印这段代码的值,我们期望打印的是100,但实际的打印结果为:

 这是因为宏只是单纯的替换,即在替换之前并没有计算a+5,替换之后为a+5*a+5,所以得到的结果是35,因此在使用宏时对变量和表达式需要加()。

3.参数列表的()要与name紧邻,如果两者之间有空格,参数列表就会解释成表达式的一部分。

4.使用宏时不能出现递归,即宏中不能出现无参数的宏名,如ADD((ADD),(5))。但是有参数可以计算的可以出现,比如:ADD((ADD(5,5),5)。

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

#define MAX 100
int main()
{
int a=MAX;
printf("MAX=%d",a);
}

 注意字符串中的MAX就没有被替换。

6.#和##

1.#

把一个宏参数变成相应的字符串。

在定义宏时使参数的值不会直接被替换,而是转化为带""的参数。

#define PRINT(n) printf("the value of "#n" is %d",n)
int main()
{
	int a = 10;
	PRINT(a);
}

注意字符串有这样的特性,当两个字符串排列在一起时,两个字符串会自动进行连接。

比如printf("hello""world");打印出来的结果就是hellworld。

所以当将n表示成字符串时也会这样进行打印。

而#就可以将n变成一个字符串,所以打印的结果为:

这样不仅使在双引号的内部的参数可以被赋值,而且使之可以链接起来。#的作用主要在想在字符串中的参数也被赋值时进行使用。

2.##

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

注意连接出来的一定要是一个合法的标识符。

#define CAT(X,Y) X##Y
int main()
{
	int class103 = 100;
	printf("%d", CAT(class, 103));
}

 即将class和103连接在了一起。

7.带副作用的宏参数

1.副作用

int a=10;
int b=a+1;//不带副作用,b被赋值,a不变
int b=a++;//带有副作用,b被赋值,a变了

 2.例子

#define MAX(X,Y) ((X)>(Y))?(X):(Y)
int main()
{
	int a = 5;
	int b = 8;
	int m = MAX(a++, b++);
	printf("%d", m);
}

打印的结果是:

 这是因为宏的参数不是计算后替换进去的,而是替换之后再计算,这里预处理之后的结果是:

((a++)>(b++))?(a++):(b++),所以打印的结果是9,而函数就不一样,函数是先计算后替换。

int Max(int a, int b)
{
	return ((a) > (b)) ? (a) : (b);
}
int main()
{
	int a = 5;
	int b = 8;
	int m = Max(a++, b++);
	printf("%d", m);
}

打印的结果是:

 因为传入的就是5和8两个值。

8.宏与函数的对比

1.代码长度

每次使用时,宏代码都会插入到程序中,除了非常小的宏之外,程序的长度会大幅度增长。

函数代码只出现于一个地方,每次使用这个函数时,都调用那个地方的同一份代码。

2.执行速度

宏执行更快。

函数存在调用和返回的额外开销,所以相对慢一点。

3.操作符优先级

宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则近邻操作符的优先级可能产生不可预料的后果,所以建议宏在书写时多一些括号。

函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。

4.带有副作用的参数

参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能产生不可预料的后果。

函数参数只在传参时求值一次,结果更容易被控制。

5.参数类型

宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。

函数的参数是与类型有关的,如果参数类型不同,就需要不同的函数,即使它们执行的任务是不同的。

6.调试

宏在调试的时候是看不到的,因为在预处理阶段就已经被替换了。

函数是可以逐语句进行调试的。

7.递归

宏是不能进行递归的,函数是可以进行递归的。

8.命名约定

把宏名全部大写,函数名不要全部大写。

9.定义类型

宏可以定义类型,但是函数做不到

比如:

宏可以把一个复杂的定义操作简洁化

#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
	int* p = MALLOC(100, int);
	int* q = (int*)malloc(100 * sizeof(int));
}

此时这两条语句进行的操作是相同的。如果进行多次的定义使用宏更加的方便。

9.#undef

作用为移除一个宏定义

 使用#undef对MAX进行移除,再使用MAX时会报错。

4.处理命令行定义

int main()
{
	int i;
	int arr[SZ] = {0,1,2,3,4,5,6,7,8,9};
	for (i = 0; i < 10; i++)
	{
		printf("%d", i);
	}
}

假如有这样一段代码,注意这里是没有定义SZ的,在linux系统下可以假如一条指令:

gcc -D SZ=10 test.c

即定义了SZ为10,那么这条指令是在预编译阶段执行的,预编译之后SZ被替换为10。

5.处理条件编译

1.与if,else语句的对比

满足条件的代码进行编译,不满足就不进行编译。和if,else语句很相近只不过是在预处理阶段执行的。

#if 1
	printf("hehe\n");
#endif

这是最简单的一段条件编译指令,即如果1为真,就打印hehe。endif是结束的标志。

与之对应的还有#elif和#else

即处理的结果一般为:

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

我们也可以对他们进行嵌套,即#if语句中还包含#if语句。

注意:#if后面必须是常量,可以是#define定义的标识符常量,而不能是变量因为变量是运行的时候创建的。

2.判断是否被定义

#if语句可以判断常量是否被定义。

#if defined(MAX);
#ifdef(MAX);

这两条语句是等价的,判断MAX是否被定义,如果被定义了,那么下面的代码就会被执行,如果没有被定义那么下面的代码就不会被执行。

与#ifdef相反的是#ifndef(MAX),即如果MAX没被定义,那么下面的代码就会被执行。

6.处理文件包含

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

这种替换的方式很简单,预处理器先删除这条指令,并用包含的文件的内容进行替换。这样一个源文件被包含十次,就实际上编译了十次。

1.头文件的包含方式

头文件一共有两种包含方式:<>包含和""包含。

1.""包含

查找策略:先在源文件所在的目录下查找,如果头文件没有找到,编译器就像查找库函数的头文件一样在标准位置查找头文件,如果找不到则编译错误。

2.<>包含

查找策略:直接在标准路径下进行查找,如果找不到则编译错误。

2.嵌套文件包含

当一个项目中某一个头文件被包含多次时:

 此时com.h在test中被包含了两次,我们要避免这个情况,需要每一次在定义头文件时进行规定:

#pragma once这样就定义了只能包含一次。防止文件被多次包含。

7.总结

预处理是源文件转换为头文件的第一步,了解了预处理操作可以加深我们对编译运行以及调试的进一步理解,熟练使用预处理中的宏也可以使你的代码事半功倍。

posted @   卖寂寞的小男孩  阅读(56)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示