宏展开中的副作用问题
C语言宏定义中的副作用问题源于宏的简单文本替换机制,可能导致带有副作用的表达式被多次求值。以FreeRTOS的listGET_OWNER_OF_NEXT_ENTRY宏为例,通过将参数赋给const局部变量,既避免了多次求值带来的副作用问题,又增强了代码安全性和可读性。编写宏时,对可能产生副作用的参数应先保存到临时变量再使用,这是防御性编程的重要实践。
副作用:
在 C 语言的宏定义中,“副作用” 指的是一个表达式在求值过程中,除了返回一个值之外,还对程序状态(如变量值、内存内容、文件位置等)产生了持久的改变。例如,自增运算 ++、自减运算 --、赋值运算、函数调用等都可能产生副作用。
宏展开中的副作用问题:
宏只是简单的文本替换,宏参数会在宏体的展开位置被原样替换多次。如果传入的参数本身是一个带有副作用的表达式(例如 pxList++),那么该表达式会被多次求值,导致意外的结果。
举例说明
假设有一个宏,它原本想获取链表的节点数并保存指针,但错误地多次使用了参数:
#define BAD_MACRO(list) ( (list)->uxNumberOfItems + (list)->uxNumberOfItems )
如果调用 BAD_MACRO(pxList++),宏展开后变成:
( (pxList++)->uxNumberOfItems + (pxList++)->uxNumberOfItems )
这里 pxList++ 被执行了两次,pxList 被错误地增加了两次,而且第二次 pxList++ 时指针已经指向了下一个节点,逻辑完全错误。
FreeRTOS 中的防御性写法
在 listGET_OWNER_OF_NEXT_ENTRY 宏中:
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \
{ \
List_t * const pxConstList = ( pxList ); \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
{ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
} \
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \
}
这里首先将参数 pxList 的值赋给一个局部常量指针 pxConstList。这有两个作用:
-
避免多次求值:
宏体中多次使用了pxConstList(即对参数pxList的求值)。如果没有这一行,直接使用pxList的话,若用户不小心传入了带有副作用的表达式(如pxList++),就会导致pxList被多次递增,产生严重的 bug。通过先保存值,确保了参数只被求值一次。 -
类型安全与可读性:
使用一个局部变量也提高了代码的可读性,并且在调试时更容易观察中间值。
为什么用了 const?
const 修饰表示这个局部指针本身的值(即它所指向的地址)在宏体内不会被修改。这可以防止宏体内部意外地改变指针的值,进一步保证了安全性。实际上,宏体中确实需要修改 pxConstList->pxIndex,但指针本身指向的地址是固定的,所以用 const 是合理的。
总结
在编写宏时,如果宏体内需要多次使用某个参数,且该参数可能是一个带有副作用的表达式,那么最安全的做法就是将参数先求值并保存在一个临时变量中,然后在宏体中只使用这个临时变量。FreeRTOS 的这个宏正是遵循了这一最佳实践,避免了因宏展开导致的副作用重复计算问题。
更多推荐



所有评论(0)