【C语言】编译和链接----预处理详解【图文详解】

欢迎来CILMY23的博客喔,本篇为【C语言】文件操作揭秘:C语言中文件的顺序读写、随机读写、判断文件结束和文件缓冲区详细解析【图文详解】,感谢观看,支持的可以给个一键三连,点赞关注+收藏。

前言 

欢迎来到本篇博客,上一篇我们详细介绍C语言中的编译和链接阶段,在C语言中,编译和链接是将源代码转换为可执行文件的关键过程。本期我们将深入了解这个过程中的预处理阶段。

上一篇博客链接:

【C语言】编译和链接----从源代码到可执行程序的转换-CSDN博客

文章目录

一、预定义符号

 二、#define定义常量

三、 #define 定义宏 

四、 带有副作用的宏参数

五、 宏替换的规则

六、宏和函数的对比

七、 #和##

7.1  #运算符

7.2 ##运算符 

八、 命名约定

九、 #undef

十、 命令行定义

十一、 条件编译 

十二、 头文件的包含 

12.1 本地头文件的包含

12.2 库文件的包含

12.3 二者的区别

 十三、 其他预处理指令


 一、预定义符号

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

 //__FILE__    //进行编译的源文件//__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", __STDC__);return 0;
}

 由于Visual Studio 2019编译器不遵循ANSI C,是未定义的。所以我们注释掉该行,如果是在gcc编译器,则__STDC__就为1

结果如下所示:

 二、#define定义常量

 #define定义常量的语法如下:

#define name  stuff

#define name stuff 将创建一个名为 name 的符号常量,其为 stuff。 

 因为在预处理阶段,文件会将#define展开和删除,所以我们可以通过gcc编译器来观察

gcc test.c -E -o test.i 

 C1,C2和Num都被替换了

那在使用#define的时候,我们需要注意几个点

1.用来省略for循环判断部分的时候,循环条件的判断恒为真,这个循环是死循环

例如:

#define forever for(;;)

 2.最好不用加分号

例如:

#define MAX 1000; 
#define MAX 1000

会将MAX替换成1000;这样会导致在语句后本来就有分号的情况下,替换后导致多出了一个分号 

 出现语法错误

三、 #define 定义宏 

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

常规解释: 

在C语言中,定义宏(Macro)指的是使用#define预处理指令为一个标识符(通常是一个常量、函数或代码片段)定义一个符号名称,以便在代码中多次使用,并在预处理阶段进行替换。宏可以是简单的标识符或者带参数的宏。

通过定义宏,可以使代码更具有可读性和可维护性,同时也可以减少代码中的重复性内容,方便后续的修改和维护。定义宏可以用于创建常量、简化复杂表达式、定义函数等。

下面是宏的声明方式: 

#define name( parament-list ) stuff

其中的  parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。 

 例如:

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

 这个宏接收一个参数  x
如果在上述声明之后,你把SQUARE( 5 ); 置于程序中,预处理器就会用下面这个x*x表达式替换上面的表达式

但是这个宏存在一定的问题

假设我们传入的是5+2

SQUARE(5+2);

那么这个式子就会被替换成5+2*5+2

 #define SQUARE( 5 + 2 )  5 + 2 * 5 + 2

原意我们是想把这个式子,算成7*7,但是结果却是十七,

那为了解决这个问题,我们就加括号

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

这样才能正确计算表达式的值

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

四、 带有副作用的宏参数

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

那么什么是副作用呢?

副作用就是表达式求值的时候出现的永久性效果

例如:

int a = 10;    //a = 10
int b = a + 1; //b = 11,a = 10int a = 10;    //a = 10
int b = ++a;   //b = 11,a = 11

也就是在使用这些运算符的时候,原变量会因为这些操作符而改变本身的值,这样就算有副作用了。就像上述代码我们使用了++a,从而导致a先加后用,让a本身的值改变成了11.而第一个代码却不会改变本身 

MAX宏可以证明具有副作用的参数所引起的问题。

#include<stdio.h>
#define MAX(a, b) ((a) > (b)?(a):(b))

int main()
{
    int x = 15;
    int y = 9;
    int z = MAX(x++, y++);

    printf("x=%d y=%d z=%d\n", x, y, z);
    return 0;
}

求输出的结果?

 我们知道宏在预处理阶段会展开替换,所以MAX会变成

MAX(a++, b++)  ( (a++) > (b++) ? (a++) : (b++) )

( (a++) > (b++) ? (a++) : (b++) )

( (15) > (9) ? (16) : (10) )

a = 17

b = 10

z = 16

五、 宏替换的规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。 

注意:

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

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

六、宏和函数的对比

 现在有一个函数max

#include<stdio.h>
#define MAX(a, b) ((a) > (b)?(a):(b))int max(int a, int b)
{return a > b ? a : b;
}int main()
{int x = 15;int y = 9;//int z = MAX(x++, y++);int z = max(x++, y++);printf("x=%d y=%d z=%d\n", x, y, z);return 0;
}

但是宏和函数到底哪个更有优势在这样的环境下?

答案是宏,宏通常被应用于执行简单的运算。

那为什么不用函数来完成这个任务?
原因有:
1.    用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜⼀筹
2.    更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整型、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。 

 但是宏本身也存在一些缺点:

和函数相比宏的劣势:
1.    每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2.    宏是没法调试的。
3.    宏由于类型无关,也就不够严谨。
4.    宏可能会带来运算符优先级的问题,导致程序容易出现错误。

但是宏的参数可以有类型,但函数不行

例如:

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

宏和函数的对比:

七、 #和##

7.1  #运算符

#include<stdio.h>int main()
{int x = 15;printf("the value of x = %d\n", x);int y = 9;printf("the value of y = %d\n", y);float z = 3.14f;printf("the value of z = %f\n", z);return 0;
}

 假设现在我们有这些代码,我们可以用函数封装

#include<stdio.h>void Print(int n)
{printf("the value of n = %d\n", n);
}int main()
{int x = 15;Print(x);int y = 9;Print(y);float z = 3.14f;Print(z);return 0;
}

我们发现我们只能打印n ,无法打印,而且受制类型限制,那如果用宏定义来呢?

#include<stdio.h>#define Print(n,format)	printf("the value of n is "format"\n",n);int main()
{int x = 15;Print(x,"%d");int y = 9;Print(y,"%d");float z = 3.14f;Print(z,"%f");return 0;
}

我们发现结果仍然是打印n,但是我们解决了类型限制的问题

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

我们发现在format前后都是字符串,这时候我们就可以用#这个运算符了,我们将n单独拿出来前后成为一个字符串,并将其搞成#n的形式

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

结果如下: 

总结:

#运算符将宏的一个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。 #运算符所执行的操作可以理解为“字符串化”。 

字符串化就是将#n用n本身字符字面量代替

7.2 ##运算符 

## 可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。 ## 被称为记号粘合
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
这里我们想想,写⼀个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数。 

虽然在C++中,我们有一个重载函数的概念,这样可以针对同一函数名解决不同的数据类型(此篇C++暂未写完有待完善)

但是在C语言里,我们可以用宏解决这个麻烦问题

这样无数堆叠类型,太过于繁琐,我们可以尝试用宏来解决这个问题 

 使用宏来定义函数(gcc编译器下实现)

#define GENERIC_MAX(type) \
type type##max(type x,type y)\
{\return x > y?x:y;\
} 
  • 代码解释:

  • #define GENERIC_MAX(type):这行定义了一个宏,宏的名称为GENERIC_MAX,并且带有一个参数type

  • type type##max(type x, type y):这行定义了一个函数模板。由于##是连接记号,这里的type##max将会在宏展开时将type替换进去,所以对于不同的type都会生成不同名字的函数。这个函数模板接受两个参数,类型为type,并且生成一个函数来返回这两个参数中的较大者。

  • \:反斜杠表示换行符,用于将宏定义延续到下一行。这样做是为了将宏定义分成多行以提高可读性。(也叫做续行符

  • {}:大括号内是函数的具体实现,其内容是返回两个参数中的较大者。

总之,这段宏定义的作用是根据所提供的类型type,创建一个名为typemax的函数模板,该函数模板接受两个相同类型的参数,并返回其中的较大者。在代码中调用这个宏并提供不同的类型type时,会生成对应类型的函数模板,可以方便地生成不同类型的最大值函数。这段代码是一个带参数的宏定义,用于创建一个通用的求最大值函数

通过预处理发现,我们生成了两个类型的函数 

 

八、 命名约定

 一般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者。

那我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写

九、 #undef

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

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

 使用如下:

#include<stdio.h>#define M 100int main()
{int x = M;printf("%d\n", x);#undef M
#define M 20int y = M;printf("%d",y);return 0;
}

十、 命令行定义

许多C的编译器提供了⼀种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同⼀个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外⼀个机器内存大些,我们需要一个数组能够大些。)

#include<stdio.h>int main()
{	int i = 0;int array[SZ];for (i = 0; i < SZ; i++){array[i] = i;}for (i = 0; i < SZ; i++){printf("%d ", array[i]);}printf("\n");return 0;
}

输入以下指令我们就可以看到结果 

gcc -D SZ=10 -o test 

 

十一、 条件编译 

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

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

#include<stdio.h>
#define FLAG 1
int main()
{//条件编译1
#if FLAG == 1printf("hehe\n");
#endif//条件编译2//多分支的条件编译
#if FLAG ==1printf("1\n");
#elif  FLAG == 2printf("2\n");
#elseprintf("3\n");
#endif//条件编译3//判断是否被定义
#if defined(MAX)printf("MAX defined\n");
#endif#if !defined(MAX)printf("MAX not defined\n");
#endif
//写法一样
#ifdef MAXprintf("MAX defined\n");
#endif#ifndef MAXprintf("MAX not defined\n");
#endif//条件编译四//嵌套指令
#if defined(MID)#if defined(MIN)printf("MIN not defined\n");#elif defined(MAX)printf(xxx);#endif#elif defined(xx)#if #endif#endifreturn 0;
}

十二、 头文件的包含 

12.1 本地头文件的包含

#include "filename.h"

12.2 库文件的包含 

#include <filename.h>

 12.3 二者的区别

  • 本地头文件(Local Header File)

    • 作用:本地头文件通常包含当前项目或当前源代码文件需要引用的自定义函数、宏、结构体等的声明和定义。
    • 用法:在源代码文件中使用#include预处理指令引入本地头文件。例如,如果有一个名为myheader.h的本地头文件,可以在源代码文件中这样包含它:#include "myheader.h"
    • 位置:本地头文件通常位于当前项目的源代码目录中,或者是当前源代码文件所在目录的子目录中。
  • 库文件(Library File)

    • 作用:库文件通常包含已经编译好的函数、数据结构、类等的定义和实现,供多个程序共享使用。
    • 用法:在源代码文件中使用#include预处理指令引入库文件的头文件。例如,如果要使用标准库中的stdio.h,可以在源代码文件中这样包含它:#include <stdio.h>
    • 位置:库文件通常位于系统或第三方提供的库目录中,编译器会在这些目录中查找所需的库文件。 

 十三、 其他预处理指令

#error
#pragma
#line

.......

  • #error

    #error 指令用于在预处理阶段生成一个错误消息,并终止程序的编译。通常用于在特定条件下中断编译过程,例如在条件判断中发现不支持的编译选项或条件时,可以使用 #error 指令中断编译并显示自定义的错误消息。

  • #pragma

    #pragma 指令用于向编译器发出特定的实现-defined 的指令,通常用于设定编译的特定行为或者使用特定的扩展特性。它是编译器指令的一种标准方式,不同的编译器可能支持不同的 #pragma 指令。

  • #line

    #line 指令用于改变源代码行号信息,可以在预处理阶段修改行号和文件名,这对于调试和跟踪预处理后的代码很有用。通在代码生成器或者宏定义中使用,可以帮助调试器或者日志工具准确定位到源码中的位置。

  • #error

    • 作用:用于在预处理阶段生成一个编译错误,并显示指定的错误消息。
    • 示例#error "Something went wrong!",这将导致编译器输出错误消息"Something went wrong!"并终止编译过程。
  • #pragma

    • 作用:用于向编译器发出特定的命令或指示,通常用于控制编译器的行为。
    • 示例#pragma warning(disable: 1234),这个指令可以告诉编译器禁用特定的警告。
  • #line

    • 作用:用于修改编译器在报告错误时所使用的行号和文件名。
    • 示例#line 100 "myfile.c",这个指令将当前行号设置为100,并且将当前文件名设置为"myfile.c"。

本篇博客,我们深入探讨了C语言中的预编译过程。通过本文的学习,相信读者对C语言编程过程中的宏定义有了更清晰的理解。希期本文对您有所帮助,谢谢阅读!如果你对编译和链接还有任何疑问或需要进一步的帮助,请随时留言,如果你觉得还不错的话,可以给个一键三连,点赞关注加收藏,本篇博客就到此结束了。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

继承和多态(2)(多态部分)

提前讲的重要知识点 一个类在没有父类的情况下默认有一个父类为Object类。 而当在有父类情况下&#xff0c;如果你那父类没有父类&#xff0c;则其父类的父类默认为object类&#xff0c;所以即使一个类有父类&#xff0c;其内部还是有object类。 object类都是隐藏起来的&…

【机器学习】基于北方苍鹰算法优化的BP神经网络分类预测(NGO-BP)

目录 1.原理与思路2.设计与实现3.结果预测4.代码获取 1.原理与思路 【智能算法应用】智能算法优化BP神经网络思路【智能算法】北方苍鹰优化算法&#xff08;NGO)原理及实现 2.设计与实现 数据集&#xff1a; 数据集样本总数2000 多输入单输出&#xff1a;样本特征24&#x…

Vue3:网页项目中路由的设计和配置

为了避免我每次建项目配路由的时候都回去翻网课&#xff0c;打算整一博客 路由设计 不同网页的路由设计思路基本相同&#xff0c;分为一级路由和二级路由&#xff0c;基本设计思路如下图 以我之前做过的招新系统管理端为例&#xff0c;可设计出如下路由 路由配置 还是以招新系…

剖析美国政府视角下的ICT供应链安全

2018 年 11 月 15 日&#xff0c;美国国土安全部&#xff08;DHS&#xff09;宣布成立了信息和通信技术 (ICT) 供应链风险管理&#xff08;SCRM&#xff09;工作组&#xff0c;这个工作组是由美国多个政府部门、IT行业企业代表及通信行业企业代表联合成立的。该组织对外宣传的目…

Docker Command

小试牛刀 # 查看docker版本 docker -v docker --version # 查看帮助 docker --help # 永远的Hello World docker run hello-world镜像操作 查看本地已有的镜像 docker images -a :列出本地所有的镜像&#xff08;含中间映像层&#xff09; -q :只显示镜像ID --digests :显示…

C# 右键快捷菜单(上下文菜单)的两种实现方式

在C#中&#xff0c;ContextMenuStrip是一种用于创建右键菜单的控件。它提供了一种方便的方式来为特定的控件或窗体添加自定义的上下文菜单选项。有两种实现方式&#xff0c;如下&#xff1a; 一.通过ContextMenuStrip控件实现 1.从工具箱中拖一个ContextMenuStrip控件到窗体上…

Java面试题总结200道(四)

76、ApplicationContext 通常的实现是什么? FileSystemXmlApplicationContext &#xff1a;此容器从一个 XML 文件中加 载 beans 的定义&#xff0c;XML Bean 配置文件的全路径名必须提供给它的构造函数。ClassPathXmlApplicationContext&#xff1a;此容器也从一个 XML 文件…

python、execl数据分析(数据描述)

一 python 1.各函数 1.1python库的安装与导入 #pip install os#pip install matplotlib#pip install seaborn#pip install scikit-learn#pip install scipy#修 改 工 作 目 录import osos.getcwd () # 查看当前工作环境os.chdir( F :\my course\database ) # 修改工作环境o…

数据挖掘不是挖土豆,而是让数据开口说话!

文章目录 1、 缘起1.1 啤酒与尿布 - 发现商业价值1.2 数据挖掘 - 让数据说话 2、数据挖掘的难点3、数据挖掘的方法 Part 1 - 专业技术流3.1 网络数据采集 - 代理技术3.2 网络数据采集 - 爬虫浏览器3.3 网络数据采集 - 网络解锁器3.4 网络数据采集 - Web Scraper IDE 4、数据挖掘…

力扣100热题[哈希]:最长连续序列

原题&#xff1a;128. 最长连续序列 题解&#xff1a; 官方题解&#xff1a;. - 力扣&#xff08;LeetCode&#xff09;题解&#xff0c;最长连续序列 &#xff1a;哈希表 官方解题思路是先去重&#xff0c;然后判断模板长度的数值是否存在&#xff0c;存在就刷新&#xff0c…

python类属性和global变量区别

数据成员是指在类中定义的变量&#xff0c;即属性&#xff0c;根据定义位置&#xff0c;又可以分为类属性和实例属性。 类属性定义在方法前面。 定义类属性&#xff0c;非全局变量 class MyClass:#global cc 10 ## 类属性def my_function(self):global qwqw 9print(this …

Vue项目使用process.env关键字及Vue.config.js配置解决前端跨域问题

1.process.env 是Node.js 中的一个环境 1.打开命令行查看环境: 2.process.env与Vue CLI 项目 Vue Cli 有以下三种运行模式 development 模式用于 vue-cli-service serve test 模式用于 vue-cli-service test:unit production 模式用于 vue-cli-service build 和 vue-cli-se…

酷炫的粒子动态表白HTML源码

源码介绍 酷炫的粒子动态表白HTML源码&#xff0c;自己自定义文字&#xff0c;动态组合文字&#xff0c;进行表白&#xff0c;喜欢的朋友可以下载使用&#xff0c;很不错的表白HTML代码 下载地址 酷炫的粒子动态表白HTML源码

深入理解与实践AB测试:从理论到实战案例解析

一、引言 在互联网产品优化和运营策略制定中&#xff0c;AB测试&#xff08;也称为分组测试或随机化对照实验&#xff09;是一种科学且严谨的方法。它通过将用户群体随机分配至不同的实验组&#xff08;通常是A组和B组&#xff09;&#xff0c;对比不同版本的产品或策略对关键…

封装一个可回车事件,不能输入配置项options没有的值的AutoComplete

要想AutoComplete支持回车事件&#xff0c;onKeyDown方法是用不了的&#xff0c;这一点在antd官方4.24.16中并没有提及。但是我们可以追踪到AutoComplete组件的源码&#xff0c;虽然并不能看很懂&#xff0c;但是可以看出组件是InternalSelectProps&#xff0c;RefSelectProps的…

【GPT概念04】仅解码器(only decode)模型的解码策略

一、说明 在我之前的博客中&#xff0c;我们研究了关于生成式预训练转换器的整个概述&#xff0c;以及一篇关于生成式预训练转换器&#xff08;GPT&#xff09;的博客——预训练、微调和不同的用例应用。现在让我们看看所有仅解码器模型的解码策略是什么。 二、解码策略 在之前…

小游戏-扫雷

扫雷大多人都不陌生&#xff0c;是一个益智类的小游戏&#xff0c;那么我们能否用c语言来编写呢&#xff0c; 我们先来分析一下扫雷的运行逻辑&#xff0c; 首先&#xff0c;用户在进来时需要我们给与一个菜单&#xff0c;以供用户选择&#xff0c; 然后我们来完善一下&#…

OceanMind海睿思入选中国信通院《2023高质量数字化转型技术解决方案集》

近日&#xff0c;由中国信息通信研究院“铸基计划”编制的《2023高质量数字化转型技术解决方案集&#xff08;第一版&#xff09;》正式发布。 中新赛克海睿思 凭借卓越的产品力以及广泛的行业实践&#xff0c;成功入选该方案集的数据分析行业技术解决方案。 为促进数字化转型…

Redis消息队列与thinkphp/queue操作

业务场景 场景一 用户完成注册后需要发送欢迎注册的问候邮件、同时后台要发送实时消息给用户对应的业务员有新的客户注册、最后将用户的注册数据通过接口推送到一个营销用的第三方平台。 遇到两个问题&#xff1a; 由于代码是串行方式&#xff0c;流程大致为&#xff1a;开…

视频号小店月入5w+,真的有那么赚钱吗?

我是电商珠珠 视频号小店是22年视频号团队发展的电商平台&#xff0c;距离现在也不过一年多的时间。我做电商已经有五年左右的时间了&#xff0c;天猫、快手、抖音小店都做过。在22年的时候&#xff0c;我开始琢磨起了视频号小店。 到现在我也拥有了视频号小店的运营团队&…