C++ 头文件解耦 · 拖鞋党的拖鞋摊

令人头疼的糟粕

It sucks!

写过 C++ 的人一定会被编译速度恶心到。一方面是因为 C++ 有很多零负担抽象,选择运行速度,那只能牺牲编译速度。

另一方面是因为随便动一个文件就要编译整个工程!

万恶的头文件

一切都得从 C 语言开始说起。

C 语言不支持调用未声明的函数

1
2
3
4
5
6
7
8
9
10
int (void) 
{
foo();
return 0;
}

void foo()
{

}

这段代码是要被编译器踢屁股的。(如果编译器过了证明编译器没有严格遵循标准,换你踢编译器屁股啦)

除非前置声明函数类型

1
2
3
4
5
6
7
8
9
10
11
void foo(); 
int (void)
{
foo();
return 0;
}

void foo()
{

}

一个文件通常有很多函数,所以就会有很多前置函数声明。

于是头文件就诞生了。

头文件放函数声明,.c 文件放实现。岂不美哉?

现实往往没有那么简单。通常为了使用外部的数据结构或者函数,需要引用其他头文件。

一个引用一个,形成了引用链。

一条链接一条,形成了引用网。

一改动,一编译,BOOM!

没有银弹

那要怎么解决这个问题呢?

基本的原则有好几条。比如引用其他头文件时,在 .c 文件中引用,而不是头文件引用。

这样别人引用你的头文件的时候就不会引入大量的头文件。

当然因为 C++ 的缺陷,这条规则并不是每次都能做到。

比如头文件函数声明中引用了外部的数据结构,这时候如果只在 .c 文件引用头文件,显然引用了外部数据结构的头文件因为没有办法知道外部数据结构类型就会编译失败。

还有一种比较恶心的情况是函数声明引用了本头文件内的数据结构,数据结构在声明后面。

这时候就只有两种选择。一种是将整个数据结构的定义前移。另一种是前置声明数据结构,相应地使用的地方全部改成指针类型。(干,为什么不能用引用,明明引用也是一种指针啊。只不过是自动解引用而已。)实际上现代编程语言都不会有这种问题。

其实还有一种办法,那就是把整个数据结构的定义放到新的头文件里面。(恭喜你,又多了一个头文件,复杂度指数级爆炸。)

最重要的原则还是声明和实现分离。最主要的是数据结构的声明和实现分离。(对,不要再把数据结构的定义放在头文件里。)具体参考《Effective C++》 第 31 条最小化文件之间的编译依赖(本质上就是利用类型系统的弱点通过指针退化来实现多态。)

回到未来

以上的方法都是治标不治本。如果想要根治,就必须废弃 C++ 的头文件引用机制。转而使用现代化的模块机制。不过这个提案又鸽了。如果你有幸使用宇宙第一的 IDE —— Visual Studio 2017 ,又不用跨平台编译,那么欢迎回到未来