为什么应该避免在 .c 文件中使用 #ifdef?

32

我尊重的一位程序员说,在 C 代码中应该尽可能避免使用 #if#ifdef,除非在头文件中。为什么在 .c 文件中使用 #ifdef 会被认为是不良编程实践呢?

11个回答

58

难以维护。最好使用接口来抽象平台相关的代码,而不是滥用条件编译,在实现中到处都散布着 #ifdef 块。

例如:

void foo() {
#ifdef WIN32
   // do Windows stuff
#else
   // do Posix stuff
#endif
   // do general stuff
}

不太好。相反,使用文件foo_w32.cfoo_psx.c

foo_w32.c:

void foo() {
    // windows implementation
}

foo_psx.c:

void foo() {
    // posix implementation
}

foo.h:

void foo();  // common interface

接下来需要有两个makefile1Makefile.win, Makefile.psx,每个makefile编译适当的.c文件并链接正确的目标文件。

小修改:

如果foo()的实现取决于在所有平台上都出现的一些代码,例如common_stuff()2,只需在你的foo()实现中调用它。

例如:

common.h:

void common_stuff();  // May be implemented in common.c, or maybe has multiple
                      // implementations in common_{A, B, ...} for platforms
                      // { A, B, ... }. Irrelevant.

foo_{w32, psx}.c:

void foo()  {  // Win32/Posix implementation
   // Stuff
   ...
   if (bar) {
     common_stuff();
   }
}

如果你正在重复调用common_stuff()函数,除非它遵循一个非常特定的模式,否则不能根据平台参数化foo()的定义。一般来说,平台之间的差异需要完全不同的实现,而不是遵循这样的模式。


  1. 在这里使用Makefiles只是举例说明。您的构建系统可能根本不使用make,例如如果您使用Visual Studio、CMake、Scons等。
  2. 即使common_stuff()实际上有多个针对不同平台的实现。

3
不需要多个 makefile,这就是配置脚本的作用。 - William Pursell
4
取决于构建系统。例如,如果您使用 Visual Studio、CMAKE、Scons 或其他工具,则可能没有任何 makefile。我的示例是为了说明问题。 - Alex Budovski
1
不,我不喜欢这个! :( 我的经验告诉我,在可能的情况下,你应该把所有平台无关的内容放在源文件中,使其尽可能地无形和无缝。头文件混乱和创建依赖于平台的makefiles非常难以维护。 - Matt Joiner
@Pod:我在生产代码中发现,绝大部分是与平台无关的,只有少量代码放在 foo_{w32,psx,...}.c 文件中。虽然在特定平台上的代码重复度可能比你想象的要高,但DRY原则鼓励重构以最小化代码重复。 - Donal Fellows
1
我觉得令人惊讶的是,维护#ifdef的困难竟然被认为是上述内容的广告... - Edmund
显示剩余2条评论

6

(有点偏离问题)

我曾经看到一条提示,建议在调试/隔离代码时使用#if(n)def/#endif块,而不是注释。

建议这样做是为了避免出现需要注释的部分已经有文档注释的情况,需要实施以下解决方案:

/* <-- begin debug cmnt   if (condition) /* comment */
/* <-- restart debug cmnt {
                              ....
                          }
*/ <-- end debug cmnt

相反,这将是:
#ifdef IS_DEBUGGED_SECTION_X

                if (condition) /* comment */
                {
                    ....
                }
#endif

对我来说,这似乎是个不错的想法。但我希望我能记得来源,这样我就可以链接它 :(


3
我只是使用 #if 0 ... #else ... #endif 来临时更改代码进行调试。然后将 0 改为 1 来回到原始代码。 - Steve Jessop
1
哦,还记得不要提交它;-) - Steve Jessop
啊,#if 0 的使用很好。我曾经使用过像 ifdef/else 这样的方式来保留原始代码的完整性,但没有想到可以启用切换功能。(我上面的示例只是为了展示删除代码)我得记住这个切换功能。谢谢 :) - paul
我认为#if 0是C语言中常用的习语,用于暂时禁用代码段,因为/* */不能嵌套(这个答案中的措辞使它看起来像某种秘密技巧)。 - mk12
这对我来说是新的,这可能解释了措辞 - 我是自学C语言的,并且在职业上没有太多接触过它。 - paul

3
  1. 因为如果不阅读代码,你无法确定它是否被包含在搜索结果中。

  2. 因为它们应该用于操作系统/平台依赖项,因此这种代码应该放在像 io_win.c 或 io_macos.c 这样的文件中。


3

我对这个规则的理解是:您的(算法)程序逻辑不应受预处理器定义的影响。您的代码功能应始终简洁明了。任何其他形式的逻辑(平台,调试)都应在头文件中进行抽象化。

在我看来,这更像是一条指导方针而非严格的规则。但我同意,基于c语法的解决方案比预处理器魔法更可取。


2

一个合理的目标,但不是一个严格的规则

尝试将预处理器条件放在头文件中是个好建议,因为它允许你有条件地选择接口,而不会在代码中添加令人困惑和丑陋的预处理器逻辑。

然而,有很多代码看起来像下面这个虚构的例子,我认为没有明显更好的替代方案。我认为你提到了一个合理的指导方针,但并不是一个伟大的金科玉律。

#if defined(SOME_IOCTL)
   case SOME_IOCTL:
   ...
#endif
#if defined(SOME_OTHER_IOCTL)
   case SOME_OTHER_IOCTL:
   ...
#endif
#if defined(YET_ANOTHER_IOCTL)
   case YET_ANOTHER_IOCTL:
   ...
#endif

有时候你需要使用 #if defined(...) && !defined(...),比如当操作系统定义了两个错误代码或信号的值相同时。但是如果可能的话,最好将这种混乱放在单独的文件中,这样大部分时间你就不必关注它了... - Donal Fellows

2
条件编译很难调试。为了找出程序将执行哪个代码块,必须知道所有设置。
我曾经花了一周时间调试一个使用条件编译的多线程应用程序。问题是标识符没有拼写相同。一个模块使用 #if FEATURE_1,而问题区域使用 #if FEATURE1(注意下划线)。
我强烈支持让makefile处理配置,包括正确的库或对象。这使得代码更易读。此外,大部分代码变得与配置无关,只有少数文件与配置相关。

我非常赞同。我修复了太多有条件编译的错误,本应在几分钟内解决的问题却花费了数天时间。 - s1n

1

CPP是一种独立的(非图灵完备)宏语言,通常在C或C++之上。因此,如果不小心,很容易混淆它和基础语言。这就是反对使用宏而不是C++模板的常见论点。但是#ifdef呢?试着阅读一下你从未见过的带有一堆ifdef的其他人的代码吧。

例如,尝试阅读这些Reed-Solomon乘以常数Galois值的函数: http://parchive.cvs.sourceforge.net/viewvc/parchive/par2-cmdline/reedsolomon.cpp?revision=1.3&view=markup

如果没有下面的提示,你可能需要花费一分钟来弄清楚发生了什么:代码有两个版本,一个简单版本,一个使用预先计算好的查找表(LONGMULTIPLY)。即便如此,在跟踪#if BYTE_ORDER == __LITTLE_ENDIAN时还是很有趣的。当我重写那一部分代码使用le16_to_cpu函数时,我发现这样更容易阅读,而它的定义在#if子句中,受到Linux的byteorder.h的启发。

如果您需要根据构建进行不同的低级行为,请尝试将其封装在低级别功能中,这些功能可以在任何地方提供一致的行为,而不是直接将#if等放在较大的函数内部。


1
好的,我只是挑刺一下,但我不确定CPP是否是图灵完备的。请考虑通过条件包含查看http://en.wikipedia.org/wiki/Lambda_calculus。 - David X
1
非图灵完备的评论只是在重申我所读到的<a href=http://en.wikipedia.org/wiki/Assembly_language#Macros>维基</a>。那里声称缺乏循环甚至goto结构是原因。Lambda演算是否需要递归才能实现图灵完备性?因为CPP宏不能递归。您无法根据参数进行条件扩展(可以吗?),因此无法定义终止条件。我必须承认,除了听说过Lambda演算并想知道是否了解它会帮助我理解emacs之外,我对Lambda演算并不了解。 :/ - Peter Cordes
非常抱歉关于格式问题,本应是两段文字。我没有意识到评论区和帖子有这么大的区别。链接地址为http://en.wikipedia.org/wiki/Assembly_language#Macros。 - Peter Cordes
1
实际上,经过尝试,看起来你是对的。 变量重新赋值不起作用(例如:#define X(X + 1)),并且一切都在一个命名空间中,因此没有“with x =(x + 1):{...}”的方式,因此我找不到传递可变参数给递归函数的方法(例如:“fact(x)=(x * fact(x-1))”)。 CPP宏可以是递归的(#include __FILE__),但不那么有用。 - David X

1
毫无疑问,应该优先考虑抽象化而非条件编译。然而,任何编写可移植软件的人都可以告诉你,环境变量的数量是惊人的。一些设计纪律可以帮助,但有时选择是在优雅和满足时间表之间做出的。在这种情况下,可能需要做出妥协。

1
考虑这种情况:你需要提供完全测试过的代码,覆盖100%的分支等。现在加入条件编译。
每个用于控制条件编译的唯一符号会使你需要测试的代码变体数量翻倍。因此,一个符号-你有两个变体。两个符号,你现在有四种不同的编译代码方式。依此类推。
而且,这仅适用于布尔测试,例如#ifdef。如果测试的形式为#if VARIABLE == SCALAR_VALUE_FROM_A_RANGE,你可以轻松想象出问题所在。

0

如果你的代码将使用不同的 C 编译器进行编译,并且你使用了特定于编译器的功能,那么你可能需要确定可用的 预定义宏


1
那么你应该在一个由你控制的头文件中完成这个操作,以抽象出差异。或者如果是内联汇编的差异,则应该在不同的代码文件中处理... - Simeon Pilgrim

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