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

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

#include<stdio.h>

void foo(int a)
{
 printf("foo(int)n");
}

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

void foo(char a)
{
 printf("foo(char)n");
}

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

以上代码通过g++编译运行结果为:foo(int)。尽管后面有更合适的函数原型,但C++在解析bar()时,只看到了void foo(int)。
这是C++重载结合单遍编译造成的困惑之一,即使现在C++并非真的单遍编译(想一下前向声明),但它要和C兼容语义,因此不得不”装傻”。对于C++类是个例外,编译器会先扫描类的定义,再解析成员函数,因此类中所有同名函数都能参加重载决议。
关于重载还有一点就是C的隐式类型转换也给重载带来了麻烦:


// Case 1
void f(int){}
void f(unsigned int){}
void test() { f(5); } // call f(int)

// Case 2
void f(int){}
void f(long){}
void test() { f(5); } // call f(int)

// Case 3
void f(unsigned int){}
void f(long){}
void test() { f(5); } // error. 编译器也不知道你要干啥

// Case 4
void f(unsigned int){}
void test{ f(5); } // call f(unsigned int)...
void f(long){}

再加上C++子类到父类的隐式转换,转换运算符的重载… 你必须费劲心思,才能确保编译器按你预想的去做。
1.2 名字查找
单遍编译给C++造成的另一个影响是名字查找,C++只能通过源码来了解名字的含义,比如 AA BB(CC),这句话即可以是声明函数,也可以是定义变量。编译器需要结合它解析过的所有源代码,来判断这句话的确切含义。当结合了C++ template之后,这种难度几何攀升。因此不经意地改动头文件,或修改头文件包含顺序,都可能改变语句语义和代码的含义。
2. 头文件
在初学C++时,函数声明放在.h文件,函数实现放在.cpp文件,似乎已经成了共识。C++没有C的隐式函数声明,也没有其它高级语言的包机制,因此,同一个项目中,头文件已经成了模块与模块之间,类与类之间,共享接口的主要方式。
C中的效率性,传递性,差异性,一致性,C++都一个不落地继承了。除此之外,C++头文件还带来如下麻烦:
2.1 顺序性
由于C++头文件包含更多的内容:template, typedef, #define, #pragma, class,等等,不同的头文件包含顺序,将可能导致完全不同的语义。或者直接导致编译错误。
2.2 又见重载
由于C++支持重载,因此如果头文件中的函数声明和源文件中函数实现不一致(如参数个数,const属性等),将可能构成重载,这个时候”聪明”的C++编译器不错报错,它将该函数的调用地址交给链接器去填,而源文件中写错了的实现将被认定为一个全新的重载。从而到链接阶段才报错。这一点在C中会得到编译错误,因为C没有重载,也就没有名字改编(name mangling),将会在编译时得到符号冲突。