【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 * x

int 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 = 10

int 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 100

int main()
{
	int x = M;
	printf("%d\n", x);

#undef M
#define M 20

	int 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 == 1
	printf("hehe\n");
#endif
	//条件编译2
	//多分支的条件编译
#if FLAG ==1
	printf("1\n");
#elif  FLAG == 2
	printf("2\n");
#else
	printf("3\n");
#endif
	//条件编译3
	//判断是否被定义
#if defined(MAX)
	printf("MAX defined\n");
#endif

#if !defined(MAX)
	printf("MAX not defined\n");
#endif
//写法一样
#ifdef MAX
	printf("MAX defined\n");
#endif

#ifndef MAX
	printf("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

#endif
	return 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语言编程过程中的宏定义有了更清晰的理解。希期本文对您有所帮助,谢谢阅读!如果你对编译和链接还有任何疑问或需要进一步的帮助,请随时留言,如果你觉得还不错的话,可以给个一键三连,点赞关注加收藏,本篇博客就到此结束了。

  • 46
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值