我们来简单了解一下C语言程序的编译和预处理过程。我们写的C语言代码,从运行,到在屏幕上生成结果,经历了比较复杂的过程。下面,我们来简单了解一下这个过程。
从源文件到可执行程序,需要经过编译和链接两个大的处理,在编译阶段,又需要经过预编译,编译和汇编三个过程。这些被称为翻译环境。
那么,在这些过程都具体做了什么呢?
组成一个程序的每一个源文件通过编译过程,会单独生成各自的.obj(windows)文件,也就是目标文件。然后,这些目标文件,在经过链接器的作用,和链接库进行连接处理,最后形成.exe的可执行文件。连接器同时会引入C标准函数库之中的任何被该程序用到的函数,而且,他还可以搜索程序员个人的程序库,将其需要的函数也链接到改程序当中。
在预处理(编译)阶段,编译器会对预处理指令进行处理,比如头文件的展开,define指令的替换,还会删除程序员写的注释等等。预处理智慧处理#开头的语句。根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有头文件(都已经被展开了)、宏定义(都已经替换了),没有条件编译指令(该屏蔽的都屏蔽掉了),没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。
在编译阶段,编译器就会对代码进行转换,把C语言的代码转换成汇编代码。将预处理完的文件逐一进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。编译是针对单个文件编译的,只校验本文件的语法是否有问题,不负责寻找实体。
到了汇编阶段,把汇编代码转换成二进制,同时形成符号表。
链接阶段,通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。 链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。在此过程中会发现被调用的函数是否被定义。需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的。
1,程序必须载入内存当中。在又操作系统的环境中,一般是由操作系统完成。在独立的环境中,程序的载入可能手工安排,也渴望能通过可执行代码植入制度内存来完成。
2,程序开始执行,接着调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4. 终止程序。正常终止main函数,返回0;也有可能是意外终止,返回随机值。
下面,我们来看看预处理。
__FILE__ //进行编译的源文件,两个_.
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这些都是C语言内置的。
语法: #define name stuff
注意define预处理在定义标识符的时候,不要再最后加上;。这是因为它只是进行了简单的替换。
上面报错,如果进行简单的替换,那么就是
printf("the value of MM::%d\n", 100;);
#define机制包括了一个规定,允许将参数替换到文本中,这种实现通常称为宏或定义宏。
宏的声明:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
比如用宏实现一个寻找最大值。
#define MAX(x, y) x>y?x:y
int main()
{
int a = 10;
int b = 20;
int ret = MAX(a, b);
printf("%d\n", ret);
return 0;
}
这里要注意参数列表的左括号要和name紧挨着,否则就被认为是stuff的一部分。在这个过程当中,宏只是做了一个简单的替换。我们传进去a,b,就会被替换为 a>b?a:b。然后在代进去值。因为宏只是简单的替换,所以要格外小心优先级的问题。
#define NUM(n) n + 1;
int main()
{
int a = 10;
int ret = 2 * NUM(a);
printf("%d\n", ret);//结果是?
return 0;
}
运算的逻辑是2 * a + 1,不是2 * (a + 1)。要避免这个问题,就要加上括号。
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数 中的操作符或邻近操作符之间不可预料的相互作用。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
要注意的是:
1. 宏参数和 #define 定义中可以出现其他 #define 定义的变量。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。比如上面定义的MM。在字符串里面的MM就没有被替换。
那么我们想,如何将参数插到字符串里面去?
由于字符串是有自动连接的特点的,所以我们可以用define来写:
#define PRINT(name, val) printf("the "name" is %d\n", val)
int main()
{
PRINT("a", 18);
return 0;
}
另一个就是可以使用#,把宏参数变成对应的字符串。 这里的a需要双引号,这是因为整个是字符串,字符串需要进行拼接,如果是单引号,就会报错。
#的作用就是把一个宏参数变成对应的字符串。经过#处理,比如#n,就会处理成”n“。
#define PRINT(n) printf("the val of "#n" is %d\n", n)
int main()
{
int a = 10;
printf("the val of a is %d\n", a);
PRINT(a);
int b = 10;
PRINT(b);
return 0;
}
##的作用是将其两边的符合合成一个符号。它允许宏定义从分离的文本片段创建标识符。要注意的是,合成的符号必须要合法,比如in##t,合成之后就是int,或者定义的变量,否则结果就是未定义的。
要注意的是,宏的参数最好是不带副作用的,比如:
x++;//带副作用
x+1; //不带副作用
其中x++在替换之后的运算中就会引发问题。
#define max(x, y) ((x)>(y)?(x):(y))
int main()
{
int a = 3;
int b = 5;
int m = max(a++, b++);//先用,在++
printf("%d %d %d\n", m, a, b);//结果是?
return 0;
}
分析:由于后置++是先使用,在++,所以a,b的值3和5先进去替换,就是((a++)>(b++)?(a++):(b++))。进去后就是3>5?结果是不成立的。那么,冒号左边不执行,冒号后边在++。结果返回后,a是3,b是6。把6给,m的值就是6。两个在++,就是a是4,b是7,
宏通常用于执行简单的运算。
一般来讲,宏的名字需要大写,函数的名字不要全部大写。
用来移除一个宏定义。
#undef NAME //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
在写代码的时候,有时我们需要暂时不需要一些代码,删除又很可惜,保留又碍事。我们可以直接注释掉,也可以使用条件编译来完成。
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__ //判断某个宏是否被定义,若已定义,执行随后的语句
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__ #if, #ifdef, #ifndef这些条件命令的结束标志.
}
return 0;
}
这里不再讨论具体的使用。
我们在写C语言代码的时候,经常要写#include预处理指令,以使另外一个文件被编译。比如头文件
头文件的包含有两种,第一种是<>方式。它是直接去标准路径下去查找,不会查找程序员个人写的头文件,如果没有就报错;第二种是” “方式。它是先去这个源文件的目录下去查找,如果没有,就去标准路径下去查找,如果没有,就报错。
对于库文件也可以使用 “” 的形式包含? 答案是肯定的,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
有的时候,我们需要写多个相同的和使用多个相同的头文件,如果包含了大量重复的头文件,编译器就会多次编译。为了解决这个问题,可以使用以下方法:
每一个头文件开头写:
#ifndef __TEST_H__
#define __TEST_H__ //头文件的内容
#endif //__TEST_H__
或者
#pragma once
预处理的指令还有很多,比如#error #pragma #line ... 就不在介绍。