程序环境和预处理

目录

一.程序的两大环境

1.翻译环境

2.执行环境

二.C语言程序的编译+链接

1.翻译环境

2.翻译环境简略过程介绍

三.预定义符号介绍

四.预处理指令 #define

4.1 define定义标识符

4.2 define定义宏

4.3  宏定义替换规则

4.4  宏与函数对比

4.4.1 优势

4.4.2 劣势

4.5  #与##运算符

4.5.1 #运算符

4.5.2 ##运算符

4.6 命名约定

4.7 宏可以使用#undef指令取消定义

五.条件编译

1.单分支条件编译

2.多分支条件编译

 3.判断是否被定义

 4.嵌套指令

六.条件编译的应用

6.1 文件包含

6.2 编写在多台机器或多种操作系统可移植的程序

6.3 为宏提供默认定义

6.4 临时屏蔽含注释的代码

七.其它指令 

#error

#pragma

​#line


一.程序的两大环境

1.翻译环境

在这个环境中源代码被转换为可执行的机器指令
也就是我们创建的test.c文件在这个环境中,将会被转换成一个叫做test.exe的文件.(a.out)
而这个test.exe文件,机器就能够识别,然后执行.

2.执行环境

执行环境,它用于实际执行代码.
也就是运行我们前面得到的test.exe文件,输出程序运行结果.

二.C语言程序的编译+链接

1.翻译环境

前面我们已经提到翻译环境和执行环境.

而对于翻译环境,从一个源文件得到可执行程序,又可以进一步细分为很多个过程.

源文件首先通过编译器编译,生成目标文件.(编译又可划分为预编译编译两个过程.)

最后,每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序

PS:同时,在链接的过程,链接器同时也会引入标准C函数库中任何被该程序所用到的函数(像

我们引入的头文件<stdio.h>中的printf,scanf函数)而且它可以搜索程序员个人的程序库,将其需

要的函数也链接到程序中.

2.翻译环境简略过程介绍

这里细节非常多,就不一一进行介绍,只简单介绍汇编中符号表的形成.

类似全局变量g_val,函数名Add,还有我们main函数的main,在机器看来都是一个个符号.

对于每一个源文件,都会形成相应的符号表,也就是把它们全部收集起来,并分配相应的地址.

而在链接过程中,我们会把符号表重新进行合并,比如我们在Add.c这个源文件中有Add函数的声

明,在test.c这个源文件也有Add函数的调用,因此两个源文件生成的符号表,都会有Add这个符

号,在合并的时候,就会将两个合成一个.

在执行的时候,最终的符号表就只会有单一一个Add符号,这样也实现了,在test.c中调用Add函数的基础.

三.预定义符号介绍

在C语言中,事先已经预定义了一些符号,如下面所示.

__FILE__       // 进行编译的源文件
__LINE__     // 文件当前的行号
__DATE__     // 文件被编译的日期
__TIME__     // 文件被编译的时间

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

四.预处理指令 #define

4.1 define定义标识符

基本语法:
#define name stuff
基本功能:
在预编译阶段,编译器会将所有出现name的地方, 直接全部替换成stuff.
PS:define定义的语句最好不要在后面加分号.
具体实例:
比如说下面这种情况,MAX会被替换为1000;
那我们编写程序的时候,每一句话都会加上一个分号,那使用的时候,程序就会显示错误,而这种
错误,在程序比较长的时候,是不太好识别的.

4.2 define定义宏

基本语法:
#define name(parament-list)  stuff
基本功能:
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义
宏(define macro).
PS:define定义宏 参数列表的左括号必须与 name 紧邻
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
具体实例:

比方说我定义了一个宏SQUARE,用作将传进去的数字平方.

在实际运算过程中,宏定义中所有出现的x都会被替换成5,而整个SQUARE(5)又会等效为5*5,

最后输出25的结果.

注意: 

但是实际上运用,还有很多需要注意的地方,比如说同样是上面的例子.

这次我没有直接传进去一个5,而是传进去4+1,因为在我们理解中,4+1得到5,然后再被传入.

但实际上呢,并不是如此,答案输出了9.

因此实际上,4+1并没有进行计算,而是直接代入了式子中

 

而乘法优先级高于加法,因此最后输出才会是9.

为了解决这个问题,需要在x外面都加上一个括号.

 

 但这实际上还不够,我们再看下面这个例子.

 按我们预期,DOUBLE(5)会得到5的2倍,也就是10,乘以10,这样最终输出就是100.

但实际上输出值是55,差别还是蛮大的.

这是因为DOUBLE(5)会被直接替换,而不是输出一个值,然后再返回回去.

为了解决这个问题,需要在整个表达式外面再加一个括号.

因此,对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中

的操作符或邻近操作符之间不可预料的相互作用.
好处:

1.程序更容易读懂

程序中假如可能会存在各式各样的数据,而程序一旦变长,这些数字对我们来说,很多时候就变成

了魔法数,假如我们使用宏,则会给这个数字,相应对应的含义,使程序可读性更强.

2.程序易于修改

这在我们以后实现小游戏的时候,就会更加深刻体会到,假设五子棋程序中,有很多位置都出现了

5,代表棋盘大小的含义,如果我们要改变棋盘的大小,一个个改,是非常不方便的.但假如一开

始,我们就定义一个#define SIZE  5,后面直接调整宏定义的标识符即可.

3.避免前后大小不一致或键盘输入错误

比如程序中有pi=3.14158625,那经常使用,则可能会输入错误,直接定义一个宏,

则不会出现这样的问题. 

4.3  宏定义替换规则

1. 宏参数和#define 定义中可以出现其他#define定义的符号.

 

但是对于宏,不能出现递归.

2. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索.
打印的字符串里面即便含有SUM,也不要想着它会输出150.

4.4  宏与函数对比

宏与函数相比,既有优点,也有明显的缺点,如何看待宏,还需要多方位思考和使用.

4.4.1 优势

1.程序可能会稍微快一些

因为define是直接在预处理阶段,就直接全部替换,而函数的使用,还需要函数栈帧的开辟等等一

系列的动作,因此在简单功能实现,比如比较两数大小上,使用宏速度上可能更高效一些.

2.宏是类型无关的

函数的声明需要指定传进去参数的类型,但我们可以很轻易发现,宏定义是类型无关的,比如同样

是比较两个数的大小,用MAX宏来实现,则可以实现long,double,float不同类型的数的比较.

4.4.2 劣势

1.编译后的代码通常会变得很大,可能大幅增加程序的长度,宏使用越频繁,问题往往也就越严重

 

 

那对应实际上就是这样一段代码,程序的长度大幅度增加.

2.宏无法调试,同时也没有类型检查

宏没有类型检查,即是它的优点,同时也是缺点.

没有类型检查,程序有时候出错,因为无法编译,往往也会成为一个头痛的问题.

3.无法用指针指向宏

我们可以用指针指向函数,也就是我们常说的函数指针,这在特定编程条件下会非常有用.

但我们知道宏在预处理阶段,就已经全部被删除了,所以不存在类似指针的说法.

4.宏可能不止一次计算它的参数,会带来诸如运算符优先级的问题,导致程序容易出现错误.

 

当i>j,i++会在式子中执行两次,造成我们不想要的结果. 

4.5  #与##运算符

C语言中,宏定义可以包含两个专用的运算符#,##,两个运算符都会在预处理阶段被处理掉,

下面将分别介绍它们.

4.5.1 #运算符

#字符串能够将宏的一个参数,转化为字符串常量,俗称"字符串化(stringzation)".

我们知道在C语言中,相邻字符串会被合并

而#操作符可以把stuff,转变为字符串

 比如上面这个例子,#VALUE会被转成i+3的字符串,然后粘合起来.

4.5.2 ##运算符

##则可以把两个记号"粘合"起来,形成一个记号.

注: 这样的连接必须产生一个合法的标识符.否则其结果就是未定义的
具体如何使用呢?我们可以举这样一个例子.
我们知道用宏定义来实现MAX函数,可能会因为运算优先级,造成我们不想要的结果.
而函数没有这样的问题,但无法实现不同类型的函数都可以进行两个数的比较.
因此我们自然而然想到,我们可不可以通过宏来实现不同的函数声明呢?

我们先通过一段和宏定义,确定max函数的具体框架,只需要往里面传入不同类型,比如float,

double等等,就可以实现两行代码,定义多个不同的比较函数.

4.6 命名约定

对于宏,一般所有字母都为大写

对于函数,一般以字母小写为主

4.7 宏可以使用#undef指令取消定义

五.条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的.因为我们有
条件编译指令
常见的条件编译,可以分为以下四种

1.单分支条件编译

 
如:
  #define __DEBUG__ 1
  #if  __DEBUG__
  //..
  #endif
先宏定义__DEBUG__的值为1,当且仅当它为1的时候,下面指令才会被实行,否则不会.

2.多分支条件编译

 3.判断是否被定义

假如DEBUG被宏定义过,则执行代码,否则不执行.

而上述代码还可以进一步进行简化,写成下列的形式,效果是相同的.

 

同理,我们也可以猜出#ifndef的作用效果.

 

 4.嵌套指令

将我们上面学到的三种情况,全部结合起来,就是嵌套指令.

#if defined(OS_UNIX)
 #ifdef OPTION1
 unix_version_option1();
 #endif
 #ifdef OPTION2
 unix_version_option2();
 #endif
#elif defined(OS_MSDOS)
 #ifdef OPTION2
 msdos_version_option2();
 #endif
#endif

六.条件编译的应用

6.1 文件包含

1.本地文件包含

比方说 #include "filename"
查找策略:先在源文件所在目录下查找(用户的工作路径),如果该头文件未找到,编译器就像查找库函数头文件一样后在标准位置查找头文件.
如果找不到就提示编译错误
2.库文件包含
比如说 #include <filename.h>
查找头文件是直接去标准路径下去查找,如果找不到就提示编译错误
那什么是标准路径呢?就是我们安装的时候,选择的路径,我们可以通过这个路径,找到我们存放
像stdio.h的库文件.
因此库文件实际上也可以用“ ”进行应用,但这样效率就会大大下降,而且也不便于区分是本地文
件还是库文件.

但这样会存在一个问题,处理包含问题时,预处理器往往是先删除这条指令,并直接用包含文件的内容替换.

这样一个源文件被包含 10 次,那就实际被编译 10 次,假如100次,那就实际被编译100次.
这对程序是非常不友好的.解决这个问题的一个办法就是条件编译.
在每个头文件前面加上这样一句代码
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif

那检测到__TEST_H__ 已经被定义过,则不会再重新编译这一段头文件.

而#pragma once也同样可以实现这个功能.

在vs2019中,在创立头文件时,会自动包含这一句代码. 

6.2 编写在多台机器或多种操作系统可移植的程序

在程序开头定义这样一个宏(有且只有一个),则可以指明程序运行在哪个操作系统上.

6.3 为宏提供默认定义

6.4 临时屏蔽含注释的代码

我们不能用注释,注释掉含注释的代码,而采用条件编译,则可以轻松实现这个功能.

七.其它指令 

#error

当机器遇到ERROR指令的时候,它会显示一条包含消息的错误消息.

有些编译器底下,甚至会直接终止编译,而不再检查其它错误.

通常它时配合条件编译进行使用,比如说我们上面提到得不同操作系统的例子.

 

#pragma

我们学过利用它调整默认对齐数,保证头文件只被调用一次,自定义编辑信息等等.

而实际上,在《C语言程序设计现代方法》一书中这样说到:

#line

改变程序行编号方式的.(通常程序行编号是从1,2到...最大整型所能表示的数)

一般有两种用法

使用后,它的后一行则从n开始进行编码.

指令后面的行会被认为来自文件,行号从n开始,n可以用宏来指定. 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值