本文面向的是刚开始使用 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 文件的行为。您知道,“诚实”的预处理器的工作方式大不相同。
您可以通过暂时禁用预编译头文件来检查您的解决方案是否实现错误。然后您可能会遇到许多有趣的错误,这会让您真诚地想知道您的项目是如何编译的。
同样,请参阅文档以获取详细信息。如果仍有不清楚的地方,请咨询我们的支持服务。
正如您所见,处理预编译头文件非常简单。那些尝试使用它们并不断遇到“编译器众多错误”的程序员只是不了解该机制的工作原理。我希望本文能帮助您消除这种误解。
预编译头文件是一个非常有用的选项,可以显著提高项目编译速度。