本文面向的是剛開始使用 Visual Studio 環境並在其中編譯 C++ 專案的程式設計師。在一個不熟悉的環境中,一切看起來都很陌生和複雜,特別是 stdafx.h 檔案在編譯過程中引起的奇怪錯誤,特別令新手感到煩惱。很多時候,他們的做法是費力地在每個專案中停用所有預編譯標頭檔案。我們寫這篇文章是為了幫助 Visual Studio 新手們弄清楚這一切。
預編譯標頭檔案的目的是加快專案構建速度。初次接觸 Visual C++ 時,程式設計師通常會在非常小的專案上嘗試它,這些專案無法體現預編譯標頭檔案帶來的效能提升。無論使用還是不使用,程式編譯所需的時間似乎都差不多。這恰恰會困擾使用者:他看不到此選項的任何好處,並認為它僅用於某些特定任務,而自己永遠不需要它。這種誤解可能會持續多年。
預編譯標頭檔案實際上是一項非常有用的技術。即使專案只有幾十個檔案,您也能注意到其優勢。使用像 boost 這樣的大型庫時,效能提升尤其明顯。
如果您檢查專案中的 *.cpp 檔案,會發現其中許多都包含了相同的標頭檔案集,例如 <vector>、<string>、<algorithm>。這些標頭檔案又會包含其他標頭檔案,以此類推。
所有這些都導致編譯器的預處理器一遍又一遍地執行相同的工作——它必須多次讀取相同的檔案,將它們相互巢狀,處理 #ifdef 指令和展開宏。因此,相同的操作會重複無數次。
在專案編譯過程中,預處理器需要執行的工作量可以大大減少。其思想是提前預處理一組檔案,然後在需要時簡單地插入已準備好的文字片段。
實際上,它還包括幾個步驟:您不僅可以儲存簡單的文字,還可以儲存更高級別的處理資訊。我不知道 Visual C++ 的具體實現方式,但我知道,例如,您可以儲存已經分割成詞素的文字。這將進一步加快編譯過程。
包含預編譯標頭檔案的副檔名為“pch”。檔名通常與專案名一致,但您當然可以在設定中更改此名稱和其他使用的名稱。*.pch 檔案的大小可能相當大,具體取決於其中展開了多少標頭檔案。例如,在 PVS-Studio 中,它佔用約 3MB。
*.pch 檔案是 stdafx.cpp 檔案編譯的結果。該檔案使用“/Yc”開關進行構建,該開關專門用於告知編譯器建立預編譯標頭檔案。stdafx.cpp 檔案可以只包含一行:#include "stdafx.h"。
最有趣的內容儲存在“stdafx.h”檔案中。所有需要預編譯的標頭檔案都應包含在此檔案中。例如,下面是我們 PVS-Studio 中使用的 stdafx.h 檔案(為文章進行了刪節):
|
|
“#pragma warning”指令是為了消除標準庫產生的警告。
現在,“stdafx.h”檔案應包含在所有 *.c/*.cpp 檔案中。您還應該從這些檔案中刪除所有已包含在“stdafx.h”中的標頭檔案。
但是,當不同的檔案使用相似但仍然不同的標頭檔案集時該怎麼辦?例如:
是否應該建立單獨的預編譯標頭檔案?嗯,您可以這樣做,但沒有必要。
您只需要建立一個預編譯標頭檔案,其中包含 <vector>、<string> 和 <algorithm> 的展開。預處理器不必讀取大量檔案並相互巢狀,其優勢超過了對附加程式碼片段進行語法分析的損失。
啟動新專案時,Visual Studio 的嚮導會建立兩個檔案:stdafx.h 和 stdafx.cpp。正是透過它們實現了預編譯標頭檔案的機制。
這些檔案實際上可以有任何其他名稱;重要的是編譯引數,而不是名稱,您可以在專案設定中指定這些引數。
一個 *.c/*.cpp 檔案只能使用一個預編譯標頭檔案。但是,一個專案可能包含幾個不同的預編譯標頭檔案。假設我們現在只有一個。
因此,如果您使用了嚮導,則已為您建立了 stdafx.h 和 stdafx.cpp 檔案,並且所有必要的編譯開關也已定義。
如果您沒有在專案中啟用預編譯標頭檔案選項,讓我們找出如何啟用它。我建議以下步驟:
現在我們已經啟用了預編譯標頭檔案選項。如果我們現在執行編譯,編譯器將建立 *.pch 檔案。但是,稍後編譯會因錯誤而終止。
我們將所有 *.c/*.cpp 檔案設定為使用預編譯標頭檔案,但這還不夠。我們現在需要將 #include "stdafx.h" 新增到每個檔案中。
“stdafx.h”標頭檔案必須是包含在 *.c/*.cpp 檔案中的第一個! 這是強制性的!否則,您將肯定會遇到編譯錯誤。
仔細想想,這確實很有道理。當“stdafx.h”檔案在最開始被包含時,您可以將已預處理的文字替換到檔案中。此文字始終保持不變,不受任何影響。
現在想象一下,我們在“stdafx.h”之前包含了一個其他檔案,該檔案包含行 #define bool char。這將導致情況不確定,因為我們更改了提到“bool”的所有檔案的內容。現在您不能簡單地插入預處理文字,“預編譯標頭檔案”的整個機制就會被破壞。我認為這是“stdafx.h”必須首先包含的原因之一。也許還有其他原因。
手動將 #include "stdafx.h" 輸入到所有 *.c/*.cpp 檔案中非常繁瑣且乏味。此外,您將在版本控制系統中獲得一個新的修訂版本,其中包含大量已更改的檔案。這樣做不好。
作為原始檔包含到專案中的第三方庫會帶來一些額外的麻煩。更改這些檔案沒有意義。最好的解決方案是為它們停用預編譯標頭檔案,但這在您使用許多小型庫時很不方便。您將不斷遇到預編譯標頭檔案的問題。
但是有一個更簡單的方法來處理預編譯標頭檔案。這種方法不是通用的,但在很多情況下對我很有幫助。
您可以使用“強制包含檔案”選項,而不是手動將 #include "stdafx.h" 新增到所有檔案中。
轉到“高階”設定選項卡。選擇所有配置。在“強制包含檔案”欄位中,鍵入以下文字:
StdAfx.h;%(ForcedIncludeFiles)
從現在開始,“stdafx.h”將自動包含在要編譯的所有檔案的開頭。利潤!
您將不再需要手動將 #include "stdafx.h" 新增到每個 *.c/*.cpp 檔案的開頭——編譯器將自動執行此操作。
這是一個非常重要的問題。盲目地將每個標頭檔案包含到“stdafx.h”中會減慢編譯速度,而不是加快速度。
所有包含“stdafx.h”的檔案都依賴於它的內容。假設“stdafx.h”包含檔案“X.h”。稍微更改“X.h”可能會導致整個專案完全重新編譯。
重要規則。確保您的“stdafx.h”檔案只包含那些從不或很少更改的檔案。系統和第三方庫的標頭檔案是最佳選擇。
如果您將自己專案的檔案包含到“stdafx.h”中,請格外小心。只包含那些更改非常非常少的檔案。
如果 *.h 檔案中的任何一個每月更改一次,那也太頻繁了。在大多數情況下,您需要多次編輯 h 檔案才能完成所有必要的編輯——通常是 2 到 3 次。完全重新編譯整個專案 2 到 3 次是一件很不愉快的事情,不是嗎?此外,您所有的同事都需要做同樣的事情。
但是,對於不變的檔案,不要過於狂熱。只包含您經常使用的標頭檔案。如果您只需要在少數檔案中使用 <set>,包含它就沒有意義了。相反,只需在需要的地方包含該檔案。
我們為什麼需要在專案中擁有多個預編譯標頭檔案?嗯,這確實是一個相當罕見的情況。但這裡有幾個例子。
假設專案同時使用 *.c 和 *.cpp 檔案。您不能為它們使用共享的 *.pch 檔案——編譯器將生成錯誤。
您必須建立兩個 *.pch 檔案。其中一個在編譯 C 檔案 (xx.c) 後建立,另一個在編譯 C++ 檔案 (yy.cpp) 後建立。相應地,您應該在設定中指定對 C 檔案使用一個預編譯標頭檔案,對 C++ 檔案使用另一個。
注意。不要忘記為這兩個 *.pch 檔案設定不同的名稱。否則它們會相互替換。
另一種情況是。專案的一部分使用一個大型庫,而另一部分使用另一個大型庫。
自然,專案的不同部分不應該知道這兩個庫:不同的庫之間可能存在(不幸的)實體名稱重疊。
建立兩個預編譯標頭檔案並在程式的各個部分使用它們是合乎邏輯的。如前所述,您可以為生成 *.pch 檔案的檔案使用任何您喜歡的名稱。嗯,甚至 *.pch 檔案本身的名稱也可以更改。這一切都應該非常小心地進行,當然,但使用兩個預編譯標頭檔案並沒有什麼特別困難的。
現在您已經仔細閱讀了上面的文字,您將能夠理解並消除與 stdafx.h 相關的任何錯誤。但我建議我們再次快速回顧新手程式設計師的典型錯誤,並調查它們的原因。熟能生巧。
您正在嘗試編譯一個使用預編譯標頭檔案的檔案,但相應的 *.pch 檔案丟失了。可能的原因是:
如果您 bother to read it,錯誤文字已經說明了一切。該檔案使用 /Yu 開關進行編譯。這意味著要使用預編譯標頭檔案,但檔案中缺少“stdafx.h”。
您需要在檔案中新增 #include "stdafx.h"。
如果無法做到,請不要為此 *.c/*.cpp 檔案使用預編譯標頭檔案。刪除 /Yu 開關。
專案同時包含 C (*.c) 和 C++ (*.cpp) 檔案。您不能為它們使用共享的預編譯標頭檔案(*.pch 檔案)。
可能的解決方案
您一定做錯了什麼。例如,#include "stdafx.h" 行不是檔案中的第一個。
看看這個例子:
|
|
此程式碼將無法編譯,編譯器將生成一個看似奇怪的錯誤訊息:
error C2065: 'A' : 未宣告的識別符號
它認為 #include "stdafx.h" 之前的文字(包括此行)是預編譯標頭檔案。編譯檔案時,編譯器會將 #include "stdafx.h" 之前的文字替換為 *.pch 檔案中的文字。這將導致“int A = 10”行丟失。
正確的程式碼應如下所示:
|
|
又一個例子
|
|
檔案“my.h”的內容將不會被使用。結果,您將無法使用在此檔案中宣告的函式。這種行為確實會讓程式設計師感到困惑。他們試圖透過完全停用預編譯標頭檔案來“解決”它,然後會傳來關於 Visual C++ 有多麼 buggy 的故事。請記住一件事:編譯器是最不容易出錯的工具之一。在 99.99% 的情況下,您應該生氣的不是編譯器,而是您自己程式碼中的錯誤(證明)。
為避免此類麻煩,請確保始終將 #include "stdafx.h" 新增到檔案最開始的位置。嗯,您可以在 #include "stdafx.h" 之前留下注釋;它們反正不參與編譯。
另一種方法是使用強制包含檔案。請參閱上面的“生活小技巧”部分。
您已將經常編輯的檔案新增到 stdafx.h 中。或者您可能錯誤地包含了自動生成的檔案。
仔細檢查“stdafx.h”檔案的內容:它只能包含從不或很少更改的標頭檔案。請記住,雖然某些包含的檔案本身不會更改,但它們可能包含對其他*.h 檔案的引用,而這些檔案會更改。
您有時可能會遇到一個問題,即即使在修復程式碼後,錯誤仍然沒有消失。偵錯程式報告了一些奇怪的資訊。
此問題可能與 *.pch 檔案有關。出於某種原因,編譯器沒有注意到其中一個頭檔案已更改,因此它不會重新編譯 *.pch 檔案,而是繼續插入之前生成的內容。這可能是由於檔案修改時間相關的一些故障引起的。
這是一種極其罕見的情況。但它是可能的,您應該知道它。我個人在多年的職業生涯中只遇到過 2 到 3 次這種情況。可以透過完全重新編譯專案來解決。
這是使用者向我們的支援服務報告的最常見問題。有關詳細資訊,請參閱文件:“PVS-Studio:故障排除”。在此我只對問題進行簡要總結。
如果一個解決方案可以正常編譯,並不意味著它實現正確。一個解決方案通常可能包含多個專案,每個專案都有自己的預編譯標頭檔案(即自己的 stdafx.h 和 stdafx.cpp 檔案)。
當程式設計師開始在另一個專案中引用一個專案的檔案時,就會出現問題。這可能很方便,而且這種方法確實很受歡迎。但是他們也忘記了 *.cpp 檔案包含行 #include "stdafx.h"。
問題是,將使用哪個 stdafx.h 檔案?如果程式編譯正常,則意味著程式設計師只是運氣好。
不幸的是,我們很難重現使用 *.pch 檔案的行為。您知道,“誠實”的預處理器的工作方式大不相同。
您可以透過暫時停用預編譯標頭檔案來檢查您的解決方案是否實現錯誤。然後您可能會遇到許多有趣的錯誤,這會讓您真誠地想知道您的專案是如何編譯的。
同樣,請參閱文件以獲取詳細資訊。如果仍有不清楚的地方,請諮詢我們的支援服務。
正如您所見,處理預編譯標頭檔案非常簡單。那些嘗試使用它們並不斷遇到“編譯器眾多錯誤”的程式設計師只是不瞭解該機制的工作原理。我希望本文能幫助您消除這種誤解。
預編譯標頭檔案是一個非常有用的選項,可以顯著提高專案編譯速度。