时间过得飞快呀,从第一篇blog到现在,已经有三四个月的时间了,而我们终于也迎来了C语言的最终章——程序环境和预处理!加油吧朋友们,ONEPIECE就在眼前~
目录
一、程序的"翻译环境"和"运行环境"
二、详解"编译环境"
三、"运行环境"
四、"预处理"详解
4.1预定义符号
4.2 小能手#define
4.2.1 #define 定义标识符
4.2.2 #define 定义宏
4.2.3 #define 替换规则
4.2.4 巧用 # 和 ##
4.2.4.1 # 的使用
4.2.4.2 ##的使用
4.2.5 带副作用的宏参数
4.2.6 宏和函数的对比
4.3 #undef
4.4 条件编译
4.5文件包含
4.5.2 嵌套文件包含
在ANSIC的任何一种实现中,存在两个不同的环境:
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是运行环境,它用于实际执行代码。
test.c是文本信息的代码——源代码 || test.exe是可执行程序——二进制的指令
编译环境其实又分为编译和链接两部分:编译由编译器执行、链接由链接器执行。
编译器和链接器我们都可以在我们的电脑中搜索到:
下面我们介绍一下程序的编译过程:
1.组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
2.每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
3.链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
下面我们具体看一下每一步系统都干了些什么:
我们可以看到几个阶段都强调了“符号”、“符号表”的操作,其实这些操作都是在为“链接”这个阶段做铺垫。
我们举个例子:假设我们的文件有两个 test.c 和 add.c
在 add.c 中,系统汇总了一个符号(系统一般只汇总应用全局的符号): add
在 test.c 中,系统汇总了符号:add、main、printf(这里忽略不谈)
生成符号表:
注:这里汇总的每一个符号都是有其地址的,在这里我们假设一下。
合并段表:
而且,目标文件都有其特定的格式,在合并符号表时,相同段的数据会合并到一起:
合并符号表:
合并两个符号表时,将 add.c 和 test.c 中的符号表合并到一起:
最终系统都是通过最后这个符号表记录的地址来找到对应的符号位置。
程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。 3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。
经过上面的学习,我们可能已经忘了,"预处理"是哪个阶段来着?
它是"编译"的第一个小阶段。
这些预定义符号都是语言内置的。
用例:
int main()
{
printf("%s\n", __FILE__);//进行编译的文件
printf("%d\n", __LINE__);//文件当前的行号
printf("%s\n", __DATE__);//文件被编译的日期
printf("%s\n", __TIME__);//文件被编译的时间
//printf("%d\n", __STDC__);//当前使用是的VS2022不遵循ANSI C
return 0;
}
语法:
#define name stuff
注意:define定义标识符时,最后不要加 ; 因为系统是完全把 stuff 替换到 name的位置。
相信小伙伴们对定义标识符已经很熟悉啦,下面我们来新介绍一个语法:
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在 stuff 中。
注意:
参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为 stuff 的一部分
举个栗子:
#define ADD(x, y) (x) + (y)
int main()
{
printf("%d\n", ADD(3, 22));
return 0;
}
是不是很神奇,用宏也可以做函数的工作,但是宏在传参时也有可能会发生小错误:
其实宏能做的不只是这些,宏能替换大部分的函数。
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。//检查宏中是否存在其他#define并替换
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。//检查程序中的#define并替换
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
使用 # ,把一个宏参数变成对应的字符串
#define PRINT(x,format) printf(#x "的值是:"format"\n", x)
int main()
{
float Love_date = 3.22;
PRINT(Love_date, "%.2f");
return 0;
}
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
注:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
x + 1; //不带副作用
x++; //带有副作用
宏的优点:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型, 宏是类型无关的。
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点 型等可以用于>来比较的类型。
宏的缺点:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。
除非宏比较短,否则可能大幅度增加程序的长度(每使用一次都要替换一次)。
2. 宏是没法调试的。宏直接替换,我们无法逐步分析。3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错
另外,为了区分宏和函数,在宏和函数的命名时,宏一般全大写,如MAX、DOUBLE...;而函数名不全大写。
#undef 用于移除一个宏定义
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
常见的条件编译指令:
1.(同if else)
#if 常量表达式//操作
#endif
//常量表达式由预处理器求值。
如:
#if 1
printf("u r a genius!\n");
#endif
//...
2.多个分支的条件编译(同if elseif else)
#if 常量表达式
//...
#elif 常量表达式 //可以无限多
//...
#else
//...
#endif //结束标志,必须有
3.判断是否被定义
#if defined(symbol)
#ifdef symbol//以上两种写法相同
#if !defined(symbol)
#ifndef symbol//以上两种写法相同
4.嵌套指令
#if defined(OS_UNIX)
#ifdef 条件1
//...
#endif
#ifdef 条件2
//...
#endif#elif defined(OS_MSDOS)
#ifdef 条件3
//...
#endif
#endif
4.5.1 头文件被包含的方式
头文件的包含有2中形式:
1.包含本地文件(自己的.h文件)
#include"xxx.h"
2.包含标准库的头文件
#include
本地文件查找:
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件;如果找不到就提示编译错误。
标准库文件查找:
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
当出现以上这种场景时,就造成了文件内容的重复。
这时我们就可以使用条件编译。或者在VS中我们创建.h文件中,它也会生成#pragma once ,这也可以避免头文件的重复引入。