Bootstrap

【第三章·基本算术运算】第一节:C 语言运算符和表达式

目录

3.1.1 算术运算符和表达式

按操作数对运算符分类

数据类型对算术运算的影响

求余运算

算术运算符的优先级与结合性

示例:计算一个三位整数的个位、十位和百位数字值

3.1.2 复合的赋值运算符

示例:简单的复合赋值表达式计算

示例:复杂的复合赋值表达式计算

3.1.3 增 1 和减 1 运算符

对操作数的要求

前缀和后缀的区别

简单示例分析

复杂示例分析1

复杂示例分析2

总结

避免复杂的变量增减运算


章节内容导读:

        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 所示:

  1. 首先计算取相反数 -3
  2. 然后计算乘法 (-3) * 2,结果为 -6
  3. 接着计算减法 -6 - 1,结果为 -7
  4. 最后计算加法 -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 * aa *= a 的区别在于后者增加了将 a * a 的结果赋值给 a 的操作。

对于语句 1:a += a -= a * a;

  1. 初始状态

    • a = 3
  2. 计算 a * a

    • a * a = 3 * 3 = 9
  3. 执行 a -= a * a

    • a -= 9 相当于 a = a - 9
    • a = 3 - 9 = -6
  4. 执行 a += a -= a * a

    • 此时 a 的值已经更新为 -6
    • a += -6 相当于 a = a + (-6)
    • a = -6 + (-6) = -12

对于语句 2:a += a -= a *= a;

  1. 初始状态

    • a = 3
  2. 计算 a *= a

    • a *= a 相当于 a = a * a = 3 * 3 = 9 
  3. 执行 a -= a *= a

    • 此时 a 的值已经更新为 9
    • a -= 9 相当于 a = a - 9
    • a = 9 - 9 = 0
  4. 执行 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++ 中,首先会使用 的当前值,然后 的值才会增加 1。

前缀形式

  • 先对变量进行增 1(或减 1)操作,然后再使用变量的新值
  • 例如,在表达式 ++n 中,的值会首先增加 1,然后整个表达式会使用 的新值。

优先级

  • 后缀增 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 编译器) 中的运行结果如下所示:

        但重要的是要强调,由于未定义行为的存在,不同的编译器可能会产生不同的结果。在实际编程中,应避免在同一表达式中多次修改同一对象,以避免这种不确定性。 

;