预处理指令详解

前言

上一节我们了解了文件操作的相关内容,本节我们来了解一下预处理指令,那么废话不多说,我们正式开始今天的学习

预定义符号

在C语言中,设置了一些预定义的符号,可以供我们直接使用,预定义符号是在程序的预处理过程中直接被处理的,那么C语言中定义的符号有下面几种:

1.__FILE__

表示当前正在编译的源文件的地址

2.__LINE__

表示文件当前所在的行号

3.__DATE__

表示文件被编译的日期

4.__TIME__

表示文件被编译的时间

5.__STDC__

如果编译器遵循ANSI C,其取值为1,若不是,则它的取值未定义(gcc编译器支持)

下面我们来尝试使用一下以上预定义符号:

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>


int main(void)
{
	printf("%s\n", __FILE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	printf("%d\n", __LINE__);

	return 0;
}

这些符号都是在编译前的预处理阶段就进行了处理

#define 定义常量

#define 有两种不同的功能:

1. #define 可以定义符号(常量)

2. #define 可以定义宏

#define 使用的基本语法如下:

#define name stuff

例如:

#define MAX 100
#define MIN 1

在程序的预处理阶段,代码中的 name 变量就会被替换为数值 stuff

stuff 定义的数值的类型不一定全要为整数,例如:

#define STR "hello world"

我们还可以使用 #define 定义相对复杂的函数,例如:

#define forever for(;;)

在这个代码当中,for 函数的初始化部分、调整部分和判断部分都被省略。由于该 for 函数判断条件没有写,这样就意味着判断条件是恒为真的,就会造成死循环

如果我们定义的 stuff 长度过长,我们可以运用续行符( \ ),除了程序的最后一行以外,程序的每一行都可以添加换行符

#define DEBUG_PRINT printf("file:%s\tline:%d\t \
 date:%s\ttime:%s\n" ,\
 __FILE__,__LINE__ ,\     
__DATE__,__TIME__ )   

那么此时,我们肯定会存在一个疑问:在 #define 定义标识符的时候,需不需要在末尾加上一个;呢?

建议是最好不要加上分号,举个简单的例子:

#define MAX 1000;

int main(void)
{
	int a = MAX;

	return 0;
}

在此代码的预处理阶段,原代码会将代码转变成如下这种形式:

#define MAX 1000;

int main(void)
{
	int a = 1000;;

	return 0;
}

此时 int a = 1000后面就有两个分号,就会造成程序的错误

还有可能会出现这样的情况:

#define MAX 1000;

int main(void)
{
	printf("%d\n", MAX);

	return 0;
}

我们在打印 MAX 的时候,由于其带了分号,就会造成打印不成功

#define 定义宏

#define 允许把参数替换到文本中去,这种实现通常被称作定义宏

宏的声明方式如下:

​
#define name( parament-list ) stuff

​

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

其中需要注意:

参数列表的左括号必须与 name 紧紧的挨在一起,如果两者之间存在着空格,那么参数列表就会被解释成 stuff 的一部分

下面来举一个例子:

例如我们需要定义一个宏来求某个数的平方,我们则可以这么写:

#define SQUARE(x) x*x

int main(void)
{
	
	int a = 4;
	int ret = SQUARE(a);
	printf("%d\n", ret);

	return 0;
}

局限性

但是这个程序存在一定的弊端:若 SQUARE 里面的参数是 a + 1:

#define SQUARE(x) x*x

int main(void)
{
	
	int a = 4;
	int ret = SQUARE(a+1);
	printf("%d\n", ret);

	return 0;
}

我们发现结果并不是我们想要的 25 ,而是 9 ,这是为什么呢?

因为宏参数在进行替换的时候,是直接采取替换的,也就是说,会被替换成:a + 1 * a + 1,由于乘法的优先级是大于加法的,所以会采取 4 + 4 + 1 的运算模式,所以算出来的结果就是 9

要解决这个问题,我们需要在宏中添加括号:

 #define SQUARE(x)  (x) * (x)

这样算出来的结果才是 25

在这个定义中我们使⽤了括号,想避免之前的问题,但是同时这个宏可能会出现新的错误

例如:

 int a = 5;
 printf("%d\n" ,10 * DOUBLE(a));

我们想要打印 100 到屏幕上,但我们实际运行程序的时候,打印的却是 55 

这个问题,的解决办法是在宏定义表达式两边加上⼀对括号

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

带有副作用的宏参数

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

例如我们要写一个宏,其功能是求两个数的较大值:

#define MAX(X,Y) ((X)>(Y)?(X):(Y))

int main(void)
{
	int a = 1;
	int b = 2;
	int m = MAX(a++, b++);
	//int m = (a++,b++) ((a++)>(b++)?(a++):(b++));
	printf("m = %d\n", m);
	printf("a = %d\n", a);
	printf("b = %d\n", b);

	return 0;
}

我们可以发现 ++ 的操作在程序中执行的次数不止一次,这样就导致了结果存在问题

宏的替换规则

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

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

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

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

我们需要注意:

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

例如:

int m = MAX(a, MAX(2, 3));

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

宏与函数的对比

我们来比较一下函数和宏:

#define MAX(X,Y) ((X)>(Y)?(X):(Y))

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

宏通常被应⽤于执⾏简单的运算,当执行较为简单的运算的时候,宏相较于函数更加具有优势,原因有两点:

1.通过 函数栈帧的创建与销毁 的知识,我们可以知道,函数在调用函数、执行运算、返回函数的时候都需要花费时间,宏在执行小型运算时所需要i的时间相较于函数使用的时间、空间会更少,所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹

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

和函数相⽐宏的劣势:

1.每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度

2.宏是没法调试的

3.宏由于类型⽆关,不够严谨

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

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

例如:

#define Malloc(n,type) (type*)malloc(n*sizeof(type))

int main(void) 
{
	int* p = (int*)malloc(10 * sizeof(int));
	Malloc(10, int);
	return 0;
}

宏和函数的对比:

属性#define定义宏函数
代码长度每次使用的时候,宏代码都会被插入程序,程序长度会大幅增加函数代码只出现在一个地方,每次使用的时候去那个地方调用对程序长度影响不大
执行速度更快因为有函数的调用与返回,相对较慢
操作符优先级需要多加括号,因为操作符优先级的缘故,不加括号往往会造成无法预料的结果参数只在调用的时候求值一次,他的结果直接传递给函数,表达式的取值更容易预测
带有副作用的参数参数可以被替换到函数的多个位置,可能导致宏的参数被多次计算,产生不可预料的结果参数只在调用的时候求值一次,他的结果直接传递给函数,表达式的取值更容易预测
参数类型宏的参数与类型无关函数的参数与类型有关
调试宏不方便调试函数可以逐语句调试
递归不支持递归支持递归

拓展:在C++中,有一个函数叫做内联函数(inline),这个函数既具有函数的特点,又具有宏的特点

# 以及 ##

# 运算符

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

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

直接写出概念可能有点难以理解,下面我们来举例说明:

我们首先铺垫一下:

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

如上面的代码,这两个字符串本质上是相同的

再看一下这个代码:

int main(void)
{
	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 %f\n", f);

	return 0;
}

这三个打印函数的格式是非常相似的,那么我们此时就考虑到另一个问题:我们能不能把它封装成一个函数呢?或者说可不可以把它写成一个宏呢?答案是可行的

#define Print(n,format)		printf("the value of n is "format"\n", n)

int main(void)
{
	int a = 1;
	Print(a,"%d");

	int b = 20;
	Print(b,"%d");

	float f = 5.6f;
	Print(f,"%f");

	return 0;
}

我们根据前置的铺垫可以写出这样的代码,但当我们在运行程序的时候发现了这样的一个问题:

我们可以观察到:其中的 n 并没有被替换,此时我们就需要用到 # 操作符了,# 操作符可以让参数直接转换成字符串

#define Print(n,format)		printf("the value of "#n" is "format"\n", n)

int main(void)
{
	int a = 1;
	Print(a,"%d");

	int b = 20;
	Print(b,"%d");

	float f = 5.6f;
	Print(f,"%f");

	return 0;
}

## 运算符

## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符

## 被称为记号粘合,这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的

我们同样的进行举例说明:

如果我们想写⼀个函数求2个数的较⼤值的时候,不同的数据类型就得写不同的函数

例如:

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

但是这样操作下来程序会显得十分冗杂,此时我们可以用##来处理,换一个写法:

#define GENERIC_MAX(type)								\
		type type##_max(type x,type y)					\
		{												\
			return x>y?x:y;								\
		}

//定义函数
GENERIC_MAX(int);
GENERIC_MAX(float);


int main(void)
{
	int r1 = int_max(3, 5);
	printf("%d\n", r1);

	float r2 = float_max(3.1f, 5.2f);
	printf("%f\n", r2);

	return 0;
}

此时我们用 ## 操作符把位于两端的符号合并成为了一个符号,并由此来定义一个函数

命名约定

因为宏和函数的使用语法很相似,所以在我们书写宏的时候,通常会这样的习惯:

1.将宏的名字全部大写

2.函数的名字不要全部大写

这样我们能更好的分辨宏和函数,当然,有时会存在着特殊情况;例如 offsetof 是一个宏,它是用来计算结构体成员相较于结构体起始位置的偏移量的宏,他虽然是宏但是它全是小写

#undef 的使用

#undef 指令⽤于移除⼀个宏定义,例如:

#define MAX 100

int main(void)
{
	printf("%d\n", MAX);
#undef MAX
	printf("%d\n", MAX);
	return 0;
}

此时我们移除了 MAX 所以会报错

命令行定义

我们在 gcc 编译器上可以在命令行里指定变量的数据,当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处,(假定某个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个机器内存⼤些,我们需要⼀个数组能够⼤些)因为我当前的环境是 VS 而且该内容并不是很重要,所以仅仅作了解就行

条件编译

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

对于某些调试性的代码,直接删除很可惜,但是保留下来又很碍事,所以我们此时可以选择性的编译,例如:

#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 //__DEBUG__
    }
    return 0;
}

假如我们不需要使用 printf 函数,我们可以注释掉 

#define __DEBUG__

这样当我们在运行代码的时候, printf 的指令就不会被执行

下面我们来盘点一下常见的条件编译指令:

1.单分支的条件编译语句           

                 #if 常量表达式

        //...

                #endif

例如:

int main(void)
{
#if 0
	printf("haha\n");
#endif
	return 0;
}

这样就不会执行打印 haha 的操作,只有满足 if 后面的条件时,里面的语句才会被执行

2.多分支的条件编译语句

使用 #elif 和 #else 来进行多分支的条件编译语句

#define M 2

int main(void) 
{
#if M==0
	printf("haha\n");
#elif M==1
	printf("hehe\n");
#elif M==2
	printf("hello world\n");
#else
	printf("luelue");
#endif
	return 0;
}

3.判断是否被定义

#define MAX 0

int main(void)
{
#if defined(MAX)
	printf("haha\n");
#endif
	return 0;
}

使用 #if defined( ) 可以判断是否被定义,他还有另外一这种写法:

​
#define MAX 0

int main(void)
{
#ifdef MAX
	printf("haha\n");
#endif
	return 0;
}

​

两者的逻辑都是一模一样的,若是想写不被定义的代码,只需要在 #define 前加上 !,或者写作 #ifndef

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

当我们代码的长度不长时,我们很难用到条件编译指令,当我们的代码很长,且想要实现多平台的功能时,就经常会用到条件编译指令

头文件的包含

我们知道,我们可以自己创造头文件

#pragma once

int Add(int x, int y) 
{
	return x + y;
}

该头文件里面的内容可以在其他其他文件里被使用,我们通常认为我们自定义的头文件需要用 " " 包含 而不是使用 < >

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>

#include "test.h"

int main(void)
{
	int a = 1;
	int b = 2;
	int c = Add(a, b);
	printf("c = %d\n", c);
	return 0;
}

其实不然," " 和 < > 的不同仅仅在于他们的查找策略不同,二者使用过程中,并不存在绝对的错误

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

也就是说,库文件可以用  " " 查找。可是,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件了

嵌套文件包含

假设头文件被多次包含会怎么样?

如果test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。 如果test.h⽂件⽐较⼤,这样预处理后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家都能使⽤,⼜不做任何的处理,那么后果真的不堪设想。 如何解决头⽂件被重复引⼊的问题呢?

答案是使用条件编译

#ifndef __TEST_H__
#define __TEST_H__


int Add(int x, int y) 
{
	return x + y;
}

#endif

这样做就只会被包含一次,或者使用:

#pragma once

结尾

条件编译指令的内容很多,远远不止我上面归纳的这些,感兴趣的朋友可以自行去了解,我这里便不再作过多的讲解了,谢谢您的浏览

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/517172.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

拓扑排序--有向无环图中一个节点的所有祖先

题目描述 给你一个正整数 n &#xff0c;它表示一个 有向无环图 中节点的数目&#xff0c;节点编号为 0 到 n - 1 &#xff08;包括两者&#xff09;。 给你一个二维整数数组 edges &#xff0c;其中 edges[i] [fromi, toi] 表示图中一条从 fromi 到 toi 的单向边。 请你返…

基于SpringBoot和Vue的金融融资管理系统的设计和实现【附源码】

1、系统演示视频&#xff08;演示视频&#xff09; 2、需要交流和学习请联系

图像处理入门 3(how to get the pixel pitch / 如何获得单个像素的尺寸)

在这里一节里面&#xff0c;将记录如何获得一个相机传感器中单个像素点的尺寸&#xff0c;为了实现不同相机照片之间的匹配。 如果我们知道了相机传感器的尺寸和分辨率的大小&#xff0c;自然就可以求出单个像素的大小。 在这里插入图片描述&#xff1a; 如何获得相机传感器的…

读《Spring实战》:面向切面

AOP术语 通知&#xff08;Advice&#xff09; 在AOP中&#xff0c;切面的工作被称为通知&#xff0c;也就是通知就是具体要干的工作。 spring中有5中通知&#xff1a; 前置通知&#xff1a; 在目标方法之前调用通知功能后置通知&#xff1a; 在目标方法之后调用通知功能返回…

SQL Server维护计划

目录 1.概述 2.启动SQL Server 代理服务 3.制定维护计划 4.验证维护计划 5.删除维护计划 1.概述 此文还是存货哈&#xff01; SQL Server 2008 R2维护计划。 2.启动SQL Server 代理服务 在设置维护计划之前&#xff0c;必须先确保SQL Server 代理服务已启动。启动方法如…

FastAPI Web框架教程 第12章 异步async-await

12-1 fastapi是异步Web框架 从本教程开篇&#xff0c;我们就说FastAPI这个web框架是异步框架&#xff0c;那它到底是如何体现异步的呢&#xff1f; 想要学习使如何使用FastAPI的异步功能&#xff0c;那就必须要先了解什么是异步&#xff0c;什么是asyncio、async/await 【基…

BoostCompass —— 搜索引擎

文章目录 一、项目简介二、Boost库简介1. 简介2. Boost 库的特点 三、项目主要模块1. 网页内容获取&#xff0c;数据预处理模块2. 建立正排索引和倒排索引&#xff0c;项目核心模块3. 编写 http_server 模块&#xff0c;进行网络开放 四、项目功能预览1. 项目文件预览2. 项目执…

基于储能电站服务的冷热电多微网系统 双层优化配置

目录 一、主要内容 二、部分代码 三、程序结果 四、下载链接 一、主要内容 随着储能技术的进步和共享经济的发展&#xff0c;共享储能电站服务模式将成为未来用户侧储能应用的新形态。提出基于储能电站服务的冷热电联供型多微网系统双层优化配置方法。 首先&#xff0c;提出…

【深入理解计算机系统第3版】有符号数和无符号数转换以及移位运算练习题2.23

题目 考虑下面的C函数&#xff1a; int fun1(unsigned word) {return (int) ((word << 24) >> 24); }int fun2(unsigned word) {return ((int) word << 24) >> 24; } 假设一个采用补码运算的机器上以32位程序来执行这些函数。还假设有符号数值的右移…

C易错注意之const修饰指针,含char类型计算,位段及相关经典易错例题

目录 前言 一&#xff1a;const修饰指针 1.const修饰变量 2.const 修饰指针 1.const int*p&m; 2. int* const p&m; 3. int const *p&m; 4. int const *const p&m; 5.总结 总之一句话为&#xff1a;左定值有定向 二&#xff1a;关于计算中char类型…

前端开发基础(HTML5 + CSS3)【第一篇】:HTML标签之文字排版、图片、链接、音频、视频 涵盖了两个综合案例 做到了基础学得会,实战写的出

点击前往前端开发基础专栏&#xff1a; 文章目录 HTML5 CSS3 开发一、开发环境搭建下载 VS Code1. 2 插件的下载1.3 项目和文件的下载 二、 什么是 HTML2.1 标签的语法2.2 代码演示&#xff1a;2.3 小结 三 、HTML基本骨架3.1 快捷键生成HTML骨架3.2 代码展示3.3 小结 四、标…

AI绘画:实例-利用Stable Diffusion ComfyUI实现多图连接:区域化提示词与条件设置

在Stable Diffusion ComfyUI中&#xff0c;有一种高级技巧可以让用户通过细致的区域化提示词来控制图像的不同部分&#xff0c;从而实现多图连接的效果。这种方法允许艺术家在同一画布上展现多个场景&#xff0c;创造出富有层次和故事性的图像。以下是实现这一效果的详细步骤。…

Transformer模型-Multi-Head Attention多头注意力的简明介绍

今天介绍transformer模型的Multi-Head Attention多头注意力。 原论文计算scaled dot-product attention和multi-head attention 实际整合到一起的流程为&#xff1a; 通过之前文章&#xff0c;假定我们已经理解了attention&#xff1b;今天我们按顺序来梳理一下整合之后的顺序。…

与机器对话:ChatGPT 和 AI 语言模型的奇妙故事

原文&#xff1a;Talking to Machines: The Fascinating Story of ChatGPT and AI Language Models 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 从 ELIZA 到 ChatGPT&#xff1a;会话式人工智能的简史 会话式人工智能是人工智能&#xff08;AI&#xff09;的一个分…

三子棋(C游戏)

文章目录 三子棋的描述思路关键代码运行代码 三子棋的描述 三子棋是一种民间传统游戏&#xff0c;又叫九宫棋、圈圈叉叉棋、一条龙、井字棋等。游戏分为双方对战&#xff0c;双方依次在9宫格棋盘上摆放棋子&#xff0c;率先将自己的三个棋子走成一条线就视为胜利&#xff0c;…

Flink运行机制相关概念介绍

Flink运行机制相关概念介绍 1. 流式计算和批处理2. 流式计算的状态与容错3. Flink简介及其在业务系统中的位置4. Flink模型5. Flink的架构6. Flink的重要概念7. Flink的状态、状态分区、状态缩放&#xff08;rescale&#xff09;和Key Group8. Flink数据交换9. 时间语义10. 水位…

【TSP旅行商问题】改进的大邻域搜索算法LNS

课题名称&#xff1a;基于改进的大规模邻域搜索算法LNS求解TSP问题 版本时间&#xff1a;2024-04-01 程序运行&#xff1a;直接运行LNS_TSP.m 文件即可 代码获取方式&#xff1a; QQ&#xff1a;491052175 VX&#xff1a;Matlab_Lover 模型介绍&#xff1a; 第一步&…

grep无法使用完整的正则表达式

问题描述 grep无法使用完整的正则表达式&#xff0c;比如前置断言、后置断言、\d和\t、\n等 问题原因 使用了扩展正则&#xff0c;而不是perl正则。规则和perl正则不同 从文档上讲得很清楚&#xff1a; -E PATTERN is an extended regular expression 他是扩展表达式&#…

ChatGPT 之联盟营销

原文&#xff1a;ChatGPT for Affiliate Marketing 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第二章 制定转化对话 制定转化对话是每个营销人员和企业所有者都应该掌握的关键技能。它涉及创建和传递引人入胜的信息&#xff0c;吸引您的受众并激励他们采取行动。…

vue给input密码框设置眼睛睁开闭合对于密码显示与隐藏

<template><div class"login-container"><el-inputv-model"pwd":type"type"class"pwd-input"placeholder"请输入密码"><islot"suffix"class"icon-style":class"elIcon"…