【发布时间】:2010-12-23 11:57:11
【问题描述】:
我尊敬的一位程序员说,在 C 代码中,应该不惜一切代价避免使用 #if 和 #ifdef,除非可能出现在头文件中。为什么在 .c 文件中使用 #ifdef 会被认为是不好的编程习惯?
【问题讨论】:
我尊敬的一位程序员说,在 C 代码中,应该不惜一切代价避免使用 #if 和 #ifdef,除非可能出现在头文件中。为什么在 .c 文件中使用 #ifdef 会被认为是不好的编程习惯?
【问题讨论】:
难以维护。最好使用接口来抽象平台特定的代码,而不是通过在整个实现中散布#ifdefs 来滥用条件编译。
例如
void foo() {
#ifdef WIN32
// do Windows stuff
#else
// do Posix stuff
#endif
// do general stuff
}
不好看。而是将文件 foo_w32.c 和 foo_psx.c 与
foo_w32.c:
void foo() {
// windows implementation
}
foo_psx.c:
void foo() {
// posix implementation
}
foo.h:
void foo(); // common interface
然后有 2 个 makefile1:Makefile.win、Makefile.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() 定义进行参数化,除非它遵循非常特定的模式。一般来说,平台差异需要完全不同的实现,不遵循这样的模式。
make,例如如果您使用 Visual Studio、CMake、Scons 等。common_stuff() 实际上有多个实现,但因平台而异。【讨论】:
(有点离题了)
我曾经看到一个提示,建议使用 #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 语言中用于暂时禁用一段代码的常用习惯用法,因为 /* */ 没有嵌套(此答案中的措辞使它看起来像是一些秘密技巧)。
因为当您搜索结果时,您不知道代码是输入还是输出。
因为它们应该用于操作系统/平台依赖项,因此这种代码应该在 io_win.c 或 io_macos.c 等文件中
【讨论】:
我对这条规则的解释: 您的(算法)程序逻辑不应受到预处理器定义的影响。您的代码的功能应始终简洁。任何其他形式的逻辑(平台、调试)都应该可以在头文件中抽象出来。
恕我直言,这更像是一个指导而不是严格的规则。 但我同意基于 c 语法的解决方案优于预处理器魔法。
【讨论】:
条件编译很难调试。必须知道所有设置才能确定程序将执行哪一段代码。
我曾经花了一周时间调试一个使用条件编译的多线程应用程序。问题是标识符的拼写不同。一个模块使用了#if FEATURE_1,而问题区域使用了#if FEATURE1(注意下划线)。
我非常支持让makefile 通过包含正确的库或对象来处理配置。使代码更具可读性。此外,大部分代码变得独立于配置,只有少数文件依赖于配置。
【讨论】:
尝试在头文件中保留预处理器条件的建议是好的,因为它允许您有条件地选择接口,但不会在代码中乱扔令人困惑和丑陋的预处理器逻辑。
但是,有很多很多很多看起来像下面虚构的示例的代码,我认为没有明显更好的替代方案。我认为你引用了一个合理的指导方针,但不是一个伟大的金板戒律。
#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(...),例如当操作系统将两个错误代码或信号定义为相同的值时。但是,如果可以的话,最好将这些杂乱无章的东西放在一个文件中,这样大多数时候你就不必注意它了……
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 内容直接放在较大的函数中。
【讨论】:
#define X (X+1))并且所有东西都在一个命名空间中,所以没有with x = (x+1):{...},所以我找不到将不同参数传递给递归函数的方法(例如:fact(x) = (x*fact(x-1)))。不过,CPP 宏可以是递归的 (#include __FILE__),只是没有用处。
无论如何,支持抽象而不是条件编译。然而,任何编写过便携式软件的人都可以告诉你,环境排列的数量是惊人的。一些设计原则可以提供帮助,但有时需要在优雅和满足时间表之间做出选择。在这种情况下,可能需要妥协。
【讨论】:
考虑您需要提供经过全面测试的代码、100% 分支覆盖率等情况。现在添加条件编译。
用于控制条件编译的每个唯一符号都会使您需要测试的代码变体数量翻倍。所以,一个符号 - 你有两个变体。两个符号,你现在有四种不同的方式来编译你的代码。以此类推。
这仅适用于布尔测试,例如#ifdef。如果测试的形式为#if VARIABLE == SCALAR_VALUE_FROM_A_RANGE,您可以轻松想象问题。
【讨论】:
如果您的代码将使用不同的 C 编译器进行编译,并且您使用特定于编译器的功能,那么您可能需要确定哪些 predefined macros 可用。
【讨论】:
确实,#if #endif 确实使代码的阅读变得复杂。但是,我已经看到很多真实世界的代码使用它没有问题并且仍然很强大。所以可能有更好的方法来避免使用#if #endif,但如果采取适当的措施,使用它们并没有那么糟糕。
【讨论】: