【C语言 |预处理指令】预处理指令详解(包括编译与链接)

目录

一、编译与链接

 1.翻译环境

          -预处理

          -编译

          -汇编

          -链接

2.执行环境

二、预定义符号

三、#define定义常量

四、#define定义宏

五、带有副作用的宏参数

六、宏替换的规则

七、 宏函数的对比

八、#和##

1.#运算符

2.##运算符

九、命名约定

十、#undef

十一、 命令行定义

十二、 条件编译

十三、 头文件的包含

1.本地头文件包含

2.库文件包含

十四、 其他预处理指令


一、编译与链接

在ANSI C的任何⼀种实现中,存在两个不同的环境

第一种呢就是翻译环境,顾名思义就是将源代码被转换为可执行的二进制指令

第二种呢就是执行环境,可用于实际执行代码,并且输出结果

然后我们再来说翻译环境,是如何将一段代码转换为可执行的二进制指令的呢

其实编译这一部分又分为了预处理(预编译),编译,汇编 

 在一个程序中可能会有多个.c文件,这些文件会单独的经过编译处理生成对应的目标文件

Windows环境下生成的目标文件后缀为.obj,Linux环境下生成的目标文件为.o

多个目标文件跟链接库一起经过链接器的处理最终生成可执行程序

链接库呢,它是指运行时库(支持程序运行的基本函数集合)第三方库

知道了上面的操作,我们就可以展开,成为了以下这个过程

 1.翻译环境

          -预处理

在预处理阶段,源文件和头文件会被处理为.i为后缀的文件,处理规则如下

  • 将所有的#define删除,并且展开所有宏定义
  • 处理所有的条件编译指令
  • 处理#include 预编译指令,将包含的头文件的内容插⼊到该预编译指令的位置
  • 删除所有的注释
  • 添加行号文件名标识, ⽅便后续编译器生成调试信息等
  • 或保留所有的#pragma的编译器指令,编译器后续会使用
经过预处理后的.i文件中不再包含宏定义,因为宏已经被展开并且包含的头文件都被插入到.i文件中

          -编译

编译过程就是将预处理后的文件进行一系列的:词法分析、语法分析、语义分析及优化,⽣成相应的汇编代码⽂件
  1. 将源代码程序被输⼊扫描器进行词法分析,把代码中的字符分割成⼀系列 的记号(关键字、标识符、字⾯量、特殊字符等)
  2. 接下来语法分析器,将对扫描产⽣的记号进⾏语法分析,从而产生语法树。这些语法树是以表达式为节点的树
  3. 语义分析器来完成语义分析,即对表达式的语法层⾯分析。静态语义分析通常包括声明和类型的匹配,类型的转换等。这个阶段会报告错误的语法信息。

          -汇编

汇编器是将汇编代码转转变成机器可执行的指令,每⼀个汇编语句⼏乎都对应⼀条机器指令
就是根据汇编指令和机器指令的对照表⼀⼀的进行翻译,也不做指令优化

          -链接

链接是⼀个复杂的过程,链接的时候需要把一堆文件链接在一起才生成可执行程序
链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤。
链接解决的是⼀个项⽬中多个文件、多模块之间互相调⽤的问题
地址修正的过程也被叫做:重定位

2.执行环境

  • 程序必须载⼊内存中。在有操作系统的环境中:⼀般这个由操作系统完成。
  • 在独⽴的环境中,程序的载⼊必须由手工安排,也可能是通过可执⾏代码置⼊只读内存成。
  • 程序的执⾏便开始。接着便调⽤main函数。
  • 开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使⽤静态(static)内存,存储于静态内存中的变量在程序的整个执行过程⼀直保留他们的值。
  • 终⽌程序。正常终⽌main函数;也有可能是意外终止

二、预定义符号

C语⾔设置了⼀些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的,所以运行速度更快
__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义


三、#define定义常量

基本语法 

# define name stuff

当然所定义的类型没有限制,可以为了定义值,可以为了替换复杂名字,也可以为了省事,下面这几种都是正确的定义方法: 

#define MAX 1000

#define float f //为 float这个关键字,创建⼀个简短的名字

#define 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 PR printf("hehe");

int main()
{
    PR;        //加了分号就相当于 printf("hehe");;  容易发生错误
    
    return 0;
}

为了避免上述的这个错误,我们定义的标识符的时候不需要加;


四、#define定义宏

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

举个例子来说明

#define SUPP( x ) x * x


int main()
{
    SUPP(2,3);

    return 0;
}

SUPP就是我们定义的一个宏,将宏置于函数内部(等预处理的时候,会自动替换成表达式 x * x)

但同时会存在一些问题

#define SUPP( x ) x * x


int main()
{
    SUPP(2);  // 2 * 2  == 4

    SUPP(2+1);  // 2 + 1 * 2 + 1  == 5

    10 * SUPP(5+2);  //10 * 5 + 2 * 5 + 2  == 62

    return 0;
}

因为被定义的宏是预处理阶段所进行的,在预处理的时候直接替换函数中的表达式,所以难免会有许多操作符,优先级之类的问题,这个解决问题的方法就是在表达式加上对括号就解决了

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

五、带有副作用的宏参数

副作⽤就是表达式求值的时候出现的永久性效果,就好比说
a = 1;

//a赋值的同时自己的值也改变了

b = a++;  // a = 2,b = 1;

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

拿下面这个例子来举例子

#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);//输出的结果是什么?

还记得我说了,宏是在预处理阶段替换宏为函数中的表达式:替换完了为

x = 5,y = 8;

MAX(x++,y++) 替换为 ((x++)>(y++)?(x++)(y++))
                      x=6   y=9         y=10
                       5 > 8假,执行y++

六、宏替换的规则

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

 


七、 宏函数的对比

宏通常被应用于执行简单的运算,比如在两个数中找出较⼤的⼀个时,更有优势⼀些
#define MAX(a, b) ((a)>(b)?(a):(b))

//定义宏和函数的两种方式

int Max(a,b)
{
    return ((a)>(b)?(a):(b));
}

 我们从以下几个方面来分析宏 和 函数 的优缺点:

  1. 代码长度:#define所定义的宏,每次使用的时候都会被插入到程序中,除了特别小的宏以外,程序的长度会大幅度增长;而函数的代码只出现一个地方,调用都用同一份
  2. 执行速度:#define所定义的宏更快;二函数存在着调用和返回等额外的步骤速度会慢一些
  3. 操作符优先级:#define所定义的宏求值是在上下文的环境中,结果不可预测 会存在着很多的问题;而函数的参数只在函数调用时候将结果传给函数,表达式课预测
  4. 带有副作用的参数:#define所定义的宏参数可能会被替换多个位置,多次被计算,对值有着不可预测的结果;函数只在传参的时候求值易控制
  5. 参数类型:#define所定义的宏与类型无关,只要操作是合法的,可以适用于任何参数类型;函数的参数是与类型有关,如果类型不同,所需的函数也不同
  6. 调试:宏是不能调试的;函数是可以逐句逐条调试
  7. 递归:宏是不能递归的;函数是可以递归的

八、#和##

1.#运算符

先给大家补充一个知识点,字符串中包含的字符串两个会合成一个字符串

printf("haha""hehe");

//两个输出的结果相同

printf("hahahehe");
#运算符将宏的⼀个参数转换为字符串字面量。仅允许出现在带参数的宏的替换列表中“字符串化
#define PRI(n) printf("the value of "#n " is %d", n);


int main()
{

    PrT(6); //printf("the value of "#n " is %d", n);
    return 0;
}

结果为the value of n is 6

不难发现#n,将转换成了一个字符串


2.##运算符

把位于它两边的符号合成⼀个符号,它允许宏定义从分离的文本片段创建标识符。
## 被称为记号粘合
大家想想看如果要实现一个结果,不同的数据类型就得写不同的函数
int int_max(int x, int y)
{
 return x>y?x:y;
}
float float_max(float x, float y)
{
 return x>yx:y;
}

那如果使用##,一切都会变的很简单

#define GENERIC_MAX(type)		    \
type type##_max(type x,typey)		\
{									\
	return (x>y?x:y);				\
}									\
	GENERIC_MAX(int);   //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
	GENERIC_MAX(float); //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名

在预处理阶段,预处理中的所有type全部被替换 


九、命名约定

把宏名全部⼤写   ,函数名不要全部⼤写

十、#undef

这条指令用于移除⼀个宏定义
#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。

可以看到 定义了一个MAX,正常打印完毕以后,#undef 移除这个宏定义,再次打印MAX就会报错


十一、 命令行定义

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

十二、 条件编译

在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的。因为我们有条件编译指令
#if 常量表达式
 //...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
 //..
#endif


#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif


#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol

十三、 头文件的包含

1.本地头文件包含

#include "filename"

拿双引号引用

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

2.库文件包含

#include <filename.h>

拿单尖括号引用

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
也可以用双引号引用库文件,但是查找的效率就低些,这样也不容易区分是库文件还是本地文件
当一个文件中包含多个头文件,重复的头文会被在预处理的时候多次替换,大大降低了效率,所以我们在声明一个头文件的前面不妨可以加上这段代码
#ifndef __TEST_H__  \\当未定义这个头文件时才会执行下面
#define __TEST_H__

.....

#endif
#pragma once

这两种都可以避免头文件重复引进


十四、 其他预处理指令

# error
# pragma
# line
...
后面再给大家介绍

希望对你有帮助 

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

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

相关文章

【03-掌握Scikit-learn:深入机器学习的实用技术】

文章目录 前言数据预处理缺失值处理数据缩放特征选择模型训练参数调整模型评估总结前言 经过了对Python和Scikit-learn的基础安装及简单应用,我们现在将更深入地探究Scikit-learn的实用技术,以进一步提升我们的数据科学技能。在本文中,我们将涵盖数据预处理、特征选择、模型…

【唯美情侣爱情表白纪念HTML单页】

唯美情侣爱情表白纪念HTML单页 效果图部分代码领取代码下期更新预报 效果图 整图 背景图 部分代码 index.html <!DOCTYPE html> <html lang"en"><head><meta http-equiv"Content-Type" content"text/html; charsetUTF-8"…

YOLOv8 实现车牌检测,生成可视化检测视频(20240424)

原项目源码地址&#xff1a;GitHub 我的源码地址&#xff1a;Gitee 环境搭建请参考&#xff1a;Win10 搭建 YOLOv8 运行环境&#xff08;20240423&#xff09;-CSDN博客 环境测试请参考&#xff1a;本地运行测试 YOLOv8&#xff08;20240423&#xff09;-CSDN博客 训练数据…

《系统架构设计师教程(第2版)》第15章-面向服务架构设计理论与实践-05-SOA设计模式

文章目录 1. 服务注册表模式1.1 服务注册表1.2 SOA治理功能1.3 注册表中的配置文件 2. 企业服务总线&#xff08;ESB&#xff09;模式3. Synchro ESB3. 微服务模式3.1 概述3.2 微服务架构模式方案3.2.1 聚合器微服务1&#xff09;概述2&#xff09;几种特殊的聚合微服务 3.2.2 …

RTT学习 cortex-m移植

Cortex-M移植 PRIMASK寄存器 PRIMASK寄存器为1位宽的中断屏蔽寄存器。在置位时&#xff0c;它会阻止不可屏蔽中断&#xff08;NMI&#xff09;和HardFault异常之外的所有异常&#xff08;包括中断&#xff09;。实际上&#xff0c;它是将当前异常优先级提升为0&#xff0c;这也…

Jenkins CI/CD 持续集成专题四 Jenkins服务器IP更换

一、查看brew 的 services brew services list 二、编辑 homebrew.mxcl.jenkins-lts.plist 将下面的httpListenAddress值修改为自己的ip 服务器&#xff0c;这里我是用的本机的ip 三 、重新启动 jenkins-lts brew services restart jenkins-lts 四 、浏览器访问 http://10.…

golang学习笔记(defer基础知识)

什么是defer defer语句用于golang程序中延迟函数的调用&#xff0c; 每次defer都会把一个函数压入栈中&#xff0c; 函数返回前再把延迟的函数取出并执行。 为了方便描述&#xff0c; 我们把创建defer的函数称为主函数&#xff0c; defer语句后面的函数称为延迟函数。延迟函数…

【Burpsuite靶场】XSS专题精讲

【个人】&#xff1a;NEUQ大一学生 【专业】&#xff1a;通信工程 (Communication Engineering) 【个人方向】&#xff1a;网安、开发双管齐下 【座右铭】&#xff1a;真正的英雄主义,就是看清生活的真相后依然热爱生活 -- 罗曼.罗兰 一、认识XSS&#xff08;跨站脚本攻击&…

fatal: unable to access ‘https://github.com/alibaba/flutter_boost.git/

Git error. Command: git fetch stdout: stderr: fatal: unable to access ‘https://github.com/alibaba/flutter_boost.git/’: Failed to connect to github.com port 443 after 75005 ms: Couldn’t connect to server exit code: 128 GitHub (国际型)代码 分发平台/托管平…

梯度下降法总是在同一点收敛吗?

梯度下降法总是在同一点收敛吗&#xff1f; 梯度下降法并不总是在同一点收敛。梯度下降法的收敛取决于多个因素&#xff0c;包括初始参数的选择、学习率的设置、损失函数的形状等。 以下是一些影响梯度下降法收敛行为的关键因素&#xff1a; 1.初始参数&#xff1a; 初始参数…

Json-server 模拟后端接口

json-server&#xff0c;模拟rest接口&#xff0c;自动生成增删改查接口。(官网地址&#xff1a;json-server - npm) 使用方法&#xff1a; 1. 安装json-server&#xff0c;npm i json-server -g 2. 创建json文件&#xff0c;文件中存储list数据&#xff0c;db.json {"…

图像超分辨率技术在AI去衣中的应用探索

在数字图像处理领域&#xff0c;图像超分辨率&#xff08;Super-Resolution, SR&#xff09;技术一直是研究的热点之一。该技术旨在从低分辨率的图像中恢复出高分辨率的图像&#xff0c;以提供更清晰、更丰富的细节信息。近年来&#xff0c;随着人工智能&#xff08;AI&#xf…

<计算机网络自顶向下> 路由器组成

路由器结构概况 路由&#xff1a;运行路由选择算法/协议&#xff08;RIP, OSPF, BGP&#xff09;生成路由表转发&#xff1a;从输入到输出链路交换数据包-根据路由表进行分组的转发中间的fabric是用来接收输入的分组交给输出端口的&#xff0c;完成局部的转发&#xff08;根据…

free 命令示例

目录 ⛳️推荐 前言 Linux 中如何使用 free 命令 1、以人类可读的形式显示信息 2、连续显示统计数据 3、定义显示统计数据的次数 4、指定输出数据类型 5、获取物理内存和交换内存的总和 总结 ⛳️推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&am…

掌握注册唤起应用的秘诀,Xinstall助你提升用户体验

在移动互联网时代&#xff0c;App已经成为我们日常生活中不可或缺的一部分。然而&#xff0c;随着App数量的激增&#xff0c;如何让自己的App在激烈的市场竞争中脱颖而出&#xff0c;成为开发者们关注的焦点。其中&#xff0c;注册唤起应用作为提升用户体验和转化率的关键环节&…

4- 24

day02 1.100个英语单词 2.vp div3 不过有点小悲惨&#xff0c;第一题正常的直接看出来答案。第二题其实是map模拟&#xff0c;一直没有读懂题目的意思&#xff0c;题目给的序列是打乱的。找出最小的&#xff0c;讲原来的序列补全&#xff0c;如果mp中没有这个数字&#xff0c;…

Linux网络-DHCP原理与配置

目录 一.DHCP工作原理 1.了解DHCP服务 1.1.使用DHCP的好处 1.2.DHCP的分配方式 2.DHCP的租约过程 2.1.DHCP工作原理 2.2.DHCP交互过程 二.DHCP服务器的配置 1.关闭防火墙 2.检查并且安装DHCP有关软件包 3.查看系统的配置文件 3.1.设置参数 4.修改网络 4.1.修改虚…

python高级进阶(一)[str字符串、set集合、dict字典]

目录 一、str字符串 1. 字符串的概念 2.字符串的特点 3. 定义字符串 4. 获取字符串中的某个元素 5. 遍历字符串 6. 字符串的常用方法 6.1 判断 6.2 转换 6.3 查找 6.4 切割 6.5 去空白 7. 小案例【用户名和密码合法校验】 8. 常用方法中 isdecimal() 和 isdigi…

前端开发攻略---封装calendar日历组件,实现日期多选。可根据您的需求任意调整,可玩性强。

1、演示 2、简介 1、该日历组件是纯手搓出来的&#xff0c;没依赖任何组件库&#xff0c;因此您可以随意又轻松的改变代码&#xff0c;以实现您的需求。 2、代码清爽干净&#xff0c;逻辑精妙&#xff0c;您可以好好品尝。 3、好戏开场。 3、代码&#xff08;Vue3写法&#xff…

分布式与一致性协议之CAP(一)

CAP理论 概述。 在开发分布式系统的时候&#xff0c;会遇到一个非常棘手的问题&#xff0c;那就是如何根据业务特点&#xff0c;为系统设计合适的分区容错一致性模型&#xff0c;以实现集群能力。这个问题棘手在当发生分区错误时&#xff0c;应该如何保障系统稳定运行而不影响…