C语言或者C++语言头文件的意义是什么,现代编译器能否把这个给优化掉?
编译器自动识别添加头文件应该不是太难实现吧?为啥要手工维护这个头文件呢
我不清楚你问的是,头文件存在的意义,还是质疑 C/C++ 的 IDE 不能帮你自动 include 头文件?我一开始是按后者去答的,看了别人的回答,发现理解成前者的居多。
关于头文件的作用:
传统的编译模型里,一个源文件构成一个编译单元。各个编译单元之间互不知道任何信息,只能靠头文件统一各个介面的调用规则。
这种编译模型有很多优点。
首先,可以支持你闭源发布你的库。只要你提供头文件和一个二进位的库文件给你库的使用者就够了。和 Java 里同样做了一定编译处理的 jar 包不同,C/C++ 库的二进位里是没有任何对类(结构体)的排布信息和对函数的调用规则描述的,这个描述的工作就是头文件做了。好处就是二进位库文件以及链接了库的程序里不需要记录任何的元信息,体积小,而且被逆向的难度相对困难。另外呢,程序发布的时候,也不需要发布头文件。
其次,如果一个头文件对应的源文件内部的实现有更改,只要调用介面保持不变的话,那只要把 callee 所属的编译单元重新编译,再把新目标文件重新链接进二进位就行了,caller 所属的编译单元都不需要重新编译。
还有呢,各个编译单元之间相互独立,有利于对各个编译单元并行编译、做增量编译,因为之间没有互相依赖嘛。这个可以提高编译速度。
后来,随著 C++ 中模板的兴起,以及 inline 语义的变化,C++ 的头文件中就不止有传统的声明语句,也可以给出函数的实现了。这就出现了不同于传统的静态库和动态库的第三种库 —— head-only library(唯头文件库),特点是使用方便,还有利于编译器做优化。另外也利于解决 ABI 冲突问题。
以下是原答案:
不,你太想当然了。要提供一个识别准确的而且识别速度还快的实现挺难。
我当时才学 Java 时,对 Java IDE 的自动 import 印象很深。只要敲出些啥,Eclipse 就在下面提示个下划线。只要滑鼠移过去,点击气泡里的自动 import 就好了。当时确实觉得挺高级,挺智能。
但是这个问题放在 C/C++ 里就完全不一样了。C/C++ 里有宏,而宏会决定头文件里实际生效的代码。如果是 IDE 比较好掌握的条件宏,比如 C++ 里通过 -std= 选项控制标注版本的 __cplusplus 宏,问题还比较容易处理。但如果是通过 -D 选项由用户指定的宏呢?比如有下面这两个头文件,PLATFORM 这个宏是通过 -D 参数定义的,你在源文件里敲个 someAPI,你告诉我该 include 哪个文件?
// posix.h
#ifndef POSIX_H
#define POSIX_H
#if PLATFORM == UNIX
inline void someAPI()
{
// implement under UNIX
}
#endif
#endif // POSIX_H
// windows.h
#define WINDOWS_H
#define WINDOWS_H
#if PLATFORM == WINDOWS
void someAPI()
{
// implement under WINDOWS
}
#endif
#endif // WINDOWS_H
对于有些 IDE,比如 vs 可以从它的解决方案的属性里,又比如 CLion 可以从 CMakeLists.txt 里提前掌握到编译时会传哪些宏,那么这些宏还相对来说好处理一些。但这也不是绝对的。假如我要用 __TIME__ 宏(一个定义编译时间的宏)搞事情呢?如果项目是早上编译的那我启用某段代码,如果是下午编译的我启用另一段,那你怎么搞?
有的人会说,那你别管这些条件宏了,把所有分支都分析一遍不就好了么?那对不起,也不行。如果有些分支启用了,会导致整个文件多了或者少了括弧怎么办?或者有些分支里有使用了其他编译器提供的扩展的内容,你的语法分析器不认识的东西怎么办?你这个分析程序还得支持模糊分析啊!比编译器难做多了啊。
另外呢,你可能对 C++ 的语法复杂度毫无概念。
class MyIntAllocator;
namespace std
{
template &
class vector;
}
extern std::vector& v1, v2;
void f()
{
std::swap(v1, v2);
}
比如这个例子,可能稍熟悉点 C++ 的都知道 std::swap(T , T) 是 & 中提供的模板函数。那对于上一段代码,应该提示缺少 & 头文件吗?如果不假思索说 yes 的那你就错了。为什么?因为 & 中有针对 std::vector 的 swap 特化啊!根据模板中的最佳匹配原则,应该优先适用特化的版本。如果只 include 了 std::swap(T , T) 所在的文件,致使编译器在编译时没「看到」特化版本,而只「看到」并用通用的 std::swap(T , T) 版本去编译,那生成的程序就 tm 有 bug 了呀。而且这种 bug 还很难去查,做这个自动 include 的人就得被骂死了啊。
如果给你再加点难度。假设这个模板特化启不启用是有利用 SFINAE 法则的呢?如果这个 SFINAE 的条件计算很耗时间呢?你可能都没见过一个 .cpp 单文件编译 15 分钟,编译器占了 10G 内存是个什么美妙的情景。
另外,不同命名空间可能有同名的东西。比如你写个 vector,该 include 标准库的 & 呢?还是 & 呢?那你是不是得要把包含目录下的所有头文件都分析一遍,最后才只能得到个「先生,您希望 include & 还是 include & 」的建议?
向你介绍下,我本机的 /usr/include 文件夹可是有 94M 的呦,/usr/local/include 文件夹可是有 192M 的呦。哦对了,还差点忘说了,编译的时候用户还可以通过 -I 再加自定义包含目录的。