除了少数操作符(函数调用操作符 ( )、&&、| |、? : 和 , (逗号)) 之外,子表达式所依据的运算次序是未指定的并会随时更改。注意,运算次序的问题不能使用括号来解决,因为这不是优先级的问题。
将复合表达式分开写成若干个简单表达式,明确表达式的运算次序,就可以有效消除非预期副作用。
1、自增或自减操作符
示例:
x = b[i] + i++;
b[i] 的运算是先于还是后于 i ++ 的运算,表达式会产生不同的结果,把自增运算做为单独的语句,可以避免这个问题。
x = b[i] + i;
i ++;
2﹑函数参数
函数参数通常从右到左压栈,但函数参数的计算次序不一定与压栈次序相同。
示例:
x = func( i++, i);
应该修改代码明确先计算第一个参数:
i++;
x = func(i, i);
3、函数指针
函数参数和函数自身地址的计算次序未定义。
示例:
p->task_start_fn(p++);
求函数地址 p 与计算 p++无关,结果是任意值。必须单独计算 p++:
p->task_start_fn(p);
p++;
4﹑函数调用
示例:
int g_var = 0;
int fun1()
{
g_var += 10;
return g_var;
}
int fun2()
{
g_var += 100;
return g_var;
}
int x = fun1() + fun2();
编译器可能先计算 fun1(),也可能先计算 fun2(),由于 x 的结果依赖于函数 fun1()/fun2()的计算次序(fun1()/fun2()被调用时修改和使用了同一个全局变量),则上面的代码存在问题。
应该修改代码明确 fun1/ fun2 的计算次序:
int x = fun1();
x = x + fun2();
5、嵌套赋值语句
表达式中嵌套的赋值可以产生附加的副作用。不给这种能导致对运算次序的依赖提供任何机会的最好做法是,不要在表达式中嵌套赋值语句。
示例:
x = y = y = z / 3;
x = y = y++;
6、volatile 访问
限定符 volatile 表示可能被其它途径更改的变量,例如硬件自动更新的寄存器。编译器不会优化对 volatile 变量的读取。
示例:下面的写法可能无法实现作者预期的功能:
/* volume变量被定义为volatile类型*/
UINT16 x = ( volume << 3 ) | volume; /* 在计算了其中一个子表达式的时候,volume的值可能已
经被其它程序或硬件改变,导致另外一个子表达式的计算结果非预期,可能无法实现作者预期的功能
*/
如下代码不合理,仅用于说明当函数作为参数时,由于参数压栈次数不是代码可以控制的,可能造成未知的输出:
int g_var;
int fun1()
{
g_var += 10;
return g_var;
}
int fun2()
{
g_var += 100;
return g_var;
}
int main(int argc, char *argv[], char *envp[])
{
g_var = 1;
printf("func1: %d, func2: %d\n", fun1(), fun2());
g_var = 1;
printf("func2: %d, func1: %d\n", fun2(), fun1());
}
上面的代码,使用断点调试起来也比较麻烦,阅读起来也不舒服,所以不要为了节约代码行,而写这种代码。
因为 if 语句中,会根据条件依次判断,如果前一个条件已经可以判定整个条件,则后续条件语句不会再运行,所以可能导致期望的部分赋值没有得到运行。
示例:
int main(int argc, char *argv[], char *envp[])
{
int a = 0;
int b;
if ((a == 0) || ((b = fun1()) > 10))
{
printf("a: %d\n", a);
}
printf("b: %d\n", b);
}
作用函数参数来使用,参数的压栈顺序不同可能导致结果未知。
看如下代码,能否一眼看出输出结果会是什么吗?好理解吗?
int g_var;
int main(int argc, char *argv[], char *envp[])
{
g_var = 1;
printf("set 1st: %d, add 2nd: %d\n", g_var = 10, g_var++);
g_var = 1;
printf("add 1st: %d, set 2nd: %d\n", g_var++, g_var = 10);
}
说明:使用括号强调所使用的操作符,防止因默认的优先级与设计思想不符而导致程序出错;同时使得代码更为清晰可读,然而过多的括号会分散代码使其降低了可读性。下面是如何使用括号的建议。
x = ~a; /_ 一元操作符,不需要括号_/
x = -a; /_ 一元操作符,不需要括号_/
x = a + b + c; /_ 操作符相同,不需要括号_/
x = f ( a + b, c ) /_ 操作符相同,不需要括号_/
if (a && b && c) /_ 操作符相同,不需要括号_/
x = (a _ 3) + c + d; /_ 操作符不同,需要括号*/
x = ( a == b ) ? a : ( a –b ); /* 操作符不同,需要括号\*/
3 .即使所有操作符都是相同的,如果涉及类型转换或者量级提升,也应该使用括号控制计算的次序以下代码将 3 个浮点数相加:
/_ 除了逗号(,),逻辑与(&&),逻辑或(||)之外,C 标准没有规定同级操作符是从左还是从右开始计
算,以上表达式存在种计算次序:f4 = (f1 + f2) + f3 或 f4 = f1 + (f2 + f3),浮点数计算过
程中可能四舍五入,量级提升,计算次序的不同会导致 f4 的结果不同,以上表达式在不同编译器上
的计算结果可能不一样,建议增加括号明确计算顺序_/
f4 = f1 + f2 + f3;
如果布尔值表达式需要赋值操作,那么赋值操作必须在操作数之外分别进行。这可以帮助避免=和= =的混淆,帮助我们静态地检查错误。
示例:
x = y;
if (x != 0)
{
foo ();
}
不能写成:
if (( x = y ) != 0)
{
foo ();
}
或者更坏的
if (x = y)
{
foo ();
}