程序的编译与执行过程+预处理详解

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

这篇博客主要介绍了程序的翻译环境和执行环境,详细解释了程序编译的阶段,预处理符号和预处理指令在程序中的简单应用。在简述了#define替换规则以后,对比了宏和函数,并简述各自的优缺点,这也是作为我C语言基础部分的最后一篇博客,未来几篇的内容都会与C语言数据结构相关


一、程序的翻译环境和执行环境

1.翻译环境

在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可以被机器“理解”和执行指令。
第2种是执行环境,这个环境用来执行我们写出的代码。

2.编译过程

先看一个简单的示意图:
在这里插入图片描述
在这里我用我之前写过的一个扫雷游戏举例:

如果你对使用C语言实现的简单扫雷游戏感兴趣,可以看看我的另一篇博客:
传送门

在这里插入图片描述
我们就以组成扫雷游戏的这三个源文件举例,它们在程序的编译过程中就会被转换为目标文件,这些目标文件又会被链接器捆绑在一起,形成可执行程序

链接器同时也会引入标准C函数库中任何被该程序所用到的函数,包括你自己在头文件中写的函数,换句话说,函数未定义的错误就是在“链接”的这个过程中被编译器发现的

(1)预编译阶段

在这个阶段中,程序会执行预处理指令、删除注释等等文本相关操作,完成后,源文件由“.c”文件变为“.i”文件

(2)编译阶段

开始对程序进行语法分析、词法分析、语义分析、以及符号汇总,程序由“'.i”文件变为“.s”文件

此处的‘符号’特指源文件中的函数名、变量名等符号

(3)汇编与链接

在编译阶段结束以后,开始进行汇编过程,此时开始形成符号表,将汇编指令转换为二进制指令,最后开始链接(合并段表,并进行符号表的合并和符号表的重定位)

3.运行环境

(1) 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序
的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
(2) 程序的执行便开始。接着便调用main函数。
(3) 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回
地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程
一直保留他们的值。
(4)终止程序。正常终止main函数;也有可能是意外终止。

二、预处理详解

1.define定义标识符

我之前写得那个扫雷,有一个小缺陷就是不好改动:
基础难度扫雷的棋盘有一个9x9的表格,但是以后我想要换一个更大一点的棋盘的时候,可能就得把“打印棋盘的函数”,“随即设置雷的函数”等等茫茫多的函数里面的“9”全部换成我想要修改的数字,非常麻烦
实际上这个缺陷在程序设计初期就是可以避免的,我完全可以在头文件开头加上这一行代码:

#define line 9

既然“表棋盘”是“line x line”这么大的,那么“里棋盘”就是“(line+2)x(line+2)”这么大的

就像上面这样,写相关函数的时候全部使用line这个参数就可以了。
这样写的优点就是方便你日后修改参数,让你改一行就完事,而不是一边找,一边把一切需要改动的地方全部修改一遍
语法长下面这样

#define m n

这样写完之后,m在编译过程中就会被替换成n,你可以用这个语法实现一些简单的函数的功能(这种实现方法就被称为“宏”),注意不要忘记中间的空格
如果你发现你的这个“n”一行写不完了,也可以把“n”的内容分成几行来写,除了最后一行以外,每一行的最后都要加上一个反斜杠“\”:

#define PRINTF printf("这只是一个例子而已,我必须为了演示语法而去说一堆废话,怎么还没\
换行啊啊啊啊啊啊啊啊啊啊啊!") 

开个玩笑啊,完全可以打完反斜杠就直接按回车加空格,这样至少可以让代码看起来干净些:

#define PRINTF printf("这只是一个例子而已,\
                      我必须为了演示语法而去说一堆废话,\
                      怎么还没换行啊啊啊啊啊啊啊啊啊啊啊!") 

如果你以后想要用PRINTF干别的事的话首先还是要移除之前的定义,像这样:

#undef PRINTF
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

2.#define定义宏与替换规则

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义
宏(define macro)。
我们需要注意:宏的替换规则仅仅是替换而已,必须时刻注意利用括号避免失误,下面是示例代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

#define biubiu(m,n) m*n
int main()
{
	printf("%d", biubiu(3 + 2, 1 + 4));
	return 0;
}

最后结果并不是5x5=25,而是3+2*1+4=9,正确用法是把m和n全部加上括号,在把整体加上一个括号:

#define biubiu(m,n) ((m)*(n))

3.宏与函数对比

首先看一下我的上几条示例代码,还有一些小细节,就是不要在最后加上;
不然别人用的时候不小心在末尾又加了一个分号,就会出现语法错误。
除此之外,也不要使用“带有副作用的宏参数”

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

这里的“副作用”指的是对参数本身的值进行了改变,“++”这个单目操作符虽然在很多场景里用起来简单,但总是用的话还是比较容易惹麻烦
宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个:

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

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


  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Alexanderite

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

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

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

打赏作者

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

抵扣说明:

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

余额充值