运算符
表达式
C语言的特点在于更加强调表达式的使用
表达式是由变量、常量和运算符所构成的
C拥有丰富的运算符集合, 包括:
五个二元算术运算符(有两个操作数):
+ 加法- 减法* 乘法/ 除法% 求余两个一元算术运算符(仅一个操作数):
+ 一元加- 一元减一元运算符只需要一个操作数:
i = +1;
j = -i;
一元+运算符什么也不做, 它主要用于强调数字常数是正数
i % j是i除以j时的余数
二元算术运算符(%除外)允许使用整数或浮点操作数, 并允许混合
当int和float/double操作数混合时, 结果的类型为float/double
/和%运算符 /和%运算符使用需要特别注意:
当两个操作数都是整数时, /会截断结果: 值1 / 2是0, 而不是0.5
%运算符要求整数作为操作数; 任一操作数不是整数, 程序将无法编译
使用零作为/或%的右操作数会导致未定义的行为
/和%与负操作数一起使用时的行为在C89中由实现定义
C99中, 除法结果总是向零截断, 并且i % j的值与i有相同符号
C标准故意不指定语言的某些部分
避免编写依赖于由具体实现所定义的行为的程序
i + j * k表示:
将i和j相加, 然后乘以k?
将j和k相乘, 然后加上i?
一种方案是添加括号, 写成(i + j) * k或i + (j * k)
若括号被省略, C使用运算符优先级规则来确定表达式含义
算术运算符 的优先级:
+ - (一元)* / % (二元)+ - (二元)示例:
i + j * k |
等价于 | i + (j * k) |
|---|---|---|
-i * -j |
等价于 | (-i) * (-j) |
+i + j / k |
等价于 | (+i) + (j / k) |
表达式有多个相同优先级操作符, 考虑运算符的结合性
如果运算符从左到右结合的, 则称它为左结合运算符
二元算术运算符*, /, %, +, -都是左结合的:
i - j – k |
等价于 | (i - j) - k |
|---|---|---|
i * j / k |
等价于 | (i * j) / k |
如果运算符从右向左结合, 则它是右结合的运算符
一元算术运算符(+和-)都是右结合的:
-+i等价于-(+i)
条形码下方数字的含义:
计算校验位流程:
1、3、5、7、9和第11位数字求和2、4、6、8和第10位数字求和3 并与第二个总和相加110 的余数9 减去余数产品编码为 013800151735 的校验位计算示例:
upc.c程序要求用户输入产品编码的前 11 位数字, 然后显示相应的校验位:
Enter the first (single) digit: 0
Enter first group of five digits: 13800
Enter second group of five digits: 15173
Check digit: 5
该程序将每个数字组读取为五个一位数字
要读取单个数字, 可以在scanf中使用%1d转换说明
upc.c
/* Computes a Universal Product Code check digit */
#include <stdio.h>
int main(void)
{
int d, i1, i2, i3, i4, i5, j1, j2, j3, j4, j5,
first_sum, second_sum, total;
printf("Enter the first (single) digit: ");
scanf("%1d", &d);
printf("Enter first group of five digits: ");
scanf("%1d%1d%1d%1d%1d", &i1, &i2, &i3, &i4, &i5);
printf("Enter second group of five digits: ");
scanf("%1d%1d%1d%1d%1d", &j1, &j2, &j3, &j4, &j5);
first_sum = d + i2 + i4 + j1 + j3 + j5;
second_sum = i1 + i3 + i5 + j2 + j4;
total = 3 * first_sum + second_sum;
printf("Check digit: %d\n", 9 - ((total - 1) % 10));
return 0;
}
简单赋值: 将值存储到变量中, 如=
复合赋值: 更新已经存储在变量中的值, 如+=
v = e先评估表达式e的值, 然后将值复制到v中
e可以是常量、变量或更复杂的表达式:
i = 5; /* i is now 5 */
j = i; /* j is now 5 */
k = 10 * i + j; /* k is now 55 */
如果 v 和 e 的类型不同, 在赋值时 e 的值将转换为 v 的类型:
int i;
float f;
i = 72.99f; /* i is now 72 */
f = 136; /* f is now 136.0 */
在许多编程语言中, 赋值是一个语句; 而在 C 中, 赋值操作的结果类似运算符, 就像运算符 + 一样
赋值表达式 v = e 的值是赋值后 v 的值
i = 72.99f 的值是 72, 而不是 72.99i = j = 50 + 3;
修改操作数的运算符被称为具有 副作用 , 如 i + j 没有 副作用
简单的赋值运算符有副作用: 它修改了左操作数
i = 0 的结果为 0, 它的副作用是将 0 赋值给i由于 赋值 是一个运算符, 因此可以将多个赋值链接在一起:
i = j = k = 0;
= 运算符是右结合的, 所以这个赋值等价于
i = (j = (k = 0));
需要注意由于类型转换导致的链式赋值中产生的非预期的结果:
int i;
float f;
f = i = 33.3f;
i 被赋值为 33, 然后 f 被赋值为 33.0, 而不是 33.3
允许 v 类型值的地方都可以进行形如 v = e 的赋值:
i = 1;
k = 1 + (j = i);
printf("%d %d %d\n", i, j, k);
/* prints "1 1 2" */
嵌入式赋值 会使程序难以阅读, 容易隐藏错误
赋值运算符需要一个 左值 作为其左操作数
左值 表示存储在计算机内存中的对象, 而不是常量或计算结果
10或2 * i这样的表达式不是左值由于赋值运算符需要一个左值作为其左操作数, 因此将任何其他类型的表达式放在赋值表达式的左侧都是非法的:
12 = i; /*** WRONG ***/
i + j = 0; /*** WRONG ***/
-i = j; /*** WRONG ***/
:fa-lightbulb-o: 编译器将产生一条错误消息, 例如"invalid lvalue in assignment"
使用变量的旧值来计算其新值的赋值很常见
例如:
i = i + 2;
使用 += 复合赋值运算符, 可改写为:
i += 2; /* same as i = i + 2; */
还有其他九个复合赋值运算符, 包括:
-= *= /= %= 位运算(^= &= >>= <<= |=)
所有复合赋值运算符的工作方式大致相同:
| v += e | 将v加e, 将结果存储在v中 |
|---|---|
| v -= e | 将v减e, 将结果存储在v中 |
| v *= e | 将v乘以e, 将结果存储在v中 |
| v /= e | 将v除以e, 将结果存储在v中 |
| v %= e | 计算v除以e的余数, 将结果存储在v中 |
v += e与v = v + e可能不等价
i *= j + k 与 i = i * j + k 不等价
v += e 不同于 v = v + e 因为 v本身有副作用(如v为++i或i++等):fa-lightbulb-o: 使用复合赋值运算符时, 不要交换构成复合运算符的两个字符
虽然i =+ j能编译通过, 但意义不同, 它相当于 i = (+j), 它只是将j的值复制到i中
对变量最常见的两种操作是 自增(加 1)和 自减(减 1):
i = i + 1;
j = j - 1;
可以使用复合赋值运算符进行自增和自减:
i += 1;
j -= 1;
C 特别提供了 ++ 自增 和 -– 自减 运算符
++ 运算符将操作数加1
-- 运算符将操作数减1
自增和自减运算符使用起来很棘手:
有前缀运算符(++i和–-i)或后缀运算符(i++和i--)
副作用是修改操作数的值
评估表达式++i (前缀自增) 的结果是i + 1, 副作用是i自增1:
i = 1;
printf("i is %d\n", ++i); /* prints "i is 2" */
printf("i is %d\n", i); /* prints "i is 2" */
评估表达式i++ (后缀自增) 的结果是i, 副作用是i随后自增1:
i = 1;
printf("i is %d\n", i++); /* prints "i is 1" */
printf("i is %d\n", i); /* prints "i is 2" */
++i表示 立即自增i, 而i++表示 暂时使用i的旧值, 稍后再自增i
i将在执行下一条语句之前一定会自增--运算符与++运算符相似:
i = 1;
printf("i is %d\n", --i); /* prints "i is 0" */
printf("i is %d\n", i); /* prints "i is 0" */
i = 1;
printf("i is %d\n", i--); /* prints "i is 1" */
printf("i is %d\n", i); /* prints "i is 0" */
当++或--在同一个表达式中多次使用时, 结果通常很难理解, 如:
i = 1;
j = 2;
k = ++i + j++;
最后一条语句等价于:
i = i + 1;
k = i + j;
j = j + 1;
i, j和k的最终值分别为 2, 3和4.
相反, 执行语句
i = 1;
j = 2;
k = i++ + j++;
i、j和k的值最终为2、3和3
到目前为止所涉及的运算符:
| 优先级 | 名称 | 符号 | 结合性 |
|---|---|---|---|
| 1 | 自增(后缀) | ++ |
left |
| 自减(后缀) | -- |
||
| 2 | 自增(前缀) | ++ |
right |
| 自减(前缀) | -- |
||
| 一元正号 | + |
||
| 一元负号 | - |
||
| 3 | 乘法类 | * / % |
left |
| 4 | 加法类 | + - |
left |
| 5 | 赋值 | = *= /= %= += -= |
right |
:fa-lightbulb-o: 运算符优先级表可用于向缺少括号的表达式添加括号
从具有最高优先级的运算符开始, 在运算符及其操作数周围加上括号
例子:
a = b += c++ - d + --e / -f |
排序 |
|---|---|
a = b += (c++) - d + --e / -f |
1 |
a = b += (c++) - d + (--e) / (-f) |
2 |
a = b += (c++) - d + ((--e) / (-f)) |
3 |
a = b += (((c++) - d) + ((--e) / (-f))) |
4 |
(a = (b += (((c++) - d) + ((--e) / (-f))))) |
5 |
表达式的值可能取决于计算其子表达式的顺序
C 没有定义子表达式评估的先后顺序(涉及逻辑与、逻辑或、条件和逗号运算符的子表达式除外)
(a + b) * (c - d) 中并未定义是应该先计算 (a + b) 还是应该先计算 (c – d)大多数表达式具有相同的值, 无论其子表达式的计算顺序如何
a = 5;
c = (b = a + 2) - (a = 1);
:fa-lightbulb-o: 避免在表达式中访问变量值的同时也修改该变量的值
在遇到这样的表达式时可能会产生编译警告消息, 例如"对a的操作可能未定义"
:fa-lightbulb-o: 为防止出现问题, 最好避免在子表达式中使用赋值运算符
相反, 建议使用一串分离的赋值表达式:
a = 5;
b = a + 2;
a = 1;
c = b - a;
c的值是确定的, 将始终为 6
除了赋值运算符之外, 唯一修改其操作数的运算符是自增和自减
:fa-lightbulb-o: 使用这些运算符时, 需注意表达式不能依赖于特定的求值顺序
i = 2;
j = i * i++;
很自然地认为j被赋值为4. 然而, j也可以被赋值为6
获取第二个操作数(i 的原始值), 然后将i递增
第一个操作数( i的新值)
i的新值和旧值相乘, 得到 6
c = (b = a + 2) - (a = 1);和j = i * i++;均会产生未定义的行为
未定义行为的可能影响:
当使用不同的编译器编译时, 程序运行的行为可能会有所不同
该程序可能一开始就无法编译
能编译但无法运行
能运行, 但程序可能会崩溃、行为不正常或产生无意义的结果
应避免未定义的行为
C 有一个不寻常的规则, 即任何表达式都可以用作语句
例子:
++i;
i首先递增, 然后获取i的新值, 然后丢弃
由于它的值被丢弃了, 除非表达式有副作用, 否则将表达式用作语句几乎没有意义:
i = 1; /* useful */
i--; /* useful */
i * j - 1; /* not useful */
一个无意的编码错误很容易会创建一个 什么都不做 的表达式语句
例如: i = j; 误编写为 i + j;
一些编译器会检测无意义的表达式语句; 给出一个警告, 比如"statement with no effect".