计算机二级考点重点(计算机等级考试必备之运算符教程学习)
本章重点
- 赋值运算符
- 算术运算符
- 比较运算符
- 逻辑运算符
大家学习了上一章中介绍的变量和常量,现在已经可以将数据保存在程序中了,下一步的工作就是操作并利用保存的数据。本章将介绍C语言中的运算符,在这些运算符的帮助下,可以完成变量赋值、变量比较、数学计算、组合逻辑关系等基本的变量操作。此外,本章还将简单介绍两种控制程序流程的方法:选择和循环。经过本章的学习之后,大家就可以编写一些简单的小程序了。
运算符与表达式
在C语言中,表达式是一种有值的语法结构,它是由变量、常量和有返回值的函数调用组合这几种基本元素而成的。如果一个表达式中包含多个基本元素,那么就需要运算符作为它们的黏合剂。
根据上述定义,变量、常量和有返回值的函数调用本身都可以看作一个表达式。对于前两者而言,表达式的值就是这两个量本身;对于函数调用而言,表达式的值就是函数调用的返回值。不清楚函数的概念不要紧,后文中会详细介绍函数的定义和使用方法。
下面是一个最简单的常量表达式:
100
同样地,下面对函数func的调用也是一个表达式:
func()
下面是一个略微复杂一点儿的表达式:
250*100 3*func()
上面的表达式也可以看成是下面两条表达式使用加号“ ”相连接的结果:
250*100
3*func()
需要注意的是,在C语言中,两个表达式基本元素之间必须由运算符相连接,这样程序才能明白如何创建一个新的包含这两个基本元素的表达式,并对其赋值。
赋值运算符
等号“=”是大家非常熟悉的一个符号,通常它被用来在等式中表示左右两边的相等关系,例如5 8 = 13就是由等号组成的一个正确的等式。
在C语言中,等号“=”的作用不是表示相等关系,而是赋值,即将等号右侧的值赋给等号左侧的变量。接下来列举几种常用的赋值运算符,如表3-1所示。
表 3‑1 常用的赋值运算符
运算符 |
名称 |
使用方法 |
= |
赋值运算符 |
变量 = 表达式 |
= |
相加后赋值 |
变量 = 表达式 |
-= |
相减后赋值 |
变量 -= 表达式 |
*= |
相乘后赋值 |
变量 *= 表达式 |
/= |
相除后赋值 |
变量 /= 表达式 |
%= |
取模后赋值 |
变量 %= 表达式 |
上表中第三行及之后的运算符都被称为复合赋值运算符,这是因为它们同时完成了算术运算和赋值的两种功能。这些算术运算符会在本章中加以介绍。
脚下留心:变量与变量值
大家可能会有疑问:变量不就是变量本身吗?为什么会有“变量值”这一说法呢?应当注意的是,一个变量在不同时刻可以对应多个不同的值,这意味着“变量”和“变量值”并不是一一对应的关系。考虑例程3-1:
例程 3‑1 为变量value进行三次赋值
int variable;
variable = 20;
variable = 30;
variable = 40;
执行第二条语句后,变量variable的值被设为20,执行第三条语句后,变量variable的值被设为30,最后一条语句执行后,variable的值又变成了40。执行结束后,变量variable仍然是开始时声明的那个变量,但是在整个执行过程中,它的值(即变量值)发生了三次变化,因此变量和变量值是不同的两个概念。
赋值语句还有其它的写法吗?能不能同时给两个变量赋值?能不能将等号左右两边的标识符交换呢?很遗憾,类似例程3-3中的语句在C语言中是无效的:
例程 3‑2 无效的赋值语句
35 = variable;
variable1, variable2 = 45, 60;
第一条语句的含义是“将variable的值赋给常量20 35”,但是常量的值是不能修改的。试问如果通过赋值语句将20 35的值修改为variable的值(例如100),那下一次在程序中出现20 35时,它是等于55呢,还是等于100呢?大家会发现这是很荒谬的,因为一个常量的值就是它本身,而不能被修改为不等于其本身的另外一个值。
第二条语句或许是要将45和60分别赋给变量variable1和variable2,但是C语言并不支持这种写法。必须将其拆分成两条语句,如例程3-4所示:
例程 3‑3 为两个变量赋值
variable1 = 45;
variable2 = 60;
不过要将同一个值赋给多个变量,可采用类似连等式的写法,如例程3-5:
例程 3‑4 多重赋值语句
variable1 = variable2 = variable3 = 9091;
多学一招:多重赋值语句背后的逻辑
多重赋值语句是按照什么顺序执行的呢?在上面的例子中,是先执行variable1 = 9091,还是先执行variable3 = 9091呢?
要解答这个问题,要先介绍一个事实:赋值语句是有返回值的!赋值语句会将等号右边的值作为返回值,因此实际上(variable3 = 9091)返回了整数9091,程序再执行下一步“令变量variable2等于(variable3 = 9091)的返回值9091”;执行完这一步之后,程序再执行最后的“令变量variable1等于右侧表达式的返回值9091”。因此应当是先执行variable3 = 9091,最后执行variable1 = 9091的。
决定赋值运算符执行顺序的是该运算符的“结合性”,可以调到本章中“运算符的优先级”一节,抢先阅读相关知识。
了解“赋值语句有返回值”这个事实有什么用呢?别急,很快就知道了。
下面来看一条比较难的语句:
例程 3‑5 一条比较难的语句
variable = variable 1;
在数学上,这个等式是不成立的,因为消去等号左右的“variable”,只能得到错误的等式0 = 1。但是在C语言中,这条语句是正确的,而且是编程中使用得最频繁的语句之一。请回想等号在C语言中的含义:将右边的值赋给左边的变量。因此这条语句的含义是“将变量variable自增1”。
这条语句是如何实现variable自增的呢?以例程3-6为例,运用变量和变量值的概念分析一下这条语句的执行过程。
例程 3‑6 分析自增语句的执行过程
int variable = 20;
variable = variable 1;
printf("variable = %d\n", variable);
- variable的初始变量值是20;
- 计算等号右边variable 1的值。variable的初始值是20,因此variable 1就是20 1,也就是21;
- 将计算结果21赋给变量variable;
- variable的新变量值是21。
- 执行结束。
多学一招:左值、右值与操作数
variable = 9091 9148;
操作数 操作数 操作数
左值 右值
图 3‑1 左值、右值与操作数
如上图所示,一条赋值语句可以分成多个部分:左值、操作符(等号“=”和加号“ ”)、右值及操作数,其中右值可能由多个操作数和操作符组成。在C语言中,“左值”是指用于标识一个数据对象的名称或表达式。例如变量variable就是一个数据对象,那么变量名“variable”就是一个左值。如果一个左值可以修改(所有变量都是可修改的),就将其称为“可修改的左值”。C语言规定等号左边只能为一个可修改的左值。
右值可以为任何的常量、变量或由常量和变量组成的有效表达式,将其放在赋值语句的右边,语句执行后,右值的值被计算出来,然后被赋给左值。
操作符和操作数的概念就很容易理解了:图中参与计算、赋值的常量和变量都是操作数,用来对操作数进行操作的符号被称为操作符。
算术运算符
算术运算符用来帮助程序员完成各种数学运算。本小节将讲解C语言里的算术运算符的使用方法,学过本小节之后,大家就可以实现出一些有意义的算术表达式了。
算术运算符示例表3-2中列出了C语言中所有的算术运算符:
表 3‑2 C语言中的算术运算符
运算符 |
解释 |
示例 |
|
加法运算符 |
3 2 |
- |
减法运算符 |
3 – 2 |
* |
乘法运算符 |
5 * 4 |
/ |
除法运算符 |
15 / 3 |
- |
负号运算符 |
-3 |
大家应该很熟悉加减乘除四种基本运算,在此不赘述它们的基本使用方法。下面将具体介绍C语言中的算术运算符与数学中运算符的使用区别。
加法运算符加号“ ”可以将两个变量或表达式相加,如例程3-7所示:
例程 3‑7 将两个变量相加
- #include <stdio.h>
int main()
{
- int value1 = 90;
- int value2 = 91;
- printf("%d\n", value1 value2);
- return 0;
- }
程序的运行结果如图3-2所示:
图 ‑ 两个变量相加
加法运算满足交换律,因此value1 value2与value2 value1的结果是相同的。
加法运算符还可以将多个数逐个相加,例如例程3-10:
例程 3‑8 将多个表达式相加
1 #include <stdio.h>
2 int main()
3 {
4 int value = 45 30;
5 printf("%d\n", 22 value 15);
6 return 0;
7 }
程序的运行结果如图3-3所示:
图 ‑ 将多个表达式相加
多学一招:单目运算符、双目运算符与三目运算符
上一小节中学过了运算符与操作数的概念。一个运算符必须和一个或多个操作数放在一起,才能构成有意义的表达式。那么一个运算符应该搭配多少个操作数呢?
C语言中,运算符可分为单目运算符、双目运算符和三目运算符三种类型。大家已经学过的加法运算符“ ”和减法运算符“-”都是双目运算符,因为表达式20 42是有意义的,而20 或者42 – 都是不完整的表达式。同样也无法在“ ”的上下左右放上多个操作数以构成合法的表达式。下文中的乘法运算符和除法运算符同样是双目运算符。
除了双目运算符之外,C还提供了单目运算符(只有一个操作数)与三目运算符(需要三个操作数)。本章中大家会学到两个单目运算符“ ”和“--”,而三目运算符会在下一章中介绍。
乘法运算符在数学表达式中,乘号往往省略不写,或是用来表示。但是C语言中,乘号是不可以省略的。因此如下语句是错误的:
例程 3‑9 错误的乘法运算表达式
int x, y, z;
x = y z;
x = yz;
x = y·z;
除法运算符整数除法和浮点数除法是不同的。正如“鸡的孩子会打鸣,老鼠的孩子会打洞”,两个数做除法,运算结果的类型一定和原来这两个数的类型保持一致;也就是说,两个整数相除,结果只能是整数;两个浮点数相除,结果一定是浮点数。
大家一定会问:“如果两个整数不能整除,结果是怎样的呢?是使用四舍五入吗?7除以3是等于2呢,还是等于3呢?”为此C语言规定了整数除法的运算规则:如果整数除法的结果有小数,那么直接扔掉小数位,只留下整数,这种处理方式被称为“向零取整”。例程3-10通过一段程序来解释“向零取整”的做法。
例程 3‑10 除法运算中的向零取整
printf("7 / 3 = %d\n", 7 / 3);
printf("-7 / -3 = %d\n", -7 / -3);
printf("-7 / 3 = %d\n", -7 / 3);
上面三条语句的输出分别为2、2和-2,符合之前的定义。
除了整数与整数相除和浮点数与浮点数相除这两种情形之外,还有一种情形:除法的两个操作数分别为整数和浮点数,例如在语句“10.5 / 2”中,10.5是浮点数,2是整数。在运算时,C语言编译器会自动把整数操作数转换成浮点数,这样就变成浮点数除法运算了,运算结果也是浮点数。
负号运算符负号“-”可以用来改变一个表达式的符号。例如 -(-80) 将返回一个正数80,-value等于-1 * value。负号和减号是同一个符号,它们的不同之处在于,减号是双目运算符,而负号是单目运算符。
与负号类似,正号“ ”也可以放在一个表达式之前,不过它的作用与负号相反:正号不改变表达式的符号。在C89标准之前是不可以在表达式之前使用正号的。
算术运算符的案例输出平方表到目前为止,大家还没有学习过如何使用C语言标准库中的函数进行平方运算,不过可以用乘法运算替代之。语句 result = factor * factor 计算了变量factor的平方,并将结果赋给变量result。类似地,可以通过 x * x * x 计算 x 的三次方。
例程3-11是一个输出一个平方表的例子。
例程 3‑11 输出1 ~ 9的平方
- #include <stdio.h>
- int main()
- {
- printf("%d %d\n", 1, 1 * 1);
- printf("%d %d\n", 2, 2 * 2);
- printf("%d %d\n", 3, 3 * 3);
- // 省略其它语句
- printf("%d %d\n", 9, 9 * 9);
- }
程序的运行结果如图3-4所示:
图 3‑4 输出1 ~ 9平方的结果
程序中有9条基本相同的语句printf(...),请使用复制(快捷键Ctrl C)与粘贴(快捷键Ctrl V)将第一条printf(...)语句粘贴八次并修改语句中相应的数字,以避免重复键入。
计算机程序可以简化或完成重复性的工作,因此像上一个例程那样将同一条语句重复九遍并不是一个优雅的做法。要让程序重复执行一段代码,就需要使用循环语句。下面先简单了解一下while循环语句的基本使用方法。
例程 3‑12 while循环语句的使用方法
x = 0;
while(x < 10)
{
// 循环代码
// 在这里写每次循环中要做的事
x = x 1;
}
进入循环之前,为变量 x 指定一个初始值0,每执行一次循环代码,x的值就增加1。每次执行循环代码之前,程序会检查变量x是否小于10,如果小于10,则执行循环代码,否则结束循环过程,继续执行后面的语句。第五章将对循环语句做进一步介绍,这里只要简单了解while语句的使用方法即可。
有了while语句助阵,终于可以简化程序了,如例程3-13:
例程 3‑13 通过while语句化简平方数输出程序
- #include <stdio.h>
- int main()
- {
- int i = 1;
- while(i < 10)
- {
- printf("%d %d\n", i, i * i);
- i = i 1;
- }
- return 0;
- }
程序的运行结果如图3-5所示:
图 ‑ 通过while化简程序
减肥小助手——计算每日消耗的热量下面来写一个小软件“减肥小助手”,在这个例子中会用到前面提到过的四种运算符。这是大家使用C语言开发的第一个实用软件,请一定认真完成哦。
“减肥小助手”需要实现如下功能:用户输入一天中每餐饭摄入的热量(以卡路里为单位),程序自动计算出三顿饭的热量、每顿饭的平均热量、白天每个小时摄入热量的平均值(假设7小时睡眠),并与标准值进行比较。为了使结果简单,要求输出当天摄取的总热量时使用千卡作为单位;平均热量也为浮点数。
为了读取用户的输入,需要调用scanf函数,这个函数也是在标准输入输出头文件stdio.h中声明的。例如要将用户输入的整数存储到变量val中,只要写scanf(“%d”, &val);即可。有关scanf的具体介绍会在之后进行。
某个小伙伴写了如下程序(见例程3-14):
例程 3‑14 小伙伴实现的“减肥小助手”程序
- #include <stdio.h>
- int main()
- {
- /* 早中晚三餐的热量*/
- int cal_breakfast, cal_lunch, cal_dinner;
- /* 三餐热量之和*/
- int cal_total;
- /* 白天平均每小时摄入的热量*/
- float cal_average_per_hour;
- /* 平均每顿饭摄入的热量*/
- float cal_average_per_meal;
- /* 成年人每天标准热量摄入值为2000 卡*/
- float cal_standard = 2;
- printf("请输入早饭摄入的热量(卡):");
- /* 读入用户输入的一个整数*/
- scanf("%d", &cal_breakfast);
- printf("请输入午饭摄入的热量(卡):");
- /* 读入用户输入的一个整数*/
- scanf("%d", &cal_lunch);
- printf("请输入晚饭摄入的热量(卡):");
- /* 读入用户输入的一个整数*/
- scanf("%d", &cal_dinner);
- /* 计算三餐热量摄入之和*/
- cal_total = (cal_breakfast cal_lunch cal_dinner) / 1000;
- /* 计算白天17 小时每小时的平均热量摄入*/
- cal_average_per_hour = cal_total / 17;
- /* 计算每餐的平均热量摄入*/
- cal_average_per_meal = cal_total / 3;
- printf("您当天摄入的总热量约为%d 千卡\n", cal_total);
- printf("每餐的平均热量为%f 千卡\n", cal_average_per_meal);
- printf("白天17 小时平均摄入热量为%f 千卡/小时\n", cal_average_per_hour);
- printf("成年人每天热量摄入标准值约为%f 千卡\n", cal_standard);
- printf("感谢使用!\n");
- }
程序的运行结果如图3-6所示:
图 3‑6 “减肥小助手”的错误输出
“咦?”小伙伴迷惑不解,“为什么计算出的平均值都是0.000000啊?难道电脑算错了?”电脑当然不会算错,问题一定出在他的程序里。请帮他改正程序中的错误吧。
请大家考虑为这个小程序增加更多的功能,然后把它献给女友吧。
自增自减运算符前面介绍的四个运算符与数学中相应的符号有着相同的用法,学起来是不是太简单了呢?下面就来看两个绝对不会在数学中看到的运算符:“ ”和“--”。
例程 3‑15 自增/自减运算符使用示例
- #include <stdio.h>
int main()
{
- int a = 105;
- printf("a = %d\n", a);
- a;
- printf("a = %d\n", a);
- --a;
- printf("a = %d\n", a);
- return 0;
- }
程序的运行结果如图3-7所示:
图 3‑7 自增/自减运算符示例程序的输出
聪明的人肯定一眼就看出来了: a就是让变量a增加1,而--a就是让变量a减少1。是的,就是这么简单!但要注意的是, a和a 是不一样的。尽管它们都会将变量a自增1,但是 a返回的是(a 1),而a 返回的是a(原来的值)。听上去不好理解?来看例程3-16。
例程 3‑16 a 与 a的区别
- #include <stdio.h>
- int main()
- {
- int a = 1, b = 1;
- printf("a = %d, b = %d\n", a , b);
- printf("a = %d, b = %d\n", a, b);
- return 0;
- }
程序的运行结果如图3-8所示:
图 3‑8 a 与 a的区别
现在应该明白a 和 a的区别了吧。要记忆它们的差别也很简单:如果符号( 或--)在变量之前,就先做自增(或自减),再返回变量值(这时返回的自增或自减之后的值了);如果符号( 或--)在变量之后,就先返回变量值(返回的是原先的值),再做自增(或自减)。不考虑返回值的因素的话,这两种写法在作用上是相同的。
比较运算符
C语言提供了比较运算符用来比较表达式之间的大小和判断是否相等。本节重点介绍C语言中的比较运算符,并指出对浮点型变量进行比较时需要注意的问题。
古语有云:“人比人得死,货比货得扔。”尽管如此,在生活中人们还是免不了进行各种比较。试看下面几条描述:
- 小明比小红高;
- 中国的国土面积比朝鲜的国土面积大;
- 芙蓉姐姐比林黛玉长得好看。
上述三句话分别进行了三次比较,出现在“比”字旁边的词语就是做比较的主体。比较的结果只有两种可能:要么是真的,要么是假的。作为练习,请写出第三句比较的结果。
在C语言中需使用比较运算符对表达式做比较。当然表达式的结果只能是具体值,例如整数或小数等等;而不能是文本、句子或图片——就目前而言,计算机还很难做到自动判断出两张脸中最好看的那一张。
C语言中共有六种关系,如表3-3所示:
表 3‑3 C语言中的比较运算符
编号 |
比较运算符 |
关系名称 |
1 |
> |
大于 |
2 |
== |
等于 |
3 |
< |
小于 |
4 |
>= |
大于等于 |
5 |
<= |
小于等于 |
6 |
!= |
不等于 |
与汉语的描述相同,可以写出C语言表达式的比较语句,称为关系表达式:
- 1 < 5
- 2 > 25
- 48 != 44
这些关系表达式的结果也只有“真”和“假”两种可能。
在上面的例子里比较的都是整数。对于浮点数,直接使用 < 和 > 比较大小当然是可行的。但需要注意的是,因为浮点数与真正的实数是有误差的,在做浮点数的相等比较时,应当避免使用“==”。请考虑例程3-17:
例程 3‑17 浮点数的比较
- #include <stdio.h>
- int main()
- {
- float a = 1.0, ret = 0.0f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- printf("Result is %f\n", ret);
- if(ret == a)
- {
- printf("Result equals to 1!\n");
- }
- return 0;
- }
程序的运行结果如图3-9所示:
图 3‑9 浮点数精度带来的问题
可以注意到结果中并没有“Result equals to 1!”这一行,意味着0.1相加10次之后并不等于1。这粗看上去是反常识的,0.1乘以10如果不等于1,还能等于多少?但是因为受float类型的精度所限,0.1是不能被精确表达的,在float类型的变量中实际存储的值比0.1要稍微大一点儿,这一点儿差距称为误差。把这一点点误差相加十次,自然就使得最后的结果比1要大一些了。
要解决这个问题,需要使用阈值比较的方法:设定一个阈值threshold,当两个待比较的浮点数差值的绝对值小于threshold时,就认为这两个浮点数是相等的。因此上述程序应当如例程3-18进行修改:
例程 3‑18 使用阈值进行浮点数的比较
- #include <stdio.h>
- #include <math.h>
- int main()
- {
- float a = 1.0, ret = 0.0f;
- float threshold = 0.000001f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- ret = 0.1f;
- printf("Result is %f\n", ret);
- if(fabs(ret - a) < threshold)
- {
- printf("Result equals to 1!\n");
- }
- return 0;
- }
程序的运行结果如图3-10所示:
图 3‑10 使用阈值进行浮点数的比较
为了使用fabs函数计算绝对值,上述程序额外引用了头文件math.h。大家也可以自行实现计算绝对值的功能。
多学一招:真真假假
如何C语言中表示“真”和“假”呢?像这样的表达式肯定是不行的:
(100 < 200) == 真
(100 < 200) == True
如果程序语句里出现了“真”或者“True”,编译器一定会报错的,因为它并不认识这个标识符——当然可以在字符串中随意使用“真”或者“True”。那该怎么表示“真”和“假”呢?大家已经知道如何判断一个C语言表达式的真假了,例程3-19将说明C语言中的“真”和“假”究竟是什么。
例程 3‑19 真真假假
- #include <stdio.h>
- int main()
- {
- int true_val, false_val;
- true_val = (100 < 200); /* 一个为真的表达式 */
- false_val = (200 < 100); /* 一个为假的表达式 */
- printf("True value equals to: %d\n", true_val);
- printf("False value equals to: %d\n", false_val);
- if(2)
- {
- printf("2 means true.\n");
- }
- if(3)
- {
- printf("3 means true.\n");
- }
- if(0)
- {
- printf("0 means true\n");
- }
- return 0;
- }
程序的运行结果如图3-11所示:
图 3‑11 真真假假
从输出结果可以看出,对C语言而言,假表达式的值是0,真表达式的值是1;0是“假”,其它非零的值都是“真”。
脚下留心:“=”和“==”的区别
某个小伙伴写了一个用来检验数字是否为0的程序,但是编译后工作不正常!程序如例程3-20所示。
例程 3‑20 “=”和“==”的区别
- #include "stdio.h"
- int main()
- {
- int a = 0, b = 0;
- if(a = b)
- {
- printf("a 和 b 相等!\n");
- }
- if(a != b)
- {
- printf("a 和 b 不相等!\n");
- }
- return 0;
- }
程序的运行结果如图3-12所示:
图 3‑12 =和==的区别
运行例程3-20之后,因为a和b都等于0,本来应该输出的是“a 和 b 相等!”,但实际上什么都没有输出,程序就结束了。
请问这是为什么呢?请大家继续阅读之前,先试着在程序中找出问题所在。
事实上问题很简单:小伙伴把关系判断“a == b”误写成了“a = b”!请回想本章开头时提到的一个事实:赋值语句是有返回值的。因此“a = b”实际上返回了b的值,也就是0。C语言中,0被认为是假,因此a = b这个条件在返回0时就不可能被满足了。
这个错误在初学者的程序中十分常见,而且有时很难排查。请在编写程序时务必注意。
逻辑运算符
逻辑运算通常用来测试一个值的真假。在C语言中,逻辑运算符可以用来组成复杂的逻辑表达式,一般用来连接多个关系表达式。本节将介绍C语言中的逻辑运算符(逻辑与、逻辑或、逻辑非)的使用。
如果要求写一个程序来判断某个整数是否在某个范围内,该怎么做呢?例程3-21给出了一个简单的例子,判断变量val中的值是否在20和50之间。
例程 3‑21 判断参数val的值是否在某个范围内
/* 如果val的值在20至50之间,则返回1;否则返回0 */
int in_range(int val)
{
if(val > 20)
{
if(val < 50)
{
return 1;
}
}
return 0;
}
多学一招:条件判断语句——if语句
这里接触到了条件判断语句if。if语句的作用是判断括号里面的表达式是否为真,如果为真,就执行if语句后面大括号中的语句,否则就跳过那些语句。下一章中会详细介绍if语句的使用。
使用上面的程序成功地实现了需求。但是大家会发现,每个判断都需要一条if语句来实现。为了满足上面的需求,需要写两条if语句,如果要判断“一个数是3、5、7、11、13、17、19的倍数”呢?难道必须接连写七条if语句才可以吗?这实在是太麻烦了。
问题的关键之处在于,关系表达式只能完成一次比较。下面是一些多次比较的例子:
- 中国的国土面积比朝鲜的国土面积大,且中国的国土面积比日本的国土面积大。
- 北京的房价比上海的房价更高高,且北京的房价比东京的房价高。
同时做多次比较,其实是要通过某些特殊的词将两次比较的结果相连(比如上述例子中的“且”字)。考虑到用来比较大小的关系表达式会以真/假作为值,就可以知道这些特殊的词就是用来为“真”和“假”这两种值做运算的。C语言提供了逻辑运算符,这样就能将两个关系表达式连接到一起,并使用它们的值做运算了。当然关系表达式的运算结果也只能是“真”或“假”。
表3-4列出了C语言提供的三个逻辑运算符。大家或许会对这三个符号感觉非常陌生和不适应,因为现实生活中几乎从不使用这三种符号。
表 3‑4 C语言中的逻辑运算符
编号 |
逻辑运算符 |
说明 |
示例 |
1 |
&& |
逻辑与运算,只有前后两个表达式都为真时,与运算的结果才为真 |
(1 < 2) && (3 > 5) ==假 |
2 |
|| |
逻辑或运算,前后两个表达式只要有一个为真,或运算的结果即为真 |
(1 < 2) || (3 > 5) == 真 |
3 |
! |
逻辑非运算,放在单个表达式之前,运算结果与原表达式的结果相反 |
!(3 > 5) == 真 !(1 < 2) == 假 |
由于在C语言中,非零的值均代表“真”,而只有0才代表“假”,因此上述逻辑运算符同样适用于直接操作数值表达式。比如表达式3 && 5为真,而表达式!(3 5)则为假。程序员经常利用此特性来简化条件判断语句,例如 if(val == 0) 可以被简化成 if(!val),同理 if(val != 0) 也可以化简成 if(val)。
使用逻辑运算符对例程3-21进行优化,结果如例程3-22所示:
例程 ‑ 使用逻辑运算符优化后的程序
/* 如果val的值在20至50之间,则返回1;否则返回0 */
int in_range(int val)
{
if(val > 20 && val < 50)
{
return 1;
}
return 0;
}
根据上面对三个逻辑运算符的描述,可以列出这样一张表格(表3-5):
表 3‑5 逻辑运算真值表
变量 |
A |
B |
!A |
!B |
A && B |
A || B |
值 |
真 |
真 |
假 |
假 |
真 |
真 |
真 |
假 |
假 |
真 |
假 |
真 | |
假 |
真 |
真 |
假 |
假 |
真 | |
假 |
假 |
真 |
真 |
假 |
假 |
上面这张表格称为“真值表”。可以看出只要知道变量或表达式A和B的真假,就能知道它们进行逻辑运算之后的结果了。请好好理解三种逻辑运算的含义,然后试着默写一下真值表。
根据真值表可以得知,在进行逻辑或运算(逻辑运算符||)的时候,只要A和B其中有一个为真即可。在C程序中,编译器很智能地做了优化,即做或运算时,如果第一个表达式为真,就跳过第二个表达式的求值。大家可能会问:这会有什么影响呢?表达式的值不都已经摆在逻辑表达式里面了吗?其实不然,请参考例程3-23。
例程 3‑23 短路表达式示例
- #include <stdio.h>
- int main()
- {
- int a = 50, b = 100;
- if(a >= 50 || ( b > 100))
- {
- printf("We are here!\n");
- }
- printf("a = %d, b = %d\n", a, b);
- return 0;
- }
程序的运行结果如图3-13所示:
图 3‑13 短路表达式示例
请注意,最后b的值不是101而是100,就是应用了短路表达式的结果。由于在条件判断语句中a >= 50已经为真,因此后面的 b > 100 并没有被执行。在开发时,请一定注意短路表达式的影响。当然,为了避免短路表达式可能带来的问题,也可以将 b 放到条件判断语句之外,这也是推荐的写法。尽管程序会因此变长,但相比于简化写法后可能带来的错误,程序长一点儿其实是没关系的。
脚下留心:&& 和 & 的区别
在开发时,经常遇到将逻辑运算符&&误写为&、以及将逻辑运算符||误写为|的情况。大多数情况下编译器是不会报错的——这是因为符号&和|都是C语言提供的运算符。更糟糕的是,这两种错误往往并不容易发现。请试着运行例程3-24。
例程 3‑24 混合使用运算符&&、&、||和| (1)
- #include <stdio.h>
- int main()
- {
- int a = 4, b = 5;
- if(a && b)
- {
- printf("a && b is true!\n");
- }
- if(a & b)
- {
- printf("a & b is true!\n");
- }
- b = 0;
- if(a || b)
- {
- printf("a || b is true!\n");
- }
- if(a | b)
- {
- printf("a | b is true!\n");
- }
- return 0;
- }
程序的运行结果如图3-14所示:
图 3‑14 混合使用运算符&&、&、||和| (1)
大家会发现在四种情况下,表达式的结果都是真。这是不是意味着&&和&、||和|都没有区别了呢?下面对例程3-24做一点儿修改,修改后的结果如例程3-25所示。
例程 3‑25 混合使用运算符&&、&、||和| (2)
- #include <stdio.h>
- int main()
- {
- int a = 4, b = 5;
- if(a && b)
- {
- printf("a && b is true! Result = %d\n", a && b);
- }
- if(a & b)
- {
- printf("a & b is true! Result = %d\n", a & b);
- }
- b = 0;
- if(a || b)
- {
- printf("a || b is true! Result = %d\n", a || b);
- }
- if(a | b)
- {
- printf("a | b is true! Result = %d\n", a | b);
- }
- return 0;
- }
程序的运行结果如图3-15所示:
图 3‑15 混合使用运算符&&、&、||和| (2)
大家会注意到返回的值并不一样。实际上&和|都是位运算符,它们的运算结果仍然是数值,而不是“真”或“假”。而C语言使用0表示“假”、非零值表示“真”的特性使得错误的输入被当做了合法语句。如果令a = 1、b = 3,就会发现&&和&的表现并不一致了。
关于位运算符和位运算的知识,将在下一节中加以详细介绍。
位运算符
在第二章中学习了二进制初步知识的基础上,本节介绍C语言中的位运算。位运算的基本思路是对数据按位进行处理,这要求大家从二进制的角度,而不是十进制的角度去思考问题。
按位与运算按位与运算符&接受两个整数,并将它们的每一个bit按照如下的真值表进行运算:
表 3‑6 &真值表
& |
0 |
1 |
0 |
0 |
0 |
1 |
0 |
1 |
简单地说,只有两个数相同位置的bit都是1,按位与的结果才会是1。如果一个bit和0按位与,那么结果一定是0;如果和1按位与,那么结果是这个bit自身。还是以简单的4位二进制数为例:假设要对0101和1001按位与:
表 3‑7 0101&1001
bit |
3 |
2 |
1 |
0 |
0101 |
0 |
1 |
0 |
1 |
1001 |
1 |
0 |
0 |
1 |
0101&1001 |
0 |
0 |
0 |
1 |
如上表所示,0101&1001的结果是0001。
动手体验:按位与运算实例
下面是一条利用&实现的条件判断语句,指出它的功能。假设n是int类型的正整数。
((n & (n-1)) == 0)
如果觉得有困难,可以试着写一个小程序,看看哪些正整数使得上述条件判断为真:
例程 ‑ 按位与运算
- #include <stdio.h>
- int main()
- {
- for (int n = 1; n < 100; n )
- {
- if ((n & (n - 1)) == 0)
- printf("%d\n", n);
- }
- return 0;
- }
程序的运行结果如图3-16所示:
图 3‑16 按位与运算
接下来就来分析动手体验中的按位与操作,它们都是2的幂。据此可以大胆猜测:该条件判断是为了寻找2的幂。验证这一点是很容易的:只要注意到所有2的幂的二进制表示都只有一个bit上是1:
0000 0000 0010 0000 …. 0000
减去1之后就变成了:
0000 0000 0001 1111 …. 1111
上面这两个数做按位与操作,结果确实是0。
另一方面,还需要搞清楚,是不是只有2的幂才会使上述条件为真。现在假设n当中有至少两个1:
0000 0001 …. 1000 0000
此时考虑最后出现的那个1,它的后面全部是0,如果把n减去1,得到的二进制数会变成:
0000 0001 …. 0111 1111
也就是说,从最后一个1开始向后各个bit反转,但是前面的bit都不变,这样当把n和n-1进行按位与时,前一部分的1就会导致n&(n-1)的结果非零。因此,这个条件判断的作用确实是筛选出2的幂。
按位或运算按位或运算|和或运算类似:只要参与操作的两个bit中有一个是1了,按位或的结果就是1。
表 3‑8 |真值表
& |
0 |
1 |
0 |
0 |
1 |
1 |
1 |
1 |
按位或的特点是:如果一个bit和0按位或,结果还是这个bit本身;如果一个bit和1按位或,结果必定是1。根据这个特点,可以在不改变其他bit的情况下,把整数的某一个bit设为1。
动手体验:按位或运算实例
猜猜下面的程序在干什么:
例程 ‑ 按位或运算
- #include <stdio.h>
- int main()
- {
- int n = 0x12345678;
- int mask = 0x55555555;
- printf("%x\n", n | mask);
- return 0;
- }
程序的运行结果如图3-17所示:
图 ‑17 按位或运算
其中%x表示输入/输出一个十六进制整数。
如果还记得十六进制和二进制的转换方式的话,应该可以看出mask其实是下面的这个数:
0101 0101 0101 0101 0101 0101 0101 0101
在根据按位或的特点:和0按位或不改变原来的bit,和1按位或的结果必定为1。因此,这个程序的作用是将输入的整数n的所有奇数位都设为1。上述程序的输出结果如下:
按位异或运算1、按位异或^接受两个操作数,它的规则是相同为0不同为1:
表 3‑9 ^真值表
& |
0 |
1 |
0 |
0 |
1 |
1 |
1 |
0 |
观察上面的真值表:如果和0按位异或,该bit不变;如果和1按位异或,该bit取反。
异或运算是满足交换律和结合律的:如果有三个整数a,b和c,那么有:
a^b=b^a
(a^b)^c=a^(b^c)
请验证上述两式。
动手体验:按位异或运算实例
猜猜下面的程序在干什么:
例程 ‑ 按位异或运算
#include <stdio.h>
int main()
{
int a = 3, b = 5;
printf("%d %d\n", a, b);
a = (a ^ b);
b = (a ^ b);
a = (a ^ b);
printf("%d %d\n", a, b);
return 0;
}
程序的运行结果如图3-18所示:
图 ‑ 按位异或运算
例程3-28中实现的是一个非常经典的功能:如何不借助临时变量来交换两个数。这段程序的精髓都在三次异或运算上。由于位运算都是按位进行的,只需要验证对每一个bit,上述三次异或运算都可以将它们正确交换即可,这样一共有四种情况需要检验:
表 3‑10 交换a和b
a和b的某一个bit |
第一次^后 |
第二次^ |
第三次^ |
0 0 |
0 0 |
0 0 |
0 0 |
0 1 |
1 1 |
1 0 |
1 0 |
1 0 |
1 0 |
1 1 |
0 1 |
1 1 |
0 1 |
0 1 |
1 1 |
2、按位取反运算
按位取反运算~可能是最简单的位运算了,顾名思义,它只接受一个操作数,并将各个bit取反,即0变1,1变0。
按位取反常用来生成辅助其他位运算的常数。考虑这样一个例子:假设我需要把一个int类型的整数n的最低四位清0,其他位不变,按位与操作&很适合完成这样的工作:只需要把这个整数和下面的数
1111 1111 1111 1111 1111 1111 1111 0000
进行按位与操作即可,即:
int result = n & 0xFFFFFFF0;
然而0xFFFFFFF0的写法不仅冗长而且容易出错,可以用取反操作来生成这个数:
int result = n & (~0xF);
左移右移运算左移运算符<<负责将一个二进制数左移指定的位数,左移之后右边会自动补0,最左边溢出的bit直接舍弃。以较短的4位二进制数0011举例:左移1位之后:
0011 -> 0110
左移2位之后:
0011 -> 0110 -> 1100
左移3位之后:
0011 -> 0110 -> 1100 -> 1000
如果把一个十进制数左移一位,并在低位补0,就相当于把这个十进制数乘以10。类似地,将一个二进制数左移一位就相当于把二进制数乘以2,左移两位相当于乘以4,左移三位相当于乘以8,等等。
和左移相反,右移运算>>把二进制位数右移指定位数,低位直接舍去。左边空出的高位有多种选择:对于无符号整数,左边的空位直接补0;对于有符号整数,有些系统选择补符号位(正数补0,负数补1),有些系统选择补0。
一般也可以把右移看做是把原来的整数除以2。
多学一招:利用左移实现整数乘法
可以利用左移和加法来实现整数的乘法。给定一个int类型的整数n,可以用下面的方法来计算14n:
n << 1 n << 2 n << 3
这是因为14的二进制表示是1110,上述三项分别是n的2倍,4倍和8倍,加起来正好是14倍。需要提醒的是,多数时候编译器会把乘2乘4这类的操作直接优化成左移运算,而实际编程中也没有必要把整数乘法手动刻意展开成上述形式。
其它运算符
除了之前讲过的那些运算符之外,C语言还为大家提供了两个特殊的运算符:sizeof和%。学过本节之后,大家就明白这两个运算符的作用了。
sizeofsizeof运算符返回某个操作数所占字节的数目。例如在C语言中,int类型的变量会占用4字节的内存,而double类型的变量则占用8字节的内存。因此就有sizeof(int) == 4,且sizeof(double) == 8。同样地,如果有一个名为val的变量,也可以使用sizoef(val)来得到这个变量在内存中实际占用空间的大小。
下表列出了C语言中常见类型占用内存的大小,供大家参考。
表 3‑11 常见数据类型占用内存的大小
类型名称 |
sizeof() 结果 |
char |
1 |
unsigned char |
1 |
short |
2 |
unsigned short |
2 |
int |
4 |
unsigned int |
4 |
long |
4 |
unsigned long |
4 |
float |
4 |
double |
8 |
在整数除法中会遇到“除不开”的情形,这时将会出现余数。例如14 / 3可以得到4,余数是2。取模运算就是用来直接取得两个操作数相除后所得余数的运算。在上面的例子中,可以写成 14 % 3 == 2。当然如果直接做取模运算的话,就无法同时得到14除以3的商了。
取模运算到底有什么作用呢?不要着急,俗话说得好,“心急吃不了热豆腐”,请大家耐心等到下一章。提前预告一下,取模运算会在循环中发挥巨大的作用。
动手体验:取模运算
下面的例程实现了判断一个数是否同时为3、5、7、11、13、17、19的整倍数的功能,供大家参考。
例程 3‑29 取模运算例程
/* 如果同时是3、5、7、11、13、17、19的整倍数,返回1;否则返回0 */
int is_multiple(int val)
{
if(!(val % 3) && !(val % 5) && !(val % 7) && !(val % 11) && !(val % 13) && !(val % 17) && !(val % 19))
{
return 1;
}
return 0;
}
多学一招:负数的模
看完上面的内容,大家肯定会问:在除法运算中,如果被除数或除数中有一个是负数,那么它们的余数是什么呢?例如对于-7除以5,余数是-2还是3呢?
C99规定了整数除法必须遵循“向零取整”的规则。例如 -7 / 5 = -1.4,经过“趋零截尾”之后,商等于 -1(注意在数轴上,-1比-1.4离0更近,如下图所示)。这样余数就等于 -7 - (5 * (-1)) = -2。
图 3‑18 -1和-1.4在数轴上的位置
0
-1
-1.4
1
C语言中,正数的模只能是0或正数,负数的模只能是0或负数。通用的计算余数的公式是:a % b = a – (a / b) * b。
运算符的优先级
运算符的优先级决定了一个表达式中不同部分的计算顺序。比如大家都知道在数学表达式中要“先算乘除、再算加减”,这意味着乘号、除号的优先级比加号和减号的优先级更高。C表达式同样有优先级的约定,高优先级的运算先进行,低优先级的运算后进行。对于同等优先级的运算,从左到右进行……不对!
C语言还规定了运算符的结合性。所谓“结合性”是指对于某个操作符而言,是从左向右进行计算,还是从右向左进行计算。例如加法运算符“ ”是左结合的,那么在计算表达式a b时,应当先计算a的值,再计算b的值(注意a和b可能不是简单的变量,而是另外的两个表达式)。赋值运算符“=”是右结合的,因此对于表达式a = b = 10,应当先执行 b = 10,再执行 a = (b = 10)。可以简单地记为运算符和哪边的操作数相结合,就先计算哪边的操作数。因此左结合的操作符是从左向右计算的,而右结合的操作符应当从右向左计算。
下表总结了到目前为止学过的所有操作符的优先级和结合性。
表 3‑12 C语言操作符的优先级与结合性
运算符 |
优先级 |
结合性 |
( ) |
1 |
左结合 |
! (正号) -(负号) -- |
2 |
右结合 |
* / % |
3 |
左结合 |
- |
4 | |
< > >= <= |
6 | |
== != |
7 | |
&& |
11 | |
|| |
12 | |
= = -= *= /= %= |
13 |
右结合 |
多学一招:记不得运算符的优先级怎么办?
初学者经常会出现忘记运算符优先级的情形。( a || b && c && d || e) 到底应该先算哪个、再算哪个,弄不清是很正常的。一个终极解决方案是:手动用括号标明想要的运算优先级。例如对于上面的例子,完全可以写成(a || (b && c && d) || e)。在比较长的表达式中适当加入括号,不仅不会使表达式变得过分臃肿,反而能起到帮助其他人理解代码的作用。
本章小结
本章介绍了C语言提供的多数运算符及其使用,覆盖了赋值运算符、算术运算符、关系运算符、逻辑运算符以及sizeof和取模运算符等。此外还在例程中简单介绍了两条基本的输入输出函数(printf与scanf函数)以及条件判断语句(if语句)的使用。
正确使用运算符是学习C语言的关键,有了这些运算符的帮助,现在大家应该可以编写一些小程序了。
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com