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 再加自定義包含目錄的。