主要进行源码级别上的操作,预处理器执行源码中的预处理命令(以‘#’号开头的语句),其中预处理命令可以分为以下几类:
宏定义命令[ #define 宏名 替换内容 、#undef 宏名]:进行代码替换, 凡是遇到标识符为宏名的都直接用“替换内容”进行替换。条件编译命令[ #if … 、 #else 、 #elseif …、 #ifdef … 、#ifndef … 、#endif 、#error messageStr]: 根据条件判断来选取代码块作为编译程序输入。包含文件命令[ #include< filename >]: 用文件内容替换这条命令。预定义宏名[ LINE、DATE、TIME、FILE]预编译模块[[#pragma]:一般被用作编译器的拓展,用来设置编译器状态或指示编译器完成特定动作,比如VC++的[#pragma once]用于防止头文件被重复包含,[#pragma omp parallel for]用于VC++对OMP加速的支持。特殊符号,比如#line, 如果在源码中出现,将会被解释成当前行号。使用的gcc命令是:gcc –E 将.c 文件转化成 .i文件 对应于预处理命令cpp
C++程序的编译过程是分模块进行的,每一个模块独立编译,生成相应的.obj或.o文件,一般情况下,每一个.c或.cpp文件作为一个独立的模块进行编译。需要注意的是,.h文件是不会被编译的。这个编译过程会检查变量和函数是否有被声明,进行词法检查,语法检查…,生成汇编代码,优化汇编代码,最后经过汇编程序翻译成目标机器代码,生成.obj或.o文件。
a.编译 使用的gcc命令是:gcc –S 将.c/.h文件转换成.s文件 对应于编译命令 cc –S
b.汇编 使用的gcc 命令是:gcc –c 将.s 文件转化成 .o文件 对应于汇编命令是 as
因为每一个模块是独立编译的,所以对于定义在其它模块的函数或变量的使用都是未定义的,编译时检查到有声明只是告诉模块——这个函数或变量定义在其它模块中了,当然这样编译后生成的文件是无法直接执行的。因此,我们需要一个过程将各个编译后的模块合理组合起来,并将各个模块中对其它模块的函数调用或变量引用设置到正确的地址,这个过程就叫做链接。链接完各个.obj或.o文件后才能生成一个可执行文件。
链接过程需要编译时收集每一个模块的相关信息才能完成,编译每一个模块的同时还要生成三个表供链接过程使用: 1. 导出符号表:提供了本编译单元具有定义,并且愿意提供给其他编译单元使用的符号及其地址。 2. 未解决符号表:提供了所有在该编译单元里引用但是定义并不在本编译单元里的符号及其出现的地址。 3. 地址重定向表:因为每个模块的逻辑地址都是从0开始的,这样的地址是相对的,在链接成可执行文件之前需要知道每个.obj文件在可执行文件中的起始位置,这样(相应.obj的起始位置+导出符号表中导出符号对应的位置)才是一个正确的的地址。这样,我们就需要告诉链接器,哪些地址是需要重定位的。这样,链接器在进行链接时,首先会决定每一个.obj文件在可执行文件中的起始位置,然后查看模块的地址重定向表,在需要重定向的位置上加上相应模块实际在可执行文件中的起始位置,这样的地址才是正确的地址。
对于每个模块,一般而言: 1. 用extern关键字修饰的符号是外部链接,它告诉编译器,这个符号定义在其它模块,应该把这个符号放入未解决符号表。 2. 用static关键字修饰的符号都是内部链接符号,也就是说这些符号仅仅在模块内部可见,不会提供给其它模块引用,也就不会被放进导出符号表。 3. 默认情况下,const常量是内部链接,不会被加到导出符号表中。 4. 默认情况下,函数和全局变量都是外部链接符号,这些符号会被放入模块的导出符号表,以供其它模块使用,但是可以用static关键字修饰,把它变成内部链接,这样就不会被放进导出符号表。
链接器一般会一次做出如下动作: 1. 决定每一个obj(模块)在可执行文件中的位置。 2. 查看每一个模块的重定向表,给需要重定向的值加上它所在模块在可执行文件中的起始位置,形成正确的地址。 3. 检查所有模块的导出符号表,如果发现导出符号有重复,会产生链接错误: duplicated external symbols…,然后停止链接过程。 4. 在导出符号表中搜索未解决符号表中的符号,找到后会在相应位置填上正确地址,如果找不到,就会产生链接错误: unresolved external link…,然后停止链接过程。 5. 完成链接过程,生成可执行文件。
在了解C++程序一般编译过程后,我们不难理解为什么头文件中一般只能放声明而不能放定义,这是因为头文件会被很多模块包含,如果头文件中有定义,那么链接这些包含这个头文件的模块时就会出现符号重定义的错误了。 当然,我说的是一般情况,当然也有特殊情况,那就是内联函数和模板类,它们的定义是允许并且必须放在头文件中的。
用关键字inline修饰的函数称为内联函数,就效果上来说,内联函数和宏定义是一样的,它会将函数调用用函数体进行代码替换,这样省去了调用函数过程的开销,对于频繁使用并且短小精悍的函数来说,改成内联,可以提高程序效率。使用内联函数要注意一下几点:
类体内实现的成员函数默认内联。用inline修饰只是建议编译器对函数使用内联,至于内不内联,由编译器视函数实际情况来决定,如果函数语句很多,或函数中有循环,条件判断等语句,编译器一般不会内联,如果内联函数的使用在内联函数定义之前,也不会内联。inline是实现用的关键字,也就是说放在定义一起才有用,放在函数声明处是没用的。为什么内联函数需要把定义一起放在头文件中? 与宏定义在预处理时就进行替换不同,内联函数的替换是在编译时执行的,因为每个模块都是独立编译的,此时如果模块本身不知道函数的定义,也就无法内联展开了,这就是为什么要知道内联函数定义的原因。那么就会存在问题了,头文件中包含了定义,就不怕重定义吗?大家不用担心这个,编译器会将内联函数视为内链接。
如果没有把内联函数的定义一起放在头文件里会发生什么? 会出现链接错误:unresolved external symbols …,这是因为在编译时,因为找不到内联函数的定义而无法内联展开,这时编译器会认为它应该是在其它模块定义了。前面说过,内联函数是内部链接,所以这个函数符号在导出符号表中找不到,于是会出现unresolved链接错误。
模板类与模板函数 — 需要把定义一起放在头文件中 要理解模板类和模板函数的编译过程,大家要记住以下几点:
模板就是模板,它本身不能被编译成二进制代码,它的作用只是在编译时根据类型生成相应代码而已。只有在模板函数(不管是普通的还是类的成员函数)被调用时,函数模板才会被实例化,也就是才会生成相应类型的函数以供调用。函数模板实例化时会增大源文件代码量,并且生成的函数都是内链接。和内联函数一样,为了能在编译时能生成相应实现代码,我们需要知道模板函数的定义,这也就要求模板类或模板函数的定义也要一起放在头文件中。
如果没有把模板类或模板函数的定义一起放在头文件中会怎样? 如果包含它的模块调用了模板函数,那么会出现Unresolved external simbols链接错误。因为如果编译时,模块找不到模板函数的定义,它会认为这个函数肯定是定义在其它模块了,把这个问题留给链接程序去解决。当然,链接程序在导出符号表中找不到这个函数(因为它压根没有被实例化过),所以出现以上错误。
关于链接的详解可见
