C语言预处理详解(C语言知识完结篇)

目录

1.引言

2.预定义符号

3.#define定义常量

4.#define定义宏

5.带有副作用的宏参数

6.宏替换的规则

7.宏函数的对比

8.#和##

8.1.#运算符

8.2.##运算符

9.命名约定

10.#undef

11.命令行定义(了解)

12.条件编译

13.头文件的包含

13.1.头文件被包含的方式

13.1.1.本地文件包含

13.1.2.库文件包含

13.2.嵌套文件包含

14.其他预处理指令

15.结语


1.引言

这篇文章就是最后的一期C语言知识类文章了,该篇文章我会主要围绕上一篇编译与链接文章中编译环节中的预处理环节进行更深入的讲解

讲解这块的目的是为了让你们更能了解你们所打的代码为什么有时候会编译错误,有的时候逻辑没问题,运行出来却会有问题,为什么宏定义在代码里可以当作直接替换,等等等,在学完该篇文后,你就会对C语言有进一步的认知,对于我们之后要学的C++也会有很大帮助,因为C++与C语言一样,同属于编译型语言

该篇讲完后,我会用C语言的知识讲解初阶的数据结构,顺序表和链表讲解完后,我会用顺序表和链表以及C语言的知识来教你们做一个非常简单的贪吃蛇小项目,在贪吃蛇项目讲解完毕后,我们C语言部分的学习就是完全结束了

那么,话不多所 ,接下来,就进入今日的正题------预处理------------------------>

​​​​​​​ ​​​​​​​ ​​​​​​​ ​​​​​​​


2.预定义符号

C语言设置了一些预定义符号,可以直接使用,当然,既然都叫预定义符号了,自然就是在预处理期间处理的,我来举几个预定义符号

FILE //进行编译的源文件

LINE //文件当前的行号

DATE //文件被编译的日期

TIME //文件被编译的时间

STDC //如果编译器遵循ANSI C,其值为1,否则未定义

举个例子

cpp 复制代码
#define _crt_secure_no_warnings
#include <stdio.h>

int main()
{
	printf("file:%s\nline:%d", __FILE__, __LINE__);
	return 0;
}

这个代码运行结果如下


3.#define定义常量

#define定义常量概念而言其实很简单,基本语法就如下,但是使用的时候需要注意的细节还是很多的

基本语法

cpp 复制代码
#define name stuff

举个例子

cpp 复制代码
#define MAX 100
#define re register//为register关键字创建一个简短的名字
#define do_forever for(;;)//用更形象符号替换一种实现

#define _PRINT printf("file:%s\tline:%d\t  \
                       date:%s\ttime:%s\n", \
                       __FILE__,__LINE__, \
                       __DATE__,__TIME__)
//如果定义的stuff过长,可以分成几行写,出了最后一行外,每行的后面价格反斜杠就可以了(续行符)

以最后一个示例运行结果如下图

那么,接下来我们来讲讲#define使用的细节方面

比如,在define定义常量的时候,要不要在最后面加上;

很显然,在上面我举得例子中可以发现,一般而言是不要在后面加上;的,因为加上可能会导致问题,我来举几个例子就好了

例子一:

如下图,我们可以发现,如果我们在宏定义部分加了;,我们想要将这个宏放在条件判断语句里时,就会出问题,毕竟判断语句里怎么能放;呢

例子二:

如下图,我们可以发现,如果我们在宏定义部分加了;,那么,我们很可能会在赋值语句最后再加个;,此时就会有俩个";"了,那么对于if else语句而言,没有大括号进行匹配的话就会出问题


4.#define定义宏

我们学了用define定义常量,那么我们可以用define定义函数名吗,也是可以的

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

下面是宏的声明方式

cpp 复制代码
#define name( parament-list ) stuff

其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中(简单点理解就是可以当作前面是个函数声明,后面就是这个函数具体执行的操作,当然这个只是简单理解,和调用函数还是不一样的,因为不会新开空间,这个的本质还是替换)

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

说起来可能会觉得晦涩难懂,那么我们直接来看点例子

cpp 复制代码
#define _crt_secure_no_warnings
#include <stdio.h>
#define qua(a,b) ((a)*(b))
int main()
{
	printf("%d", qua(3, 4));
	return 0;
}

这个例子就是定义了一个宏函数,这个宏接收俩个参数,一个a,一个b,然后我们在程序中使用了这个宏函数,预处理器就会用后面的表达式替换宏函数

那么,为什么在后面的表达式里我们要用这么多括号呢,接下来我来一一讲解下内外括号的作用

首先,我们要明白,宏函数的本质是替换,且a和b这俩个不代表一个数,传过来可能会是表达式

那么我们先说里面的括号,如果我们传的是qua((3+2),3)的话,我们的目的是为了让5*3得到15,但是如果我们去掉了里面的括号,传过去替换就成了 3+2*3,结果自然就不是我们所预期的结果了,所以这就是加内部括号的重要性

接下来我们来说说外面的括号,如果我们传的是qua(2,3)%qua(4,5),我们的目的肯定是得到6%20得到6,但是 如果不加外面的括号,传过去替换就成了2*3%4*5,得到的就是10了,结果自然就不是我们所预期的结果了,所以这就是加外部括号的重要性

当然,我们可以保持一种习惯,就是对于所有对数值表达式进行求值的宏定义时都应该用这种方式加上括号,内部括号是避免传参时的影响,外部括号是避免外在的影响,只要有了这种习惯,我们就可以避免在使用宏时由于参数中的操作符或邻近操作符的优先级问题造成的不可预料的相互作用


5.带有副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么我们在使用这个宏的时候就可以出现不可预测的后果,副作用就是表达式求值的时候出现的永久性效果(因为是宏本质是替换,所以比如++,--等运算符就会带有副作用)

我先举个例子来表示什么是有副作用的,什么是没有副作用的

cpp 复制代码
x++//有副作用的
x+1//无副作用的

我们就来举个例子理解理解副作用的危害

cpp 复制代码
#define MAX(a,b) ( (a)>(b) ? (a) : (b) )

.
.
.
x = 5;
y = 6;
z = MAX(x++,y++);
printf("%d %d %d",x,y,z);

这个代码中,我们通过替换,可以得到预处理器处理之后的结果是

cpp 复制代码
z = ( (x++) > (y++) ? (x++) : (y++) );

因为是后置++,所以我们先判x和y,即5和6,为假,所以进y++,y加了2次,x加了1次,z接收的是y++,即z = y-1;

所以最后输出到结果是 x = 6 y = 8 z = 7

:我们在使用宏的时候,要尽量避免传带有副作用的宏参数


6.宏替换的规则

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

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

2.替换文本随后被插入到程序中原本文本的位置,对于宏,参数名被他们的值所替换(这也就是我先前所说的,#define定义的本质就是替换)

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

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

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

7.宏函数的对比

那么,接下来我们来讲讲#define定义宏和函数的区别是什么,有了函数了,为什么还要有这个#define定义宏呢,我先讲讲宏通常用在哪,接下来再讲宏相比函数的优势

首先,宏通常被应用于执行简单的运算,比如在俩个数中找出较大的一个时,通过宏来实现会更有优势,如下

cpp 复制代码
#define MAX(a,b) ((a)>(b)?(a):(b))

那为什么不用函数来实现这个功能呢,原因有二:

原因一:在函数栈帧的创建和销毁部分我们讲过,调用函数会开辟新的空间来维护函数空间,还会有临时拷贝变量等等操作,函数返回还要销毁空间,相比宏直接替换,既不用开辟空间,也不用传参而言,函数所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹,当然这是在代码量少的情况下

原因二:更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用,反之宏的话没有类型限制,既适用于整形,浮点型,字符型等可以用>来比较的类型,宏的参数和类型无关就是他的强大之处

当然,宏与函数相比肯定还是缺点更多的,不然现在肯定都在用宏而不是函数了,那么我来讲讲宏相比于函数所存在的劣势

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

2.宏时没法调试的,因为宏在预处理阶段就已经替换完了,而我们调试的阶段连链接都已经完毕了

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

4.宏可能会带来运算符优先级的问题,导致程序容易出错(这也就是我先前所提的加括号的问题)

宏虽然劣势很多,但他可以做到函数做不到事情,比如:宏的参数可以出现类型,但是函数 做不到,用代码举例的话如下

cpp 复制代码
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))

MALLOC(10,int);//类型作为参数 

//预处理替换后

(int*)malloc(10*sizeof(int))

最后,我们来看下宏和函数的各个方面的区别,如下图

那么,到此,宏就讲解完毕啦


8.#和##

8.1.#运算符

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

#运算符所执行的操作可以理解为"字符串化"

这么讲可能有点难以理解,那么我们来举个例子来帮我们深入理解下这个#运算符

比如当我们有一个变量int a = 10;的时候,我们想要打印出:a = 10的话,用我们先前的方法来实现就是如下代码

cpp 复制代码
printf("a=%d",a);

但是这么实现的有一劣势,那便是如果我们想要实现同样的效果,但是我们想要看别的变量的时候,前面的变量名就要自己手动更改,但是如果使用下面的方式,就不需要那么麻烦了,代码如下

cpp 复制代码
#define _crt_secure_no_warnings
#include <stdio.h>
#define PRINT(n) printf(#n"=%d",n)
int main()
{
	int a = 0;
	PRINT(a);
	return 0;
}

我们可以用define定义一个宏,当我们调用这个宏的时候,对于宏而言,#a就会转换为"a",所以上面的代码在调用a时就会被预处理为

cpp 复制代码
printf("a=%d",a)

打印在屏幕上自然就是a=0

这便是#运算符的作用了,但是这个运算符也有局限性,便是这个运算符仅允许出现在带参数的宏的替换列表中


8.2.##运算符

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

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

这个运算符在实际开发中非常少见,所以了解即可,这里我就举个小例子来说明这个运算符的作用

我们先前想要写一个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数,代码如下

cpp 复制代码
int int_max(int x,int y)
{
    return x>y?x:y;
}

float float_max(float x,float y)
{
    return x>y?x:y;
}

但是这么写太繁琐了,虽然也有用宏实现的函数,但是如果用宏实现的话,传过去的数据就没有限制了,有隐患,但是有了##运算符之后,我们可以将上述代码改成如下形式

cpp 复制代码
#define TYPE_MAX(type)   \
type type##_max(type x,type y)\
{                             \
    return (x>y?x:y);        \
}

这样定义后,我们传进去不同类型,就会生成不同的函数(注意是函数!不是宏),测试如下

cpp 复制代码
TYPE_MAX(int)//替换到宏体内后int##_max,生成了新的符号 int_max做函数名
TYPE_MAX(float)//替换到宏体内后float##_max,生成了新的符号 float_max做函数名

int main()
{
    int m = int_max(2,3);
    printf("%d\n",m);
    float n  = float_max(3.5f,2.1f);
    printf("%f\n",n);
    return 0;
}

因为在实际开发过程中使用##的很少,所以只需要了解了解就行了


9.命名约定

一般来讲函数和宏的使用语法很相似,所以语言本身无法帮我们区分二者,这个时候我们就要养成一种习惯

把宏名全部大写

函数名不要全部大写


10.#undef

这个指令用于移除一个宏定义,示例如下

cpp 复制代码
#undef MAX
//如果现存的一个宏需要被重新定义,那么它的旧名字要先移除

11.命令行定义(了解)

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

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

因为是linux环境下演示的,所以了解就行

我们先看代码,代码如下

cpp 复制代码
#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;
}

编译指令

cpp 复制代码
//linux 环境演示 
gcc -D ARRAY_SIZE=10 program.c

12.条件编译

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

比如说:

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

cpp 复制代码
#if 0

#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;
}


#endif

这个代码就不会被编译,那么我来总结一下常见的条件编译指令,如下

cpp 复制代码
1.
#if 常量表达式
    //...
#endif
//预处理器求出常量表达式的值后判断是否为0,非0就编译,为0就不编译

如:
#define __DEBUG__ 1
#if __DEBUG__
//....
#endif

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

//这个跟if elseif else语句逻辑神似

3.判断是否被定义
#if defined(symbol)
#ifdef symbol
//这俩个是定义了编译,未定义不编译
#if !defined(symbol)
#ifndef symbol
//这俩个是未定义的话编译,定义的话不编译
//这些也是要跟上#endif结尾的

4.嵌套指令
//这个没什么好说的,就是上面那三种可以进行嵌套定义,跟分支语句差不多

13.头文件的包含

13.1.头文件被包含的方式

13.1.1.本地文件包含

cpp 复制代码
#include "Queue.h"

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

如果查找不到就提示编译错误

13.1.2.库文件包含

cpp 复制代码
#include <stdio.h>

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

这也是为什么stdio.h即可以用<>,也可以用"",但是最好还是用<>,因为如果用""的话,查找的效率就低了,也不容易区分是库文件还是本地文件


13.2.嵌套文件包含

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

这种替换的方式很简单,预处理器先删除这条指令,并用包含文件的内容替换

但是在一个项目中,如果一个头文件被包含了很多次,就会被编译很多次,那么对编译的压力就会比较大,而且如果头文件比较大的话,这样预处理后代码量也会剧增。如果工程比较大,又有公共使用的头文件,还不做处理,被大家都能使用,那么后果不可设想

那么 ,如何解决头文件被重复引入的问题呢?

很简单 ,便是我们刚刚讲的条件编译

每个头文件的开头写:

cpp 复制代码
#ifndef __TEST_H__
#define __TEST_H__
//头文件内容
#endif

或者在开头写下面这行代码

cpp 复制代码
#pragma once

就可以避免头文件的重复引入


14.其他预处理指令

除开我上面所讲的预处理指令外,还有许多的预处理指令,例如#error,#line,#pragma等等,这里就不过多赘述了,感兴趣的可以自己去了解了解

15.结语

那么,至此,C语言知识部分的讲解就讲解完毕啦,等C语言贪吃蛇讲解完毕后,C语言专栏就完结啦,那么,感谢观看,我们数据结构专栏见,若觉得写的还可以,可以分享给朋友一起来看哦,毕竟一起进步更有动力嘛,当然能关注一下就更好啦

相关推荐
9稳2 小时前
基于plc的自动化立体仓库控制系统设计
开发语言·网络·数据库·嵌入式硬件·plc
MX_93592 小时前
基于注解方式配置声明式事务
java·开发语言·后端·spring
wjs20242 小时前
《jQuery 滑动:深入浅出的探索与实践》
开发语言
程序员老乔2 小时前
Java 新纪元 — JDK 25 + Spring Boot 4 全栈实战(五):FFM API,告别JNI在Spring Boot中直连推荐引擎
java·开发语言·spring boot
NGC_66112 小时前
详解Java包装类
开发语言·windows·python
cm6543202 小时前
C++代码切片分析
开发语言·c++·算法
共享家95272 小时前
Java入门( 日期类与 BigDecimal 工具类 )
java·开发语言
Lhan.zzZ2 小时前
深入浅出 Qt 信号槽连接方式:从 AutoConnection 到 BlockingQueuedConnectionQt
开发语言·c++·qt
好好学习叭~2 小时前
正则表达式
java·开发语言