代码改变世界

【译】初学者应该知道的关于StdAfx.h的方方面面

2014-08-09 21:17  muzinian  阅读(5500)  评论(0编辑  收藏  举报

原文在此

这篇文章是为了那些将要在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”的程序员只是没有理解这个机制背后的工作原理。我希望本文可以帮你摆脱那些错误理解。预编译头是一个十分有效的选项帮你极大地提升项目编译时间。