目录
章节内容导读:
C 语言提供了 34 种运算符,为了使初学者能够即学即用,本章将重点介绍算术运算符、增 1 和减 1 运算符以及强制类型转换运算符。本章内容对应 “C 语言程序设计精髓” MOOC 课程的第 2 周视频,主要内容包括:
- 使用算术运算符和标准数学函数将数学表达式写成 C 表达式;
- 增 1 和减 1 运算符的前缀与后缀形式的区别;
- 宏常量与 const 常量;
- 赋值表达式中的自动类型转换与强制类型转换。
3.1.1 算术运算符和表达式
C 语言中的算术运算符(Arithmetic Operators)如表 3-1 所示。由算术运算符及其操作数组成的表达式称为算术表达式。其中,操作数(Operand)也称为运算对象,它既可以是常量、变量,也可以是函数。
按操作数对运算符分类
- 一元运算符(或单目运算符):只需一个操作数的运算符。
- 二元运算符(或双目运算符):需要两个操作数的运算符。
- 三元运算符(或三目运算符):需要三个操作数的运算符。条件运算符是 C 语言提供的唯一一个三元运算符,将在第 5 章介绍。
对于上表中提到的运算符,除计算相反数是一元运算符以外,其余的算术运算符都是二元运算符。
我们注意到,同样是减号,做取相反数运算时,是将其放在一个操作数的前面,而如果将其放在两个操作数中间,则执行的是减法运算,它又变成二元运算符了。
数据类型对算术运算的影响
不同于数学中的算术运算,C 语言中的算术运算的结果与参与运算的操作数类型相关。
以除法运算为例,两个整数相除后的商仍为整数。例如,1/2 与 1.0/2 运算的结果值是不同的,前者是整数除法(Integer Division),后者则是浮点数除法(Floating Division)。
- 整数除法 12/5 的结果值不是浮点数 2.4,而是整数 2,其中小数部分被舍去了。
- 浮点数除法 12.0/5.0(或者 12/5.0,或者 12.0/5)的计算结果是浮点数 2.4,这是因为整数与浮点实数运算时,其中的整数操作数在运算之前被自动转换为了浮点数(隐式类型转换),从而使得相除后的商也是浮点数。
求余运算
注意,在 C 语言中,求余运算限定参与运算的两个操作数必须为整型,不能对两个实型数据进行求余运算。
将求余运算符的左操作数作为被除数,右操作数作为除数,二者整除后的余数(Remainder)即为求余运算的结果,余数的符号与被除数的符号相同。例如:
算术运算符的优先级与结合性
算术运算符的优先级与结合性如表 3-1 所示。其中,取相反数运算符的优先级最高,其次是 *、/、%,而 +、- 的优先级最低。并且 *、/、% 具有相同的优先级,+、- 也有相同的优先级。需要注意的是,C 语言中没有幂运算符(需要借助 pow 函数来实现,后续讲解)。
当相同优先级的运算符进行混合运算时,需要考虑运算符的结合性。一元的取相反数运算符的结合性为右结合(即自右向左计算),其余的算术运算符为左结合(即自左向右计算)。
例如,在下面的语句中:
a = -3 * 2 - 1 + 3;
- 第一个 “-” 是一元的取相反数运算符;
- 第二个 “-” 是二元的减法运算符。
其运算过程如图 3-2 所示:
- 首先计算取相反数
-3
。 - 然后计算乘法
(-3) * 2
,结果为-6
。 - 接着计算减法
-6 - 1
,结果为-7
。 - 最后计算加法
-7 + 3
,最终结果为-4
。
因为在任何表达式中都优先计算括号内表达式的值,所以可以使用圆括号来控制运算的先后顺序。这样做不仅更直观、更方便,还有助于避免因误用运算符的优先级而导致的计算错误。
示例:计算一个三位整数的个位、十位和百位数字值
【例 3.1】计算并输出一个三位整数的个位、十位和百位数字之和。
【问题求解方法分析】:要计算一个三位整数的个位、十位和百位数字值,必须从一个三位整数中分离出它的个位、十位和百位数字。利用整数除法和求余运算可以解决这个问题。
例如,整数 153 的个位数字 3 刚好是 153 对 10 求余的余数,即 153 % 10 = 3,因此可用对 10 求余的方法求出个位数字 3;百位数字 1 说明在 153 中只有 1 个 100,由于在 C 语言中整数除法的结果仍为整数,即 153 / 100 = 1,因此可用对 100 整除的方法求得百位数字;中间的十位数字既可通过将其变换为最高位后再对 10 整除的方法得到,即 (153 - 1 * 100) / 10 = 53 / 10 = 5,也可通过将其变换为最低位再对 10 求余的方法得到,即 (153 / 10) % 10 = 15 % 10 = 5。
根据上述分析,可编写程序如下:
#include <stdio.h>
int main(void)
{
int x = 153, b0, b1, b2, sum;
b2 = x / 100; // 计算百位数字
b1 = (x - b2 * 100) / 10; // 计算十位数字,注意 () 不能少
b0 = x % 10; // 计算个位数字
sum = b2 + b1 + b0;
printf("b2=%d, b1=%d, b0=%d, sum=%d\n", b2, b1, b0, sum);
return 0;
}
程序的运行结果如下:
由于算术运算符 *
、/
、%
的优先级高于 +
、-
,因此为了保证减法运算先于除法运算,程序第 8 行语句中的圆括号是必不可少的。
思考题:本例程序还可以利用 b0 = x - b2 * 100 - b1 * 10; 来计算个位数字 b0。请重新编写例 3.1 的程序,观察运行结果,并分析其原理。
#include <stdio.h>
int main(void)
{
int x = 153, b0, b1, b2, sum;
b2 = x / 100; // 计算百位数字
b1 = (x - b2 * 100) / 10; // 计算十位数字,注意 () 不能少
b0 = x - b2 * 100 - b1 * 10; // 计算个位数字
sum = b2 + b1 + b0;
printf("b2=%d, b1=%d, b0=%d, sum=%d\n", b2, b1, b0, sum);
return 0;
}
原理解析:
b2 = x / 100;
计算百位数字,结果为 1。b1 = (x - b2 * 100) / 10;
计算十位数字,结果为 5。b0 = x - b2 * 100 - b1 * 10;
计算个位数字,这里通过减去百位和十位的值,剩下的就是个位数字,结果为 3。
3.1.2 复合的赋值运算符
第 2 章 2.5 节介绍了两种赋值方法:简单的赋值和多重赋值。本节介绍一种利用复合的赋值运算符(Combined Assignment Operators)实现的简写的赋值(Shorthand Assignment)方法。
涉及算术运算的复合赋值运算符有 5 个,分别为 +=
、-=
、*=
、/=
、%=
。注意:在 +
与 =
、-
与 =
、*
与 =
、/
与 =
、%
与 =
之间不应有空格。
其一般形式及其等价表示如图 3-3 所示。相对于它的等价形式而言,复合的赋值运算书写形式更简洁,而且执行效率也更高一些。
示例:简单的复合赋值表达式计算
例如,计算下面的赋值表达式:
n *= m + 1 // 应把复合赋值后面的当做一个整体
等价于计算下面的表达式:
n = n * (m + 1)
但不等价于计算下面的表达式:
n = n * m + 1
可以用如图 3-4 所示的助记形式来理解上面复合赋值运算符的运算规则。其他复合的赋值运算的例子如表 3-2 所示(下文)。
示例:复杂的复合赋值表达式计算
思考题:已知变量 a
的值为 3,请问分别执行下面两个语句后,变量 a
的值分别为多少?
语句1:a += a -= a * a;
语句2:a += a -= a *= a;
提示:不仅要考虑运算符的优先级,还要考虑运算符的结合性。读者可按图 3-5 来分析其运算过程。注意,第一步 a * a
与 a *= a
的区别在于后者增加了将 a * a
的结果赋值给 a
的操作。
对于语句 1:a += a -= a * a;
-
初始状态:
a = 3
-
计算
a * a
:a * a = 3 * 3 = 9
-
执行
a -= a * a
:a -= 9
相当于a = a - 9
a = 3 - 9 = -6
-
执行
a += a -= a * a
:- 此时
a
的值已经更新为-6
a += -6
相当于a = a + (-6)
a = -6 + (-6) = -12
- 此时
对于语句 2:a += a -= a *= a;
-
初始状态:
a = 3
-
计算
a *= a
:a *= a 相当于 a = a * a = 3 * 3 = 9
-
执行
a -= a *= a
:- 此时
a
的值已经更新为 9 a -= 9
相当于a = a - 9
a = 9 - 9 = 0
- 此时
-
执行
a += a -= a *= a
:- 此时
a
的值已经更新为 0 a += 0
相当于a = a + 0
a = 0 + 0 = 0
- 此时
3.1.3 增 1 和减 1 运算符
对变量进行加 1 或减 1 是一种很常见的操作,为此,C 语言专门提供了执行这种功能的运算符,即增 1 运算符(Increment Operator)和减 1 运算符(Decrement Operator)。
对操作数的要求
增 1 和减 1 运算符都是一元运算符,只需要一个操作数,且操作数必须具有 “左值性质”,即必须是变量,不能是常量或表达式。
- 增 1 运算符是对变量本身执行加 1 操作,因此也称为自增运算符。
- 减 1 运算符是对变量本身执行减 1 操作,因此也称为自减运算符。
前缀和后缀的区别
增 1 运算符既可以写在变量的前面(如 ++x
),也可以写在变量的后面(如 x++
)。二者实现的功能有所差异:
- 前缀形式 (
++x
):在变量使用之前先对其执行加 1 操作,具有右结合属性。 - 后缀形式 (
x++
):先使用变量的当前值,然后对其进行加 1 操作,具有左结合属性,且优先级高于前缀形式。
因此,++
作为前缀运算符与作为后缀运算符相比,对变量(即运算对象)而言,运算的结果都是一样的,但增 1 表达式本身的值却是不同的。这种差异通常在赋值语句或打印语句中才能体现出来。
简单示例分析
例如,设有如下变量定义语句:
int n = 3;
分别执行以下两条语句:
语句1:m = n++; // m = 3,先赋值,后自增
语句2:m = ++n; // m = 4,先自增,后赋值
虽然变量 n 的值都进行了加 1 操作,但变量 m 的值却是不同的。
- 语句 1 是将增 1 操作之前的 n 值 3 赋值给了变量 m(即 m = 3)。
- 语句 2 是将增 1 操作之后的 n 值 4 赋值给了变量 m(即 m = 4)。
同理,分别执行下面两条语句:
语句1:printf("%d\n", n++); // 3
语句2:printf("%d\n", ++n); // 4
虽然变量 n 的值都进行了加 1 操作,但二者打印的结果却是不同的。
- 语句 1 是后缀增 1 运算符,是左结合的,打印的是增 1 操作之前的 n 值 3。
- 语句 2 是前缀增 1 运算符,是右结合的,打印的是增 1 操作之后的 n 值 4。
而分别单独执行以下两条语句后,n 的结果值是相同的:
语句1:n++;
语句2:++n;
减 1 运算符 -- 同样可以写在变量的前面或后面,它的用法与增 1 运算符 ++ 的用法相同。
归纳起来,在赋值操作中使用的增 1 和减 1 运算符主要有 4 种情况,如表 3-2 所示。
提示:
对于大多数 C 编译器,利用增 1 和减 1 运算生成的代码比等价的赋值语句生成的代码执行效率更高一些。
复杂示例分析1
下面来看一个稍微复杂一点的例子。如果 n 值仍为 3,那么执行下面语句后,m 和 n 的值各为多少呢?
m = -n++;
- 在上面赋值的右侧表达式中,出现了
++
和-
两个运算符,尽管它们都是一元运算符,但优先级却是不同的,后缀++
/--
运算符的优先级高于取负(-)运算符,并且后缀++
/--
运算符是左结合的。 - 因此,语句 m = -n++; 等价于 m = -(n++);。
- 进一步地,根据后缀的
++
运算符的意义,将其等价为如下两个语句:
m = -n; // 先赋值
n = n + 1; // 后自增
- 它表示先把
-n
的值拿去给m
赋值,然后再执行对n
加 1 的操作。
你一定会问,为什么将 n++
用圆括号括起来了,却不先计算 n++
?
n++
只表示++
的运算对象是n
,而不是-n
,(-n)++
表示++
的运算对象是-n
,但遗憾的是,它是一个不合法的操作,因为不能对一个表达式进行增 1 运算。n++
究竟是先算还是后算完全取决于++
是后缀还是前缀。- 经过上述分析可知,执行该语句以后,
m
值为 -3,n
值为 4。
不难发现,从程序的可读性角度而言,后面的两条等价语句比语句 “m=-n++;” 的可读性更好。
复杂示例分析2
经过上述综合练习,现在应该已经具备了正确分析以下程序的能力。
#include <stdio.h>
int main()
{
// 优先级大小:后缀++、-- 大于 前缀++、-- 大于取负运算符
// 注意:即使后缀形式的自增自减优先级高于取负运算符,但是也得先使用变量的值,然后再增 1(减 1)
int n = 3;
int m = -(n++); // 先赋值,后自增
printf("%d\n", m); // -3
printf("%d\n", n); // 4
n = 3;
m = -(++n); // 先自增,后赋值
printf("%d\n", m); // -4
printf("%d\n", n); // 4
n = 3;
m = -(n--); // 先赋值,后自减
printf("%d\n", m); // -3
printf("%d\n", n); // 2
n = 3;
m = -(--n); // 先自减,后赋值
printf("%d\n", m); // -2
printf("%d\n", n); // 2
return 0;
}
程序的运行结果如下所示:
提示:
括号在这里的作用是确保取负运算符 - 作用于整个自增或自减表达式的结果上。然而,后缀自增 / 自减运算符的 “读取当前值然后修改” 的行为是由其定义决定的,并且这个行为在表达式求值过程中是最后发生的(相对于读取当前值而言)。因此,即使使用了括号来改变运算的优先级,后缀自增 / 自减的这种 “延迟修改” 的特性仍然保持不变。
简而言之,括号改变了运算的优先级,但它不能改变后缀自增 / 自减运算符 “先使用值后修改” 的行为模式。
总结
运算符 | 描述 | 使用顺序 | 优先级 | 结合性 |
---|---|---|---|---|
n++ | 后缀自增 | 先使用值,后增 1 | 较高 | 左结合 |
n-- | 后缀自减 | 先使用值,后减 1 | 较高 | 左结合 |
++n | 前缀自增 | 先增 1,后使用值 | 较低 | 右结合 |
--n | 前缀自减 | 先减 1,后使用值 | 较低 | 右结合 |
后缀形式:
- 先使用变量的当前值,然后再对变量进行增 1(或减 1)操作。
- 例如,在表达式
n++
中,首先会使用n
的当前值,然后n
的值才会增加 1。
前缀形式:
- 先对变量进行增 1(或减 1)操作,然后再使用变量的新值。
- 例如,在表达式
++n
中,n
的值会首先增加 1,然后整个表达式会使用n
的新值。
优先级:
- 后缀增 1(减 1)运算符的优先级高于前缀增 1(减 1)运算符。
- 这意味着在表达式中,后缀运算符会先于前缀运算符被计算。
结合性:
- 后缀运算符是左结合的。
- 前缀运算符是右结合的。
避免复杂的变量增减运算
通常,良好的程序设计风格提倡在一行语句中一个变量最多只出现一次增 1 或减 1 运算。因为过多的增 1 和减 1 混合运算,会导致程序的可读性变差。同时,C 语言规定表达式中的子表达式以未定顺序求值(即不保证对同一变量进行多次修改的顺序),从而允许编译程序自由重排表达式的顺序,以便产生最优代码。这样就会导致当相同的表达式用不同的编译器编译时,可能产生不同的运算结果。
良好的程序设计风格不建议在语句中使用复杂的增 1 和减 1 表达式,如下面程序所示,不仅晦涩难懂,而且在不同的编译环境下会产生不同的结果,实践中很少使用。
#include <stdio.h>
int main(void)
{
int a = 5;
int sum = ++a + ++a;
printf("%d\n", a);
printf("%d,%d,%d\n", a, a++, ++a);
printf("a=%d,sum=%d\n", a, sum);
return 0;
}
在 VS code(使用 GCC 编译器) 中的运行结果如下所示:
但重要的是要强调,由于未定义行为的存在,不同的编译器可能会产生不同的结果。在实际编程中,应避免在同一表达式中多次修改同一对象,以避免这种不确定性。