原文在此
這篇文章是為了那些將要在VS下編譯C++的初學者而寫的。在一個不熟悉的環境中,所有東西看起來都是奇怪且復雜的,對於初學者來說,StaAfx.h這個會在編譯期導致奇怪錯誤的文件會讓他們特別憤怒。到最后,結局就是他們會在每一個項目中刪掉預編譯頭文件。我們寫這個文章的目的是幫助VS新手徹底解決這問題。
預編譯頭的目的
預編譯頭文件的目的是為了提示項目的構建速度。當使用VC++時,程序員通常是寫一個很小的程序,這不會展現預編譯頭帶來的性能提升。無論有沒有它,程序的編譯似乎都會消耗相同的時間。這正是迷惑程序員的地方:他看不到這個選項的任何用處,並認為這個是某些特定的任務所需要的而且他不會需要它,這個錯覺將會持續很多年。
預編譯頭是一個十分有用的技術。你可以在即使只有幾十個文件的項目中看到好處。當時使用類似Boost這些比較重的庫,性能提升更加明顯。
如果你看一看你的項目中的*.CPP文件,你會發現它們中的很多會包含相同的一組頭文件。例如 <vector>, <string>, <algorithm>等,這些頭文件也會包含其他的頭文件。
這些將會導致在編譯器的預處理器會重復地做相同的事很多次。它必須多次讀取相同的一組頭文件,再相互插入這些文件,處理 #ifdef並擴展宏。這些相同操作將占據大量的時間。
預處理器在上述編譯期間不得不做的大量工作是可以大量刪減的。我們可以提前先預處理一組文件然后在需要的地方簡單的插入已經准備好的文本片段。
實際上這還包含跟多的細節:你可以存儲高度加工過的信息而不是簡單的文本。我不知道他具體在VC++這些是怎么實現的。但我知道,作為一種可行的例子,你可以存儲已經按照語素分離的文本。它將會更有效的提升編譯時間。
預編譯頭文件是如何工作的
包含了預編譯頭的文件的擴展名為“*。pch”。通常文件名與項目名一致,但是你在設置里可以更改它。*.pch文件的大小根據你在預編譯頭里展開的文件數多少而定。
*.pch文件是stdafx.cpp文件編譯而成的。stdafx.cpp文件使用“/Yc”選項告訴編譯器生成預編譯頭。stdafx.cpp文件可以只包含一行:#include“stdafx.h”。
最有趣的東西被存在“stdafx.h”文件中。所有的要被預編譯的頭文件都要包含在里面。這有一個例子:
#include "VivaCore/VivaPortSupport.h" //For /Wall #pragma warning(push) #pragma warning(disable : 4820) #pragma warning(disable : 4619) #pragma warning(disable : 4548) #pragma warning(disable : 4668) #pragma warning(disable : 4365) #pragma warning(disable : 4710) #pragma warning(disable : 4371) #pragma warning(disable : 4826) #pragma warning(disable : 4061) #pragma warning(disable : 4640) #include <stdio.h> #include <string> #include <vector> #include <iostream> #include <fstream> #include <algorithm> #include <set> #include <map> #include <list> #include <deque> #include <memory> #pragma warning(pop) //For /Wall
“#pragma warning”指令對於去掉標准庫生成的警告是必須的。現在,“stdafx.h”文件醫改被所有的*.c/*.cpp文件所包含。你也應該刪掉那些已經存在於stdafx.h中的頭文件。
但是,如果不同的文件使用有些類似但又不同的一組頭文件該如何?例如:
文件A: <vector>, <string>
文件B: <vector>, <algorithm>
文件C: <string>, <algorithm>
是否應該生成不同的預編譯頭?不,不必這樣。你可以只生成一個展開了<vector>, <string>和<algorithm>的預編譯頭。預編譯所帶來的好處要大於對額外代碼段的語法分析所造成的損失。
如何使用預編譯頭
每當開始一個新的項目,Visual Studio的Wizard將會生成兩個文件:stdafx.h和stdafx.cpp。通過他們就可以實現預編譯頭的機制。實際上這些文件可以使用其他名字。相關的不是名字而是你在項目設定時指定的編譯參數。
每個*.C/*.CPP文件只能使用一個預編譯頭。但是,一個項目可以包含幾個不同的預編譯頭。假設我們現在只有一個。因此,如果你是用了Wizard,stdafx.h和stdafx.cpp文件將自動創建,同時所有必須的編譯選項也被定義好了。如果你沒有用過這些預編譯頭選項,讓我們看看如何開啟它。我建議按照下面的步驟:
1. 確保對於所有的*.CPP文件預編譯頭都在配置中。在“預編譯頭”選項卡中可以完成(項目->“項目名”屬性->配置屬性->C/C++->預編譯頭):
1.設置 預編譯頭 選項為“/Yu”;
2.設置 預編譯頭文件 選項為 “stdafx.h”;
3設置 預編譯頭輸出文件 選項為“$(IntDir)$(TargetName).pch”;
2.創建一個stdafx.h文件並把它添加到項目中。我們可以包括那些我們想提前預處理的頭文件包括在里面。
3.創建一個stdafx.cpp文件把它添加到項目中。這個文件只有一行:#include "stdafx.h”。
4.在全部配配置中更改stdafx.cpp的設置: 設置 預編譯頭 選項為“/Yc”。
現在我們就開啟了預編譯頭的選項。如果我們執行編譯,編譯器將會創建*.pch文件。然而,編譯會因為錯誤而終止。雖然我們已經設置了所有的*.C/*CPP文件使用預編譯頭,但是這是不夠的。我們要在每個文件中加入"stdafx.h"。而且在們個*.C/*CPP文件中,”stdafx.h“文件必須在第一行。這是強制的否則你會得到一個編譯錯誤。你思考之后就會發現這樣做是有意義的。當這個語句使”stdafx.h“文件被包含在最頂端時,你可以在這個文件中用一個已經預處理過的文本作為替代,這個文本將會始終保持一致並且不會影響任何事情。如果你在”stdafx.h“文件之前插入了其他文件,而這個文件包含一些#define語句比如#define bool char。這個情況是未定義的,因為我們更改了所有包含bool的文件。因此,必不能僅僅簡單地插入一個預處理過的文本,而整個預編譯頭的機制也被破壞了。我認為這是為什么“stdafx.h”文件必須放在第一行的原因之一。可能還會有其他原因,如果你知道,請告訴我。
小秘訣
手動在*.C/*.CPP文件中添加“stdafx.h”文件是十分無聊的。在版本控制系統里你將會得到一個大量文件沒改變的版本,這同樣不好。在項目中作為源文件的第三方庫也會引起一些額外的問題。更改這些文件也不是很好的辦法。最好的方法是對這些文件禁用預編譯頭,但當你使用了很多小的庫,這樣會很不方便。在預編譯頭的路上,你會走的有一些蹣跚。這里有一個簡單的方法解決這個問題,雖然它不是很通用,但在很多情況下,對我很有用。我們可以使用“強制包含文件”選項(項目->“項目名”屬性->配置屬性->C/C++->高級)。在這個選項中填寫StdAfx.h;%(ForcedIncludeFiles)。這樣就可以編譯器就可以自動在所有*.C/*.CPP文件的最開始的包含#include “stdafx.h”語句了。
什么應該被包含在stdfx.h中
這是一個很重要的問題,如果盲目的把每一個頭文件添加到“stdafx.h”中,反而會降低編譯速度。頭文件應該依據其內容被添加到“stdafx.h”文件中,如果“X.h”在被包含了,那么"X.h"的一丁點兒改變都會導致整個項目的重新編譯。主要的原則:確保被包含的文件是那些不會或者很少改變的頭文件。最佳候選是標准庫和第三方庫的頭文件。如果你包含了你自己的項目,一定要小心,只包含那些極少會更改的文件。一個月改一次的頭文件已經屬於頻繁的了,在大多數情況下, 他會花費你不止一次的時間去做所有在頭文件里必須的編輯,通常是兩三次。完全編譯一個項目兩三次可不是一個讓人高興的事,而且,你的同事也要這樣。但是,也不要迷信那些不會改變的文件,只包含你最長使用的文件,如果你只在幾個文件中使用<set>,把它包含進stdafx.h沒有意義。一個簡單的include指令就可以了。
使用幾個預編譯頭
在什么樣的情況下我們在一個項目可能會需要幾個預編譯頭?當然,只是十分少見的情況,這有幾個例子。想象在一個項目中既有*.C又有*.CPP文件。你不能使用一個共享的*.pch文件。編譯器會產生錯誤。你需要兩個*.pch文件,一個是在編譯完C文件之后產生的,一個是在編譯完CPP文件之后產生的。因此,你要在設定中分別指定哪個是為C文件使用的預編譯頭,哪個CPP文件使用的。記住,兩個*.pch文件要使用不同的名字,否則一個會覆蓋另一個。還有一個情況,在項目中,一部分文件使用一個大型庫而另一部分使用另一個大型庫。自然地,不同的部分不應該知道所有的庫:很可能在不同的庫中會有命名沖突。生成兩個不同的預編譯頭並在項目不同的部分使用它們是符合邏輯的。正如我們提醒過的,你可以使用任何你想用的名字,並且還可以更改。在做些事時,你應該要仔細。但是使用兩個預編譯頭並沒有什么很特別的困難。
典型錯誤
在你讀過上述文字之后,你就可以明白stdafx.h並可以消除相關的錯誤。但我還是建議要快速的查看初學者的錯誤並了解背后的原因。畢竟孰能生巧。
Fatal error C1083: Cannot open precompiled header file: 'Debug\project.pch': No such file or directory
你正在編譯一個使用了預編譯頭但他相應的*.pch文件丟失的源文件。原因可能有:
1.stdafx.cpp文件沒有被編譯,所以相應的*.pch文件沒有生成。可能是你清理了解決方案但是又要編譯一個CPP文件。解決方法是,重新編譯解決方案或者stdafx.cpp文件
2.沒有文件在設置中被指定生成*.pch文件。這個跟“/Yc”選項相關,對於VS新手這是一個常見的問題,解決方法看“如何使用預編譯頭”。
Fatal error C1010: unexpected end of file while looking for precompiled header. Did you forget to add '#include "stdafx.h"' to your source?
這個錯誤已經說明的原因。這個源文件使用的"/Yu"選項,預編譯有已經使用了,但是在源文件中沒有使用stdafx.h文件。所以,你要在源文件中添加#include“stdafx.h”。如果你沒法改變源文件,不要使用預編譯頭並且刪掉“/Yu”選項。
Fatal error C1853: 'project.pch' precompiled header file is from a previous version of the compiler, or the precompiled header is C++ and you are using it from C (or vice versa)
這個項目同時包含C文件和CPP文件,你不能只是用一分pch文件。解決方法:
1.對C文件禁用預編譯頭,經驗顯示C文件的預處理階段要比CPP文件快幾倍。所以,如果你只有幾個C文件,可以考慮對它們禁用這個功能,這並不會簡單性能。
2.生成兩個預編譯頭。一個使用stdafx_cpp.cpp,stdafx_cpp.h生成;另一個是使用stdafx_c.c, stdafx_c.h生成。對於CPP文件使用第一個生成的預編譯頭,對於C文件使用第二個生成的預編譯頭。兩個預編譯頭使用的名字。
使用預編譯頭時編譯器錯誤的行為
你可能做了錯誤的事,比如#include“stdafx.h”不在第一行
int A = 10; #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { return A; }
編譯器會產生一個錯誤
error C2065: 'A' : undeclared identifier
編譯器會認為所有在#include"stdafx.h"之前(包括這一行)的語句都是預編譯頭。當編譯這個文件時,編譯器會用*.pch文件的文本替換這行之前的文本。所以變量A的定義語句就會消失。正確的寫法是:
#include "stdafx.h" int A = 10; int _tmain(int argc, _TCHAR* argv[]) { return A; }
另一個例子:
#include "my.h" #include "stdafx.h"
my.h的內容將不會被使用,你將我發使用頭文件里聲明的函數。這個行為很是的讓程序員迷惑。他們試圖通過禁止使用預編譯頭來解決問題並以為是Visual C++的bug。記住一件事:編譯器是bug最少的工具。在99.99%的情況下,你應該怪罪的是你的代碼而非編譯器。解決方法是:永遠將#include"stdafx.h"放置在文件的第一行,當然,在它前面可以添加注釋,注釋不會參加編譯,或者可以使用強制包含文件選項,查看小秘訣了解。
使用預編譯頭這個項目重新完全編譯
你包含了一個你經常改變的頭文件在stdafx.h中,或者你錯誤的包含了一個自動生成文件。認真檢查stdafx.h文件中的內容:一定要只包含那些你很少很少會改變的頭文件。記住,雖然有些文件他們本身不會改變,但是它們可能包含其他可能會改變的文件的引用。
仍然會發生一些奇怪的事情
有時候,即使你修復代碼之后,也會遇到錯誤不會消失的問題。調試器會報告一些奇怪的東西。這個問題可能很pch文件相關。因為一些原因編譯器編譯器沒有發現頭文件已經改變
所以編譯器不會重新編譯pch文件而是依然插入之前生成的文本。這可能是文件修改時間相關的錯誤導致的。這是極少見的情況,但也是有可能發生的,所以你因該知道。就我本人(原作者)而言,在我職業生涯期間只遇到兩三次,你可以通過重新編譯整個項目解決這個問題。
總結
正如你所見,使用預編譯頭很簡單。那些試圖使用它並不斷面對“編譯器的許多bug”的程序員只是沒有理解這個機制背后的工作原理。我希望本文可以幫你擺脫那些錯誤理解。預編譯頭是一個十分有效的選項幫你極大地提升項目編譯時間。