为啥要在 .c 文件中避免使用 #ifdef?

Posted

技术标签:

【中文标题】为啥要在 .c 文件中避免使用 #ifdef?【英文标题】:Why should #ifdef be avoided in .c files?为什么要在 .c 文件中避免使用 #ifdef? 【发布时间】:2010-12-23 11:57:11 【问题描述】:

我尊敬的一位程序员说,在 C 代码中,应该不惜一切代价避免使用 #if#ifdef,除非可能出现在头文件中。为什么在 .c 文件中使用 #ifdef 会被认为是不好的编程习惯?

【问题讨论】:

【参考方案1】:

难以维护。最好使用接口来抽象平台特定的代码,而不是通过在整个实现中散布#ifdefs 来滥用条件编译。

例如

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

然后有 2 个 makefile1Makefile.winMakefile.psx,每个都编译适当的 .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() 定义进行参数化,除非它遵循非常特定的模式。一般来说,平台差异需要完全不同的实现,不遵循这样的模式。


    这里使用 Makefiles 作为说明。您的构建系统可能根本不使用 make,例如如果您使用 Visual Studio、CMake、Scons 等。 即使common_stuff() 实际上有多个实现,但因平台而异。

【讨论】:

不需要多个makefile。这就是配置脚本的用途。 嗯,取决于构建系统。您可能没有任何 makefile,例如,如果您使用 Visual Studio、CMAKE、Scons 或任何其他工具。我的例子是说明性的。 “做一般事情”发生了什么?是否复制并粘贴到两个文件中? 不,我不喜欢这个! :( 我的经验表明,在可能的情况下,您应该将所有与平台无关的东西放在源文件中,您希望使其尽可能不可见和无缝。标题混乱,创建依赖于平台的 makefile 很难维护。 令我惊讶的是,维护#ifdefs 的难度被视为上述内容的广告......【参考方案2】:

(有点离题了)

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

建议帮助避免要评论的部分已经有文档 cmets 并且必须实施如下解决方案的情况:

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

取而代之的是:

#ifdef IS_DEBUGGED_SECTION_X

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

对我来说似乎是一个绝妙的主意。希望我能记住来源,以便我可以链接它:(

【讨论】:

我只是使用#if 0 ... #else ... #endif 临时更改代码以进行调试。然后把 0 改成 1 就可以回到原来的代码了。 哦,记住不要签入;-) 啊,请致电#if 0。我使用了类似的 ifdef/else 来保留原始代码的完美无瑕副本,但没想到启用这样的切换。 (我上面的初始示例只是为了显示直接删除代码)我必须记住切换。谢谢:) 我认为 #if 0 是 C 语言中用于暂时禁用一段代码的常用习惯用法,因为 /* */ 没有嵌套(此答案中的措辞使它看起来像是一些秘密技巧)。 这对我来说是新的,这或许可以解释这个措辞——我是自学 C 语言,并没有很专业地使用它。【参考方案3】:

    因为当您搜索结果时,您不知道代码是输入还是输出。

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

【讨论】:

【参考方案4】:

我对这条规则的解释: 您的(算法)程序逻辑不应受到预处理器定义的影响。您的代码的功能应始终简洁。任何其他形式的逻辑(平台、调试)都应该可以在头文件中抽象出来。

恕我直言,这更像是一个指导而不是严格的规则。 但我同意基于 c 语法的解决方案优于预处理器魔法。

【讨论】:

【参考方案5】:

条件编译很难调试。必须知道所有设置才能确定程序将执行哪一段代码。

我曾经花了一周时间调试一个使用条件编译的多线程应用程序。问题是标识符的拼写不同。一个模块使用了#if FEATURE_1,而问题区域使用了#if FEATURE1(注意下划线)。

我非常支持让makefile 通过包含正确的库或对象来处理配置。使代码更具可读性。此外,大部分代码变得独立于配置,只有少数文件依赖于配置。

【讨论】:

我完全同意。我已经修复了太多有条件编译的错误,这些错误本应在几分钟内解决。【参考方案6】:

一个合理的目标,但没有严格的规则那么伟大

尝试在头文件中保留预处理器条件的建议是好的,因为它允许您有条件地选择接口,但不会在代码中乱扔令人困惑和丑陋的预处理器逻辑。

但是,有很多很多很多看起来像下面虚构的示例的代码,我认为没有明显更好的替代方案。我认为你引用了一个合理的指导方针,但不是一个伟大的金板戒律。

#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(...) &amp;&amp; !defined(...),例如当操作系统将两个错误代码或信号定义为相同的值时。但是,如果可以的话,最好将这些杂乱无章的东西放在一个文件中,这样大多数时候你就不必注意它了……【参考方案7】:

CPP 是(通常)C 或 C++ 之上的独立(非图灵完备)宏语言。因此,如果您不小心,很容易将其与基础语言混淆。这是反对宏而不是例如的通常论点。无论如何,c ++模板。但是#ifdef?去尝试阅读你以前从未见过的其他人的代码,其中包含一堆 ifdef。

例如尝试阅读这些 Reed-Solomon 将一个块乘以一个常数伽罗瓦值函数: 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 内容直接放在较大的函数中。

【讨论】:

好的,这里只是吹毛求疵,但我不确定 CPP 不是图灵完备的。通过条件包含考虑en.wikipedia.org/wiki/Lambda_calculus。 非图灵完备的评论只是在重申我阅读的内容en.wikipedia.org/wiki/Assembly_language#Macros>onwiki。声称的原因是缺少循环甚至 goto 构造。 lambda演算是否需要递归图灵完整性?因为 CPP 宏不能递归。您不能根据参数进行条件扩展(可以吗?),因此您无法定义终止条件。我不得不承认,除了听说过 Lambda 演算之外,我对它知之甚少,并且想知道知道它是否会帮助我了解 emacs。 :// 抱歉格式化;那应该是两段。我没有意识到评论字段与帖子如此不同。链接是en.wikipedia.org/wiki/Assembly_language#Macros 其实试过了,看来你是对的。变量重新分配不起作用(例如:#define X (X+1))并且所有东西都在一个命名空间中,所以没有with x = (x+1):...,所以我找不到将不同参数传递给递归函数的方法(例如:fact(x) = (x*fact(x-1)))。不过,CPP 宏可以是递归的 (#include __FILE__),只是没有用处。【参考方案8】:

无论如何,支持抽象而不是条件编译。然而,任何编写过便携式软件的人都可以告诉你,环境排列的数量是惊人的。一些设计原则可以提供帮助,但有时需要在优雅和满足时间表之间做出选择。在这种情况下,可能需要妥协。

【讨论】:

【参考方案9】:

考虑您需要提供经过全面测试的代码、100% 分支覆盖率等情况。现在添加条件编译。

用于控制条件编译的每个唯一符号都会使您需要测试的代码变体数量翻倍。所以,一个符号 - 你有两个变体。两个符号,你现在有四种不同的方式来编译你的代码。以此类推。

这仅适用于布尔测试,例如#ifdef。如果测试的形式为#if VARIABLE == SCALAR_VALUE_FROM_A_RANGE,您可以轻松想象问题。

【讨论】:

【参考方案10】:

如果您的代码将使用不同的 C 编译器进行编译,并且您使用特定于编译器的功能,那么您可能需要确定哪些 predefined macros 可用。

【讨论】:

那么你应该在你控制的头文件中这样做,以抽象出差异。或者,如果是内联汇编的不同,应该在不同的代码文件中......【参考方案11】:

确实,#if #endif 确实使代码的阅读变得复杂。但是,我已经看到很多真实世界的代码使用它没有问题并且仍然很强大。所以可能有更好的方法来避免使用#if #endif,但如果采取适当的措施,使用它们并没有那么糟糕。

【讨论】:

“我看过很多真实世界的代码”不知道为什么

以上是关于为啥要在 .c 文件中避免使用 #ifdef?的主要内容,如果未能解决你的问题,请参考以下文章

为啥要在 Bash 中避免使用 eval,而应该使用啥?

c++#ifndef 的作用,高分在线等,举例!

为啥要避免使用递增赋值运算符 (+=) 创建集合

C++ 为啥赋值运算符应该返回一个 const ref 以避免 (a=b)=c

为啥 ofstream 在存储字符串的值时会避免前 4 个字符?

C和C++如何混合编程