文章目录
- 8.12 表达式(Expressions)
- Rule 12.1 表达式中运算符的优先级应明确
- Rule 12.2 移位运算符的右操作数应在零到比左操作数基本类型的位宽度小一的范围内
- Rule 12.3 不得使用逗号(,)运算符
- Rule 12.4 常量表达式的求值不应导致无符号整数的回绕
8.12 表达式(Expressions)
Rule 12.1 表达式中运算符的优先级应明确
等级:建议
分析:可判定,单一编译单元
适用:C90,C99
展开:
下表用于此规则的定义。
选择该表中使用的优先级是为了简明地描述规则。它们不一定与其他操作符优先级描述中可能遇到的相同。
出于本规则的目的,表达式的优先级是位于该表达式解析树根的元素(操作数或操作符)的优先级
例如:表达式a << b + c的解析树可以表示为:
该解析树的根元素是’<<',因此表达式的优先级为10
此规则提供以下建议:
•sizeof操作符的操作数应该用圆括号括起来;
•优先级在2到12范围内的表达式应该在具有这两个优先级的操作数周围加上括号:
-优先级小于13,并且
-优先级大于表达式的优先级
原理: C语言的运算符数量很多,并且它们的相对优先级并不直观。这会导致经验不足的程序员犯错误。使用括号使运算符优先级明确,可以消除程序员的期望与事实不符的可能性。这也使代码的审阅者或维护者可以清楚地了解程序员的原始意图。
众所周知,过度使用括号会使代码混乱并降低其可读性。这条规则的目的是在那些因为括号太多或太少而难以理解的代码之间达成妥协。
示例:
下面的示例显示具有单目或后缀运算符的表达式,其操作数是主表达式或顶级运算符具有优先级 15 的表达式。
a[ i ]->n; /* 合规 - 无需写成 (a[i])->n,优先级为15 */
*p++; /* 合规 - 无需写成 *(p++),优先级为14 */
sizeof x + y; /* 违规 - 应写成 sizeof(x) + y 或 sizeof(x + y) */
以下示例显示了包含具有相同优先级的运算符的表达式。 所有这些都是合规的,但取决于 a,b 和 c 的类型,具有多个运算符的任何表达式都可能违反其他规则。
相同指的是操作数的优先级和表达式的优先级
a + b;
a + b + c;
(a + b) + c;
a + (b + c);
a + b - c + d;
(a + b) - (c + d);
以下示例列举了各种混合运算符表达式:
/* 合规 - 无需写成 f((a + b), c) */
x = f (a + b, c);
/* 违规
* 条件运算符(优先级2)的操作数为:
* == 优先级8 需要括号
* a 优先级16 不需要括号
* - 优先级11 需要括号
*/
x = a == b ? a : a - b;
/* 合规 */
x = (a == b) ? a : (a - b);
/* 合规
* << 运算符(优先级10)的操作数为:
* a 优先级16 不需要括号
* b+c的优先级为11,大于10,需要括号(原文中注释的不对)
*/
x = a << (b + c);
/* 合规
* && 运算符(优先级4)的操作数为:
* a 优先级16 不需要括号
* && 优先级4 优先级相同 不需要括号
*/
if (a && b && c)
{
}
/* 合规
* && 运算符(优先级4)的操作数为:
* defined(X) 优先级14 不需要括号
* x+y 优先级11大于10,需要括号 >Z优先级9,需要括号(原文中注释的不对)
*/
#if defined(X) && ((X + Y) > Z)
/* 合规
* && 运算符(优先级4)的操作数为:
* !defined(X) 优先级14 不需要括号
* defined(Y) 优先级14 不需要括号
* ! 运算符(优先级14)的操作数为:
* defined(X) 优先级14 优先级相同 不需要括号
*/
#if !defined(X) && defined(Y)
注意:该规则不要求将,操作符的操作数加括号。规则12.3禁止使用,操作符。
x = a, b; /* 合规 - 解析为 (x = a), b */
解读:对于运算符的优先级,一般很难记得清楚,特别是在逻辑运算和加减乘除一起作为判断时,所以在指定的操作数之间加上括号,可以防止优先级导致的意外运算与操作,同时也更便于自己或其他人理解。这个规则还是很有必要执行的
Rule 12.2 移位运算符的右操作数应在零到比左操作数基本类型的位宽度小一的范围内
等级:必要
分析:不可判定,系统范围
适用:C90,C99
原理:
右操作数为负或大于或等于左操作数的宽度的行为未定义。
举例来说,如果左移或右移的左操作数是 16 位整数,则确保仅将其移位 0 到 15 之间的数字非常重要。
有关移位运算符的基本类型和基本类型的限制的说明,请参见第 8.10 节。
我们有多种方法可以确保遵守此规则。最简单的方法是将右操作数设为常数(然后可以静态检查其值)。
另一种方法是使用无符号整数类型以确保操作数为非负数,然后仅需要检查上限(在运行时动态检查或通过评审(review)检查)。否则,将需要检查两个限制
示例:
u8a = u8a << 7; /* 合规 */
u8a = u8a << 8; /* 违规 ,uint8左移8位导致无意义*/
u16a = (uint16_t)u8a << 9; /* 合规 */
为便于理解以下示例,应注意,1u 的基本类型是 unsigned char,而 1UL 的基本类型是 unsigned long。
1u << 10u; /* 违规 */
( uint16_t ) 1u << 10u; /* 合规 */
1UL << 10u; /* 合规 */
解读:移位运算应该指定长度,且长度不能高于左操作数的数据类型,否则移位无意义,代码是可能异常的。这个规则也需要执行
Rule 12.3 不得使用逗号(,)运算符
等级:建议
分析:可判定,单一编译单元
适用:C90,C99
原理:
使用逗号运算符通常不利于代码的可读性,并且通常可以通过其他方式实现相同的效果。
示例:
f((1, 2), 3); /* 违规 - 究竟有几个形参? */
以下示例违背 了此规则和其他规则:
for (i = 0, p = &a[0]; i < N; ++i, ++p)
{
}
解读:逗号运算符不应该使用,无意义
Rule 12.4 常量表达式的求值不应导致无符号整数的回绕
等级:建议
分析:可判定,单一编译单元
适用:C90,C99
展开:
此规则适用于满足常量表达式约束的表达式,无论它们是否出现在需要常量表达式的上下文中。
如果未对表达式求值(例如,它出现在逻辑 AND 运算符的右操作数中,而其左操作数始终为 false),则此规则不适用。
原理: 无符号整数表达式不会严格溢出,而是会回绕。 尽管在运行时使用模运算可能有充分的理由,但是在编译时故意使用模运算的可能性较小。
示例:与 case 标签关联的表达式必须是常量表达式。如果在对 case 表达式求值时发生无符号回绕,则可能是无意的。在具有 16 位宽度的 int 类型的计算机上,在下面的示例中,任何大于或等于 65024 的 BASE 值都将导致回绕:
#define BASE 65024u
switch(x )
{
case BASE + 0u:
f();
break;
case BASE + 1u:
g();
break;
case BASE + 512u: /* 违规 - 会回绕回 0 */
h();
break;
}
预处理指令#if 和#elif 的控制表达式必须是常量表达式。
#if 1u + (0u - 10u) /* 违规 - 因为 (0u - 10u) 会回绕 */
在下面这个示例中,表达式 DELAY + WIDTH 的值为 70000,但是在具有 16 位 int 类型的计算机上,该
值将回绕为 4464。
#define DELAY 10000u
#define WIDTH 60000u
void fixed_pulse(void)
{
uint16_t off_time16 = DELAY + WIDTH; /* 违规 */
}
在下面的合规示例中,此规则不适用于表达式 c +1,因为它访问了对象,因此不满足常量表达式的约束:
const uint16_t c = 0xffffu;
void f ( void )
{
uint16_t y = c + 1u; /* Compliant */
}
在下面的示例中,子表达式(0u-1u)导致无符号整数环绕。在 x 的初始化中,不评估子表达式,因此表达式是合规的。但是,在y 的初始化中,可能会对其进行求值,因此表达式违规。
bool_t b;
void g(void)
{
uint16_t x = (0u == 0u) ? 0u : (0u - 1u); /* 合规 */
uint16_t y = b ? 0u : (0u - 1u); /* 违规 */
}
解读:该规则中的wrap-around虽然不是overflow,但本质上还是会导致计算结果异常,之前一直理解的就是溢出,只是说异常的结果是可以计算出来的。该规则有必要执行,不过一般不会犯这种错误