第4章 表达式

作者: 橡树人 | 来源:发表于2020-02-23 14:50 被阅读0次

英文原版:P53

一个表达式就是一个用于展示如何计算一个值的公式。
C语言与众不同的特征之一就是强调表达式,而不是语句。
最简单的表达式就是变量和常量。
一个变量表示一个可随着程序运行变化的值。
一个常量表示一个随着程序运行不会变化的值。

更复杂的表达式可应用运算符到操作数上,注意操作数本身也是表达式。比如a + (b*c),就是将运算符加号+应用到操作数a(b*c)上,其中a(b*c)自己本身就是一个表达式。

构建表达式的基本工具就是运算符,C运算符的种类非常丰富。开始阶段,C语言提供了在大部分编程语言中都可找到的基本运算符:

  • 算术运算符,包括加法、减法、乘法、除法等;
  • 用于执行比较的关系运算符,比如"i is greater than 0";
  • 用于构建条件的逻辑运算符,比如"i is greater than 0 and i is less than 10";

C语言不会止步于此,C语言还提供了许多其他的运算符。有如此多的运算符,以致于要在本书的前20章里逐步介绍。掌握如此多的运算符虽然很繁琐,但是对于精通C语言却是必需的

4.1节介绍算术运算符,解释运算符的优先级和关联性质,这些对包含多个运算符的表达式很重要。
4.2节介绍赋值运算符。
4.3节介绍自增和自减运算符。
4.4节描述如何求表达式的值。
4.5节介绍表达式语句,这是一个不寻常的特征,允许任何表达式充当一个语句。

章节介绍逻辑:

  • 有哪些运算符?
  • 运算符都有哪些规则?
  • 运算符的优先级和关联性

4.1 算术运算符

算术运算符是许多编程语言(包括C语言)的基石。
算术运算符包括加减乘除四种运算。

加法和乘法运算符是双目运算符,因为它们需要两个操作数。

取余运算符%

  • 双目运算符;
  • 两个操作数必须是整数;

单目运算符需要1个操作数;
双目运算符需要2个操作数;

C语言的通用规则

  • C语言允许使用括号来分组表达式;

有哪些算术运算符?

  • 单目运算符:+-

  • 双目运算符:+-*/

取余运算符%

规则:

  • 要求两个操作数必须是整数,如果有一个不是整数,则编译不通过;
  • 右操作是0时的结果是不确定的;

除法运算符/

规则:

  • 右操作是0时的结果是不确定的;
  • 当除法运算符/的两个操作数都是整数时,除法运算符/会以丢掉小数部分的形式截断结果,比如1/2的结果是0,而不是0.5;

剩余算术运算符:加减乘

规则:

  • 两个操作数可以是整数,也可以是浮点数,或者两者混合;
  • intfloat混合时,结果是float,比如9+2.5f的结果是11.5,6.7f/2的结果是3.35;

运算符优先级和关联性

当一个表达式包含有多于1个的运算符时,比如i+j * k,是表示”先执行i+j,然后把结果乘以k“,还是表示”先j乘以k,后将结果和i相加?

方法一:添加括号,比如(i+j)*k或者i+(j*k)

方法二:不使用括号,使用运算符的优先级规则来消除潜在的歧义;

运算符的优先级

算术运算符的优先级法则

最高:单目(+、-)、双目(*、/)

最低:双目(+、-)

当一个表达式里包含至少两个同优先级的运算符时,该如何解释表达式?

利用运算符的关联性;

如果一个运算符从左向右进行分组,则该运算符是左关联。

双目算术操作符(*、/、%、+、-)都是左关联的。

i-j-k等价于(i-j)-k
i*j/k等价于(i*j)/k

如果一个运算符从右向左进行分组,则该运算符是右关联。

单目算术运算符都是右关联的

-+i等价于-(+i)

运算符和关联性规则在许多语言中都很重要,在C语言中尤其重要。

但是C语言有如此多的运算符(将近50个),以致于几乎没有程序员去记忆这些优先级和关联性法则。反而,程序员通常都是当有疑问时查运算符表,或者使用大量的括号。

程序示例:计算UPC的校验数

多年以来,在美国和加拿大的许多零售店里售卖的商品的制造商会在每个产品上放入一个条形码。

众所周知,条形码就是UPC,可确认产品和制造商的身份。

每个条形码都是一个有12个数字的数,常被打印在条形码的下面。

认识条形码

第1个数字表示物品的类型

0或7表示大部分物品;

2表示必须称重的物品;

3表示药品和保健品;

5表示购物券;

第一个5连数用于确认制造商的身份。

第2个5连数用于确认商品的身份。

最后一个数字是一个校验位,用于帮助确认前11位是不是有错误。

这里使用如下方法来计算校验数字:

  • 将第1个、第3个、第5个、第9个、第11个数加起来;
  • 将第2个、第4个、第6个、第8个、第10个数加起来;
  • 将第一个和乘以3加到第2个和上;
  • 将总数减去1;
  • 计算将调整后的和除以10的余数;
  • 从9中减去余数;

源文件upc.c

/* Computes a Univeral 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));
}

4.2 赋值运算符=

在C语言中,赋值运算符=的作用就是保存表达式的值到某个变量,留待后续使用。

为了更新已经保存在变量中的值,C语言提供了各种复合赋值运算符。

简单的赋值v=e

  • 形式:v=e
  • 效果:计算出表达式e的值,将该值拷贝到v中。
    其中右操作数e可以是一个常数、变量或者更复杂的表达式;
    左操作数v必须是lvalue,不能是常数;
    当右操作数v和左操作数e的类型不同时,需要将e的值需要转换成v的类型。
  • 赋值运算符=是右关联的;
  • C语言中的赋值运算符有副作用,会修改左操作数的值;

例1 左右操作数类型不同

int i;
float f;

i = 72.99f;
f = 136;

这里i值是72,f的值是136.0。

int i;
float j;

f = i = 33.3f;

这里的i值是33,f的值是33.0。

例2 赋值运算符是右关联的
语句i = j = k = 0等价于i = (j = (k = 0))

不建议使用如下的形式的赋值,因为这会使代码很难读。

int i, j ,k;

i = 1;
k = 1 + (j = i);
printf(“%d %d %d”, i , j, k);

4.3 加1运算符++和减1运算符--

对单个变量最常见的两个操作就是增加1和减小1,比如:

i = i + 1;
j = j - 1;

复合赋值运算符允许将上述语句进行压缩:

i += 1;
j -= 1;

C语言允许使用++表示给变量加1,--表示给变量减1

++运算符和--运算符的效果

  • ++的效果就是将操作数加1,--就是将操作数减1
  • 使用++运算符和--运算符要小心,因为使用++运算符和--运算符有两种形式:
    • 前缀运算符,比如++i--i;
    • 后缀运算符,比如i++i--
  • ++运算符和--运算符是有副作用的,即他们会修改操作数的值
  • ++运算符和--运算符的优先级比单目正号运算符+和负号-高。
  • ++运算符和--运算符都是左关联的。

例1 前缀表达式++i求值

i = 1;
printf("i is %d\n", ++i);// i is 2
printf("i is %d\n", i);// i is 2

例2 后缀表达式i++求值

i = 1;
printf("i is %d\n", i++);// i is 1
printf("i is %d\n", i);// i is 2

例3 前缀表达式--i求值

i = 1;
printf("i is %d\n", --i);// i is 0
printf("i is %d\n", i);// i is 0

例4 后缀表达式i--求值

i = 1;
printf("i is %d\n", i--);// i is 1
printf("i is %d\n", i);// i is 0

4.4 表达式求值

例1 给表达式添加上合适的括号

a = b += c++ - d + -- e / -f

答:

(a = (b += (((c++) - d) + ((--e) / (-f)))))

子表达式的顺序

  • 运算符优先级和关联性允许我们将任何一个C表达式划分成多个子表达式,但是并不能决定表达式的值,因为表达式的值可能取决于子表达式的求值顺序。

  • C语言没有定义子表达式的求值顺序,除了包含逻辑运算符and、逻辑运算符or、条件运算符、逗号运算符等子表达式外。

  • 无论子表达式的求值顺序是怎样的,大部分表达式的结果都是相同的。

  • 当有子表达式会修改它的某个操作数时,表达式的结果就跟子表达式的求值顺序相关了。

  • 可通过使用赋值操作来代替子表达式中的赋值运算等;

例1 赋值运算符导致的结果不确定

a = 5;
c = (b = a + 2) - (a = 1);
// 如果b = a + 2先执行,则c=6。
// 如果a=1先执行,则c=2;

例2 加1运算符导致的结果不确定

i = 2;
j = i * (i++);
//j的结果可能是4,也可能是6。

4.5 表达式语句

规则:任何一个表达式都可用作一条语句

推论:任何一个表达式,无论类型和计算结果,都可通过在结尾添加分号;转换成一条语句

例1 表达式转语句

++i//表达式
++i;//语句

Q&A

问题1 注意到C语言中没有指数运算符。如何求一个数的幂次?

  • 如果是求一个整数的小正整数次幂,最好使用重复乘法,比如i*i*i
  • 如果是求一个非整数次数,最好调用pow函数;

问题2 如何求浮点数的余数?
答:尝试使用fmod函数

问题3 为什么当操作数有负数时,除法运算符/和取余运算符%非常复杂?

在C89和C99中,规则:保证(a/b)*b+a\%b=a恒成立

问题4 如果C语言有lvalues,则C语言有rvalues吗?

  • 有;
  • lvalues是一个表达式,该表达式出现在赋值运算符的左边;
  • rvalues是一个表达式,该表达式出现在赋值运算符的右边;
  • 一个rvalues可以是变量、常数、或者更复杂的表达式;
  • 在C语言中使用表达式这一术语代替rvalues;

问题5 如果v有副作用,则v += e就不等价于v = v + e。这该怎么理解?

//v += e:v只求了一次值;
//v = v + e:v被求了两次值;
a[i++] += 2;
a[i++] = a[i++] + 2;

问题6 如何理解表达式的值被丢掉了?

i = 5;
i+1;//整个表达式的结果为6,但是由于没有保存该值,所以被丢掉了。

i = 1;//整个表达式的结果为1,被丢掉了。

练习

  1. 答:
    (a) 1 2
    (b) 0
    (c) 1
    (d) 0

  2. 一样

(a) 1
(b) -1
(c) -1
(d) 1

答:
(a) 3
(b) -3
(c) 3
(d) -3

  1. 这种方法的问题出在10-total%10会产生10,占用了两位数字,而校验位只有1位数字

  2. 起作用

相关文章

网友评论

    本文标题:第4章 表达式

    本文链接:https://www.haomeiwen.com/subject/slsjqhtx.html