【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://xiahunao.cn/news/2905049.html

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

相关文章

Swagger添加JWT验证(ASP.NET)

文章目录 JWT1、解析2、配置JWT JWT 1、解析 1&#xff09;客户端向授权服务系统发起请求&#xff0c;申请获取“令牌”。 2&#xff09;授权服务根据用户身份&#xff0c;生成一张专属“令牌”&#xff0c;并将该“令牌”以JWT规范返回给客户端 3&#xff09;客户端将获取到的…

【C语言基础】:数据在内存中的存储

文章目录 一、整数在内存中的存储二、大小端字节序和字节序判断1. 为什么有大小端&#xff1f;2. 练习 三、浮点数在内存中的存储1. 浮点数的存储1.1 浮点数的存储过程1.2 浮点数取的过程 四、题目解析 书山有路勤为径&#xff0c;学海无涯苦作舟。 创作不易&#xff0c;宝子们…

牛客NC26 括号生成【中等 递归 Java,Go,PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/c9addb265cdf4cdd92c092c655d164ca 思路 答案链接&#xff1a;https://www.lintcode.com/problem/427/solution/16924 参考答案Java import java.util.*;public class Solution {/*** 代码中的类名、方法名、参…

从小白-入门-进阶-高阶,四个阶段详细讲解单片机学习路线!

大家好&#xff0c;今天给大家介绍从小白-入门-进阶-高阶&#xff0c;四个阶段详细讲解单片机学习路线&#xff01;&#xff0c;文章末尾附有分享大家一个资料包&#xff0c;差不多150多G。里面学习内容、面经、项目都比较新也比较全&#xff01;可进群免费领取。 单片机学习路…

冥想打坐睡觉功法

睡觉把手机放远一点&#xff0c;有电磁辐射&#xff0c;我把睡觉功法交给你&#xff0c;这样就可以睡好了。

es6 Class基本语法和继承

es6 Class基本语法 class的基本语法&#xff1a; ES6 的class只是一个语法糖&#xff0c;它的绝大部分功能&#xff0c;ES5 都可以做到&#xff0c;新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已 传统用构造函数生成实例 function Point(x, y) {th…

政安晨:【Keras机器学习实践要点】(七)—— 使用TensorFlow自定义fit()

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: TensorFlow与Keras实战演绎机器学习 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; 在TensorFlow中&#xff0c;fit()是一个非常…

【算法题】三道题理解算法思想--滑动窗口篇

滑动窗口 本篇文章中会带大家从零基础到学会利用滑动窗口的思想解决算法题&#xff0c;我从力扣上筛选了三道题&#xff0c;难度由浅到深&#xff0c;会附上题目链接以及算法原理和解题代码&#xff0c;希望大家能坚持看完&#xff0c;绝对能有收获&#xff0c;大家有更好的思…

Jackson 2.x 系列【6】注解大全篇二

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Jackson 版本 2.17.0 源码地址&#xff1a;https://gitee.com/pearl-organization/study-jaskson-demo 文章目录 注解大全2.11 JsonValue2.12 JsonKey2.13 JsonAnySetter2.14 JsonAnyGetter2.15 …

【嵌入式机器学习开发实战】(十二)—— 政安晨:通过ARM-Linux掌握基本技能【C语言程序的安装运行】

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: 嵌入式机器学习开发实战 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; 在ARM-Linux系统中&#xff0c;C语言程序的安装和运行可…

Yarn简介及Windows安装与使用指南

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

RTSP应用:实现视频流的实时推送

在实现实时视频流推送的项目中&#xff0c;RTSP&#xff08;Real Time Streaming Protocol&#xff09;协议扮演着核心角色。本文将指导你通过安装FFmpeg软件&#xff0c;下载并编译live555&#xff0c;以及配置ffmpeg进行视频流推送&#xff0c;来实现一个基本的RTSP流媒体服务…

Synchronized锁、公平锁、悲观锁乐观锁、死锁等

悲观锁 认为自己在使用数据的时候一定会有别的线程来修改数据,所以在获取数据前会加锁,确保不会有别的线程来修改 如: Synchronized和Lock锁 适合写操作多的场景 乐观锁 适合读操作多的场景 总结: 线程8锁🔐 调用 声明 结果:先打印发送短信,后打印发送邮件 结论…

网络:udptcp套接字

目录 协议 网络传输基本流程 网络编程套接字 udp套接字编程 udp相关代码实现 sock函数 bind函数 recvfrom函数 sendto函数 udp执行指令代码 popen函数 udp多线程版收发消息 tcp套接字编程 tcp套接字代码 listen函数 accept函数 read/write函数 connect函数 recv/…

计算机网络——29ISP之间的路由选择:BGP

ISP之间的路由选择&#xff1a;BGP 层次路由 一个平面的路由 一个网络中的所有路由器的地位一样通过LS&#xff0c;DV&#xff0c;或者其他路由算法&#xff0c;所有路由器都要知道其他所有路由器&#xff08;子网&#xff09;如何走所有路由器在一个平面 平面路由的问题 …

数据结构与算法 双链表的转置

一、实验内容 有一个带头结点的双链表L&#xff0c;设计一个算法将其所有元素逆置&#xff0c;即第一个元素变为最后一个元素&#xff0c;第2个元素变为倒数第2个元素&#xff0c;最后一个元素变为第1个元素。 二、实验步骤 1、dlinklist.cpp 2、reverse.cpp 三、实验结果 四…

JAVA 源码分析Integer的128陷阱

128陷阱介绍及演示 首先什么是128陷阱&#xff1f; Integer包装类两个值大小在-128到127之间时可以判断两个数相等&#xff0c;因为两个会公用同一个对象&#xff0c;返回true&#xff0c; 但是超过这个范围两个数就会不等&#xff0c;因为会变成两个对象&#xff0c;返回fal…

《Vision mamba》论文笔记

原文出处&#xff1a; [2401.09417] Vision Mamba: Efficient Visual Representation Learning with Bidirectional State Space Model (arxiv.org) 原文笔记&#xff1a; What&#xff1a; Vision Mamba: Efficient Visual Representation Learning with Bidirectional St…

啥也不会的大学生看过来,这8步就能系统入门stm32单片机???

大家好&#xff0c;今天给大家介绍啥也不会的大学生看过来&#xff0c;这8步就能系统入门stm32单片机&#xff0c;文章末尾附有分享大家一个资料包&#xff0c;差不多150多G。里面学习内容、面经、项目都比较新也比较全&#xff01;可进群免费领取。 对于没有任何基础的大学生来…

HTTP状态 405 - 方法不允许

方法有问题。 用Post发的请求&#xff0c;然后用Put接收的。 大家也可以看看是不是有这种问题 <body><h1>HTTP状态 405 - 方法不允许</h1><hr class"line" /><p><b>类型</b> 状态报告</p><p><b>消息…