1.程序翻译环境和运行环境
假设一个test.c文件经过编译器编译运行后生成可执行文件test.exe,这中间存在两个过程:
一个是翻译,在这个环境中源代码被转换为可执行的机器指令。
一个是运行,它用于实际执行代码。
在翻译环境阶段,会进行编译和链接操作。
在汇编阶段,是将汇编指令转换成二进制指令。
1.1程序翻译中的的编译和链接
我们先来看这段代码:
extern Add(int a, int b); int main() { printf("%d\n", Add(2, 5)); }
这是在test.c文件中的代码,
int Add(int a, int b) { return a + b; }
这是在Add.c文件中的代码。
编译运行后,我们走到代码源文件所在目录下,
会发现有两个obj文件,这两个obj文件就是通过编译器编译源码生成的目标文件。
而这仅仅是目标文件,想要生成可执行程序,还需要通过链接器链接,调用链接库,才能生成可执行文件。
在链接器将目标文件链接成可执行程序期间,会做两件事:
1.合并段表
一个目标文件:可能是一个.o文件,该文件内部有一个许多关于该文件的信息,并且是分区存放的,也
以上面的例子为例,既然目标文件test.o是这样的,那么另一个目标文件Add.o的分段也应该是如上图,只是里面的内容存放不同而已。
所以我们可以将test.o和Add.o中的各个段的信息合并,就叫做合并段表
2.符号表的合并和重定位
在上面的两个文件中,我们假设main函数在内存中的地址是0x20000000,Add函数的地址是0x10000000,如下图:
执行test.c程序时,首先进入执行extern Add语句,发现这是一个函数声明,意思就是我只知道有Add这个函数,但是具体在哪里不知道,接着进入main函数,发现main函数在内存中的地址是0x20000000,记录下来,执行完test.c文件后,接着进入Add.c文件中,发现Add.c文件中有一个Add.c函数,地址是0x10000000,记录下来。
随后,将两个目标文件通过链接器合并时,会将test.c和Add.c文件中的地址合并,即
这就是符号表的合并,那么重定位呢?
重定位就是在符号表合并后,程序只认识新的合成后的符号表,并将该符号表作为运行时的信息,不再以之前的符号表作为信息,这个就是重定位。
当然,上述的讲解只是表层的介绍,具体的内容还会更加深入。
2. 预编译详解
2.1 预定义符号
__FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这是几个c语言内置的符号,来看他们的用法:
1.__ FILE __
我们打印__FILE__时,显示的是该源文件所在的路径。
2.__ LINE__
__LINE __ 显示打印的位置。
3.__DATE __
显示编译代码的日期
4.__TIME __
显示文件被编译的时间
5.__ STDC__
而在VS2019环境下,__STDC __没有被定义,如果被定义,其值为1。
注意,上述的五个预定义符号,在书写时均为大写!
2.2 #define 用法
2.2.1#define定义标识符
语法: #define name stuff
凡是以#开头的,都是预处理指令。 后续还会讲到#pragma,#include,#line等等
举个例子:
#define MAX 1000 int main() { printf("%d\n",MAX); } 0
注意:#define定义标识符在程序运行的时候进行的是替换!是替换!替换!
#define定义的标识符不会参与任何运算。
#define还可以定义各式各样的东西,甚至可以定义代码
#define reg register //为 register这个关键字,创建一个简短的名字 #define do_forever for(;;) //用更形象的符号来替换一种实现 #define CASE break;case //在写case语句的时候自动把 break写上。 #define DEBUG_PRINT printf("file:%s\tline:%d\tdate:%s\ttime:%s\n",__FILE__,__LINE__ ,__DATE__,__TIME__ )
这样写也可以实现。
注意这里有个问题:
#define do_forever for(;;) 1. int main() { do_forever return 0; } 2. int main() { do_forever; return 0; }
当我们这样定义for循环时,请问运行1和2的结果分别是什么?
答案:第一个运行的结果是,直接程序什么都不做,就结束。
第二个运行的结果是,程序死循环。
因为
#define定义标识符在程序运行的时候进行的是替换!
所以do_forever会替换成for(; ; ) , 当for循环后面不跟大括号时,默认跟一条语句。
对于第一个代码,return 0 是for循环里面的语句,
对于第二个代码,for循环内部的语句是一个分号,
for(; ; ) ;
也就是这样,所以会死循环。
那么就有一个问题,
在define定义标识符的时候,要不要在最后加上 ; ?
到底需不需要加呢?
举个简单的例子
#define MAX 100; int main() { int a = MAX; printf("%d\n", a); return 0; }
我们知道,MAX会被替换成100;
所以在赋值给a时,是这样的:
int a = 100;; 有两个分号
这样打印出来可能没什么问题,但是当我们把打印a换成打印MAX时,就有问题了
打印的是
printf("%d\n",100;);
就会有错误
所以,在使用#define定义标识符的时候最好还是不要加分号
2.2.2 #define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
意思就是:name要和小括号紧紧相连在一起,如果中间有空格,那么(parament-list)就会被当作是stuff的一部分。
举个例子:
#define SQUARE( x ) x * x int main() { printf("%d\n",SQUARE(5)); }
结果会输出什么?
5将会被传入x中,x就是5,宏也一样,是被替换的,所以
SQUARE(5) 会被替换成 5*5。
但是,这样的写法会有一些问题:
看下面的例子:
#define SQUARE( x ) x * x int main() { int a = 5; printf("%d\n" ,SQUARE( a + 1) ); }
请问输出结果是什么呢?乍一看,你可能会认为是36,但是,不对。
原因是,还是那句话,
宏一样是被替换的!!!
宏一样是被替换的!!!
宏一样是被替换的!!!
a是5,a+1会被放入SQUARE(a+1), 然后被替换成 a+1a+1,计算的是这个结果,5+15+1,结果就是11.
所以记住这句话:
宏一样是被替换的!!!
所以我们这样改就可以了
#define SQUARE(x) (x) * (x)
这样结果就是36了。
这里还有一个宏定义:
#define DOUBLE(x) (x) + (x) int main() { int a = 5; printf("%d\n" ,10 * DOUBLE(a)); }
请问结果是什么?
可能会说,100,但是,结果不正确,记住那句话
宏一样是被替换的!!!
打印结果是10*(5)+ (5),结果是55
那么,我们怎么改呢?
#define DOUBLE(x) ((x) + (x))
这样改正才是正确的,才是完美的。
总结,对宏进行定义时,应该对每个替换后的参数都加上括号,避免操作顺序不当出现错误。
2.2.3 #define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
被替换。
替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
述处理过程。
注意:
1.宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
4. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
2.2.4 #和##
首先来看这段代码:
void print(int x) { printf("the value of a is %d\n", x); } int main() { int a = 10; int b = 20; print(a); print(b); return 0; }
我们想把a和b的值都打印出来,
然而结果不是我们想要的,两次都是打印a,虽然值不同。
那么我们应该怎么做才能达到我们想要的效果呢?
先看下面,
int main() { printf("Hello World\n"); printf("Hello " "World\n"); return 0; }
一样的,原因是,在同一个printf中,两个双引号引起来的两个字符串会被当成同一个字符串处理。
当然,中间的空格可有可无,可以有很多个,也可以没有,这里放一个空格隔开只是方便看。
了解了这个之后,我们就可以改造上面如何打印a和b的值出来了。
#define PRINT(X) printf("the value of " #X "is %d\n",X)
当我们这样改造时,前面的双引号引起的是the value of(这里有个空格) ,后面的双引号引起的是is %d\n ,两个字符串之间使用一个#来吧X也变成一个字符串,这样三个字符串连在一起,就完成了。
相当于
printf("the value of ""a""is %d\n",a) printf("the value of ""b""is %d\n",b)
所以#的作用是,把宏参数对应的内容变成一个字符串
有些东西是函数无法做到但是宏能够做到的。比如上面这个例子。
##
(2) ## 的作用
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
举个例子:
#define CAT(X,Y) X##Y int main() { int student = 100; printf("%d\n", CAT(stu,dent)); printf("%d\n", student); return 0; }
该段代码的输出结果是两个100,##的作用就是,把X,Y宏参数合成一个新的符号。
stu是参数X,dent是参数Y,合成后成为一个新的符号student,打印出来就是100.
注意:
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。