【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.mzph.cn/news/778093.shtml

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

相关文章

如何备考2025年AMC8竞赛?吃透2000-2024年600道真题(免费送题)

最近有家长朋友问我&#xff0c;现在有哪些类似于奥数的比赛可以参加&#xff1f;我的建议可以关注下AMC8的竞赛&#xff0c;类似于国内的奥数&#xff0c;但是其难度要比国内的奥数低一些&#xff0c;而且比赛门槛更低&#xff0c;考试也更方便。比赛的题目尤其是应用题比较有…

Redis开源协议变更!Garnet:微软开源代替方案?

Garnet&#xff1a;微软开源的高性能替代方案&#xff0c;秉承兼容 RESP 协议的同时&#xff0c;以卓越性能和无缝迁移能力重新定义分布式缓存存储&#xff01; - 精选真开源&#xff0c;释放新价值。 概览 最近&#xff0c;Redis修改了开源协议&#xff0c;从BSD变成了 SSPLv…

第二十一章 Jquery ajax

文章目录 1. jquery下载2. jquery的使用3. jquery页面加载完毕执行4. jquery属性控制6. 遍历器 2. ajax1. 准备后台服务器2. ajax发送get请求3. ajax发送post请求 1. jquery下载 点击下载 稳定版本1.9 2. jquery的使用 存放到html文件的同级目录 3. jquery页面加载完毕执行…

Unity | 射线检测及EventSystem总结

目录 一、知识概述 1.Input.mousePosition 2.Camera.ScreenToWorldPoint 3.Camera.ScreenPointToRay 4.Physics2D.Raycast 二、射线相关 1.3D&#xff08;包括UI&#xff09;、射线与ScreenPointToRay 2.3D&#xff08;包括UI&#xff09;、射线与ScreenToWorldPoint …

Linux安装redis(基于CentOS系统,Ubuntu也可参考)

前言&#xff1a;本文内容为实操记录&#xff0c;仅供参考&#xff01; 一、下载并解压Redis 1、执行下面的命令下载redis&#xff1a;wget https://download.redis.io/releases/redis-6.2.6.tar.gz 2、解压redis&#xff1a;tar xzf redis-6.2.6.tar.gz 3、移动redis目录&a…

“直播曝光“有哪些媒体直播分流资源?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 我们线下举办活动时&#xff0c;往往希望活动进行更大的曝光&#xff0c;随着视频直播越来越被大众认可&#xff0c;甚至成了活动的标配&#xff0c;那么做活动视频直播的时候&#xff0…

admin端

一、创建项目 1.1 技术栈 1.2 vite 项目初始化 npm init vitelatest vue3-element-admin --template vue-ts 1.3 src 路径别名配置 Vite 配置 配置 vite.config.ts // https://vitejs.dev/config/import { UserConfig, ConfigEnv, loadEnv, defineConfig } from vite im…

|行业洞察·趋势报告|《2024旅游度假市场简析报告-17页》

报告的主要内容解读&#xff1a; 居民收入提高推动旅游业发展&#xff1a;报告指出&#xff0c;随着人均GDP的提升&#xff0c;居民的消费能力增强&#xff0c;旅游需求从传统的观光游向休闲、度假游转变&#xff0c;国内人均旅游消费持续增加。 政府政策促进旅游市场复苏&…

公众号的AI聊天机器人已修复!谷歌Gemini Pro 10大使用场景解析

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;所以创建了“AI信息Gap”这个公众号&#xff0c;专注于分享AI全维度知识…

WorkPlus Meet构建局域网视频会议解决方案,助力企业协同与沟通

在当今数字化时代&#xff0c;局域网视频会议扮演着企业协同与沟通的重要角色。而选择适合的局域网视频会议平台能够提升企业的协作效率与沟通效果。WorkPlus Meet以其卓越的性能和强大的功能&#xff0c;成为企业局域网视频会议的首选。 局域网视频会议的优势与作用不言而喻。…

蓝桥杯-卡片换位

solution 有一个测试点没有空格&#xff0c;要特别处理&#xff0c;否则会有一个测试点运行错误&#xff01; 还有输入数据的规模在变&#xff0c;小心顺手敲错了边界条件 #include<iostream> #include<string> #include<queue> #include<map> #incl…

进程与文件

目录 Linux的 > 和 >> 文件的本质 &#xff1a; 操作系统的系统调用函数 open&#xff1a; close&#xff1a;关闭文件 write&#xff1a; open的返回值&#xff1a; 操作系统视角中的“文件与进程之间的关系”&#xff1a; 从上图可以得知以下论点&#xff1a…

LabVIEW电动汽车直流充电桩监控系统

LabVIEW电动汽车直流充电桩监控系统 随着电动汽车的普及&#xff0c;充电桩的安全运行成为重要议题。通过集成传感器监测、单片机技术与LabVIEW开发平台&#xff0c;设计了一套电动汽车直流充电桩监控系统&#xff0c;能实时监测充电桩的温度、电压和电流&#xff0c;并进行数…

基于微信小程序的民宿短租系统设计与实现(论文+源码)_kaic

摘 要 随着社会的发展&#xff0c;出差、旅游成为常态&#xff0c;也就造成民宿短租市场的兴起。人们新到陌生的环境里找民宿一般都是通过中介。中介虽然可以快速找到合适的民宿但会收取大量的中介费用&#xff0c;这对刚到新环境里的人们来说是一笔大的资金支出。也有一些人通…

Unity TMP 使用教程

文章目录 1 导入资源包2 字体制作3 表情包制作4 TMP 控件4.1 属性4.2 富文本标签 1 导入资源包 “Window -> TextMeshPro -> Import TMP Essential Resources”&#xff0c;导入完成后会创建一个名为"TextMehs Pro"的文件夹&#xff0c;这里面包含所需要的资源…

Zabbix6 - Centos7源码编译部署HA高可用集群手册

Zabbix6 - Centos7源码编译部署HA高可用集群手册 HA高可用集群 总所周知,在我们IT运维的圈圈中,HA高可用集群服务算是逼格最高的吧也是运维里保障力度最大的环境。 HA是HighlyAvailable缩写,是双机集群系统简称,提高可用性集群,是保证业务连续性的有效解决方案,一般有两个…

GitHub开源项目权限管理-使用账号和个人令牌访问

1.打开后台账号设置 2.找到左下角的Developer settings 3.找到Personal access tokens 的 Tokens(classic) 4.选择创建新证书 5.填写证书信息 6.点击生成证书&#xff0c;复制证书并且保存起来&#xff08;血泪教训&#xff0c;证书只会在创建时显示一次&#xff0c;以后就再也…

用于 Linux 运维的专门发行版,以及工具

Linux 运维必备的13款实用工具 https://blog.csdn.net/m0_46426259/article/details/121681374 2023 适用于安全专业人士的十款 Linux 发行版 https://www.sysgeek.cn/linux-distributions-for-security/#google_vignette 6个用于黑客攻击的最佳Linux发行版 https://zhuan…

Django安装及第一个项目

1、安装python C:\Users\leell>py --version Python 3.10.6 可以看出我的环境python的版本3.10.6&#xff0c;比较新 2、 Python 虚拟环境创建 2.1 官网教程 目前&#xff0c;有两种常用工具可用于创建 Python 虚拟环境&#xff1a; venv 在 Python 3.3 及更高版本中默…

安全的内网通讯软件,WorkPlus定制化 IM/办公门户解决方案

如今处于数字化转型的“加速期”&#xff0c;政企正经历着一场数字化迭代升级的时代浪潮。而不少企业都已具备了数字化管理的意识&#xff0c;数字化应用场景也在全面推开。WorkPlus不断推动信息技术与企业业务深度融合&#xff0c;作为安全的内网通讯软件&#xff0c;为企业提…