如何在C中解析链式宏?

如果我想使用预处理程序#define语句来轻松定义和计算常量和常用函数,并利用较少的RAM开销(而不是使用const值)。 但是,如果使用多个宏,我不确定它们是如何解决的。

我正在设计自己的DateTime代码处理,类似于linux时间戳,但对于一个代表1/60秒的tick更新的游戏。 我更愿意声明链接的值,但想知道硬编码值是否会更快。

 #include  // my time type, measured in 1/60 of a second. typedef int64_t DateTime; // radix for pulling out display values #define TICKS_PER_SEC 60L #define SEC_PER_MIN 60L #define MIN_PER_HR 60L #define HRS_PER_DAY 24L #define DAYS_PER_WEEK 7L #define WEEKS_PER_YEAR 52L // defined using previous definitions (I like his style, write once!) #define TICKS_PER_MIN TICKS_PER_SEC * SEC_PER_MIN #define TICKS_PER_HR TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR #define TICKS_PER_DAY TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY // ... so on, up to years //hard coded conversion factors. #define TICKS_PER_MIN_H 3600L // 60 seconds = 60^2 ticks #define TICKS_PER_HR_H 216000L // 60 minutes = 60^3 ticks #define TICKS_PER_DAY_H 5184000L // 24 hours = 60^3 * 24 ticks // an example macro to get the number of the day of the week #define sec(t)((t / TICKS_PER_DAY) % DAYS_PER_WEEK) 

如果我使用sec(t)宏,它使用由3个先前的宏TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY定义的TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY ,它会在我的代码中到处调用sec(t)

 (t / 5184000L) % 7L) 

或者每次扩展到:

 (t / (60L * 60L * 60L * 24L)) % 7L) 

以便在每一步执行额外的乘法指令? 这是宏和const变量之间的权衡,还是我误解了预处理器的工作原理?

更新:

根据许多有用的答案,链接宏的最佳设计扩展为常量表达式是将括号括括号

1.适当的操作顺序:

 (t / 60 * 60 * 60 * 24) != (t / (60 * 60 * 60 * 24)) 

2.鼓励编译器通过将常量值组合在一起来进行常量折叠:

 // note parentheses to prevent out-of-order operations #define TICKS_PER_MIN (TICKS_PER_SEC * SEC_PER_MIN) #define TICKS_PER_HR (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR) #define TICKS_PER_DAY (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY) 

请参阅gcc预处理器宏文档 ,特别是类似对象的宏

我认为编译器也在这里发挥作用。 例如,如果我们只考虑预处理器,那么它应该扩展到

(t / (60L * 60L * 60L * 24L)) % 7L)

但是,编译器(无论优化?)可能会将其解析为

(t / 5184000L) % 7L)

因为这些是独立的常量,因此代码执行速度更快/更简单。

注1:您应该在定义中使用“(t)”来防止意外的扩展/解释。 注2:另一个最佳实践是避免使用undef ,因为这会降低代码的可读性。 请参阅有关宏扩展如何受此影响的说明( 类似于对象的宏 )。

更新 :从类似对象的宏部分 :

当预处理器扩展宏名称时,宏的扩展将替换宏调用,然后检查扩展以查找更多要扩展的宏。 例如,

#define TABLESIZE BUFSIZE #define BUFSIZE 1024 TABLESIZE ==> BUFSIZE ==> 1024 TABLESIZE首先展开以产生BUFSIZE,然后扩展该宏以产生最终结果1024。

请注意,在定义TABLESIZE时未定义BUFSIZE。 TABLESIZE的’#define’完全使用您指定的扩展 – 在本例中为BUFSIZE – 并且不检查它是否也包含宏名称。 仅当您使用TABLESIZE时,才会扫描其扩展以获取更多宏名称。

(强调我的)

预处理器只进行文本替换。 它将使用“额外”乘法来评估第二个表达式。 编译器通常会尝试优化常量之间的算术,只要它可以在不改变答案的情况下这样做。

为了最大化其优化的机会,您需要注意保持常量“彼此相邻”,以便它可以看到优化,尤其是浮点类型。 换句话说,如果t是一个变量,你想要30 * 20 * t而不是30 * t * 20

宏扩展只不过是简单的文本替换。 扩展宏之后,编译器将解析结果并执行其通常的优化,其中应包括常量折叠。

但是,此示例说明了初学者在C中定义宏时常犯的错误。如果宏要扩展为表达式,那么良好的C实践规定,如果结果否则将包含公开的运算符,则值应始终包含在括号中。 在此示例中,查看TICKS_PER_DAY的定义:

 #define TICKS_PER_DAY TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY 

现在看一下sec (注意分号不应该出现,但我现在会忽略它):

 #define sec(t)((t / TICKS_PER_DAY) % DAYS_PER_WEEK); 

如果将其实例化为sec(x) ,它将扩展为:

 ((x / 60L * 60L * 60L * 24L) % 7L); 

这显然不是预期的。 它只会除以最初的60L ,之后剩余的值将成倍增加。

解决此问题的正确方法是修复TICKS_PER_DAY的定义以正确封装其内部操作:

 #define TICKS_PER_DAY (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY) 

当然, sec应该是一个表达式宏,不应该包含分号,这会阻止它被使用,例如,在sec(x) + 10这样的上下文中:

 #define sec(t) ((t / TICKS_PER_DAY) % DAYS_PER_WEEK) 

现在让我们看看如何使用这些错误修复扩展sec(x)

 ((x / (60L * 60L * 60L * 24L)) % 7L) 

现在,这实际上将实现预期目标。 编译器应该对乘法进行常数折叠,在单个除法中进行弹性,然后是单个mod。

编辑:看起来丢失的括号已添加到原始post中。 没有他们,它根本就行不通。 此外,已从原始post中删除了额外的分号。

它扩展到:

 (t / (60L * 60L * 60L * 24L)) % 7L) 

这是因为宏由预处理器处理,它只是将宏扩展到它们的值(必要时递归)。

但这并不意味着整个计算将在你使用sec(t)的每个点重复。 这是因为计算在编译时发生。 所以你不要在运行时付出代价。 编译器预先计算此类常量计算,并使用生成的代码中的计算值。