C语言的预处理环境和预处理

内容速递


一.程序的预编译环境与执行环境

ANSI C(标准C)的任何一种实现中,存在两个不同的环境。

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。

翻译环境要进行的事务如图所示:

 接下来我们详细解释一下翻译环境:

二·详解翻译环境和链接

2.1翻译环境:

翻译过程其实就是把我们写的以“.c”为后缀的文件转换为“.obj”(Windows环境下)(若是Linux环境下生成‘.o’为后缀的文件)文件。

而翻译的过程又分为三个阶段:预编译、编译、汇编

以如下代码为例,我们解释这三个过程:

#include <stdio.h>

//定义全局变量,赋值2023
int g_val=2023

#define M 100

int main()
{
   int a=M;
   printf("%d",M);
   return 0;
}

我们先解释预编译阶段:

在预编译阶段编译器会进行:注释的删除头文件的包含#define符号的替换

 接下来是编译过程:

编译过程实际上就是将我们所写的代码转换为汇编指令(这里就展示一部分)并进行:

词法分析、语法分析、语义分析、符号汇总

最后就是汇编过程:

在这一过程中编译器会把汇编指令转化为可被机器识别的二进制指令(这样的指令在编译器上是无法直接读取(文件格式是ELF,文本编译器无法识别)(编译器是不进行展示的),需要借助linux环境下readelf来读取)并形成符号表,如果强行打开,则会是一堆乱码。

我们借助readelf工具输入特定指令后可对符号表进行查看

对于这样的代码:

有以下符号表:

但遗憾的是:这种显示只能显示全局性的变量和库中定义的函数,诸如上述代码中的a、b、c等都没有在符号表中体现。

其次,当多个.c文件编译时,有相同的符号时,在多个.c文件翻译结束后的链接过程中,符号表会进行合并,合并过程中对重复的Add合并时,声明处的Add是没有实际意义的,因而合并后的符号表中的Add将重定位到定义处Add的位置

比如:

 所以我们在写函数的时候有时会有这样的错误,当声明函数的名字有误,或根本未定义时,编译器会无法解析这一符号。

三·预处理详解

3.1 预定义符号

__FILE__ //进行编译的源文件

__LINE__ //文件当前的行号

__DATE__ //文件被编译的日期

__TIME__ //文件被编译的时间

__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

(这些预定义符号都是语言内置的)

代码演示
代码演示

 3.2#define的介绍及衍生问题

3.2.1#define用来定义标识符的用法

//第一种:宏定义变量       #define MAX 1000  
//第二种:改变符号名       #define reg register  //为 register这个关键字,创建一个简短的名
//第三种:替换短程序       #define do_forever for(;;)     //用更形象的符号来替换一种实现
//第四种:自定义习惯       #define CASE break;case        //在写case语句的时候自动把 break写上。

//第五种:宏定义过长运用续行符

// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ ,       \
__DATE__,__TIME__ )

解释: 

 第一种宏定义变量:

乍看一眼与主函数内部定义变量有同工之处,实则有异曲之妙:

宏定义的变量是全局性的,跨文件的,且不可用赋值符“=”修改的

函数内部的变量在没有static或const修饰的情况下,是可修改的,不跨文件的。

因而在有些时候定义数组的时候有这样的写法:

#define MAX 100
int main()
{
  .....
  int arr[MAX];
  .....
}

在不支持变长数组的编译器中,这种方法也实现了某种意义上的“可变数组”,但遗憾的是这种可变性只能由程序的设计者来进行手动改变。并不能随着用户的使用而自然增加。

不知道大家还对这条预编译指令熟悉吗?

下图是vs编译器对上述符号的定义。

 我们知道若未对#define定义的变量赋值,那么其值为零,也就意味着程序会报错,若我们自行宏定义改变,将其定义为1或其他非零值,使报错失效。

第二种改变符号名 :

通常我们在一些较大程序中会用(但不仅仅使用#define,还会使用typedef,这个稍后介绍)#define来改变符号的名字,比如

#define Seplist sep

我们就可以使用‘sep’来代替‘Seplist’使用,这样解释也许很难让大家理解其用途,此时来解释这项功能显得较为突兀,我们稍后将其与typedef一同介绍

 第三种替换短程序:

我们在学习C语言的时候,常常要分装某些函数来实现某些特定的功能,比如:

我们要写一个加法函数:

int Add(int x,int y)
{
return x+y;
}

int main()
{
int a=10;
int b=20;
printf("%d",Add(a,b));
return 0;
}

我们不难发现对于像这种短函数,我们似乎也可以这样写

#define Add(x,y) x+y

这种写法优于函数的写法在于,一个函数的实现是经程序编译,链接后实现的,

而#define定义的“函数”(一种比喻,并不能称之为函数)其在预编译阶段就已经完成替换,

我们若把光标移动到这个Add处便不难发现,其就是等效为x+y(即a+b),这种特性使得这样合理的写法在相同机器下拥有更快的执行效率。

第四种自定义习惯:

这种其实就是在写代码的时候的一种习惯,在其他的语言程序中,以switch为例,其实现与C语言实现略有不同,C语言中每一种情况需要break进行分割。而有一些语言不需要

因而有的人在使用时便有了这种写法:

#define break;case CASE
int main()
{

switch(...)
{
case 1:
CASE 2:
CASE 3:

}


return 0;
}

第五种宏定义过长时使用续行符

这个不太好用文字解释,这里就直接演示代码:

和这样写代码等价

如果删去续行符,编译器会报错 

3.2.2#define定义内容后要不要加‘:’?

 再问这个问题之前我们先看这样一段代码

#define MAX 1;
int main()
{
  if(MAX)
{
  printf("%d\n",hehe); 
}

  return 0;
}

 为什么会报错呢?

因为#define定义实际上就是等价替换

上面的代码等价替换MAX为1;后就是:

 所以#define定义时,在末尾加;在语法上是无误的,但是在使用过程中有可能会导致一些想不到的错误。

3.2.3#define定义宏

我们先看一段代码:

#define Add(a,b) a+b

int main()
{

printf("%d",Add(3,5)*2);
return 0;
}

 这段代码的结果是多少???

 不知道上面的结果与你的猜想是否相符?

我们来解释一下:

还是那句话,#define定义语句其实质就是等价替换,你怎样写,我就怎样换,#define定义的语句实在预编译阶段就已经完成替换了,所以其也仅仅是替换,而不能进行计算。

 这其实就是上述代码替换后的结果。那么结果显而易见就是13

所以我们不难看出,在使用#define定义某些东西时,其会因替换后周遭运算符的优先级,而产生差错,所以我们再写代码时不妨这样写:

#define Add(a,b) ((a)+(b))

int main()
{

printf("%d",Add(3,5)*2);
return 0;
}

这样的话我们再看结果:

3.2.4#define的替换规则

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

 注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

对于第二点稍作解释:

对于下图代码:

#include <stdio.h>

#define PI "3.14159"

int main() {
    printf("PI value: %s\n", PI);
    return 0;
}

 

 3.2.5‘#’和‘##’在#define中的作用

我们先看一段代码:

#include <stdio.h>
int main()
{
char p[]="hello""world!";
printf("%s",p);

return 0;
}

我们可以看到尽管我们在赋值的时候用两个字符串给p数组赋值,但是我们不难发现最后输出时,是合并后的一个字符串 

既然如此,那么:

#include <stdio.h>
#define PRINT(FORMAL,VALUE) printf("the value is"FORMAL"\n",VALUE)

int main()
{
PRINT("%d",10);
return 0;
}

而#的作用就是把一个宏参数变成对应字符串 

##的作用是将其左右的两个符号合并成一个符号

 3.2.6带有副作用的宏

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

x+1;//不带副作用
x++;//带有副作用

如下图代码所示,请问结果是什么??? 

#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);//输出的结果是什么?
z = ( (x++) > (y++) ? (x++) : (y++));

 

 

3.2.7宏和函数的对比

 宏通常被应用于执行简单的运算。
比如在两个数中找出较大的一个。

#define MAX(a, b) ((a)>(b)?(a):(b))


那为什么不用函数来完成这个任务?
原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以 用于>来比较的类型。
宏是类型无关的。
宏的缺点:当然和函数相比宏也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序
的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型(我们前面介绍#的作用时提到的将“%d”作为参数就是其中一种),但是函数做不到。

3.3#undef 用于移除一个定义

 

3.4 命令行定义

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

例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个 程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器 内存大些,我们需要一个数组能够大些。)

3.5 条件编译

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

常见的条件编译指令:

#if 常量表达式
 //...
#endif
//常量表达式由预处理器求值。

比如:
如:
#define __DEBUG__ 1
#if __DEBUG__
 printf(“vrey good”);
#endif

2.多个分支的条件编译
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif

与上放的二分枝相似,可自己尝试

3.判断是否被定义
#if defined(symbol)   //等价于   #ifdef symbol

#if !defined(symbol)  //等价于  #ifndef symbol

 这种判断条件即使symbol为0,也依然执行语句,其只是判断是否定义,而非定义真假(非零或为零)

 

3.6 文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方 一样。

这种替换的方式很简单:

预处理器先删除这条指令,并用包含文件的内容替换。

这样一个源文件被包含10次,那就实际被编译10次(我们要知道头文件的替换是一个大工程,通常一个头文件的引入,都会增加成百上千行代码,如果频繁的增加后面的编译,汇编负担,因而避免头文件的重复引用是十分必要的)

 3.6.1头文件的包含方式

对于自己建立的头文件最好使用#include “.....”来包含

对于标准库中的头文件最好使用#include <....>来包含

二者的区别在于,使用“....”包含头文件会先从用户定义的头文件里查找所需头文件,若未检索成功,将在标准库中检索,若检索失败,则报错

使用<.... >包含头文件会直接从标准库中检索所需头文件,若检索失败,则报错

3.6.2头文件的嵌套包含

 

如果要解决这种重复包含的行为,可以使用条件编译来解决

下面代码的意思是:如果没有定义__TEST_H__,那么便定义 __TEST_H__,此时引用头文件即可

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif   //__TEST_H__

 

或者在每一个文件中加入下述代码也可以

#pragma once

  四.typedef与#define 的区别

1.typedef是一个关键字它的生效时间一定在程序启动后,而#define则是预处理指令,他的生效时间在预编译时期就已完成

2.typedef的重定义是面向类型的,它不具有#define能够定义变量的等等特点

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木鱼不是木鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值