C++的编译模型
单遍编译
在单遍编译时,编译器只能根据目前看到的代码做出决策,读到后面的代码也不会影响前面做出的决定。这很影响名字查找和函数重载决议。
对于名字查找,C++中的名字包括类型名,函数名,变量名,typedef 名,template 名。
对于下面这行代码:
Foo<T> a; //Foo,T,a这三个名字都不是macro
根据之前出现的代码不同,上面这行语句至少有三种可能性。
Foo 是个template<typename X> class Foo; ,T 是type ,那么这句话就是以T 为模板类型参数具体化了Foo<T> 类型,并定义了变量a 。Foo 是个template<int X> class Foo ,T 是const int 变量,这句话以T 为非类型模板参数具体化了Foo<T> 类型,并定义了变量a 。Foo、T、a 都是int ,没啥用的表达式。
C++只能通过解析源代码来了解名字的含义,不能像其他语言通过直接读取目标代码中的元数据来获得所需要的信息。如果要想准确理解一行C++代码的含义,我们需要通读这行代码之前的所有代码,并理解每个符号的定义。然而,由于头文件的存在,可能出现某人不经意间改变了头文件,这样就会破坏了代码的功能。
C++编译器的符号表至少要保存目前已看到的每个名字的含义,包括class 的成员定义,已声明的变量,已知的函数原型等,才能正确解析源代码。对于编译模板,难度十分大,除此之外,编译器还要正确处理作用域嵌套引发的名字的含义变化:内层作用域中的名字有可能遮住外层作用域中的名字。
对于函数重载决议,当C++编译器读到一个函数调用语句时,必须从目前已看到的同名函数中选出最佳函数。哪怕后面的代码中出现了更合适的匹配,也不能影响当前的决定。
对于下面一段代码
void foo(int)
{
printf("foo(int);\n");
}
void bar()
{
foo('a'); //调用foo(int)
}
void foo(char)
{
printf("foo(char);\n");
}
int main()
{
bar();
}
如果在重构的时候把void bar() 的定义挪到void foo(char) 之后,程序的输出就不一样了。
前向声明
C++规范建议尽量使用前向声明来减少编译期依赖。
如果在代码中调用函数foo ,C++编译器解析到此处函数调用时,需要生成函数调用的目标代码。为了完成语法检查并生成调用函数的目标代码,编译器需要知道函数的参数个数和函数的返回值类型,它并不需要知道函数体的实现(除非使用inline 展开)。因此通常把函数原型放到头文件中,这样每个包含了此头文件的源文件都可以使用这个函数。
光有函数原型不够,程序其中某一个源文件应该定义这个函数,否则会造成链接错误。定义foo() 函数的源文件通常也会包括foo() 的头文件。但是,假设在定义foo() 函数时把参数类型写错了,会出现什么情况。
//in foo.h
void foo(int); //原型声明
//in foo.cc
#include "foo.h"
void foo(int, bool) //可能会抄错
{
//do something
}
这块编译foo.cc 不会出错,因为编译器认为foo 有两个重载。但是链接整个程序会报错:因为找不到void foo(int) 的定义。
这是C++的一种典型缺陷,即一样东西区分声明和定义,代码放到不同的文件中,就有可能出现不一致的可能性。
对于函数的原型声明和函数体定义而言,这种不一致表现在参数列表和返回类型上,编译器通常能查出参数列表不同,但不一定能查出返回类型不同。也可能是参数类型相同,但是顺序调换了。例如原型声明为draw(int height, int width) ,定义的时候写成draw(int width, int height) ,编译器无法查看此类错误,因为原型声明中的变量名是没用的。
如果要写一个库给别人用,通常要把接口函数的原型声明放到头文件中.但是在写库的内部实现的时候,如果没有出现函数相互调用的情况,可以适当组织函数定义的顺序,让基础函数出现在代码的前面,这样就不必前向声明函数原型了。
函数原型声明可以看作对函数的前向声明,还有对类的前向声明。
有时候类的前向声明是必须的。有时候类的完整定义是必须的,例如要访问类的成员,或者要知道类的大小以便分配空间。其他时候,有类的前向声明就够了,编译器只需要知道有这么个名字的类。
对于class Foo ,以下几种使用不需要看到其完整定义:
- 定义或声明
Foo* 和Foo& ,包括用于函数参数,返回类型,局部变量,类成员变量等等。 - 声明一个以
Foo 为参数或返回类型的函数,如Foo bar() 或void bar(Foo f) ,但是,如果代码里调用这个函数就需要知道Foo 的定义,因为编译器要使用Foo 的拷贝构造函数和析构函数,因此至少要看到它们的声明。
|