在编写完程序,在编译器的菜单里选择“编译”后,编译器所做的大致可以分为三个过程:首先,代码通过一个预处理器(preprocessor),这个预处理器会识别出代码的有关元信息。其次,代码经过编译(compile),或翻译为机器可读的对象文件。最后,单个的对象文件链接(link)到一起,构成了一个应用。在vc中编译选项的设置里,“C++”中设置的是与编译过程相关的项,在“链接器”中设置的是与链接相关的项。
预处理器是在真正的翻译开始之前由编译器调用的独立程序。预处理器可以删除注释、包含其他文件以及执行宏替代。包含文件就是把包含的头文件内容替换在include该文件语句的位置。使用头文件要注意的是要避免同一个头文件的循环引用和多重包含。#ifndef机制和#program once可以用于避免循环包含和多重包含,如果使用vc的菜单中的添加新类,则会自动添加上述机制的一种(依编译器版本而定)。要避免头文件的这些问题,另一种工具是超前引用。如果需要引用一个类,但是不能包含它的头文件(比如,因为这个类相当依赖于你正在编写的类),那么可以只是告诉编译器存在这样的一个类(如下所示),而不是通过#include机制提供正式的类定义。当然,还不能在代码中真正使用这个类,因为编译器只是知道会有这样一个类存在。不过,还是可以在类定义中使用指向这个类的指针或者引用。
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/1327ab569c1ae82736693a50b8e33378.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
此外,由于包含文件就是在include的位置换上包含的文件。那么,如果类声明中需要另一类,则不一定要在头文件中包含另一类的头文件,也可以在cpp文件中包含另一个类的头文件,但该include文件的位置要在include本类头文件的前面,即:
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
但是编译时还是可能会报错,如“missing type specifier”, "syntax error : missing ',' before identifier '","undeclared identifier"。那么就要看该头文件(如上例是client.h)有没有在其他cpp文件中被包含,有的话则也需在其前面加上所需的其他头文件(如上例是RealInterface.h)。
这其实涉及到下面要说的编译的一些特点。编译是分别编译单个实现文件,将其转换为机器代码,那么这个阶段会检查语法等,符合语法的才能被转换为机器代码。但当有多个实现文件时,编译器不能保证其编译的顺序,而且在这一阶段只检查单个文件的合法性。那么如前所述的超前引用,尽管只是声明,就可以编译通过,因为从语法上讲是合理的了。
编译阶段可能出现的错误很多,这里不想总结了。因为语法错误的可能太多了。编译针对的是cpp文件,头文件只有被包含在cpp文件中,成为cpp文件的一部分,才能被编译。编译阶段其实也做了许多事,比如将this指针放到成员函数的第一个参数,在构造函数中添加代码对vtable初始化等,这里就不一一详述了。
链接则是要将单个的对象文件链接(link)到一起,形成一个可执行文件了。这个过程就不再允许任何错误了。那么既然编译时已经检查了语法错误,这里还会有错误吗?常见的就是“无法解析的外部符号”和“发现重复的定义”。前者的原因是编译器只找到了函数或类的声明,而没有找到实现;后者是找到了两个实现。
造成“无法解析的外部符号”的情况有很多,如前所述的超前引用,如果你只是给出了一个声明,没有在其他文件给出实现,这是很明显的没有实现;还有隐蔽的一些是,使用了其他dll的导出函数,但却没有链接其导入库。
还有总结一下最近在项目中遇到的一些命名空间的链接错误。个人编程时命名空间很少用,到了公司后作产品才开始使用。最近遇到的情况有:不小心把一个包含头文件的语句放到了命名空间内,但实际上该头文件和实现文件并没有声明在该命名空间内,这样把包含文件的语句放入命名空间内,则声明在命名空间内,但在命名空间内找不到实现,就会有链接错误;把声明全局对象的语句放到了命名空间内,则在另一个cpp文件(属另一个命名空间)要用到该对象时,想将其声明为extern,则也要将其包含在原命名空间内,否则链接错误,因为会在同一作用域内去找原来定义的全局对象;如果把全局对象原来的定义放在外面,也是链接错误,因为在全局作用域内,找不到该类的实现,它的实现在命名空间内。
造成“发现重复的定义”的情况一般都能找到原因。不过想阐述一下:C++源文件中的每个名字,包括函数和全局变量,都有一个链接,可能是内部链接,也可能是外部链接。外部链接是指,对于其他源文件,这个名字是可用的。内部链接是指对于其他源文件,这个名字不可用。函数和全局变量默认有外部链接,但是可以在声明前加上关键字static,来指定内部链接。