- 程序的翻译环境
- 程序的执行环境
- 详解:C语言程序的编译+链接
- 预定义符号介绍
- 预处理指令 #define
- 宏和函数的对比
- 预处理操作符#和##的介绍
- 命令定义
- 预处理指令 #include
- 预处理指令 #undef
程序的翻译环境和执行环境
下面我来画出程序的编译过程:
- 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
- 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
- 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序。
程序的翻译环境
编译环境本身也分为两个阶段:编译和链接
编译期间也分三个阶段:预处理、编译、汇编 。它们各自的作用是:
我们首先写出一个简单的代码来查看编译期间的过程:
#include<stdio.h>
#define MAX 100
int main()
{
printf("%d\n",MAX);
return 0;
}
我们选择使用vs code 编译器,然后配置好gcc环境,来查看编译期间的每一步发生了什么。
第一步我们在终端输入:
gcc test.c - E - o test.i // 进行预处理,预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。
接下来我们来查看test.i 文件:
我们能看见代码上面有很长一段代码,而我们原本代码中的 #include<stdio.h> 和 #define MAX 100 不见了,因而我们可以得知,预处理阶段进行的是文本操作,将源代码中的头文件和定义的符号进行文本替换。
第二步我们在终端输入:
gcc test.c -S //编译,编译完成之后就停下来,结果保存在test.s中。
我们来查看test.s 文件:
我们可以看出来这个文件里面都是汇编代码,由此我们可以得知:编译期间就是将我们的源代码转换成汇编代码。
第三步我们在终端输入:
gcc test.c -c //汇编,汇编完成之后就停下来,结果保存在test.o中。
这时我们打开test.o 文件就可以看到,这个文件里面是二进制代码 ,由此我们可以得知汇编过程就是将汇编代码转换成二进制指令。
程序的运行环境
程序的执行过程:
预处理详解
预定义符号
__FILE__ // 进行编译的源文件__LINE__ // 文件当前的行号__DATE__ // 文件被编译的日期__TIME__ // 文件被编译的时间__STDC__ // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义
这些预定义符号都是语言内置的。
我们来举个例子:
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d-----%s %s %s %d\n", arr[i], __FILE__, __DATE__, __TIME__, __LINE__);
}
return 0;
}
该代码的运行结果为:
#define
#define 定义标识符
#define name stuff
举例:
#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__ )
#define定义宏
#define name( parament - list ) stuff其中的parament - list是一个由逗号隔开的符号表,它们可能出现在stuff 中。
注意: 参数列表的左括号必须与 name 紧邻。如果两者之间有任何空白存在,参数列表就会被解释为 stuff 的一部分。
我们首先来写一个简单的定义宏的代码:
#define SQUARE(x,y) ((x)*(y))
int main()
{
int c=SQUARE(5,9);
return 0;
}
这时候我们我们来查看预编译时候的代码:
我们发现传参的数被代到了上面的公式里完成了替换。
#define的替换规则
在程序中扩展 #define 定义符号和宏时,需要涉及几个步骤。1. 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先被替换。2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。3. 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和 #define 定义中可以出现其他 #define 定义的变量。但是对于宏,不能出现递归。2. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。
那我们如何把参数插入到字符串中?
#和##
我们来试试这样写代码:
#define PRINT(FORMAT, VALUE) \
printf("the value is "FORMAT"\n", VALUE)
int main()
{
PRINT("%d", 10);
return 0;
}
我们再来在终端输入 gcc test.c -E -o test.i 来查看预处理后的结果:
这时我们看到参数被替换到字符串当中。但是这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。
还有一个方法就是使用#号:
int i = 10;
#define PRINT(FORMAT, VALUE) \
printf("the value of " #VALUE "is "FORMAT "\n", VALUE)
int main()
{
PRINT("%d", i+3);
return 0;
}
预处理后的结果为:
##号的作用
## 可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符。
#define ADD_TO_SUM(num, value) \
sum##num += value
int main()
{
int sum5=0;
ADD_TO_SUM(5, 10);
return 0;
}
预编译后的结果为:
我们可以看到我们把sum和5结合到一起形成了一个符号。
宏和函数的对比
#define MAX(a, b) ((a)>(b)?(a):(b))
在这里宏有两个优点:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序 的规模和速度方面更胜一筹。
下面我们列举一些宏和函数的优缺点:
命名约定
#undef
#undef NAME// 如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
举例一个代码:
这里的代码在#undef MAX 之前的MAX没有报错,但在之后报错了,这就说明前面的定义被移除了