为什么要使用预处理指令?

28

这个问题可能看起来很基础,但对于某些工程(非计算机科学)背景的人来说,不确定 C++ 代码中的 '#' 片段是什么意思。

快速搜索让我找到了简明易懂的cplusplus预处理指令教程页面。

但为什么要费心去理解预处理指令的概念呢?难道不能编写等效的代码来分配常量值,定义子程序/函数/宏并处理错误吗?

我想最终想知道的是,在什么情况下使用此类预处理指令是好的做法,而在什么情况下不是。


11
这让我感到有趣。 :) - epochwolf
1
我应该将这个问题设置为社区 Wiki 吗? - Zaid
3
我认为不需要。这是一个有效的编程问题。 - Graeme Perrow
14个回答

31

当您需要执行超出实际应用程序范围的操作时,可以使用预处理器指令。例如,您会看到预处理器根据可执行文件构建的架构来包含或不包含代码。例如:

#ifdef _WIN32 // _WIN32 is defined by Windows 32 compilers
#include <windows.h>
#else
#include <unistd.h>
#endif

预处理指令也用于保护包含文件,以防类/函数等被重复定义。

#ifndef MY_CLASS_6C1147A1_BE88_46d8_A713_64973C9A2DB7_H_
#define MY_CLASS_6C1147A1_BE88_46d8_A713_64973C9A2DB7_H_
    class MyClass {
    ....
    };
#endif

另一个用途是在代码和库中嵌入版本控制。

在Makefile中,你会看到类似这样的内容:

-D MY_APP_VERSION=4.5.1

在代码中你有

cout << "My application name version: " << MY_APP_VERSION << endl;

1
这在存在某些函数只存在于一个平台而另一个平台不存在的情况下尤为重要,比如编译器内置函数,在x86平台上包装了SSE指令,而在PPC平台上则是VMX指令等。在这种情况下,没有可行的替代方案来使用#ifdef,因为模板将尝试编译使用不存在的函数的代码,即使只是为了稍后将它们丢弃。 - Crashworks
一个小问题:WIN32被定义是因为这是默认的项目设置。编译器本身定义了_WIN32(我相信MinGW或其他基于Windows的编译器也定义了它),并可选地定义_WIN64来描述平台。它还将_MSC_VER定义为编译器版本本身。 - Tom
哈哈..抱歉...又挑剔了一下。__MY_HEADER_H__ 不太好,因为以 __[A-Z] 开头的命名在技术上是保留的。大多数编译器定义了 许多 配置设置,所以很可能最终会遇到一个与宏相关的奇怪错误。 - Tom
2
/^_[A-Z]/也是保留的,/__/也是一样。所以你也不能使用 _MY_CLASS_H。在你问之前,/^_[a-z]/ 在全局命名空间中被保留了,因此也不适合用作宏。这就是为什么#pragma once如此有吸引力的原因 - 不要担心编译速度,它可以节省你选择包含保护名称的时间。 - Steve Jessop
没有根本的理由证明 #pragma once 比传统的 include-guards 更快。我刚刚通过在 gcc 上运行 strace 进行了测试,正确保护的头文件永远不会被打开超过一次。我怀疑 cl.exe 没有这种优化,因为微软对于非可移植选项和供应商锁定有着既得利益。 - Tom

10

答案1:有条件地编写代码,取决于它在哪种类型的计算机上运行。

答案2:在编译时启用和禁用语言扩展和兼容性功能。

预处理器来自C语言,因为有许多无法表达的内容。良好的C++代码找到使用它的较少原因,但遗憾的是它并非完全无用。


2
另外,由于C++有“inline”,因此您不应在C++中使用#define宏来尝试提高速度。请参见http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Preprocessor_Macros。 - Harold L
@Harold:这个答案中哪里提到了inline? - Martin York

8

预处理是在代码编译之前发生的。在以下情况下使用它是合适的:

#ifdef WIN32
#include <something.h>
#elseif LINUX
#include <somethingelse.h>
#endif

显然,编译时需要包含所需的头文件,而不是运行时。我们不能在变量中实现这一点。

另一方面,在C++中,将像以下示例中的常量表达式替换为变量是良好的实践并且极大地被鼓励。

#define PI 3.141592654
with
const double PI=3.141592654;

原因是您可以获得正确的类型转换和数据类型处理。

此外,

#define MAX(x,y) (x) > (y) ? (x) : (y)

这并不好,因为你可以写任何东西

int i = 5
int max = MAX(i++, 6);

预处理器将用以下内容替换它:
int max = (i++) > (6) ? (i++) : (6);

很明显这样不能得到预期的结果。

相反,MAX 应该是一个函数而不是宏。如果它是一个函数,它也可以在参数中提供类型。

我见过预处理器用于各种有趣的事情,比如语言关键字声明。在这种情况下,它可以帮助提高可读性。

简而言之,将预处理器用于必须在编译时发生的事情,例如条件包含指令。避免使用它来定义常量。避免使用宏,在可能的情况下使用函数。


我很少需要进行跨平台兼容性,但我理解这一点。 - Zaid
1
请注意,max() 已经在 C++ 标准库中了,而且非常好地使用了模板。 - ceo

7
由于预处理指令在编译时执行,而您编写的代码将在运行时执行。因此,预处理指令有效地为您提供了以编程方式修改源代码的能力。
请注意,C预处理器对这种事情来说是一种相当粗糙的机制;C++的模板系统为代码的编译时构建提供了更强大的框架。其他语言具有更强大的元编程功能(例如Lisp的宏系统)。

3
有点吹毛求疵,预处理指令在编译时执行。 - Nemanja Trifunovic
1
不,代码在编译器查看之前确实需要进行预处理。 - Ed S.
编译时与运行时产生了一个有趣的问题。通过使用预处理器指令,能否实现可观的速度提升呢? - Zaid
是的,例如,如果您的应用程序在紧密循环中调用一个方法来设置某个值,宏将加速它,因为您将避免调用函数的开销。 - Ed S.
1
@Ed:如果编译器比程序员更擅长决定是否内联代码,那么通常情况下,在紧密循环中使用宏来加速与等效函数相比并不一定会提高速度。如果编译器正确地确定在这种情况下内联是一种恶化,则可能会减慢速度。如果编译器本来就会内联调用,则可能根本没有任何区别。而答案可能因不同的编译器和平台而异。 - Steve Jessop
显示剩余6条评论

6

它最常用于两个方面,如果没有它将更难组织:

  1. 包含保护
  2. 针对不同平台的不同代码部分。

3
许多编程语言都有元编程功能,您编写的代码是供编译器遵循而不是运行时环境。例如,在C++中,我们有模板可以根据类型或编译时常量生成特定的代码。Lisp可能是最著名的具有高级元编程功能的语言之一。C预处理器指令/宏只是“元编程”的另一种形式,虽然比其他语言中可用的形式相对较粗糙。预处理器指令在编译时指示编译器执行某些操作,例如在某些平台上忽略某些代码,或查找并替换代码中的字符串。这在编译后运行时将无法完成。因此,本质上,C预处理器是早期的“元编程”或编译器编程形式。

哦...以前从未听说过元编程。不过我喜欢你概括了预处理器指令的使用方式。 - Zaid

2
通常情况下,不应使用预处理指令。可悲的是,在C和C++中有时必须这样做。
最初,C语言的定义方式使得你无法在没有使用预处理器的情况下进行任何严肃的操作。该语言没有其他内置支持来创建模块化程序、常量、内联代码或进行通用编程。
C++解决了大多数这些问题,但该功能仍然存在,因此仍然会被使用。(有趣的是,并非模块化功能。我们仍然受制于#include)
如果您想与一个类似级别的语言进行比较,以执行类似任务但不具有预处理器,请看看Ada

我想知道#include指令有什么不好的地方。 - user13947194
与一个合适的模块化设施相比,#include存在很多问题。其中最大的问题之一是人们认为它是一个模块化设施,而实际上它只是一个机械式的代码复制工具,这不仅会导致一些难以追踪的错误,还会出现一些惊人的用法(例如:通过在同一源文件的不同位置多次包含相同的文件可以实现一些有趣的事情)。 - T.E.D.
你会建议什么替代方案呢?请记住,Java的import语句并不像include语句那样做相同或类似的事情。在Java中,你可以通过javac命令行来包含文件。而在C/C++中,你需要使用命令行指向一个目录,并使用include语句来指向相对文件路径。 - user13947194
@user13947194 - 正确的编程语言应该有一种完整的语言特性来处理这种情况。看起来C++20已经为此设置了一个“模块”导入/导出语句(https://en.cppreference.com/w/cpp/language/modules),但我从未有机会使用支持它的编译器,因此无法对其效用发表评论。 - T.E.D.
我猜你可能没有时间。因为仅仅说“你应该……”并不足够。他们为什么应该这样做?C/C++不是一种合适的语言吗?归根结底,这并不重要。这只是一个主观的问题,只能用主观的看法来回答。这就像问黄色比蓝色好一样主观。我对包含指令没有喜恶之分。你们可能更愿意放弃它。我唯一的问题是我无法理解为什么。但这只是一个小问题。 - user13947194

2
这里有一些历史:C++是从C发展而来的,相比C++,C需要更多地使用预处理器。例如,在C++中定义常量,您会写类似于const int foo = 4;这样的代码,而不是#define FOO 4,后者是C的等价代码。不幸的是,太多人将他们在C中使用预处理器的习惯带到了C++中。
在C++中,有几种合理使用预处理器的方法。使用#include包含头文件是必要的。它还对条件编译很有用,包括头文件包含保护,因此可以将一个头文件#include多次(例如在不同的头文件中),并且只处理一次。 assert语句实际上是一个预处理器宏,还有一些类似的用法。
除此之外,在C++中几乎没有合法的使用预处理器的方法。

#include和相关的#ifdef...#define之所以需要,是因为C和C++都没有创建一个适当的接口工具,就像在Pascal、Modula-2、Ada以及几乎所有现代语言中一样。这只是一种预处理器技巧,每个人都已经习惯了它。 - T.E.D.

1

不,实际上并非所有情况下都可以不使用预处理器。我最喜欢的宏之一是

#define IFDEBUG if(DEBUG==1)
//usage:
IFDEBUG{
  printf("Dump of stuff:.,,,");
}else{
  //..do release stuff
}

如果没有宏,最终可执行文件中可能会浪费(很多)空间。

而且你必须意识到,C/C++ 没有任何类型的包管理系统,如 require 等。因此,如果没有预处理器,就无法防止代码重复。(头文件无法被包含)


4
这将在最终的可执行文件中留下一个“if”测试:你需要在 DEBUG-only 代码周围加上 #ifdef 守卫,在编译时添加或不添加 -DDEBUG 来控制 printf 是否在程序代码中。 - Harold L
你说得对,我举的例子不好。但是我希望我使用的编译器足够聪明,可以优化掉 if(0){..} - Earlz
在我看来,这是一个糟糕的例子;为什么不直接使用“#if defined DEBUG”呢? - Clifford
1
我已经学会使用枚举来表达这个概念,实际上很好用,因为你可以将其声明限定到一个文件/作用域中(我不喜欢使用 #define #define #undef #define... 这样的定义方式)。 - justin
/* 这是在根命名空间中定义的,必要时可以在范围内进行本地化 */ enum { LogEnabled = 0 };/* 用法:*/ if (LogEnabled) { printf("Dump of stuff:.,,,"); }/* ..执行发布操作 */ - justin
显示剩余2条评论

1
为什么还要费心去理解预处理器指令的概念呢?难道不能编写等效的代码来给常量赋值、定义子程序/函数/宏以及处理错误吗?
在C++中,它的使用非常少,语言特性被创建出来就是为了避免与预处理器相关的问题。
我想最终我想知道的是,在什么情况下使用这样的预处理器指令是好的实践,而在什么情况下不是。
通常在C++源代码中,这被认为是不良实践,特别是当有其他语言特性可以完成同样的功能时。但对于某些事情(例如平台/构建依赖程序和生成程序),它是必需的。简而言之,通常有一种更好的替代方法(例如将常量定义为枚举,或者使用内联模板代替宏)。如果你发现自己正在使用其中一种,并且不确定是否有更好的方法在C++中声明此代码片段,请询问/搜索。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接