解析解析C语言与语言与C++的编译模型的编译模型
首先简要介绍一下首先简要介绍一下C的编译模型:的编译模型:
限于当时的硬件条件,C编译器不能够在内存里一次性地装载所有程序代码,而需要将代码分为多个源文件,并且分别编译。
并且由于内存限制,编译器本身也不能太大,因此需要分为多个可执行文件,进行分阶段的编译。在早期一共包括7个可执行
文件:cc(调用其它可执行文件),cpp(预处理器),c0(生成中间文件),c1(生成汇编文件),c2(优化,可选),as(汇编器,生成
目标文件),ld(链接器)。
1. 隐式函数声明隐式函数声明
为了在减少内存使用的情况下实现分离编译,C语言还支持”隐式函数声明”,即代码在使用前文未定义的函数时,编译器不会
检查函数原型,编译器假定该函数存在并且被正确调用,还假定该函数返回int,并且为该函数生成汇编代码。此时唯一不确
定的,只是该函数的函数地址。这由链接器来完成。如:
int main()
{
printf("ok");
return 0;
}
在gcc上会给出隐式函数声明的警告,但能编译运行通过。因为在链接时,链接器在libc中找到了printf符号的定义,并将其地
址填到编译阶段留下的空白中。PS:用g++编译则会生成错误:use of undeclared identifier ‘printf’。而如果使用的是未经定
义的函数,如上面的printf函数改为print,得到的将是链接错误,而不是编译错误。
2. 头文件头文件
有了隐式函数声明,编译器在编译时应该就不需要头文件了,编译器可以按函数调用时的代码生成汇编代码,并且假定函数返
回int。而C头文件的最初目的是用于方便文件之间共享数据结构定义,外部变量,常量宏。早期的头文件里,也只包含这三样
东西。注意,没有提到函数声明。
而如今在引入将函数声明放入头文件这一做法后,带来了哪些便利和缺陷:
优点:
项目不同的文件之间共享接口。
头文件为第三方库提供了接口说明。
缺点:
效率性:为了使用一个简单的库函数,编译器可能要parse成千上万行预处理之后的头文件源码。
传递性:头文件具有传递性。在头文件传递链中任一头文件变动,都将导致包含该头文件的所有源文件重新编译。哪怕改动无
关紧要(没有源文件使用被改动的接口)。
差异性:头文件在编译时使用,动态库在运行时使用,二者有可能因为版本不一致造成二进制兼容问题。
一致性:头文件函数声明和源文件函数实现的参数名无需一致。这将可能导致函数声明的意思,和函数具体实现不一致。如声
明为 void draw(int height, int width) 实现为 void draw(int width, int height)。
3. 单遍编译单遍编译( One Pass )
由于当时的编译器并不能将整个源文件的语法树保存在内存中,因此编译器实际上是”单遍编译”。即编译器从头到尾地编译源
文件,一边解析,一边即刻生成目标代码,在单遍编译时,编译器只能看到已经解析过的部分。 意味着:
C语言结构体需要先定义,才能访问。因为编译器需要知道结构体定义,才知道结构体成员类型和偏移量,并生成目标代码。
局部变量必须先定义,再使用。编译器需要知道局部变量的类型和在栈中的位置。
外部变量(全局变量),编译器只需要知道它的类型和名字,不需要知道它的地址,就能生成目标代码。而外部变量的地址将留
给连接器去填。
对于函数,根据隐式函数声明,编译器可以立即生成目标代码,并假定函数返回int,留下空白函数地址交给连接器去填。
C语言早期的头文件就是用来提供结构体定义和外部变量声明的,而外部符号(函数或外部变量)的决议则交给链接器去做。
单遍编译结合隐式函数声明,将引出一个有趣的例子:
void bar()
{
foo('a');
}
int foo(char a)
{
printf("foobar");
return 0;
}
int main()
{
bar();
return 0;
}
gcc编译上面的代码,得到如下错误:
test.c:16:6: error: conflicting types for 'foo'
void foo(char a)