🎉个人名片:
🐼作者简介:一名乐于分享在学习道路上收获的大二在校生
🐻❄个人主页🎉:GOTXX
🐼个人WeChat:ILXOXVJE
🐼本文由GOTXX原创,首发CSDN🎉🎉🎉
🕊系列专栏:零基础学习C语言----- 数据结构的学习之路
🐓每日一句:如果没有特别幸运,那就请特别努力!🎉🎉🎉
————————————————
🎉文章简介:
本篇文章对 程序环境和预处理详解(含C语言程序的编译+链接) 的相关知识详细讲解!
如果您觉得文章不错,期待你的一键三连哦,你的鼓励是我创作动力的源泉,让我们一起加油,一起奔跑,让我们顶峰相见!!!🎉🎉🎉
1. 程序的翻译环境和执行环境
在ANSI C(标准C)的任何一种实现中,存在两个不同的环境;
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令;第2种是执行环境,它用于实际执行代码;
换种说法就是:
计算机时能够执行二进制指令的;
但是我们写出的代码是文本信息,计算机不能够直接理解;
翻译环境-->代码转换为--->二进制指令;
执行环境-->执行二进制代码;
2. 详解编译+链接
2.1 翻译环境
每个源文件都会单独经过编译器的处理,生成一个对应的目标文件;
例如:test.c源文件 经过编译器处理生成 test.obj文件;
然后多个目标文件+链接库经过连接器的处理生成可执行程序,最终生成 test.exe 的文件;
其中:链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人 的程序库,将其需要的函数也链接到程序中;这个连接器的处理过程就叫链接;
实例:
当我们写好这两个文件进行编译链接后:
如图:
生成了两个obj的目标文件;
一个exe的可执行程序;
2.3 运行环境(简单介绍)
程序执行的过程:1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成;2. 程序的执行便开始。接着便调用 main 函数;3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址,程序同时也可以使用静态(static )内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值;4. 终止程序,正常终止 main 函数;也有可能是意外终止;
3. 预处理详解
3.1 预定义符号
__FILE__ // 进行编译的源文件__LINE__ // 文件当前的行号__DATE__ // 文件被编译的日期__TIME__ // 文件被编译的时间__STDC__ // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义
这些预定义符号都是语言内置的
运行结果:
3.2 #define
3.2.1 #define 定义标识符
语法:#define name stuff
在define定义标识符的时候,要不要在最后加上 ; ?
这里会出现语法错误
3.2.2 #define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义 宏(define macro)
#define name( parament - list ) stuff其中的 parament - list 是一个由逗号隔开的符号表,它们可能出现在 stuff 中
参数列表的左括号必须与 name 紧邻;如果两者之间有任何空白存在,参数列表就会被解释为 stuff 的一部分
如例子一:
这个宏接收一个参数 x如果在上述声明之后,你把
置于程序中,预处理器就会用下面这个表达式替换上面的表达式:
这个宏存在一个问题:观察下面的代码段:
乍一看,你可能觉得这段代码将打印36这个值事实上,它将打印11.为什么?
替换文本时,参数 x 被替换成 a + 1 ,所以这条语句实际上变成了:
printf (" %d \ n", a + 1 * a + 1 );
这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。在宏定义上加上两个括号,这个问题便轻松的解决了:
定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误
这将打印什么值呢?warning :看上去,好像打印 100 ,但事实上打印的是 55.我们发现替换之后:
乘法运算先于宏定义的加法,所以出现了 55这个问题,的解决办法是在宏定义表达式两边加上一对括号就可以了
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中 的操作符或邻近操作符之间不可预料的相互作用;
2.2.3 #define 替换规则
在程序中扩展 #define 定义符号和宏时,需要涉及几个步骤1. 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先被替换;2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换;3. 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上述处理过程;
1. 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归;2. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索;
3.2.4 #和##
知识铺垫:
C语言支持这样的写法:
两个字符串和在一起,相当于一个字符串;
我们发现字符串是有自动连接的特点的;
如:
当我们写一个这样的代码:
我们发现几个printf函数打印的内容都相似,那我们能不能定义一个宏来实现打印呢?
答案是:可以的。
这里就要用到#的作用了,将参数插入到字符串中:
#的作用:
使用 # ,把一个宏参数变成对应的字符串,就是把一个宏的参数,以字符串的形式,插入到
一个字符串中去;
下面讲解 ## 的作用及用法:
##的作用:()只能在宏定义里使用
##可以把位于它两边的符号合成一个符号;它允许宏定义从分离的文本片段创建标识符;
例如:
分析:
注:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的
3.2.5 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能 出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。例如:
MAX宏可 以证明具有副作用的参数所引起的问题
例如:
运行结果是什么呢?
这里我们得知道预处理器处理之后的结果是什么:
输出结果分析:
3.2.6 宏和函数对比
宏通常被应用于执行简单的运算;比如在两个数中找出较大的一个;
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多;所以宏比函数在程序的规模和速度方面更胜一筹;2. 更为重要的是函数的参数必须声明为特定的类型;所以函数只能在类型合适的表达式上使用,反之这个宏怎可以适用于整形、长整型、浮点型等可以 用于>来比较的类型;宏是类型无关的
宏的缺点:当然和函数相比宏也有劣势的地方:1. 每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序 的长度;2. 宏是没法调试的;3. 宏由于类型无关,也就不够严谨;4. 宏可能会带来运算符优先级的问题,导致程容易出现错;
比如:宏的参数可以出现 类型 ,但是函数做不到
宏和函数的一个对比:
属 性 |
#define 定义宏 | 函数 |
代码 长度 | 每次使用时,宏代码都会被插入到程序中。除了非常 小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每 次使用这个函数时,都调用那个 地方的同一份代码 |
执行 速度 | 更快 | 存在函数的调用和返回的额外开 销,所以相对慢一些 |
操作符 优先级 | 宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括 号。 | 函数参数只在函数调用的时候求 值一次,它的结果值传递给函 数。表达式的求值结果更容易预 测 |
带有副 作用的 参数 | 参数可能被替换到宏体中的多个位置,所以带有副作 用的参数求值可能会产生不可预料的结果 | 函数参数只在传参的时候求值一 次,结果更容易控制。 |
参数 类型 | 宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型 | 函数的参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 相同的 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二那我们平时的一个习惯是:把宏名全部大写函数名不要全部大写
本章完~
剩余未讲解完的知识在下章