c语言-预处理详解【求个关注!】

预处理详解

  • 一 预处理阶段
    • 1 知识背景:
    • 2 预定义符号
    • 3 #define 定义常量
      • 当定义的标识符的值过长时:
      • 注意,如果#define定义的标识符,其值的末尾有; 则说明; 是该标识符值的一部分
    • 4 #define 定义宏
      • 宏的声明方式:
      • 当传入的参数是一个符号时:
      • 当传入的参数是一个表达式时:
      • 带有副作用的宏参数
      • 宏替换的规则
    • 5 宏与函数的对比
      • (1)在执行小型运算时,宏更有优势:
      • (2)宏对于函数的劣势
      • (3)总的对比
    • 6 #与##操作符
      • 6.1 #操作符
      • 6.2 ## 操作符
    • 7 #undef
    • 8 条件编译
      • 举例:
      • 常见的条件编译指令
    • 9 头文件的包含
      • 9.1 头文件包含的形式:
        • 9.1.1 本地文件的包含:
        • 9.1.2 库文件的包含
        • 两种包含方式查找策略的不同:
      • 9.2 嵌套文件包含


一 预处理阶段

1 知识背景:

一个c语言项目可能由多个.c文件与.h文件组成,那么它是如何转换成可执行程序的呢?
每一个.c文件单独通过编译器转换成目标文件
在windows系统中,目标文件的后缀为.obj,在linux系统中,目标文件的后缀为.o
通过链接器,将多个目标文件与链接库链接起来,生成可执行程序
链接库是指运行时的库(支持程序运行的基本函数的集合)或者第三方库(比如c语言标准库)
用高级编程语言所写的代码,要执行,需要在转换成操作系统可执行的二进制
代码
大体的转换流程是:编译-链接 转换成可执行程序
如果再细分,则是预处理-编译-汇编-链接 转换成可执行程序

如图:

在这里插入图片描述
本篇所写仅涉及与预处理阶段相关的内容【处理的代码方面】

2 预定义符号

c语言中定义了一些预定义符号,这些预定义符号可以直接使用,当然这些预定义符号也在预处理阶段处理!

//_ _FLIE_ _  //用来编译的源文件  
//__LINE__  //在当前文件中被编译的代码行号
//__DATE__  //文件被编译的日期
//__TIME__  //文件被编译的时间
//__STDC__  如果编译器完全实现 ANSI C 则值为1,否则未定义 
// 至于这个是关于什么的信息,我不清楚
#include<stdio.h>
int main() {
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
//	printf("%d\n", __STDC__);//在VS编译器中并没有STDC的规定!
	return 0;
}

在这里插入图片描述

在预处理之后,生成的文件test.i 为:
在这里插入图片描述

3 #define 定义常量

在程序中#define 定义的符号,在经过预处理之后,直接被其值替换
#include<stdio.h>
#define MAX 100           // 定义符号 值为数值常量
#define Str  "hello World"// 定义符号 值为字符串常量
#define MA  'a'           // 定义符号 值为字符
int main() {
// #define 可以定义符号,其值为常量
	printf("%d\n", MAX);
	printf("%s\n", Str);
	printf("%c\n", MA);
	return 0;
}

在这里插入图片描述

当定义的标识符的值过长时:

#define Print printf("file:%s\tline:%d\tDATE:%s\tTIME:%s\t",__FILE__,__LINE__,__DATE__,__TIME__);

#include<stdio.h>
//在默认情况下#define定义标识符只能在一行中定义,但如果想换行的话,则需在每一行末尾加上 \,意为这一行的扩展
#define Print printf("file:%s\t \
line:%d\t                   \
DATE:%s\t                    \
TIME:%s\t",                   \
__FILE__,__LINE__,__DATE__,__TIME__);
int main() {
   
	return 0;
}

注意,如果#define定义的标识符,其值的末尾有; 则说明; 是该标识符值的一部分

在这里插入图片描述

4 #define 定义宏

宏是带有参数的标识符,参数与标识符对应的值有关

宏的声明方式:

#define name(parament-Iist ) stuff 
其中stuff是宏体,parament-list代表用,隔开的参数集
注意:name与()的左括号,中间不能有间隔,否则编译器会认为(parament-Iist)是宏体的一部分

当传入的参数是一个符号时:

#include<stdio.h>
#define SQUARE(x) x*x
int main() {
	int c = SQUARE(5);
	printf("%d\n", c);
	return 0;
}

在这里插入图片描述

当传入的参数是一个表达式时:

#include<stdio.h>
#define SQUARE(x) x*x
int main() {

	int c = SQUARE(5+1);
	printf("%d\n", c);
	//预期值是36 ,但结果是;
	return 0;
}

在这里插入图片描述
这是因为,在经历预处理时,先将宏的形参变为实参变为:

// 5+1*5+1

再将转换后的文本替换到原来程序所在的文本的位置即:

#include<stdio.h>
#define SQUARE(x) x*x
int main() {

	int c = 5+1*5+1;
	//此时的结果为1*5 + 6 ==11
	printf("%d\n", c);
	
	return 0;
}

如何解决这个问题呢?只需要在宏的定义中,将每一个参数用()括起来,再将整个宏的体括起来
这是为了防止外在的程序直接与宏的部分体进行计算,例如:

#include<stdio.h>
#define ADD(x) x+x
int main() {
	int a = 5;
	//目标打印出50,但是结果为:
	printf("%d\n", ADD(5) * a);
	return 0;
}

在这里插入图片描述
解决方法:

#include<stdio.h>
#define ADD(x) ((x)+(x)) // 将参数括起来,再将整个宏体括起来
int main() {
	int a = 5;
	printf("%d\n", ADD(5) * a);
	return 0;
}

在这里插入图片描述

带有副作用的宏参数

当带副作用的参数(所谓副作用即在计算结束后,自身的值发生变化)在宏体中,超过一次,
就会可能导致错误的结果

例:

#include<stdio.h>
#define MAX(x,y) ((x)>(y)?x:y)

int main() {
	int a = 3;
	int b = 5;
	//如果是按照函数的思维,c 返回值应该是5,但是结果是:
	int c = MAX(a++,b++);
	printf("%d\n", a);
    printf("%d\n", b);
	printf("%d\n", c);
	return 0;
}

在这里插入图片描述

所以尽量不要用这种带有副作用的形参
x++//有副作用
x+1//挺好

宏替换的规则

有以下几个步骤:
1   首先扫描宏的参数,如果参数中有#define定义的符号或宏,则替换成对应的值
2   随后将替换文本插入到程序中原文本处
3   最后重新扫描结果文本,将#define定义的宏或符号转换成宏体或值

举例:

#include<stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
#define M 10 
int main() {
	int a = 3;
	int b = 5;
	
	int c = MAX(M, a);
	
	// 预处理时,1 首先扫描宏的参数,发现#define定义的M,将其转换成其值 10:
	         // 2   int c = MAX(10,a);这条语句代替int c = MAX(M,a);插入到程序中
	        // 3  最后再扫描结果文本 int c = MAX(10,a);发现MAX也是#define定义,
	        //转换为其对应的宏体
            //    ((10)>(a)?(10):(a))
// 最后变为:
  //int c = ((10)>(a)?(10):(a));

	return 0;
}
注意:1  宏参数或#define定义的符号中可以出现其他#define定义的符号或宏,但是对于宏不能出现递归
           2   当预处理器搜索#define定义的符号与宏时,字符串常量的内容并不被搜索!

5 宏与函数的对比

(1)在执行小型运算时,宏更有优势:

举例:

#include<stdio.h>
//#define MAX(x,y) ((x)>(y)?(x):(y))
int Max(int x, int y) {
	return x > y ? x : y;
}
int main() {
	int a = 3;
	int b = 5;
	//int c = MAX(a, b);
	int c = Max(a, b);
	return 0;
}

用函数所需的指令:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面是调用函数执行的指令,共用了11条

这是执行函数中的语句所用的指令:
在这里插入图片描述
这是返回函数值所执行的指令:
在这里插入图片描述
在这里插入图片描述
用宏需要的指令:
在这里插入图片描述

结果一目了然,说明当功能运算语句占比较小时,宏的执行更能够节省时间

(2)宏对于函数的劣势

1        除非宏比较小,否则宏替换到程序中的代码量极大,预处理之后会产生极大的冗余代码
        而函数则只有固定的一份代码
2       宏是无法调试的,因为我们看到的代码与预处理替换后的代码不同,我们不能查看到问题出现在哪里
3       宏的参数没有类型,不够严谨(当然这也是宏对于函数的优势,只能说是一把双刃剑)

(3)总的对比

在这里插入图片描述

6 #与##操作符

6.1 #操作符

此操作符的作用将宏的参数转换成**字符串字面量**(就是转换成参数的标识符的字符串形式) 
它仅允许出现在带参数的宏体中(或者说是宏的替换列表中)

举例:

// 在举例之前,先补充一个背景知识点
#include<stdio.h>
int main() {
	printf("helloworld\n");
	// 如果用两对双引号打印呢?
	printf("hello"   "world\n"); 
	return 0;
}

在这里插入图片描述

#include<stdio.h>
//用#操作符将参数n变为字符串字面量,即参数的标识符的字符串状态
#define DIGIT(n) printf("the value " #n " is %d\n",n); 
int main() {
	int a = 3;
	//在传入实参后,替换的文本实际为:
	//printf("the value " "a" "is %d\n", a);
	DIGIT(a);

	return 0;
}

在这里插入图片描述

6.2 ## 操作符

##操作符可以把它两边的符号合成一个符号,它允许宏定义从分离的文本片段
中创建标识符,这样的连接必须产生一个合法的标识符

在这里插入图片描述

7 #undef

用于移除一个宏定义
#undef NAME  // 如果移除一个宏定义,移除其名字即可
#include<stdio.h>
#define SQUARE(x) x*x
int main() {

//#undef SQUARE	
	printf("%d\n", SQUARE(3));

	return 0;
}

在这里插入图片描述

#include<stdio.h>
#define SQUARE(x) x*x
int main() {

#undef SQUARE	
	printf("%d\n", SQUARE(3));
	return 0;
}


在这里插入图片描述

8 条件编译

我们可以通过一些语句来设定一些代码是否进行编译,这样即避免删掉可能有用的代码,又防止暂时不需要的代码消耗资源

举例:

#include<stdio.h>
#define DEBUG
int main() {
	int arr[10] = { 0 };
	for (int i = 0; i < 10; i++) {
		arr[i] = i;

#ifdef DEBUG  // 如果DEBUG被定义则下面这条语句可执行,反之不可
		printf("%d\n", arr[i]);

#endif 
	}
	return 0;
}

常见的条件编译指令

1    格式: #if  常量表达式
        #endif
  #if作为开始的标志,#endif作为结束的标志,如果常量表达式的返回值>0
  则指令中间的代码块便可执行

举例:

#include<stdio.h>
#define M 10
int main() {
#if M>0
	printf("hehe\n");
#endif // M>0

	return 0;
}

在这里插入图片描述

   2   多分支的条件编译指令

举例:

#include<stdio.h>
#define M 3
int main() {
#if M==1
	printf("hehe");
#elif M==2
	printf("haha");
#else
	printf("wawa");
#endif // M=1
	return 0;
}

在这里插入图片描述

3       判断是否被定义的条件编译
#include<stdio.h>
#define DEBUG
int main() {
	int arr[10] = { 0 };
	for (int i = 0; i < 10; i++) {
		arr[i] = i;
//#if defined(symbol) #endif 组合 与#ifdef symbol #endif 组合的功能相同,
// 如果symbol被定义,则指令中的代码块可执行
		
#if defined(DEBUG)
//#ifdef DEBUG 
		printf("%d\n", arr[i]);

//#endif 
#endif 

	}
	return 0;
}

3.2    如果是不定义情况下可执行代码的条件编译:
#include<stdio.h>
#define M 10
int main() {
#ifndef M
	printf("haha");
#endif // !M

	return 0;
}

在这里插入图片描述

 4  嵌套指令,前面的几个指令可以嵌套,就像是if else语句一样,
 大家可以尝试一下

9 头文件的包含

头文件是存放函数声明的文件,让其他函数调用使用

9.1 头文件包含的形式:

9.1.1 本地文件的包含:
 当我们引用本地文件时,即自己写的文件,格式为:
 #include " FileName"

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

9.1.2 库文件的包含

当我们调用开发环境为我们提供的标准库函数时,就需要引用相应的头文件

格式为:#include <FileName>

比如调用下面的printf函数,就需要包含stdio.h文件
在这里插入图片描述

两种包含方式查找策略的不同:
1   对于用" " 包含头文件的形式,在查找相应的头文件时,先在本文件的
    目录中查找,如果找到了则包含进来,如果没找到则去存放库函数的
    位置所在的路径去查找。
2   而用<>包含头文件的形式,在查找相应的头文件时,直接去存放库函数
的位置所在的路径去查找
 所以库文件也可以用" " 来包含,但是执行起来比<>慢

举例1:在这里插入图片描述
找到了.h文件
在电脑中找到存放stdio.h头文件的路径
在这里插入图片描述

9.2 嵌套文件包含

当我们在调用头文件时,可能不止调用一个 ,如果出现这种情况:

在这里插入图片描述
如果文件的调用结构是这样的,那对于test.c来说,是调用了两次con.h
如果结构更复杂呢?会导致代码量的极大冗余
这个问题怎么解决呢?用条件编译!
举例:

#ifndef DEBUG
#define DEBUG
int Add(int x, int y);
#endif
// 这些代码的执行规则是:如果.h文件第一次被调用,那此时DEBUG还没有被定义
// 就定义DEBUG,然后将文件包含过去,即将声明赋值过去,当.h文件又被调用时,则
// 因为DEBUG已经被定义,所以指令中间的代码不被编译,不会被包含过去,而总项目
// 只需要声明这个.h文件一次即可,这样就避免了重复声明!

在这里插入图片描述
在这里插入图片描述
或者用

#pragma once 
来避免头文件的重复引入

在这里插入图片描述

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

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

相关文章

【系统分析师】数据库部分

文章目录 1、数据库模式2、数据库设计过程2.1ER模型 3、关系代数 ☆5、规范化理论☆5.1 非规范存在的问题5.2 相关概念5.3范式5.3.1 第一范式-1NF5.3.2 第二范式-2NF5.2.3 第三范式5.2.4 BC范式 5.4 函数依赖分解5.4.1保持函数依赖分解5.4.2 无损分解 5.5 Armstong公理系统 6、…

FANUC机器人单轴零点标定的具体方法(全轴零点标定不方便时可采用)

FANUC机器人单轴零点标定的具体方法(全轴零点标定不方便时可采用) 前面和大家分享了FANUC机器人进行零点标定的原因和方法,具体可参考以下链接中的内容:: FANUC机器人进行零点标定的目的和具体方法步骤详解

从零开始学习Linux(1)---基本命令(1)

1.学习准备 我学习Linux是使用xshell远程登录自己的云服务器来进行。 xshell是一个远程终端管理软件&#xff0c;下载官网&#xff1a; https://www.netsarang.com/products/xsh_overview.htm 下载安装的时候选择 "home/school"…

如何在OceanBase v4.2 中快速生成随机数据

在使用传统数据库如 MySQL 和 Oracle 时&#xff0c;由于缺乏多样化的随机数据生成方案&#xff0c;或者实现成本过高&#xff0c;构造随机数据的开发成本受到了影响。OceanBase在老版本中虽然有相应的解决方案&#xff0c;但语法复杂和性能较差等问题仍然存在。 现在&#xf…

主播美颜SDK:实现精细化美颜功能的关键技术分析

主播美颜SDK作为实现精细化美颜功能的关键技术&#xff0c;其背后蕴含着丰富的算法和工程技术。本文将对主播美颜SDK的关键技术进行深入分析&#xff0c;探讨其实现精细化美颜功能的原理与方法。 图像识别与面部分析 通过图像识别技术&#xff0c;SDK能够准确地识别出人脸位置…

策略模式类图与代码

某大型购物中心欲开发一套收银软件&#xff0c;要求其能够支持购物中心在不同时期推出的各种促销活动&#xff0c;如打折、返利(例如&#xff0c;满300返100),等等。现采用策略(Strategy)模式实现该要求&#xff0c;得到如图7.13 所示的类图。 【Java 代码】 import java.util…

数字时代的引领者:揭示Facebook的社交创新

随着信息技术的飞速发展&#xff0c;人们的社交方式也发生了巨大的变化。从最初的互联网聊天室到如今的社交网络平台&#xff0c;我们已经见证了数字社交的不断演变和发展。而随着区块链技术的兴起&#xff0c;Web3时代的到来将为数字社交带来全新的可能性和挑战。本文将探讨社…

【北京迅为】《iTOP-3588开发板系统编程手册》第4章 目录IO和文件属性

RK3588是一款低功耗、高性能的处理器&#xff0c;适用于基于arm的PC和Edge计算设备、个人移动互联网设备等数字多媒体应用&#xff0c;RK3588支持8K视频编解码&#xff0c;内置GPU可以完全兼容OpenGLES 1.1、2.0和3.2。RK3588引入了新一代完全基于硬件的最大4800万像素ISP&…

[MySQL]数据库原理8——喵喵期末不挂科

希望你开心&#xff0c;希望你健康&#xff0c;希望你幸福&#xff0c;希望你点赞&#xff01; 最后的最后&#xff0c;关注喵&#xff0c;关注喵&#xff0c;关注喵&#xff0c;大大会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的…

构建鸿蒙ACE静态库

搭建开发环境 根据说明文档下载鸿蒙全部代码&#xff0c;一般采取第四种方式获取最新代码(请保证代码为最新) 源码获取Windows下载编译环境 MinGW GCC 7.3.0版本 请添加环境变量IDE 可以使用两种 CLion和Qt,CLion不带有环境需要安装MinGW才可以开发,Qt自带MinGW环境&#xff0…

Itasca pfc3d/3dec/flac3d/massflow 9.0 授权

所有 Itasca 软件都建立在每个程序基础的共同元素层之上——无论程序使用何种数值方法或元素。因此&#xff0c;无论是使用 DEM 软件&#xff08;如 3DEC 或 PFC&#xff09;&#xff0c;还是使用 FLAC3D 等连续体软件&#xff0c;都会有许多流程、实用程序和功能是所有这些软件…

靠Python实现经济自由,学会了你也可以

不知道大家有没有注意到&#xff0c;最近关注的很多人都在聊“副业and兼职”这件事。 毕竟单一收入已经不能满足现代人的需求了。 对于普通人来说&#xff0c;想要跳出固定思维和舒适圈&#xff0c;相比于孤注一掷的创业&#xff0c;更推荐兼职。 很多人想要创业&#xff0c;…

【Qt】:对话框(二)

对话框 一.消息对话框&#xff08;QMessageBox&#xff09;1.自己构建2.使用静态函数构建 二.颜色对话框&#xff08;QDialog&#xff09;三.文件对话框&#xff08;QFileDialog&#xff09;四.字体对话框&#xff08;QFontDialog&#xff09;五.输入对话框&#xff08;QInputD…

JMM(Java Memory Model java内存模型

目标&#xff1a; 搞清楚高并发场景下&#xff0c;java内存模型是怎么支持的&#xff0c;对象在内存中是怎么布局的&#xff1f; 目录 目标&#xff1a; 搞清楚高并发场景下&#xff0c;java内存模型是怎么支持的&#xff0c;对象在内存中是怎么布局的&#xff1f; 1.硬件层…

华火电焰灶全国经销商加盟_优势怎么样_费用多少_华焰天下

随着科技的不断进步&#xff0c;电焰灶作为现代厨房的重要设备&#xff0c;其市场需求持续增长。华火电焰灶&#xff0c;凭借其独特的技术优势和广泛的市场前景&#xff0c;吸引了众多投资者的目光。本文将从华火电焰灶的优势、加盟费用以及华焰天下的机遇三个方面&#xff0c;…

配置IP地址并验证连通性

1.实验环境 主机 A和主机 B通过一根网线相连&#xff0c;如图6.13所示。 图6.13 实验案例一示意图 2.需求描述 为两台主机配置!P地址&#xff0c;验证P地址是否生效&#xff1b;验证同一网段的两台主机可以互通&#xff0c;不同网段的主机不能直接互通。 3.推荐步骤 为两台…

第四百五十四回

文章目录 1. 问题描述2. 优化方法2.1 缩小范围2.2 替代方法 3. 示例代码4. 内容总结 我们在上一章回中介绍了"如何获取AppBar的高度"相关的内容&#xff0c;本章回中将介绍关于MediaQuery的优化.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 问题描述 我们在…

自己开发的App如何上架,详细解读App上架操作流程

对于企业或个人开发的App&#xff0c;上架是必经之路。然而&#xff0c;许多人不清楚如何进行App上架。工信部在2023年规定&#xff0c;App必须备案才能上架。那么&#xff0c;让我们一起了解App上架流程吧。 1. 准备上架所需材料 在上架App之前&#xff0c;需要准备应用图标…

【学习笔记十三】EWM常见上架策略介绍

一、手工维护上架策略 系统不确定Storage type 和 bin&#xff0c;需要在创建仓库任务时或者确认仓库任务时手工输入仓位 1.后台配置-定义存储类型的类型0010 ①存储行为&#xff1a;标准仓位 ②入库规则&#xff1a;空仓未或添加至现有库存/空仓位 ③通用仓库任务&#x…

【学习笔记十二】EWM上架仓位确定逻辑及操作演示

一、前言 关于EWM上架仓位确定的过程&#xff0c;我在【学习笔记十一】EWM上架目标仓位确定过程及配置-CSDN博客中讲到了 EWM根据仓库类型&#xff08;storage type&#xff09;、仓库分区&#xff08;storage section&#xff09;和上架策略&#xff08;putaway strategies&…