预处理/预编译详解(C/C++)

        在上一篇的bolg中的编译与链接中提到过预处理,但只是较为简单的讲解,本篇将会对预处理进行详细的讲解。

        其中在预处理中很重要的一个一个知识点是#define定义常量与宏,还区分了宏与函数的区别,以及#和##符号,还涉及条件编译头文件的包含等等。

        如果想看对应的讲解可以直接看文章旁边的目录。

1.预定义符号

        在预处理详解中,首先就是关于一些预定义符号,其中包含以下的预定义符号:

__FILE__   //进行编译的源文件的地址
__LINE__   //文件当前的行号
__DATA__   //文件被编译的日期
__TIME__   //文件被编译的时间
__STDC__   //编译器是否遵循ANSI C,是输出1,不是输出0(vs2022不支持/gcc环境支持)

        对于这些预定义符号的使用如下:

 2.#define定义常量

         在预处理中对于#define的处理也是直接将#define所定义的直接转换,特别是一些常量定义,#define对于常量定义的语法如下:

#define name stuff

        例如: 

#define MAX 1000
#define PRINT_DEBUG printf("%s\t%d\t%s\t%s\n",	__FILE__, __LINE__, __DATE__, __TIME__)  
//打印当前文件地址,当前代码行数,日期和时间
#define ElemType int     //将ElemType定义为int类型
#define forever for(;;)  //创建一个无限循环
#define reg register     //为register(寄存器)这个关键字,创建一个简短的名字

        如下:

        在预处理阶段都将对应的#define定义的直接替换掉,这也是在预处理阶段的一个特点,操作系统只会将#define定义的直接替换而不会做计算。 

 3.#define定义宏

        #define机制包括了一个规定,允许把参数替换到文本中(如上),这种实现通常被称为宏或者定义宏。以下为宏的申明方式:

#define name( parament-list ) stuff

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

        注:参数列表的左括号必须与name紧邻,不能存在空格,若出现空格,参数列表就会被解释为stuff的一部分。 

         以下举出一个简单的宏:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

#define SQUARE(X) X*X  //计算一个数的平方

int main() {
	printf("%d\n", SQUARE(5));
	printf("%d\n", SQUARE(4+1));

	return 0;
}

        以下为计算结果: 

        如上,计算出的结果为25,9。那么为什么不是25,25呢,因为在预处理阶段,对于#define定义的宏,只是直接将参数进行替换,对于第一个计算的是5*5,而对于第二个计算的是4+1*4+1,所以计算出的结果为25,9。由这个例子也可以得出,对于宏的定义我们需要更严谨,所以在定义宏时,最好将每个参数都加上括号,防止被操作符的优先级所影响。所以我们对以上的可以修改为:

        这样可以使得宏的定义更加严谨。 但是这样的定义方式真的够严谨吗,在给出下面这个宏定义:

        计算出的结果为10和55,那么为什么不是10和100呢,是应为第二个计算的是10*5+5,所以我们不仅仅需要对每一个元素加上一个括号,我们还需要对计算出的结果加上一个括号,这样的计算结果才能保证真正的正确。 

4.带有副作用的宏参数

        对于以上的一些参数只要我们宏定义正确,那么就可以计算正确的结果,但是在宏定义中也存在一些一定会出现错误的形式,以下将列举:

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

int main() {
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n", x, y, z);
	printf("%lf\n", MAX(9, 30.5));
	printf("%c\n", MAX(3, 'c'));
	return 0;
}

        以下为计算结果: 

        计算出的结果为什么x=6,y=10,z=9呢?还是按照预处理的步骤分析,先直接将x++与y++带入进去,然后在分别计算。 如如下步骤

        所以计算出的结果为z=9 x=6 y=10。在图中我们不仅发现宏计算出的结果,不会受到类型的限制,也就是说,关于宏的参数我们可以传入许多不同类型的参数。这也是宏很重要的一点。

5.宏替换的规则/宏与函数的区别

5.1宏替换的规则

        我们总结以上的规律可以得出,在程序中扩展#define定义符号和宏时,需要涉及以下结果步骤:

        1.在调用宏时,首先对参数进行检查,看看是否包含#define定义的符号如果是,那么这些符号首先被替换。

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

        3.再次对结果文件进行扫描,看看是否包含任何由#define定义的符合。如果发现,则重复以上流程。

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

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

5.4宏与函数的对比

        宏通常被用于执行简单的运算,比如以上找最大值的运算,使用宏更有优势。

        那么为什么不使用函数来完成找最大值呢?如下:

        当我们分别用宏和函数求解出最大值的时候,我们发现在汇编代码中,宏下面只有一行,而在函数下面则由4行,所以说宏的计算会更少,对于一些逻辑简单的运算,我们可以用宏来实现。

        所以可以得出以下对比的几点:

        宏相对函数的优点

        1.用于条用函数和从函数返回的代码可能比实际执行这个小型计算工作所需的时间更长。所以宏比函数在程序的规模和速度方面更胜一筹

        2.函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之对于宏,可以适用于整型、长整型、浮点型等可以用>来比较的类型。宏是类型无关的

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

#define MALLOC(num,type) (type*)malloc(num*sizeof(type))

int main() {
	int* arr = MALLOC(10, int);
	if (arr == NULL) {
		perror("MALLOC:");
		return 1;
	}
	for (int i = 0; i < 10; i++) {
		arr[i] = i;
	}
	for (int i = 0; i < 10; i++) {
		printf("%d ",arr[i]);
	}
	return 0;
}

        宏相对函数的缺点

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

        2.宏不可以调试。

        3.宏由于类型无关,相对来说不够严谨。

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

6.#和##

6.1 #运算符

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

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

        如下操作:

        #运算符可以直接将#n替换为“a”,直接将整个字符串连接起来。运算出的代码就如以上的代码相同。

6.2 ##运算符

        ##运算符可以把位于它两边的符号合成一个符号,允许宏定义从分离的文本片段创建标识符。##符号被称为记号粘合。(就是将左右两边的符号联合在一起)。

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

        如下,实现一个函数求解2个数较大值,不同类型的数据类型写出不同的函数。

#define GENERIC_MAX(type)\      //“\”表示连接符,与下一行直接连接
type type##_max(type x,type y)\
{							\
	return x>y?x:y;			\
}

GENERIC_MAX(int)
GENERIC_MAX(float)

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

       

        通过如上的操作,我们就可以得出多个同结构的函数。但是这样的操作在实际的开放工程中使用的很少。 

 7.条件编译

        在编译一个程序的时候我们如果将一条(一组)语句编译或者放弃,我们可以使用条件编译指令。比如一些调试性语句,删除可惜,保留又碍事。我们就可以使用条件语句,如下:

// #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 ",arr[i]);
        #endif
    }
    return 0;
}

        对于如上的结果,可以知道#ifdef与#endif的功能为判断一个宏是否被定义,如果被定义,则执行,如果没有被定义,则忽略 #ifdef与#endif 之间的代码。

        以下还有许多的定义,我就一下将其列举出来,不做过多的解释:

#define MAX 1

int main() {
    int i = 0;
    int arr[10] = { 0 };

    for (i = 0; i < 10; i++) {
        arr[i] = i;

        #if 1           //常量表达式的条件编译
        printf("yes\n");
        #endif
    
        #if MAX==1      //多分支的条件编译
        printf("MAX=%d", 1);
        #elif MAX==2
        printf("MAX=%d", 2);
        #else
        printf("MAX=%d", 3);
        #endif
        printf("MAX未知\n");

        #ifdef MAX       //判断是否被定义
        printf("MAX被定义");
        #endif
        #ifndef MAX      //判断是否没有被定义
        printf("MAX未被定义\n");
        #endif

        #if defined(MAX)  //嵌套定义
            #if MAX==1      //多分支的条件编译
            printf("MAX=%d", 1);
            #elif MAX==2
            printf("MAX=%d", 2);
            #else
            printf("MAX=%d", 3);
            #endif
            printf("MAX未知\n");
        #elif defined(DEBUG)
            #ifdef MAX       //判断是否被定义
            printf("MAX被定义");
            #endif
            #ifndef MAX      //判断是否没有被定义
            printf("MAX未被定义\n");
            #endif
        #endif

    }

    return 0;
}

        其中值得注意的一点是:每一个条件编译都需要与一个#endif相对应。 

8.头文件的包含

8.1 头文件的包含

        头文件的包含包括本地头文件库文件的包含

        本地头文件的包含一般指的是由自己定义在当前路径的头文件,一般形式如下:

#include "filename"

        库文件的包含的一般形式如下: 

#include <filename.h>

        关于这两种包含形式的区别在于:

        1.包含头文件的形式不一样,一个使用 ”“ 另一个使用 <>。

        2.查找策略不同,<> 包含的头文件直接在标准路径查找,"" 包含的头文件现在源文件当前路径查找,然后在到标准路径去查找。也就是说 "" 的查找方式其实是包含了 <> 的查找方式,但是一般采取相对应的头文件引用格式,若都采用 "" 会使效率变差。

8.2 嵌套文件的包含

        在同一路径中的多源文件中,可能多个源文件都包含了同一个头文件,那么就相当于一个头文件在预处理阶段被多次导入到内存中,即浪费空间又浪费时间,对于这样的问题,我们同样可以使用条件编译来处理。

        若多个源文件中包含同一个头文件,就用如下的形式表示,因为最后都会链接成一个.exe文件。

#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"


int main()
{
    return 0;
}

        对应的解决方案如下(只需要加入相应的条件编译即可): 

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#endif

int main()
{
    return 0;
}

        预编译阶段还有许多的知识,这里只列举出一些较为重要的内容。 

       

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

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

相关文章

ADA-YOLO:YOLOv8+注意力+Adaptive Head,mAP提升3%

生物医学图像分析中的目标检测和定位至关重要&#xff0c;尤其是在血液学领域&#xff0c;检测和识别血细胞对于诊断和治疗决策至关重要。虽然基于注意力的方法在各个领域中目标检测方面取得了显著的进展&#xff0c;但由于医学影像数据集的独特挑战&#xff0c;其在医学目标检…

【用队列实现栈】【用栈实现队列】Leetcode 232 225

【用队列实现栈】【用栈实现队列】Leetcode 232 225 队列的相关操作栈的相关操作用队列实现栈用栈实现队列 ---------------&#x1f388;&#x1f388;题目链接 用队列实现栈&#x1f388;&#x1f388;------------------- ---------------&#x1f388;&#x1f388;题目链…

vue2使用qiankun微前端(跟着步骤走可实现)

需求&#xff1a;做一个vue2的微前端&#xff0c;以vue2为主应用&#xff0c;其他技术栈为子应用&#xff0c;比如vue3&#xff0c;本文章只是做vue2一套的微前端应用实现&#xff0c;之后解决的一些问题。vue3子应用可以看我另一篇vue3vitets实现qiankun微前端子应用-CSDN博客…

IDEA 2022.3.3 安装教程

1.下载2022.3.3版本IDEA 链接&#xff1a;https://pan.baidu.com/s/1z-Yfl7fWHgqz8SQLn2-u0g?pwd949u 提取码&#xff1a;949u 2.安装 下载完成后&#xff0c;双击exe安装包&#xff0c; 点击next 3.选择方式3 4.将下面文件复制到任意位置&#xff08;不要有中文路径&…

❤ Uniapp使用二 ( 日常使用篇)

❤ Uniapp使用二 ( 日常使用篇) 一、表单 1、基础表单验证 form <form submit"formSubmit" reset"formReset"> <view class"uni-form-item uni-column"><view class"title">请选择类型{{selectvalue}}</view&…

VC++中使用OpenCV对原图像中的四边形区域做透视变换

VC中使用OpenCV对原图像中的四边形区域做透视变换 最近闲着跟着油管博主murtazahassan&#xff0c;学习了一下LEARN OPENCV C in 4 HOURS | Including 3x Projects | Computer Vision&#xff0c;对应的Github源代码地址为&#xff1a;Learn-OpenCV-cpp-in-4-Hours 视频里面讲…

【ubuntu】ubuntu 20.04安装docker,使用nginx部署前端项目,nginx.conf文件配置

docker 官网&#xff1a;Install Docker Engine on Ubuntu 一、安装docker 1.将apt升级到最新 sudo apt update2.使用apt安装 docker 和 docker-compose &#xff08;遇到提示输入y&#xff09; sudo apt install docker.io docker-compose3.将当前用户添加到docker用户组 …

网页设计(二)格式化文本、段落与列表

一、孔融让梨 【思政素材】 孔融&#xff0c;字文举&#xff0c;东汉时期中国山东曲阜人&#xff0c;是孔子的第二十世孙&#xff0c;是中国古代东汉末文学家。 孔融四岁的时候&#xff0c;和哥哥吃梨&#xff0c;总是拿小的吃。有人问他为什么这么做。他回答说&#xff1a;“…

Qt6入门教程 7:信号和槽机制(原理和优缺点)

目录 一.简介 二.信号和槽 1.信号和槽机制是类型安全的 2.信号和槽是松散耦合的 三.信号&#xff08;signals&#xff09; 四.槽&#xff08;slots&#xff09; 五.信号与槽的简单模拟 六.第三方信号槽实现 七.在Qt中使用第三方的Signals和Slots 八.总结一下优点和缺…

搜维尔科技:SenseGlove Nova 2力反馈技术手套,虚拟培训的沉浸感达到新高度!

SenseGlove Nova 2-虚拟培训的沉浸感达到新高度&#xff01; 通过集成主动接触反馈&#xff0c;Nova 2 使用户能够在手掌中感知虚拟现实物体的感觉。虚拟训练、研究和多人互动现在感觉比以往更加自然。这项创新增强了与整个手掌接触的任何虚拟物体的真实感。使用第一款也是唯一…

select子句简单查询

Oracle从入门到总裁:https://blog.csdn.net/weixin_67859959/article/details/135209645 目录 数据查询 起别名 连接 ​编辑 去重 ​编辑 另外补充几个不常用的命令 如果要进行查询,那么需要使用数据操纵语言&#xff08;Data Manipulation Language&#xff0c;DML&am…

mysql 下载和安装和修改MYSQL8.0 数据库存储文件的路径

一、第一步:下载步骤 下载链接&#xff1a;MySQL :: Download MySQL Installer 选择版本8.0.35&#xff0c;社区版&#xff0c; 点击 Download 下载 安装包 二、第二步:安装步骤 添加环境变量&#xff0c;C:\Program Files\MySQL\MySQL Server 8.0\bin 可以点开MySQL 8.0 Co…

三、基础篇 vue Class与Style绑定

操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是 attribute&#xff0c;所以我们可以用 v-bind 处理它们&#xff1a;只需要通过表达式计算出字符串结果即可。不过&#xff0c;字符串拼接麻烦且易错。因此&#xff0c;在将 v-bind 用于 class 和 style…

小程序基础学习(页面跳转传参)

目录 正向传参 原理&#xff1a;直接在url里面拼接参数即可 接受参数 ​编辑 已经跳转到的页面用onLoad函数来接受即可然后写回页面展示即可 逆向传参 原理&#xff1a;通过使用 getCurrentPages()这个方法来获取返回页面列表&#xff0c;然后再用页面.setData&#xff…

弟12章 网络编程

文章目录 网络协议概述 p164TCP协议与UDP协议的区别 p165TCP服务器端代码的编写 p166TCP服务器端流程 TCP客户端代码的编写 p167TCP客户端流程主机和客户端的通信流程 tcp多次通信服务器端代码 p168TCP多次通信客户端代码 p169UDP的一次双向通信 p170udp通信模型udp接收方代码u…

【C语言】ipoib驱动 - ipoib_cm_handle_rx_wc_rss

一、ipoib_cm_handle_rx_wc_rss函数定义 void ipoib_cm_handle_rx_wc_rss(struct net_device *dev, struct ib_wc *wc) {struct ipoib_dev_priv *priv ipoib_priv(dev);struct ipoib_cm_rx_buf *rx_ring;unsigned int wr_id wc->wr_id & ~(IPOIB_OP_CM | IPOIB_OP_RE…

Spring Boot框架中Controller层API接口如何支持使用多个@RequestBody注解接受请求体参数

一、前言 众所周知&#xff0c;在Spring Boot框架中&#xff0c;Controller层API接口编码获取请求体参数时&#xff0c;在参数上会使用RequestBody注解&#xff1b;如果一次请求中&#xff0c;请求体参数携带的内容需要用多个参数接收时&#xff0c;能不能多次使用RequestBody…

通过OpenIddict设计一个授权服务器02-创建asp.net项目

在这一部分中&#xff0c;我们将创建一个ASPNET核心项目&#xff0c;作为我们授权服务器的最低设置。我们将使用MVC来提供页面&#xff0c;并将身份验证添加到项目中&#xff0c;包括一个基本的登录表单。 创建一个空的asp.net core项目 正如前一篇文章中所说&#xff0c;授权…

智能时代,让AI为你撰写专业应用文

大家好我是在看&#xff0c;记录普通人学习探索AI之路。 何谓应用文&#xff1f;简单来说&#xff0c;应用文是指在日常生活中以及工作中撰写的&#xff0c;旨在传递信息、处理事务的一种文体类型。其范畴广泛&#xff0c;涵盖了诸如请假条、通知书、辞职信、检查报告、欠条、…

【控制篇 / 分流】(7.4) ❀ 01. 对指定IP网段访问进行分流 ❀ FortiGate 防火墙

【简介】公司有两条宽带&#xff0c;一条ADSL拨号用来上网&#xff0c;一条移动SDWAN&#xff0c;已经连通总部内网服务器&#xff0c;领导要求&#xff0c;只有访问公司服务器IP时走移动SDWAN&#xff0c;其它访问都走ADSL拨号&#xff0c;如果你是管理员&#xff0c;你知道有…