23 预编译详解

news2024/12/26 14:12:20

目录

一、预定义符号

二、#define定义常量

三、#define定义宏

四、带有副作用的宏参数

五、宏替换的规则

六、宏函数的对比

七、#和##

 (一)#运算符

(二)##运算符

八、命名约定

九、#undef

十、命令行定义

十一、条件编译

十二、头文件的包含

 (一)头文件被包含的方式

1、本地文件包含 " "

2、库文件包含 < >

(二)嵌套文件包含

十三、其他预处理指令


一、预定义符号

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

        如下所示:

__FILE__ //代表进⾏编译的源⽂件
__LINE__ //代表⽂件当前的⾏号
__DATE__ //代表⽂件被编译的⽇期
__TIME__ //代表⽂件被编译的时间
__STDC__ //代表如果编译器遵循ANSI C,其值为1,否则未定义

        使用示例如下:

printf("file:%s line:%d\n", __FILE__, __LINE__);

二、#define定义常量

         基本语法如下:

#define 名字 替换名字的内容

        使用示例如下:

#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break 写上。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
 date:%s\ttime:%s\n" ,\
 __FILE__,__LINE__ , \
 __DATE__,__TIME__ ) 

        注意:

        ① 在预编译的时候,#define会展开,把名字改成替换名字的内容。

                如下所示:

//预编译前:
#define MAX 1000;

int max = MAX;

//预编译后:
// #define被删除,名字内容被替换
int max = 1000;

        ② 如果定义的【原符号】过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符),相当于续着写成一行。

        ③ 在 define 定义标识符的时候,建议不要在最后加上【 ; 】。

                如下示例:

#define MAX 1000;

if(条件)
 max = MAX;
else
 max = 0;

        如果是加了分号的情况,等替换后,if 和 else 之间就是2条语句(max = MAX;为一条,空语句;为一条),而没有大括号的时候,if 后边只能有一条语句。这里会出现语法错误。

三、#define定义宏

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

        下面是宏的申明方式:

#define 名字(参数列表) 替换名字的内容

        其中的【参数列表】是⼀个由逗号隔开的符号表,它们可能出现在【替换名字的内容】中。

        注意:

        参数列表的左括号必须与【名字】紧邻,如果两者之间有任何空白存在,参数列表就会被解释为【替换名字的内容】的一部分。

        宏的使用示例如下:

#define SQUARE(x) x * x

        这个宏接收一个参数 x ,如果在上述声明之后,若下面的代码写了 SQUARE( 5 );  那么预处理器就会用【5 * 5】这个表达式替换 SQUARE( 5 ) 。

        警告:宏存在一个问题,宏只会把参数原原本本地替换进文本,并不会先后计算,如下代码所示:

#define SQUARE(x) x * x

int a = 5;
printf("%d\n" ,SQUARE( a + 1) );

        乍一看,可能觉得这段代码将打印 36,事实上它将打印11,为什么呢?

        替换文本时,参数 x 被替换成 a + 1 ,所以这条语句实际上变成了:

printf ("%d\n",a + 1 * a + 1 );

        替换产生的表达式并没有按照预想的次序进行求值。

        在宏定义上加上两个括号,这个问题便轻松的解决了:

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

        这样预处理之后就产生了预期的效果:

printf ("%d\n",(a + 1) * (a + 1) );

        这里还有一个宏定义:

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

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

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

        替换之后如下:

printf ("%d\n",10 * (5) + (5));

        结果为:55。

        这个问题,的解决办法是在宏定义表达式两边加上一对括号就可以了。

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

        注意:

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

        使用宏的时候不要吝啬括号。

四、带有副作用的宏参数

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

        例如:

x+1;//不带副作⽤
x++;//带有副作⽤

        副作用参数示例如下:

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);

        替换之后为:

z = ( (x++) > (y++) ? (x++) : (y++));

        结果为:x=6 y=10 z=9。

五、宏替换的规则

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

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

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

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

        注意:

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

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

六、宏函数的对比

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

        比如在两个数中找出较大的一个时,写成下面的宏,更有优势一些。 

#define MAX(a, b) ((a)>(b)?(a):(b))

        那为什么不用函数来完成这个任务?

        原因如下:

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

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

        注意:宏与函数相比也有劣势:

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

        ② 宏是没法调试的。

        ③ 宏由于类型无关,也就不够严谨。

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

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

#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
 ...
//使⽤
 MALLOC(10, int);//类型作为参数
//预处理器替换之后:
 (int *)malloc(10*sizeof(int));

        参数与宏的对比:

七、#和##

 (一)#运算符

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

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

        使用示例如下:

#define PRINT(n) printf("the value of "#n " is %d", n);

        这里的 #n 代表的就是"n"。

        代码转换为:

#define PRINT(n) printf("the value of " "n" " is %d", n);

        这样就能打印:the value of n is 参数数值。

(二)##运算符

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

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

        想写一个函数求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); \
}

        这里 type##_max 相当于:type_max。

        在实际开发过程中##使用的很少,很难取出非常贴切的例⼦。

八、命名约定

        一般来讲函数的宏的使用语法很相似。所以语⾔本身没法帮我们区分⼆者。 那我们平时的⼀个习惯是:

把宏名全部大写;

函数名不要全部大写。

九、#undef

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

#undef NAME

        如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

十、命令行定义

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

        例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。

        示例:假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要⼀个很小的数组,但是另外⼀个机器内存大些,我们需要一个数组能够大些。

        代码如下:

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

        编译指令如下:

//linux 环境演⽰
gcc -D ARRAY_SIZE=10 programe.c

十一、条件编译

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

        条件编译:满足条件,参与编译;若不满足,就不参与编译。

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

        

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

        常见的条件编译指令:

1.
#if 常量表达式 //常量表达式为真就参与编译,否则就不参与
 //...
#endif
//常量表达式由预处理器求值。

如:
#define __DEBUG__ 1
#if __DEBUG__ //替换后为1,是真,下面的代码参与编译
 //..
#endif

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

3.判断是否被定义
#if defined(symbol)
上面代码等价于下面代码:
#ifdef symbol

#if !defined(symbol)
上面代码等价于下面代码:
#ifndef symbol

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

        注意:

        ① #if 后面一定要是常量表达式。

十二、头文件的包含

 (一)头文件被包含的方式

1、本地文件包含 " "

#include "filename"

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

        Linux环境的标准头文件的路径:

/usr/include

        VS环境的标准头文件的路径:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径

        注意按照自己的安装路径去找。 

2、库文件包含 < >

#include <filename.h>

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

        这样是不是可以说,对于库文件也可以使⽤ " " 的形式包含? 答案是肯定的,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

(二)嵌套文件包含

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

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

        一个头文件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较大。

test.c

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

int main()
{
    return 0;
}

test.h

void test();
struct Stu
{
    int id;
    char name[20];
};

        如果直接这样写,test.c文件中将test.h包含5次,那么test.h文件的内容将会被拷贝5份在test.c中。 如果test.h 文件比较大,这样预处理后代码量会剧增。如果工程比较大,有公共使用的头文件,被大家都能使用,又不做任何的处理,那么后果真的不堪设想。

        如何解决头文件被重复引入的问题?答案:条件编译

        每个头文件的开头写:

#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__

        或者

#pragma once

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

        一些笔试题:

        1. 头文件中的 ifndef/define/endif是干什么用的?

        2. #include <filename.h> 和 #include "filename.h" 有什么区别?

十三、其他预处理指令

#error
#pragma
#line
...
不做介绍,⾃⼰去了解。
#pragma pack() //修改默认对齐数


        以上内容仅供分享,若有错误,请多指正

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TCP协议中的可靠性机制

目录 确认应答 滑动窗口 快重传 流量控制 窗口探测 拥塞控制 延迟应答 捎带应答 总结 相较于UDP协议&#xff0c;TCP协议由于要确保通信过程中的可靠性与尽可能提高通信效率提供了很多可靠性机制&#xff0c;因此TCP比较复杂。 确认应答 滑动窗口 滑动窗口是发送方/接…

Jmeter执行多机联合负载

1、注意事项&#xff0c;负载机必须要安装jre&#xff0c;控制机则必须安装jdk。要配置同网段ip&#xff0c;双向关闭防火墙。 每个负载机要平均承担线程数。 具体执行事项查看上面截图所示&#xff0c;控制机和负载机配置。 2、先给负载机设置ip地址&#xff0c;保持与控制…

网络安全新视角:人工智能在防御中的最新应用

人工智能在网络安全中的最新应用 概述 人工智能&#xff08;AI&#xff09;在网络安全领域的应用正日益成熟&#xff0c;它通过机器学习和深度学习技术&#xff0c;为网络安全带来了革命性的变革。AI技术不仅能够自动化、智能化地检测、分析和应对安全威胁&#xff0c;还能够…

Transformer-BiLSTM神经网络多输入单输出回归预测的MATLAB实现

在现代人工智能和机器学习领域&#xff0c;深度学习模型已经成为解决复杂问题的重要工具。Transformer和双向长短期记忆网络&#xff08;BiLSTM&#xff09;是两种非常强大的神经网络架构&#xff0c;它们在自然语言处理、时间序列预测、图像处理等多个领域表现出色。本文将介绍…

黑马JavaWeb企业级开发(知识清单)07——Ajax、Axios请求、前后端分离开发介绍、Yapi配置步骤

文章目录 前言一、Ajax1. 概述2. 作用3. 同步异步4. 原生Ajax请求&#xff08;了解即可&#xff09;5. Axios&#xff08;重点&#xff09;5.1 基本使用5.2 Axios别名&#xff08;简化书写&#xff09; 二、前后端分离开发1. 介绍1.1 前后台混合开发1.2 前后台分离开发方式&…

ChatGPT真的那么牛吗?

ChatGPT 很受欢迎&#xff0c;主要因为它在很多任务上表现出色&#xff0c;比如回答问题、写作、编程辅助等等。它的强大之处在于它可以理解和生成与上下文相关的自然语言文本&#xff0c;使得它在许多领域中都有用武之地。 和咱国内的文心一言一比较比较就知道了 不抖机灵&…

史上最全软件测试面试题集(含答案),进大厂涨薪必备,赶紧收藏

前阵子一位读者告诉我&#xff0c;某位大厂HR给他发了我之前做的面试题答案合集。 这个消息让我开心了一整天&#xff0c;因为这说明我之前做的面试题系列真的能帮助到部分测试同学&#xff0c;也算是侧面得到了一种认可吧。 今天写的这份面试题我之前就整理分享过&#xff0…

HTB-Explosion(rdp连接)和preignition(目录遍历)

前言 各位师傅大家好&#xff0c;我是qmx_07&#xff0c;今天给大家讲解Explosion靶机 - Explosion 渗透过程 信息搜集 发现服务器开起了3389端口远程服务 远程连接rdp服务 xfreerdp /v:10.129.172.157 /u:Administrator /p: /v 主机名 /u 用户名 /p密码 这篇靶机是对rdp服…

问题记录:树莓派3B+安装OpenMediaVault(OMV)后无WiFi连接处理

目录 实验环境参考教程安装前直接避免出现该问题的方法问题&#xff1a;安装完OpenMediaVault后&#xff0c;此前已配置好的WiFi&#xff0c;无法正常连接解决方法 OpenMediaVault 登录 实验环境 时间&#xff1a;2024年08月27日 硬件&#xff1a;树莓派3B 系统&#xff1a;Ra…

代码随想录算法训练营第三十九天| 图论理论基础

今天是图论入门的第一天&#xff0c;主要的学习内容主要是图论的理论基础。 图论理论基础 图的种类 图一般可以分为有向图和无向图&#xff0c;无向图是指边没有方向&#xff0c;有向图是指边有方向&#xff0c;其中&#xff0c;还存在一种加权有向图&#xff0c;指的是每条…

ATR - LSIs supported BIT

6.3.3 Global Interface bytes ts_102221v170400p.pdf

【人工智能】多模态AI:如何通过融合文本、图像与音频重塑智能系统未来

我的主页&#xff1a;2的n次方_ ​ 随着人工智能技术的飞速发展&#xff0c;多模态AI逐渐成为构建智能系统的重要方向。传统的AI系统通常依赖于单一模态的数据&#xff0c;如文本、图像或音频。而多模态AI通过结合多种数据类型&#xff0c;能够在更复杂的场景下提供更智能的解…

给自己复盘的随想录笔记-链表

链表 定义 数字域和指针域 种类 单链表&#xff0c;双链表&#xff0c;循环链表 链表的存储方式 链表是通过指针域的指针链接在内存中各个节点。 所以链表中的节点在内存中不是连续分布的 &#xff0c;而是散乱分布在内存中的某地址上&#xff0c;分配机制取决于操作系统…

FFmpeg下载与集成:.NET开发者入门指南

文章目录 前言一、FFmpeg下载1.访问FFmpeg官网2.选择合适的版本3.下载并安装 二、.NET项目中集成FFmpeg1.引入FFmpeg库2.编写代码调用FFmpeg3.处理FFmpeg输出和错误 三、进阶使用总结 前言 在音视频处理领域&#xff0c;FFmpeg无疑是一个强大的工具库&#xff0c;它支持几乎所有…

3min 告诉你!如何用波特五力分析模型解读行业格局?

在瞬息万变&#xff0c;竞争激烈的市场环境中&#xff0c;企业不仅要关注自身的发展&#xff0c;还需要时刻了解竞争对手的动态。通过波特五力模型来分析行业竞争状况&#xff0c;企业可以提前制定战略&#xff0c;获得竞争优势。 什么是波特五力模型&#xff1f; 波特五力模…

edge跟谷歌浏览器配置浏览器可跨域

在D盘下创建 vscode\test 文件夹 D:\xxxx\vscode\test 直接复制edge或者谷歌的一个快捷方式 名字就叫可跨域 可以随便取 右键选中目标浏览器点击属性 配置edge的目标路径 "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --disable-web-security …

【操作系统】实验:内存管理

目录 一、实验目的 二、实验要求 三、实验步骤 四、核心代码 五、记录与处理 六、思考 七、完整报告和成果文件提取链接 一、实验目的 1、加深对固定和动态分区分配内存管理方式的理解 2、理解动态分区分配算法&#xff1a;首次适应算法&#xff0c;循环首次适应算法&a…

文心一言 VS 讯飞星火 VS chatgpt (334)-- 算法导论23.1 3题

三、证明&#xff1a;如果图G的一条边(u&#xff0c;v)包含在图G的某棵最小生成树中&#xff0c;则该条边是横跨图G的某个切割的一条轻量级边。 文心一言&#xff1a; 为了证明这个命题&#xff0c;我们可以按照以下步骤进行&#xff1a; 第一步&#xff0c;假设图 G G G的一…

程序员对未来思考:面向AI编程;继2B,2C后,2AI开发工程师能否成为未来?

目录 前言当前AI状况幻觉问题上下文长度限制人在回路、提示工程师 AI对未来的影响AI改变了交互形式穿孔纸带命令行时代图形用户界面对话式交互 AI无法直接影响物理世界AI 与开发者AI2B\2C\2G\2AI2AI、AI的幻想 前言 在科技日新月异的今天&#xff0c;人工智能&#xff08;AI&a…

【三指针法】颜色分类

目录 1.前言2.题目简介3.求解思路4.示例代码 1.前言 2.题目简介 题目链接&#xff1a;LINK 3.求解思路 求解思路&#xff1a;三指针法 4.示例代码 class Solution { public:void sortColors(vector<int>& nums) {int i 0;int left -1;int right nums.size…