標籤:

C++ 編譯時會把標準庫里所有的函數都編譯嗎?還是其他的實現方式?如果全部編譯不是太費時間了?


C++ 標準庫的一部分實現(模板類、模板函數、內聯函數)需要寫在頭文件中,如果包含這些頭文件,就需要編譯這些部分。另一部分的實現是預先編譯在程序庫里。

不過模板和非模板的類/函數在編譯時是有差異的,在編譯模板時是不會立即生成代碼,當中調用一些非模板函數也不會完成所有決議,直至在模板實例化的時候,才會把用到的部分生成代碼。關於這方面可以去了解一下 SFINAE(Substitution Failure Is Not An Error)。

為了減少編譯時間,應該只包含需要的頭文件。C++ 標準庫提供了前置聲明頭文件,如 &(事實上現時只有這個)。這樣可避免在程序的頭文件中包含 iostream、fstream 這些頗大的頭文件。

具體 C++ 編譯器怎麼優化編譯時間,我作為使用者就不太懂了,可以問做編譯器的同學如 @藍色。


不用重新編譯的。我先簡單描述 C++ 源碼到生成可執行文件的過程。

每個 .cpp 都是獨立的編譯單元。.cpp 源文件經過預處理,將 #include, #ifndef, #define 之類的預處理指令替換成具體的源碼,得到獨立的,沒有文件依賴的 C++ 源碼。之後才進入編譯過程,生成一個的目標文件。目標文件在 Unix 平台上後綴為 .o,在 Windows 平台上後綴為 .obj。編譯之後進入鏈接過程,多個 .o 文件可以鏈接成庫,包括靜態庫(.a)或者動態庫(.so)。多個庫和 .o 文件,經過鏈接過程,也可以鏈接成可執行文件。

這整個生成過程,可以粗略分解成,預處理(preprocess),編譯(complie),鏈接(link) 三個過程。上面從我將這個過程描述成線性的,實際上的編譯系統也可以設計成一邊預處理一邊編譯。但從概念上理解成上一過程的輸出是下一過程的輸入,這樣會容易些。

C++ 標準庫的函數很多都是預先編譯好的,生成 C++ 程序的時候,只需要鏈接就行了。也會有編譯選項,選擇採用動態鏈接還是靜態鏈接。而 STL 之類的模板庫通常不能預先編譯,採用 #include 源碼方式來使用,經過預處理過程之後,模板庫的源碼也會被重新編譯,鏈接的時候,模板生成的重複代碼剔除。STL 也會分拆成多個頭文件,用到的才會編譯。

而一個程序項目,為便於管理和加快編譯速度,有時會拆開成一個可執行文件和一大堆動態庫,還有資源包。這些動態庫也可以單獨修改,單獨編譯,最後鏈接就成。假如是更大的項目,可能還會採用組件的方式,每個組件單獨分拆成一個工程,每個組件工程專門有人來維護。項目可以配置組件的依賴關係,直接將相應版本的組件拉取下來配置到工程當中。

你可以打開 Windows 上的軟體安裝目錄,會看到一個或幾個 .exe 文件,還有一大堆 .dll 文件。.dll 文件就是 windows 平台上的動態庫。平常的雙擊安裝過程,通常是做些解壓文件,拷貝到對應位置,配置註冊表之類。Mac OS X 上的軟體管理更方便,將多個文件和資源打包成 Bundle, 也沒有全局註冊表。將其拖到對應位置就安裝好了。

題主的疑問,可能是不知道鏈接過程的存在。將 C++ 的,預處理、編譯、鏈接,這三個過程,都當成是編譯。可能是日常的 IDE 封裝得太好了,只提供一個生成(Build)按鈕,大家只知道按這個按鈕,程序就生成好了,沒有弄清楚整個過程是怎麼樣的。

而這個問題要真正弄明白,並非是三言兩語的事,別人也很難幫助,需要自己看書並實踐。可以去看看《深入理解計算機系統》,還有一本書《鏈接器和載入器》也描述了這個過程。


大致來說,凡是你包含的頭文件中的內容,就會重新編譯一遍。其他的內容,就不會。

C++和C一樣,有自己的runtime,全局(準確來說是std空間)函數實現,類的成員函數實現等都是runtime的一部分。他們都是預先編譯好的。

不過C++的標準庫有極大一部分是模板。這些內容基本上必然用一遍編譯一遍。


題主想說的應該是STL,一般情況下是的。

所以需要預編譯頭文件和extern template。


看看程序員的自我修養,這本書還是很有針對性的,時間充裕如果還是學生的話,多看看計算機體系結構吧


如果答案是三個字以內的話,那就是

不會

C/C++ 從代碼(源文件)到程序(可執行文件)一般要經歷以下幾個過程:

1. 預處理:處理各種宏指令(最主要的就是#include指令),以及模板的特化,主要是文本替換的工作

2. 編譯:將代碼源文件編譯成目標文件.o(linux)/.obj(windows)。但是目標文件是不能運行的,因為裡面有許多只有聲明沒有定義的代碼與數據,它們在目標文件里僅僅是一個符號。

3. 鏈接:將所有的目標文件整合成一個可執行文件,其中最重要的工作就是解析目標文件中遺留下來的符號,將符號聲明和代碼/數據定義聯繫在一起。

對於庫函數來說,除了涉及到模板的情況之外,是通過鏈接來解決這個問題的。即將編譯好的目標文件事先放置好,然後將用戶程序員寫的代碼與其鏈接。

這裡的鏈接分為靜態鏈接和動態鏈接。嚴格意義上來講我們在第三步中提到的鏈接是靜態鏈接過程,即將程序在庫中用到的代碼與數據一起寫進可執行文件當中;而動態鏈接則是將庫中用到的代碼與數據在可執行文件當中仍然作為符號保存,而符號的解析則在運行時由操作系統幫助完成。現在常用的鏈接方式是動態鏈接,我們看到的.so(linux)/.dll(windows)文件就都是動態鏈接文件。

所以,一般來說,庫函數的使用,只需要#include相應庫的頭文件,得到庫函數的聲明,再由鏈接過程負責將聲明/符號解析為代碼和數據的定義。


以gcc為例,編譯階段走的流程是:預處理(頭文件展開、宏、模板替換等)-&>語言分析(這步遇到語法問題就會報error、warning等)-&>彙編階段
(代碼轉換為指定CPU的指令集)-&>優化階段(這步會刪去不需要執行的代碼)-&>目標文件生成(即.o這類二進位機器碼的文件)

----------------------------------------------------

引申一下:在優化階段中,實際不需執行的代碼會被優化掉。所以不能說你包含了某個頭文件,這個頭文件的所有代碼最終就一定會生成在目標文件中。

那麼樓主提問到的庫——什麼是庫?庫是打包好的目標文件的集合,在此我僅以最簡單的靜態庫和為例。庫對外提供的頭文件,是會參與編譯過程的,但是這些頭文件對應的代碼文件都以目標文件集合的形式被封裝在了庫文件中(啊咧,這玩意就是編譯結束,生成的二進位結果件嘛,所以這時候你又如何會讓它參與你當前工程的「編譯」?),自然不需要再參與真正意義上的編譯動作。那幹嘛?只需要定位鏈接需要的部分就行了,這不算編譯,僅是參與鏈接。

至於動態庫的例子,見 Yan Ni 的回答即可。先睡了ZZZ

還有《程序員的自我修養》在我看來並不是一本適合研究的書,關鍵細節上還是略欠詳細,作者更像是編,而不是著。這書是我領導的同學與人合著的,囧,缺乏原創性描述與見解,適合科普用。

想稍微深入下,可以閱讀米蘭·斯特瓦諾維奇的《高級C/C++編譯技術》,對問題的闡述和引申非常富有啟發性。


我覺得樓上說的有點麻煩。

1. 一個c和cpp, cxx是否參加變異,是makefile決定的。編譯器嚴格按照makefile的文件list啟動編譯。

2. 每一個c和cpp,cxx編譯的時候都會把include展開,然後層層嵌套。如果你的文件沒有在這個嵌套當中,那麼編譯器是如何發現它的呢?不會。

3. libstdc++等,是各個版本的靜態鏈接庫都在path裡面,不會每次編譯代碼都編譯一下這個庫。但是這些庫不是都參與鏈接,只有你的代碼中用到了才鏈接進你的二進位中。

4. 至於樓上有人提到stl,這個東西也不會每次都被全部編譯,不過這些模板的確是在編譯時候include到代碼中的。有一些比較mini的stl實現vector、list等等,只有幾個h,樓主可以看一下。至於ms的那個stl或者stlport那種重量級實現,他們帶有鏈接庫,是被編譯好隨時準備被鏈接的。這些代碼仔細研讀的可能性不大。


推薦閱讀:

如何從只會 C++ 語法的水平到達完成項目編寫軟體的水平?
這段c++代碼存在內存泄露的可能嗎?
如何格式化代碼能夠將類成員/函數的名字對齊?
請問這個程序為什麼會死在18,19行?
應該如何熟悉GNU工具鏈?例如GCC/Makefile/GDB

TAG:C |