解析C语言与C++的编译模型

2020-01-06 15:15:06王冬梅

3. 单遍编译( One Pass )
由于当时的编译器并不能将整个源文件的语法树保存在内存中,因此编译器实际上是”单遍编译”。即编译器从头到尾地编译源文件,一边解析,一边即刻生成目标代码,在单遍编译时,编译器只能看到已经解析过的部分。 意味着:
C语言结构体需要先定义,才能访问。因为编译器需要知道结构体定义,才知道结构体成员类型和偏移量,并生成目标代码。
局部变量必须先定义,再使用。编译器需要知道局部变量的类型和在栈中的位置。
外部变量(全局变量),编译器只需要知道它的类型和名字,不需要知道它的地址,就能生成目标代码。而外部变量的地址将留给连接器去填。
对于函数,根据隐式函数声明,编译器可以立即生成目标代码,并假定函数返回int,留下空白函数地址交给连接器去填。
C语言早期的头文件就是用来提供结构体定义和外部变量声明的,而外部符号(函数或外部变量)的决议则交给链接器去做。
单遍编译结合隐式函数声明,将引出一个有趣的例子:


void bar()
{
 foo('a');
}

int foo(char a)
{
 printf("foobarn");
 return 0;
}

int main()
{
 bar();
 return 0;
}

gcc编译上面的代码,得到如下错误:


test.c:16:6: error: conflicting types for 'foo'
void foo(char a)
 ^
test.c:12:2: note: previous implicit declaration is here
  foo('a');

这是因为当编译器在bar()中遇到foo调用时,编译器并不能看到后面近在咫尺的foo函数定义。它只能根据隐式函数声明,生成int foo(int)的函数调用代码,注意隐式生成的函数参数为int而不是char,这应该是编译器做的一个向上转换,向int靠齐。在编译器解析到更为适合的int foo(char)时,它可不会认错,它会认为foo定义和编译器隐式生成的foo声明不一致,得到编译错误。将上面的foo函数替换为 void foo(int a)也会得到类似的编译错误,C语言严格要求一个符号只能有一种定义,包括函数返回值也要一致。
而将foo定义放于bar之前,就编译运行OK了。
C++ 编译模型
到目前为止,我们提到的3点关于C编译模型的特性,对C语言来说,都是利多于弊的,因为C语言足够简单。而当C++试图兼容这些特性时(C++没有隐式函数声明),加之C++本身独有的重载,类,模板等特性,使得C++更加难以理解。
1. 单遍编译
C++没有隐式函数声明,但它仍然遵循单遍编译,至少看起来是这样,单遍编译语义给C++带来的影响主要是重载决议和名字解析。
1.1 重载决议