我尊敬的一位程序员在 C 代码中说过,#if
应该#ifdef
不惜一切代价避免使用,除了可能在头文件中。为什么#ifdef
在 .c 文件中使用会被认为是不好的编程习惯?
11 回答
难以维护。更好地使用接口来抽象平台特定的代码,而不是通过#ifdef
在整个实现中分散 s 来滥用条件编译。
例如
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
}
富.h:
void foo(); // common interface
然后有 2 个 makefile 1 : Makefile.win
, Makefile.psx
,每个都编译适当的.c
文件并链接到正确的对象。
小修改:
Iffoo()
的实现取决于出现在所有平台上的某些代码,例如common_stuff()
2foo()
,只需在您的实现中调用它。
例如
常见的.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()
每个平台的定义,除非它遵循非常特定的模式。通常,平台差异需要完全不同的实现,并且不遵循这样的模式。
- 此处使用 Makefile 进行说明。你的构建系统可能根本不使用
make
,例如如果你使用 Visual Studio、CMake、Scons 等。 - 即使
common_stuff()
实际上有多个实现,也因平台而异。
(有点离题了)
我曾经看到一个提示,建议使用#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
对我来说似乎是个不错的主意。希望我能记住来源,以便我可以链接它:(
因为当您搜索结果时,您不知道代码是在还是在不阅读它的情况下。
因为它们应该用于操作系统/平台依赖项,因此这种代码应该在 io_win.c 或 io_macos.c 之类的文件中
我对此规则的解释:您的(算法)程序逻辑不应受到预处理器定义的影响。您的代码的功能应始终简洁。任何其他形式的逻辑(平台、调试)都应该在头文件中是可抽象的。
恕我直言,这更像是一个指导方针而不是严格的规则。但我同意基于 c 语法的解决方案优于预处理器魔法。
一个合理的目标,但没有严格的规则那么伟大
尝试在头文件中保留预处理器条件的建议很好,因为它允许您有条件地选择接口,但不会在代码中乱扔令人困惑和丑陋的预处理器逻辑。
但是,有很多很多很多看起来像下面虚构的示例的代码,我认为没有明显更好的选择。我认为你引用了一个合理的指导方针,但没有引用一个伟大的金片戒律。
#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 FEATURE_1
在使用问题区域时使用了一个模块#if FEATURE1
(注意下划线)。
我非常支持makefile
通过包含正确的库或对象来处理配置。使代码更具可读性。此外,大部分代码变得独立于配置,只有少数文件依赖于配置。
CPP 是(通常)C 或 C++ 之上的独立(非图灵完备)宏语言。因此,如果您不小心,很容易将其与基础语言混淆。无论如何,这是反对宏而不是例如 c++ 模板的常用论点。但是#ifdef?只是去尝试阅读你以前从未见过的其他人的代码,其中有一堆 ifdefs。
例如,尝试阅读这些 Reed-Solomon multiply-a-block-by-a-constant-Galois-value 函数: http ://parchive.cvs.sourceforge.net/viewvc/parchive/par2-cmdline/reedsolomon.cpp?revision= 1.3&view=标记
如果您没有以下提示,您可能需要一分钟才能弄清楚发生了什么: 有两个版本:一个简单版本,一个带有预先计算的查找表 (LONGMULTIPLY)。即便如此,追踪#if BYTE_ORDER == __LITTLE_ENDIAN 也很有趣。当我重写该位以使用受 Linux byteorder.h 东西启发的 le16_to_cpu 函数(其定义在 #if 子句中)时,我发现它更容易阅读。
如果根据构建需要不同的低级行为,请尝试将其封装在提供一致行为的低级函数中,而不是将 #if 内容直接放在较大的函数中。
无论如何,支持抽象而不是条件编译。然而,任何编写过便携式软件的人都可以告诉你,环境排列的数量是惊人的。一些设计原则可以提供帮助,但有时需要在优雅和满足时间表之间做出选择。在这种情况下,可能需要妥协。
考虑您需要提供经过全面测试的代码、100% 分支覆盖率等的情况。现在添加条件编译。
用于控制条件编译的每个唯一符号都会使您需要测试的代码变体数量翻倍。所以,一个符号 - 你有两个变体。两个符号,你现在有四种不同的方式来编译你的代码。等等。
这仅适用于布尔测试,例如#ifdef
. 如果测试的形式为 ,您可以很容易地想象出问题#if VARIABLE == SCALAR_VALUE_FROM_A_RANGE
。
如果您的代码将使用不同的 C 编译器进行编译,并且您使用特定于编译器的功能,那么您可能需要确定哪些预定义的宏可用。
#if #endif 确实使代码的阅读复杂化。然而,我已经看到很多真实世界的代码使用它没有问题并且仍然很强大。因此,可能有更好的方法来避免使用#if #endif,但如果采取适当的措施,使用它们并不是那么糟糕。